proplot
proplot copied to clipboard
Support axis sharing between arbitrary subplot rows, columns
Description
Hi, I like the way you configured sharex and sharey with different levels, but there is a slight difference between matplotlib sharex/sharey. In proplot, sharex only works in the same row whereas matplotlib works in different rows as well.
I think this difference is fine but can you add an option (such as sharex=4
) to allow sharing in different rows?
Thanks.
Steps to reproduce
import numpy as np
import proplot as plot
rng = np.random.default_rng()
x = np.arange(10)
y = rng.standard_normal(10)
fig, axs = plot.subplots(ncols=2, nrows=2, share=3)
for i, ax in enumerate(axs):
ax.plot(x+i, y+i)
axs.axvline(0, c='k', ls='--')
axs.axhline(0, c='k', ls='--')
Actual behavior: [What actually happened]
Equivalent steps in matplotlib
import numpy as np
import matplotlib.pyplot as plt
rng = np.random.default_rng()
x = np.arange(10)
y = rng.standard_normal(10)
fig, axes = plt.subplots(2, 2, sharex=True, sharey=True)
for i, ax in enumerate(axes.flat):
ax.plot(x+i, y+i)
ax.axvline(0, c='k', ls='--')
ax.axhline(0, c='k', ls='--')
Proplot version
0.6.4
can you add an option (such as sharex=4) to allow sharing in different rows
Really like that idea! Will do.
I think this would also be a good opportunity to add optional string-keys for the different axis sharing levels so they are a bit more intuitive -- even I have to look up the level numbers sometimes. For example, sharex=False
is everything off (level 0), sharex='labels'
is just the axis labels (level 1), sharex='limits'
is the axis labels + limits, scales, and ticks (level 2), sharex=True
is share the tick labels (level 3), and sharex='all'
is share all axes even if they are not in the same row/column (level 4). This approach of mixing boolean values with string keys is used elsewhere in matplotlib and other python projects.
My initial thinking is that True
should be level 3, and that level 3 should remain the default, because level 4 might feel unintuitive sometimes. Open to arguments against this though.
Sounds great idea! I also think True
should be level 3.
For better user-control, I think the choice of which columns and rows to share (or all subplots, like asked for in this case) and what to share should be handled separately.
For a lot of cases, either sharing all subplots everything y and sharing everything x, or sharing this row-wise and column-wise and having levels - so the current state - is probably enough. But there certainly are cases where you have same axis on all plots, but only want to share labels (so level 1), or maybe share labels and limits, but keep tick labels (level 2).
Something like share=which, what
with options like which = row, col, all
and what = None, labels, limits, full
.
So that sharing all axes doesn't mean sharing full options.
I personally think having any sharing on as default - and that span is also on if share is on - is the wrong way to go about it. In general, I like that plotting defaults are as limited as possible, and don't add anything. It's only one personal opinion, but I don't really like that ProPlot makes sharing "automatic". Especially because figuring this out took finding the entry in the API under Figure, because the defaults are not mentioned in the documentation neither under The Basics, nor Subplots menus.
I can easily see the argument that the most common case should be the default, if that is the going philosophy. I just take a minimalistic approach to default settings, and then wish for great control for customization (which ProPlot seems lengths better at than Matplotlib!).
If really wanting to extend this control of which and what to share between subplots, you could also want to share axes between a subset of your subplots. Maybe column 1 and 2 share x, but column 3, 4 and 5 share another x. And maybe column 1 and 2 share only labels (level1 ), but 3, 4 and 5 share everything (level 3, or "full" as I would call it). And maybe even the label on column 1,2,3,4,5 AND 6 are the same. And of course equivalent for the y axis. This could lead to pretty complicated settings, though, as you would have to pass a list of arguments
As far as I understand, share, sharex and sharey "belongs" to the Figure, not the axes. So the share properties cannot be set by the regular slicing and use of format(), right?
@snowzky I appreciate your comments -- finally got the chance to address them.
Something like share=which, what with options like which = row, col, all and what = None, labels, limits, full. So that sharing all axes doesn't mean sharing full options.
I think this is what I was advocating in my comment (see above). Unless you are also suggesting supporting e.g. sharing x-axes across columns with a sub-3 sharing level, e.g. sharex=1
?
The reason this wouldn't work is that "sharing levels" only makes sense for x-axes in the same column. For example, it makes sense for the x-axis labels and tick labels in the bottom-left of a 2x2 plot to stand-in for missing labels in the top-left plot, but not the top-right. Also, I think a tuple keyword arg would probably get confusing. So, adding a 4th "level", along with adding optional string arguments for all the "levels", initially feels intuitive.
However I see my approach disallows some use cases -- e.g. limits are shared but tick labels are still printed everywhere... the alternative would be destroying the "level" concept and using args like sharex_rows
, sharex_columns
, sharex_limits
, sharex_ticks
, sharex_labels
that can be mix-and-matched. On that thought maybe as the best of both worlds, I can keep the "level" concept as a shorthand to capture most use cases, but allow customization with these args! I think I'll do that.
I personally think having any sharing on as default - and that span is also on if share is on - is the wrong way to go about it.
Going to disagree on this because as you said:
I can easily see the argument that the most common case should be the default,
I'd be more concerned about disabling the "sharing" feature by default, then having users not know about the "sharing" feature and ending up with redundant labels. And "make nice/publishable default plots" is a main point of this package.
Especially because figuring this out took finding the entry in the API under Figure, because the defaults are not mentioned in the documentation neither under The Basics, nor Subplots menus.
This is definitely a fair point. The documentation in the next version tries to make this feature more prominent (there will be a warning above the very first example in the user guide, and prominent links to the rc
setting that disables this feature).
If really wanting to extend this control of which and what to share between subplots, you could also want to share axes between a subset of your subplots.
This could be implemented in the more specialized keyword args -- for example sharex_rows=(1, 2)
to share only a subset of rows or sharex_rows=[(1, 2), (3, 4, 5)]
to share different groups of rows. I think this idea is a winner: Short keyword args for convenience, long keyword args for special cases.
As far as I understand, share, sharex and sharey "belongs" to the Figure, not the axes. So the share properties cannot be set by the regular slicing and use of format(), right?
That's right -- at one point I tried to make axis sharing settable, but that turned out to require a huge amount of work for a feature that would be very seldom used (see v0.6.2 header in the changelog/"what's new"). I think these fancy keyword args will do the trick instead.
You also bring up a good point: There is definitely some overlap with format()
. You don't really "need" axis sharing since proplot makes it easy to call format()
on subplots simultaneously. So someone can "share" limits or ticks by slicing with e.g. axs[:, 0].format(xlim=(0, 10))
. However the sharing feature is useful when you want to go with matplotlib's defaults (e.g. default limits, default scales, default tick labels). So despite the overlap I think it's still useful.
Will see if I can get to this before the next release.
Okay, I can't remember what use-case I had in mind a year ago. And also, maybe I misunderstood the way you were thinking of implementing this. Or maybe I am thinking more about "exploring my data"-plots than "make nice/publishable default plots", and if so, you are the creator of your package so of course you get final say in what the purpose is :)
So, I use matplotlib's sharex and sharey mostly, as a first thing, for being able to control what happens when I zoom in in the Spyder window to explore my data: share limits of the axes, and of updated limits when zooming - if I understand what matplotlib is doing correctly.
Secondly, I sort of use it to avoid seeing my repeated x-axis with tick and ticklabels across many subplots in random 2D grid structure (so not just x-axis across rows, in same column).
Thirdly, I sometimes use it to share the y-axis, but mostly only across columns (so same row), but that only happens if I have a variable with the same unit like that.
So for me, it's not a weird case to have the same x-axis on ALL subplots, and have a different y-axis on ALL subplots, in a random 2D grid, at the same time, but not necessarily share the full (or True, as you call it) option. Or, have same type variable - so label - on yaxis, but all the same indexer - so label, limits, ticks (and ticklabels), on x-axis. For exploratory though, often nice to keep the ticklabels to easier see the numbers of one's data. Maybe you have plots of time and temperature on all x and y-axis, but while the time axis is the same, the temperature, despite same label, could need different limits.
What settings would I do for that in proplot...? I am not quite sure this is covered by what you propose. What does the sharex='all'
share, what level (of the current ones)...?
Same question for your long keyword, sharex_rows=[(1, 2), (3, 4, 5)]
- what are these collections sharing ...? And, can the groups share different levels from eachother...?
But, actually, if you have subplots where you have the same limits of all, I would wonder why your elements aren't plotted in the same axes to begin with?! (Of it is lineplots, that is.) If not, there must be another "coordinate" variable that explains why to plot data in two different subplots than in the same, if they both share their x and y.
But maybe, all of this is just covered by using .format() with appropriate slices, after creation. It does, however, seem a bit stupid/weird to slice a full slice of all axes to get this behavior.
It is quite possible I am trying to solve for both exploratory and presentable cases at the same time, and that is a bad idea. But maybe I just want nice graphs to also be exploratory-friendly ... ;)
Also, I am aware matplotlib does not do what I want either. Neither does pandas and their direct plotting, nor xarray. I must also admit I haven't actually used proplot since this post a year ago, as I found I didn't know matplotlib well enough to feel secure in proplot; and when having a problem, obviously the stackexchange support for matplotlib is richer.
So I ended up mostly staying in matplotlib. But I do keep running into issues, and I think so many of the ones I hit are exactly the ones you identify and solve with your package. Some of the missing elements/settings (eg. easy putting legend outside plot) and choices of matplotlib are just inexplicable and makes me miss matlab.
Or maybe I am thinking more about "exploring my data"-plots than "make nice/publishable default plots", and if so, you are the creator of your package so of course you get final say in what the purpose is :)
Yeah tbh data exploration is not what this package is focused on. You definitely can use it for that but it's focused on the "publication-quality" part.
So for me, it's not a weird case to have the same x-axis on ALL subplots, and have a different y-axis on ALL subplots, in a random 2D grid, at the same time, but not necessarily share the full (or True, as you call it) option. Or, have same type variable - so label - on yaxis, but all the same indexer - so label, limits, ticks (and ticklabels), on x-axis. For exploratory though, often nice to keep the ticklabels to easier see the numbers of one's data. Maybe you have plots of time and temperature on all x and y-axis, but while the time axis is the same, the temperature, despite same label, could need different limits. What settings would I do for that in proplot...?
Right you just want to share x-axis limits but not labels so that it's easier to read. That's currently unsupported but if I add these long options it would look something like sharex_limits=True, sharex_labels=True, sharex_ticklabels=False
. The y-axis sharing you want could be done with sharey=1
(which just shares labels, not ticklabels or limits).
I am not quite sure this is covered by what you propose. What does the sharex='all' share, what level (of the current ones)...?
The sharex='all'
(or sharex=4
) option would keep the same sharing level as sharex=3
, but also share across columns -- so, compared to 3
, 4
just changes what is shared. Maybe confusing at first but I think shorthand "sharing levels" are still useful because they cover the most common use cases. But for your use case you'd need the long labels.
The API reference is helpful -- see this link.
Same question for your long keyword, sharex_rows=[(1, 2), (3, 4, 5)] - what are these collections sharing ...? And, can the groups share different levels from eachother...?
In this case the numbers would indicate row numbers ("row numbers" are used elsewhere in proplot, like for Figure.colorbar
). So this would mean share x-axes between rows 1 and 2, and share x-axes of rows 3, 4, and 5. Then the sharing level would be determined by the other arguments. Keep in mind I'm just toying with this idea -- not sure if the keyword arguments are correct/intuitive.
If not, there must be another "coordinate" variable that explains why to plot data in two different subplots than in the same, if they both share their x and y.
Don't really understand this point.
But maybe, all of this is just covered by using .format() with appropriate slices, after creation. It does, however, seem a bit stupid/weird to slice a full slice of all axes to get this behavior.
Yeah that's why I like the "sharing" thing even if it is a little redundant.
Also keep in mind... the default "sharing" set up by proplot's subplots
actually just replicates matplotlib's subplots
. Matplotlib also does sharing by default.
Also, I am aware matplotlib does not do what I want either. Neither does pandas and their direct plotting, nor xarray. I must also admit I haven't actually used proplot since this post a year ago, as I found I didn't know matplotlib well enough to feel secure in proplot; and when having a problem, obviously the stackexchange support for matplotlib is richer.
Totally fair! This package is still new and experimental. Better support/larger community is a great reason to use matplotlib.
For the time being I decided to add the more intuitive aliases for axis-sharing in v0.8: now True
is the same as 3
("turn on all axis sharing"), 'limits'
is the same as 2
(share just limits and tick locations), 'labels'
is the same as 1
(share just axis labels), and 0
is False
as before. Think this is the right choice -- in my own usage I've always been bothered by the awkward combination of setting span
to a boolean but share
to an integer.
The fancier axis sharing features discussed above will be for another release.
I've added a level 4
(or 'all'
) option now. So the limits/scales/ticks can be locked across a whole grid using e.g. sharex=4
or sharey=4
. Decided to submit a quick bugfix release since v0.8 had a couple very notable bugs, and this feature turned out to be really easy to implement, so will include it.
Configuring sharing between rows, columns will probably be part of a larger (...eventual) overhaul of the axis sharing implementation related to #205.
To chime in on this discussion. I am often faced with a situation in which subplots need shared information either on the columns or the rows. I find it a bit tricky to get the similar functionality happening in proplot. That is, is there a way where I can share the axes limits (y or x) only on a row basis or columns basis? For example something equivalent to matplotlib's code
import matplotlib.pyplot as pplt
import networkx as nx
g = nx.florentine_families_graph()
fig, axs = pplt.subplots(2, 2, sharey = "row", sharex = 'row')
for axi in axs[0, :]:
nx.draw_forceatlas2(g, ax = axi, node_size = 20)
for idx, axi in enumerate(axs[1, :]):
axi.plot(np.random.rand(2, 100) + idx)
Note how the y and x limits are shared only on the second row.
@cvanelteren I see, you want to share x limits along rows (i.e., share x limits of left subplots with right subplots?). Right now that is indeed not possible in proplot -- the sharing level sharey=True
(or sharey=3
) automatically gives the equivalent of sharey='row'
, but the only options for x limits is to use sharex='all'
(or sharex=4
), which shares limits along both rows and columns rather than just rows.
For now the best workaround would probably be to disable x axis sharing with pplt.subplots(..., sharex=False)
then manually set the axis limits, but I think you could also use matplotlib's lower-level methods like axs[1, 0].sharex(axs[1, 1])
if you really want those automatically-synced limits. It should still work with proplot axes.
In the future I think the features discussed above would satisfy this case.... e.g. something like sharex=('row', 'lims')
(possibly with a shorthand sharex='row'
), or instead something like share_rows=[(1,)]
to disable sharing in the top row.
@lukelbd Thanks for the clarification! Is there a roadmap for adding this in? If I can help in any way let me know.