2222from matplotlib .backend_bases import RendererBase
2323from matplotlib .colors import Colormap , LogNorm , Normalize
2424from matplotlib .figure import Figure
25+ from matplotlib .transforms import Bbox
2526from mpl_toolkits .axes_grid1 import make_axes_locatable
2627from mpl_toolkits .axes_grid1 .axes_divider import AxesDivider
2728from mpl_toolkits .axes_grid1 .axes_size import Fixed
9192# so the colorbar keeps its relative pad (matching the historical placement) instead of an absolute one.
9293_CBAR_DECORATION_EPS_INCHES = 0.05
9394
95+ # Per colorbar location: the axis ("x"/"y") its ticks live on, and the opposite side.
96+ _CBAR_TICK_SIDE = {"left" : ("y" , "right" ), "right" : ("y" , "left" ), "top" : ("x" , "bottom" ), "bottom" : ("x" , "top" )}
97+
98+
99+ def _extent_beyond_box_inches (box : Bbox , tight : Bbox | None , dpi : float , side : str ) -> float :
100+ """Inches that ``tight`` (an artist's full extent incl. ticks/labels) sticks out past ``box`` on ``side``.
101+
102+ Returns ``0.0`` when there is no overhang, when ``tight`` is ``None`` (an axes with nothing
103+ drawable), or when ``dpi`` is non-positive. Used both to clear a panel's own decorations and to
104+ clear a stacked colorbar's labels before placing the next one.
105+ """
106+ if tight is None or dpi <= 0 :
107+ return 0.0
108+ if side == "left" :
109+ delta = box .x0 - tight .x0
110+ elif side == "right" :
111+ delta = tight .x1 - box .x1
112+ elif side == "bottom" :
113+ delta = box .y0 - tight .y0
114+ else : # top
115+ delta = tight .y1 - box .y1
116+ return max (0.0 , float (delta ) / dpi )
117+
94118
95119@register_spatial_data_accessor ("pl" )
96120class PlotAccessor :
@@ -1563,7 +1587,17 @@ def _draw_colorbar(
15631587 if location not in {"left" , "right" , "top" , "bottom" }:
15641588 location = CBAR_DEFAULT_LOCATION
15651589 orientation = "vertical" if location in {"right" , "left" } else "horizontal"
1566- cbar_kwargs .pop ("orientation" , None )
1590+ # Orientation is fixed by the location (the divider places a left/right colorbar
1591+ # vertically and a top/bottom one horizontally); warn rather than silently ignore a
1592+ # conflicting user-supplied orientation.
1593+ user_orientation = cbar_kwargs .pop ("orientation" , None )
1594+ if user_orientation is not None and user_orientation != orientation :
1595+ warnings .warn (
1596+ f"`orientation` is determined by the colorbar location ('{ location } ' -> '{ orientation } '); "
1597+ f"the requested '{ user_orientation } ' is ignored." ,
1598+ UserWarning ,
1599+ stacklevel = 2 ,
1600+ )
15671601
15681602 fraction = float (cast (float | int , layout .get ("fraction" , base_layout ["fraction" ])))
15691603 pad = float (cast (float | int , layout .get ("pad" , base_layout ["pad" ])))
@@ -1594,22 +1628,11 @@ def _draw_colorbar(
15941628 )
15951629 side_counts [location ] = n_on_side + 1
15961630 cb = fig .colorbar (spec .mappable , cax = cax , orientation = orientation , ** cbar_kwargs )
1597- if location == "left" :
1598- cb .ax .yaxis .set_ticks_position ("left" )
1599- cb .ax .yaxis .set_label_position ("left" )
1600- cb .ax .tick_params (labelleft = True , labelright = False )
1601- elif location == "top" :
1602- cb .ax .xaxis .set_ticks_position ("top" )
1603- cb .ax .xaxis .set_label_position ("top" )
1604- cb .ax .tick_params (labeltop = True , labelbottom = False )
1605- elif location == "right" :
1606- cb .ax .yaxis .set_ticks_position ("right" )
1607- cb .ax .yaxis .set_label_position ("right" )
1608- cb .ax .tick_params (labelright = True , labelleft = False )
1609- elif location == "bottom" :
1610- cb .ax .xaxis .set_ticks_position ("bottom" )
1611- cb .ax .xaxis .set_label_position ("bottom" )
1612- cb .ax .tick_params (labelbottom = True , labeltop = False )
1631+ tick_axis , opposite = _CBAR_TICK_SIDE [location ]
1632+ cb_axis = cb .ax .yaxis if tick_axis == "y" else cb .ax .xaxis
1633+ cb_axis .set_ticks_position (location )
1634+ cb_axis .set_label_position (location )
1635+ cb .ax .tick_params (** {f"label{ location } " : True , f"label{ opposite } " : False })
16131636
16141637 final_label = global_label_override or layer_label_override or spec .label
16151638 if final_label :
@@ -1618,20 +1641,14 @@ def _draw_colorbar(
16181641 with contextlib .suppress (Exception ):
16191642 cb .solids .set_alpha (spec .alpha )
16201643
1621- # Measure how far this colorbar's own ticks/labels/axis-label extend beyond its box on
1622- # the outer side, so the next stacked colorbar on this side can be padded clear of them.
1644+ # Measure how far this colorbar's ticks/labels/axis-label extend beyond its box, so the
1645+ # next stacked colorbar on this side clears them. The draw is kept unconditionally: it is
1646+ # also load-bearing for the layout engine (skipping it shifts wide colorbars), so it is not
1647+ # safe to skip even for the last colorbar on a side.
16231648 fig .canvas .draw ()
1624- cb_box = cax .get_window_extent (renderer )
1625- cb_tight = cax .get_tightbbox (renderer )
1626- if location == "right" :
1627- outer = (cb_tight .x1 - cb_box .x1 ) / dpi
1628- elif location == "left" :
1629- outer = (cb_box .x0 - cb_tight .x0 ) / dpi
1630- elif location == "top" :
1631- outer = (cb_tight .y1 - cb_box .y1 ) / dpi
1632- else : # bottom
1633- outer = (cb_box .y0 - cb_tight .y0 ) / dpi
1634- prev_outer [location ] = max (0.0 , outer )
1649+ prev_outer [location ] = _extent_beyond_box_inches (
1650+ cb .ax .get_window_extent (renderer ), cb .ax .get_tightbbox (renderer ), dpi , location
1651+ )
16351652
16361653 # go through tree
16371654
@@ -1837,12 +1854,7 @@ def _draw_colorbar(
18371854 # the axes box, not its decorations).
18381855 box = axis .get_window_extent (renderer )
18391856 tight = axis .get_tightbbox (renderer )
1840- clearance = {
1841- "left" : max (0.0 , (box .x0 - tight .x0 ) / dpi ),
1842- "right" : max (0.0 , (tight .x1 - box .x1 ) / dpi ),
1843- "bottom" : max (0.0 , (box .y0 - tight .y0 ) / dpi ),
1844- "top" : max (0.0 , (tight .y1 - box .y1 ) / dpi ),
1845- }
1857+ clearance = {side : _extent_beyond_box_inches (box , tight , dpi , side ) for side in _CBAR_TICK_SIDE }
18461858 axes_size_in = (box .width / dpi , box .height / dpi )
18471859 # One divider per panel so multiple colorbars on the same panel stack via the divider.
18481860 divider = make_axes_locatable (axis )
0 commit comments