Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 32fff69

Browse files
authored
Merge pull request #22635 from greglucas/cbar-inverted
FIX: Handle inverted colorbar axes with extensions
2 parents 5295e9a + ec374f5 commit 32fff69

File tree

2 files changed

+70
-15
lines changed

2 files changed

+70
-15
lines changed

lib/matplotlib/colorbar.py

Lines changed: 46 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -417,6 +417,7 @@ def __init__(self, ax, mappable=None, *, cmap=None,
417417
self._filled = filled
418418
self.extendfrac = extendfrac
419419
self.extendrect = extendrect
420+
self._extend_patches = []
420421
self.solids = None
421422
self.solids_patches = []
422423
self.lines = []
@@ -483,6 +484,11 @@ def __init__(self, ax, mappable=None, *, cmap=None,
483484
setattr(self.ax, x, getattr(self, x))
484485
# Set the cla function to the cbar's method to override it
485486
self.ax.cla = self._cbar_cla
487+
# Callbacks for the extend calculations to handle inverting the axis
488+
self._extend_cid1 = self.ax.callbacks.connect(
489+
"xlim_changed", self._do_extends)
490+
self._extend_cid2 = self.ax.callbacks.connect(
491+
"ylim_changed", self._do_extends)
486492

487493
@property
488494
def locator(self):
@@ -598,17 +604,20 @@ def _draw_all(self):
598604
# extensions:
599605
self.vmin, self.vmax = self._boundaries[self._inside][[0, -1]]
600606
# Compute the X/Y mesh.
601-
X, Y, extendlen = self._mesh()
607+
X, Y = self._mesh()
602608
# draw the extend triangles, and shrink the inner axes to accommodate.
603609
# also adds the outline path to self.outline spine:
604-
self._do_extends(extendlen)
605-
610+
self._do_extends()
611+
lower, upper = self.vmin, self.vmax
612+
if self._long_axis().get_inverted():
613+
# If the axis is inverted, we need to swap the vmin/vmax
614+
lower, upper = upper, lower
606615
if self.orientation == 'vertical':
607616
self.ax.set_xlim(0, 1)
608-
self.ax.set_ylim(self.vmin, self.vmax)
617+
self.ax.set_ylim(lower, upper)
609618
else:
610619
self.ax.set_ylim(0, 1)
611-
self.ax.set_xlim(self.vmin, self.vmax)
620+
self.ax.set_xlim(lower, upper)
612621

613622
# set up the tick locators and formatters. A bit complicated because
614623
# boundary norms + uniform spacing requires a manual locator.
@@ -661,12 +670,19 @@ def _add_solids_patches(self, X, Y, C, mappable):
661670
patches.append(patch)
662671
self.solids_patches = patches
663672

664-
def _do_extends(self, extendlen):
673+
def _do_extends(self, ax=None):
665674
"""
666675
Add the extend tri/rectangles on the outside of the axes.
676+
677+
ax is unused, but required due to the callbacks on xlim/ylim changed
667678
"""
679+
# Clean up any previous extend patches
680+
for patch in self._extend_patches:
681+
patch.remove()
682+
self._extend_patches = []
668683
# extend lengths are fraction of the *inner* part of colorbar,
669684
# not the total colorbar:
685+
_, extendlen = self._proportional_y()
670686
bot = 0 - (extendlen[0] if self._extend_lower() else 0)
671687
top = 1 + (extendlen[1] if self._extend_upper() else 0)
672688

@@ -708,12 +724,17 @@ def _do_extends(self, extendlen):
708724
if self.orientation == 'horizontal':
709725
xy = xy[:, ::-1]
710726
# add the patch
711-
color = self.cmap(self.norm(self._values[0]))
727+
val = -1 if self._long_axis().get_inverted() else 0
728+
color = self.cmap(self.norm(self._values[val]))
712729
patch = mpatches.PathPatch(
713730
mpath.Path(xy), facecolor=color, linewidth=0,
714731
antialiased=False, transform=self.ax.transAxes,
715-
hatch=hatches[0], clip_on=False)
732+
hatch=hatches[0], clip_on=False,
733+
# Place it right behind the standard patches, which is
734+
# needed if we updated the extends
735+
zorder=np.nextafter(self.ax.patch.zorder, -np.inf))
716736
self.ax.add_patch(patch)
737+
self._extend_patches.append(patch)
717738
if self._extend_upper():
718739
if not self.extendrect:
719740
# triangle
@@ -724,12 +745,17 @@ def _do_extends(self, extendlen):
724745
if self.orientation == 'horizontal':
725746
xy = xy[:, ::-1]
726747
# add the patch
727-
color = self.cmap(self.norm(self._values[-1]))
748+
val = 0 if self._long_axis().get_inverted() else -1
749+
color = self.cmap(self.norm(self._values[val]))
728750
patch = mpatches.PathPatch(
729751
mpath.Path(xy), facecolor=color,
730752
linewidth=0, antialiased=False,
731-
transform=self.ax.transAxes, hatch=hatches[-1], clip_on=False)
753+
transform=self.ax.transAxes, hatch=hatches[-1], clip_on=False,
754+
# Place it right behind the standard patches, which is
755+
# needed if we updated the extends
756+
zorder=np.nextafter(self.ax.patch.zorder, -np.inf))
732757
self.ax.add_patch(patch)
758+
self._extend_patches.append(patch)
733759
return
734760

735761
def add_lines(self, *args, **kwargs):
@@ -1049,6 +1075,9 @@ def remove(self):
10491075
self.mappable.callbacks.disconnect(self.mappable.colorbar_cid)
10501076
self.mappable.colorbar = None
10511077
self.mappable.colorbar_cid = None
1078+
# Remove the extension callbacks
1079+
self.ax.callbacks.disconnect(self._extend_cid1)
1080+
self.ax.callbacks.disconnect(self._extend_cid2)
10521081

10531082
try:
10541083
ax = self.mappable.axes
@@ -1151,7 +1180,7 @@ def _mesh(self):
11511180
These are scaled between vmin and vmax, and already handle colorbar
11521181
orientation.
11531182
"""
1154-
y, extendlen = self._proportional_y()
1183+
y, _ = self._proportional_y()
11551184
# Use the vmin and vmax of the colorbar, which may not be the same
11561185
# as the norm. There are situations where the colormap has a
11571186
# narrower range than the colorbar and we want to accommodate the
@@ -1172,9 +1201,9 @@ def _mesh(self):
11721201
self._y = y
11731202
X, Y = np.meshgrid([0., 1.], y)
11741203
if self.orientation == 'vertical':
1175-
return (X, Y, extendlen)
1204+
return (X, Y)
11761205
else:
1177-
return (Y, X, extendlen)
1206+
return (Y, X)
11781207

11791208
def _forward_boundaries(self, x):
11801209
# map boundaries equally between 0 and 1...
@@ -1322,11 +1351,13 @@ def _get_extension_lengths(self, frac, automin, automax, default=0.05):
13221351

13231352
def _extend_lower(self):
13241353
"""Return whether the lower limit is open ended."""
1325-
return self.extend in ('both', 'min')
1354+
minmax = "max" if self._long_axis().get_inverted() else "min"
1355+
return self.extend in ('both', minmax)
13261356

13271357
def _extend_upper(self):
13281358
"""Return whether the upper limit is open ended."""
1329-
return self.extend in ('both', 'max')
1359+
minmax = "min" if self._long_axis().get_inverted() else "max"
1360+
return self.extend in ('both', minmax)
13301361

13311362
def _long_axis(self):
13321363
"""Return the long axis"""

lib/matplotlib/tests/test_colorbar.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,30 @@ def test_colorbar_extension_length():
124124
_colorbar_extension_length('proportional')
125125

126126

127+
@pytest.mark.parametrize("orientation", ["horizontal", "vertical"])
128+
@pytest.mark.parametrize("extend,expected", [("min", (0, 0, 0, 1)),
129+
("max", (1, 1, 1, 1)),
130+
("both", (1, 1, 1, 1))])
131+
def test_colorbar_extension_inverted_axis(orientation, extend, expected):
132+
"""Test extension color with an inverted axis"""
133+
data = np.arange(12).reshape(3, 4)
134+
fig, ax = plt.subplots()
135+
cmap = plt.get_cmap("viridis").with_extremes(under=(0, 0, 0, 1),
136+
over=(1, 1, 1, 1))
137+
im = ax.imshow(data, cmap=cmap)
138+
cbar = fig.colorbar(im, orientation=orientation, extend=extend)
139+
if orientation == "horizontal":
140+
cbar.ax.invert_xaxis()
141+
else:
142+
cbar.ax.invert_yaxis()
143+
assert cbar._extend_patches[0].get_facecolor() == expected
144+
if extend == "both":
145+
assert len(cbar._extend_patches) == 2
146+
assert cbar._extend_patches[1].get_facecolor() == (0, 0, 0, 1)
147+
else:
148+
assert len(cbar._extend_patches) == 1
149+
150+
127151
@pytest.mark.parametrize('use_gridspec', [True, False])
128152
@image_comparison(['cbar_with_orientation',
129153
'cbar_locationing',

0 commit comments

Comments
 (0)