Skip to content

Matplotlib colorbar moves second x axis drawn with twiny #5904

Closed

Description

@Gabriel-p

Bug report originated in SO post here: http://stackoverflow.com/q/34979781/1391441


I'm trying to add a second x axis to the top of a plot using twiny.

If I make a simple scatter plot with no colorbar, the top x axis is correctly aligned with the bottom x axis (MWE is below):

enter image description here

If I add a colorbar though, the top x axis is displaced:

enter image description here


MWE

import numpy as np
import matplotlib.pyplot as plt
from mpl_toolkits.axes_grid1 import make_axes_locatable
import matplotlib.gridspec as gridspec


X = np.array([0., 0.5, 1., 1.5, 2., 2.5, 3., 3.5, 4.])
X2 = np.array([122, 85, 63, 50, 23, 12, 7, 5, 2])
Y = np.cos(X*20)
Z = np.sin(X*20)

fig = plt.figure()
gs = gridspec.GridSpec(1, 2)
ax1 = plt.subplot(gs[1])
ax2 = ax1.twiny()

ax1.set_xlim(-0.2, max(X)+0.2)
plt.tick_params(axis='both', which='major', labelsize=10)
ax1.minorticks_on()
ax1.grid(b=True, which='major', color='gray', linestyle='--', lw=0.3)

SC = ax1.scatter(X, Y, c=Z)
ax1.set_xlabel("Original x-axis")

ax2.set_xlim(ax1.get_xlim())
ax2.set_xticks(X)
ax2.set_xticklabels(X2)
ax2.set_xlabel("Second x-axis")

# Colorbar.
the_divider = make_axes_locatable(ax1)
color_axis = the_divider.append_axes("right", size="2%", pad=0.1)
cbar = plt.colorbar(SC, cax=color_axis)
cbar.set_label('B', fontsize=10, labelpad=4, y=0.5)
cbar.ax.tick_params(labelsize=10)

plt.show()

Activity

  1. added this to the milestone on Jan 25, 2016
  2. modified the milestones: , on May 3, 2017
  3. modified the milestones: , on Oct 9, 2017
  4. QuLogic commented on Jul 21, 2020

    @QuLogic
    Member

    This works properly on master if you a) use constrained layout and b) use a normal colorbar Axes:

    import numpy as np
    import matplotlib.pyplot as plt
    
    
    X = np.array([0., 0.5, 1., 1.5, 2., 2.5, 3., 3.5, 4.])
    X2 = np.array([122, 85, 63, 50, 23, 12, 7, 5, 2])
    Y = np.cos(X*20)
    Z = np.sin(X*20)
    
    fig = plt.figure(constrained_layout=True)
    gs = fig.add_gridspec(1, 2)
    ax1 = plt.subplot(gs[1])
    ax2 = ax1.twiny()
    
    ax1.set_xlim(-0.2, max(X)+0.2)
    plt.tick_params(axis='both', which='major', labelsize=10)
    ax1.minorticks_on()
    ax1.grid(b=True, which='major', color='gray', linestyle='--', lw=0.3)
    
    SC = ax1.scatter(X, Y, c=Z)
    ax1.set_xlabel("Original x-axis")
    
    ax2.set_xlim(ax1.get_xlim())
    ax2.set_xticks(X)
    ax2.set_xticklabels(X2)
    ax2.set_xlabel("Second x-axis")
    
    # Colorbar.
    cbar = fig.colorbar(SC, ax=ax1, aspect=100, pad=0)
    cbar.set_label('B', fontsize=10, labelpad=4, y=0.5)
    cbar.ax.tick_params(labelsize=10)
    
    plt.show()

    Screenshot from 2020-07-21 01-13-00

  5. QuLogic commented on Jul 21, 2020

    @QuLogic
    Member

    In fact, it works on 3.3 as well using the above changes. I don't think there's any way to make this work without constrained layout. @jklymak ?

  6. jklymak commented on Jul 21, 2020

    @jklymak
    Member

    You can also use secondary_axis. Or manually make the twin the same size as the parent. But in general the colorbar steals from the parent and the second axes here is not the parent.

  7. anntzer commented on Jul 21, 2020

    @anntzer
    Contributor

    I don't think this is related to constrained_layout, isn't it just that make_axes_locatable doesn't interact properly with twinning? ... which appears to be relatively easily fixed with

    diff --git i/lib/matplotlib/axes/_base.py w/lib/matplotlib/axes/_base.py
    index 327198596..d11d472c3 100644
    --- i/lib/matplotlib/axes/_base.py
    +++ w/lib/matplotlib/axes/_base.py
    @@ -4265,6 +4265,13 @@ class _AxesBase(martist.Artist):
             self.yaxis.tick_left()
             ax2.xaxis.set_visible(False)
             ax2.patch.set_visible(False)
    +
    +        @ax2.set_axes_locator
    +        def axes_locator(_, renderer):
    +            self_locator = self.get_axes_locator()
    +            if self_locator is not None:
    +                return self_locator(self, renderer)
    +
             return ax2
     
         def twiny(self):
    @@ -4294,6 +4301,13 @@ class _AxesBase(martist.Artist):
             self.xaxis.tick_bottom()
             ax2.yaxis.set_visible(False)
             ax2.patch.set_visible(False)
    +
    +        @ax2.set_axes_locator
    +        def axes_locator(_, renderer):
    +            self_locator = self.get_axes_locator()
    +            if self_locator is not None:
    +                return self_locator(self, renderer)
    +
             return ax2
     
         def get_shared_x_axes(self):
  8. QuLogic commented on Jul 29, 2020

    @QuLogic
    Member

    That seems pretty reasonable, actually.

  9. added
    Difficulty: Easyhttps://matplotlib.org/devdocs/devel/contribute.html#good-first-issues
    🌱 Good first issueOpen a pull request against these issues if there are no active ones!
    on Jul 29, 2020
  10. QuLogic commented on Jul 29, 2020

    @QuLogic
    Member

    I marked this as a good first issue, since there's a patch available, and I think it should be easy as it's a matter of applying the patch and coming up with a test. The original post is maybe a bit too much for a test, so a possible test is creating an Axes, twinning it, and then modifying its position (Axes.set_position), then seeing that the twin is correctly moved as well.

  11. anntzer commented on Jul 29, 2020

    @anntzer
    Contributor

    (Probably(?) this should actually be done using _InsetLocator instead (locating to [0, 0, 1, 1]) for picklability, but the idea is the same.)

  12. tacaswell commented on Nov 15, 2020

    @tacaswell
    Member

    The bug only happens if you use both gridspec and make_axes_locatable.

    import numpy as np
    import matplotlib.pyplot as plt
    from mpl_toolkits.axes_grid1 import make_axes_locatable
    import matplotlib.gridspec as gridspec
    
    
    X = np.array([0., 0.5, 1., 1.5, 2., 2.5, 3., 3.5, 4.])
    X2 = np.array([122, 85, 63, 50, 23, 12, 7, 5, 2])
    Y = np.cos(X*20)
    Z = np.sin(X*20)
    
    fig = plt.figure()
    gs = gridspec.GridSpec(1, 2)
    ax1 = plt.subplot(gs[1])
    ax2 = ax1.twiny()
    
    ax1.set_xlim(-0.2, max(X)+0.2)
    
    plt.tick_params(axis='both', which='major', labelsize=10)
    ax1.minorticks_on()
    ax1.grid(b=True, which='major', color='gray', linestyle='--', lw=0.3)
    
    SC = ax1.scatter(X, Y, c=Z)
    ax1.set_xlabel("Original x-axis")
    
    ax2.set_xlim(ax1.get_xlim())
    ax2.set_xticks(X)
    ax2.set_xticklabels(X2)
    ax2.set_xlabel("Second x-axis")
    
    # Colorbar.
    # the_divider = make_axes_locatable(ax1)
    # color_axis = the_divider.append_axes("right", size="2%", pad=0.1)
    # cbar = plt.colorbar(SC, cax=color_axis)
    # cbar.set_label('B', fontsize=10, labelpad=4, y=0.5)
    # cbar.ax.tick_params(labelsize=10)
    
    cbar = fig.colorbar(SC, ax=ax1)
    cbar.set_label('B', fontsize=10, labelpad=4, y=0.5)
    cbar.ax.tick_params(labelsize=10)
    
    plt.show()

    and

    import numpy as np
    import matplotlib.pyplot as plt
    from mpl_toolkits.axes_grid1 import make_axes_locatable
    import matplotlib.gridspec as gridspec
    
    
    X = np.array([0., 0.5, 1., 1.5, 2., 2.5, 3., 3.5, 4.])
    X2 = np.array([122, 85, 63, 50, 23, 12, 7, 5, 2])
    Y = np.cos(X*20)
    Z = np.sin(X*20)
    
    fig, ax1 = plt.subplots()
    # fig = plt.figure()
    # gs = gridspec.GridSpec(1, 2)
    # ax1 = plt.subplot(gs[1])
    
    ax2 = ax1.twiny()
    
    ax1.set_xlim(-0.2, max(X)+0.2)
    
    plt.tick_params(axis='both', which='major', labelsize=10)
    ax1.minorticks_on()
    ax1.grid(b=True, which='major', color='gray', linestyle='--', lw=0.3)
    
    SC = ax1.scatter(X, Y, c=Z)
    ax1.set_xlabel("Original x-axis")
    
    ax2.set_xlim(ax1.get_xlim())
    ax2.set_xticks(X)
    ax2.set_xticklabels(X2)
    ax2.set_xlabel("Second x-axis")
    
    # Colorbar.
    the_divider = make_axes_locatable(ax1)
    color_axis = the_divider.append_axes("right", size="2%", pad=0.1)
    cbar = plt.colorbar(SC, cax=color_axis)
    cbar.set_label('B', fontsize=10, labelpad=4, y=0.5)
    cbar.ax.tick_params(labelsize=10)
    
    plt.show()

    @kir0ul worked on this during the pydata global sprint and while the patch does fix the bug, the naive testing approach I suggested (checking ax1.get_position() == ax2.get_position()) did not work as the reported position seems to never change and always be equal to each other.

  13. jklymak commented on Nov 15, 2020

    @jklymak
    Member

    I'm not fully parsing this, but what is the modern reason for needing make_axes_locatable ?

    EDIT: I guess if the axes changes aspect ratio...

  14. jklymak commented on Mar 29, 2022

    @jklymak
    Member

    I'm going to close this. Yes if you use make_axes_locatable to make your Colorbar, you may have problems, but there are a number of ways to get around this using the main library. If someone wants to re-open, please include a modern self-contained example that is broken.

  15. removed this from the future releases milestone on Oct 6, 2022
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

    Difficulty: Easyhttps://matplotlib.org/devdocs/devel/contribute.html#good-first-issuesstatus: has patchpatch suggested, PR still needed🌱 Good first issueOpen a pull request against these issues if there are no active ones!

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions