Skip to content

ENH: Allow axes to have child axes. #11005

Closed

Description

@jklymak

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.

Activity

  1. ImportanceOfBeingErnest commented on Apr 11, 2018

    @ImportanceOfBeingErnest
    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.

  2. jklymak commented on Apr 22, 2018

    @jklymak
    MemberAuthor

    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.
  3. ImportanceOfBeingErnest commented on Apr 22, 2018

    @ImportanceOfBeingErnest
    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.

  4. jklymak commented on Apr 22, 2018

    @jklymak
    MemberAuthor
    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.
  5. ImportanceOfBeingErnest commented on Apr 22, 2018

    @ImportanceOfBeingErnest
    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.

  6. jklymak commented on Apr 22, 2018

    @jklymak
    MemberAuthor

    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.

  7. ImportanceOfBeingErnest commented on Apr 22, 2018

    @ImportanceOfBeingErnest
    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?

  8. jklymak commented on Apr 23, 2018

    @jklymak
    MemberAuthor

    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...

  9. ImportanceOfBeingErnest commented on Apr 23, 2018

    @ImportanceOfBeingErnest
    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.

  10. jklymak commented on Apr 23, 2018

    @jklymak
    MemberAuthor

    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.

  11. jklymak commented on Jan 18, 2021

    @jklymak
    MemberAuthor

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions