Skip to content

ENH: Allow axes to have child axes. #11005

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
jklymak opened this issue Apr 9, 2018 · 11 comments
Closed

ENH: Allow axes to have child axes. #11005

jklymak opened this issue Apr 9, 2018 · 11 comments

Comments

@jklymak
Copy link
Member

jklymak commented Apr 9, 2018

Suggestion

This would subsume #10976: see #10961 #10960 for secondary axes requests.

See #8952 for the poor state of documentation and confusing behaviour of axes_grid1.inset_axes. #10986, #10756

There are a few instances where it would make sense for an axes to have an axes as a child artist:

  1. an inset_axes with a subsection of the data, or some other information (ala
    def inset_axes(parent_axes, width, height, loc=1,
    bbox_to_anchor=None, bbox_transform=None,
    )
  2. a secondary scale (i.e a second x-scale at the top of the axes in a different unit).

I'd propose we create an API for these. I would expect the legend API would be fine for inset axes, and would closely follow the one already in axes_grid1. An example would be:

# inset from x=0.65 to 0.95, y=0.75 to 0.95
inax = ax.inset_axes(location='upper_right', width=0.3, height=0.2, padding=0.05) 
# inset from x=0 to 0.25, y=0.3 to 0.7
inax = ax.inset_axes(bbox_to_anchor=(0, 0.3, 0.25, 0.4))

Secondary axes would be basically an inset_axes, except with the appropriate spines turned off:

secx = ax.extra_spine_x(location='top', convert=(lambda x: 1/x))
thirdx = ax.extra_spine_x(location=0.5, convert=(lambda x: 1/x))

secy = ax.extra_spine_y(location='right', convert=(lambda y: y**2))

Implementation

I would like to have these be children of the parent axes simply for the reason that they are decorators, and should be included in the list of artists a) at draw time, and b) when the bbox for the parent axes is calculated for tools like constrained_layout and tight_layout. Also having them as children makes it possible to change the limits if the parent limits change, and avoids the messiness of twin axes, which have a peer-to-peer relationship.

@ImportanceOfBeingErnest
Copy link
Member

To avoid confusion with the existing axes_grid1.inset_locator.inset_axes function what about naming this

 ax.add_inset(...)

The question is then in how far this would be compatible with all the inset_locator module's functionality like bbox connectors, mark_inset etc.

@jklymak
Copy link
Member Author

jklymak commented Apr 22, 2018

inset axis is well on its way in #11026

Lets hash out an API for ax.extra_spine_x/y? Proposal would be that this would be called as

def conversionx(x):
    return np.log10(x) + 31.5

extrax = ax.extra_spine_x(location='top', convert=conversionx)
# or
extrax = ax.extra_spine_x(location=1.0, convert=conversionx)
# or 
ax.plot(range(10))
extrax = ax.extra_spine_x(location=4.0, convert=conversionx, transform=ax.transData)
# this last one would put the new x-spine at 4.0 in *data* space.  

One could also imagine an extent and offset parameter...

ax.plot(range(10))
extrax = ax.extra_spine_x(location=4.0, convert=conversionx)
# this last one would put the new x-spine at 4.0 in *data* space.  

You could also imagine having an extent and offset keywords so the spine doesn't span the whole axis, but that would also need a way to map the xlims (which presumably would not be the full extent of the parent x axis. Seems complicated, and rarely used, and a need that could be met w/ inset_axis.

Issues

  • Would have to play around w/ code to make sure axes that change size due to set_aspect() work, but I think that will work OK.
  • tick direction and label direction would need to be set to some default. I'd propose if transform=ax.transAxes and location>0.5 (or "top") then put everything on the top by default.
  • the child axes and the parent will have to talk to each other, so this probably needs a subclass of axes that queries the parent for its xlims at draw time. Conversely, we could have parent.set_xlim propagate to the children, but it'd probably be nicer to just have the complicating code in the child axes.

@ImportanceOfBeingErnest
Copy link
Member

In general 👍

  • If I understand correctly, the return type is a matplotlib.axes.Axes or a matplotlib.axes.Subplot or a subclass of that? In that case it should have the usual ax.spines available, which can be used to translate the spine to a different position? As in spine_placement_demo?

  • What's the transform doing exactly? Is that necessary at all? Is it doing something other than placing the spine?

  • Why do you need ax.extra_spine_x and ax.extra_spine_y? Wouldn't it be sufficient to have

    ax.extra_spine(loc="top", convert=None)
    

    where loc can be any of "top", "bottom", "left", "right"?

  • I think it would be good to give a clear explanation of the difference to a twinx. (Because although I was involved in some of the discussion that led to this, I'm not certain I got the point of why that is needed. Is it just because of the aspect?). I think it depends on this if and how the limits are being propagated or not?

  • What's extent and offset doing? That example isn't very enlightening.

@jklymak
Copy link
Member Author

jklymak commented Apr 22, 2018

  1. I need to look at whether the axes needs to be a subclass or not. I think it does because I think that it will need to decide on the xlimits at draw time, but maybe with sharing it can get around this without making a mess. I think spine placement will be an issue, because that is done in the parent axes co-oridinate system, whereas this child will not necessarily know anything about the y-coordinate of the parent.
  2. transform is to assign a transform for the location. So if its ax.transAxes then its in the axes co-ordinates, if its ax.transData its in the data coordinates.
  3. Could drop the x and y but then location can't take a numeric argument and be unambiguous.
  4. twinx is really meant for if you want two different data streams to have the same x-axis but different y-axes (think humidity and temperature in a weather balloon profile; https://matplotlib.org/examples/api/two_scales.html). For one thing, its orthogonal to extra_spine_x. To get that you'd use twiny. But then the extra x-axis doesn't (and shouldn't) scale with the new one:
import matplotlib.pyplot as plt

fig, ax = plt.subplots()
ax.plot(range(20))
xl = ax.get_xlim()

secondax = ax.twiny()
secondax.set_xlim(xl[0]*10+30., xl[1]*10+30)
secondax.tick_params('y')

ax.set_xlim([0, 10])
plt.show()

doesn't return the correct x-axis because there is nothing tying the x-axis-es together (its the y-axis that is shared). Thats as it should be for a twinned axis; the two data types in the two axes should definitely be allowed to have independent x-axes.

  1. extent might make the extra x-axis shorter than the parent x-axis, but then you'd need offset to tell it where on the parent to go. Not 100% sure that this is that useful.

@ImportanceOfBeingErnest
Copy link
Member

  1. Ok, but let's suppose for a moment that the child is a subclass. Then it does provide the ax.spines['left'].set_position method, which can hence simply be used as with any other axes. I think this can be argument enough to actually make it a subclass.

  2. If transform is only needed for the location argument, one can refer to 1. and leave it out.

  3. No numeric argument is needed if the spine is set via ax.spines

So in total I would say, similar to what exists now one would do

extrax = ax.extra_spine(loc="top", convert=None)
extrax.spines["top"].set_position(('data', 4))

to place the spine at 4 in data coordinates.

  1. Thanks for reminding, that makes perfect sense. In a way the extra axis is like a shared axes with a data conversion in between? Maybe it's worthwhile looking into the shared axes mechanism and see if it would be possible to add some conversion to it or at least extend it somehow.

  2. Not sure either on the usefulness of extent. One could argue that if you want to have the extra axes at a different position (i.e. not using the same box as the parent) you'd use inset_axes and turn the other spines off.

@jklymak
Copy link
Member Author

jklymak commented Apr 22, 2018

I was conceiving of the extra_spine as not having a cross-spine dimension and having to track all the cross-spine info from the parent axes. That makes life a lot easier and we avoid all the issues with aspect ratios and twinning.

If we were to go that route, then spines['top'].set_position() won't do anything because the y-direction has no data and height=0.

Probably we could make it work the spine[loc].set_position() way, but that robs the method of most of its simplicity. It also makes the exact placement an extra call, but maybe that not a big deal.

@ImportanceOfBeingErnest
Copy link
Member

I was under the impression that the extra_axes would create a normal boxed axes, which just has 3 spines tuned off and whose scaling, limits, and position is tied to the parent. In that case, calling extrax.spines["right"] on an axes extrax = ax.extra_spine(loc="top") would simply not do anything; or, it would, but you'd only see it if you turned the respecive spine on again. (One could of course add .get_spine() to it, which returns the spine in question.)
I do not see the problem of the extra line. The same extra line is needed if spines of usual axes are to be manipulated. Why should the extra axes get special treatment here?

@jklymak
Copy link
Member Author

jklymak commented Apr 23, 2018

Yes, but that wasn't the plan given the PITA that fully-twinned axes are....

A specially-placed extra spine gets special treatment because its being specially placed.

I'm not 100% against the alternative of having a fully formed axes, but it just seems prone to inconsistencies.. i.e. if you set the aspect ratio on the main axes, and the child axes has a conversion on the x-axis, then it cannot share the aspect ratio of the parent, but rather needs to set its position based on the aspect-applied position of the parent. It can be done, but it all gets pretty complex, just for the ability to manually place the spine, which I was planning to allow anyways...

@ImportanceOfBeingErnest
Copy link
Member

Ok, I think I finally understand.. this is really just about getting a scale in some other units attached to an axes.

So at the end you have

  • twin axes, which cannot be shared and cannot have some aspect set to them
  • shared axes, which cannot be at the same position as another axes, and cannot use some different scale
  • extra spines, which can have a different scale, but which are no propper axes.

... each of which is somehow incomplete.

@jklymak
Copy link
Member Author

jklymak commented Apr 23, 2018

I don't think "completeness" is necessarily desirable. Thats how you end up with the El Camino (https://en.wikipedia.org/wiki/Chevrolet_El_Camino), something which doesn't do anything well or clearly. In this case, I don't think its necessarily good for twiny to be the way that a second x axis gets added to a parent axis, using a magical recipe that most users don't understand and is not readily discoverable.

@jklymak
Copy link
Member Author

jklymak commented Jan 18, 2021

I think this functionality is all in now....

@jklymak jklymak closed this as completed Jan 18, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants