@@ -1877,43 +1877,34 @@ def _draw_colorbar(
18771877 trackers_axes [location ] = pad_axes + (bbox_axes .width if vertical else bbox_axes .height )
18781878
18791879
1880- def _layout_panel_legends (ax : Axes , fig : Figure , gap : float = 0.01 ) -> None :
1880+ def _layout_panel_legends (ax : Axes , fig : Figure , gap : float = 0.02 ) -> None :
18811881 """Title and stack the per-render categorical legends (#364) in the right margin.
18821882
18831883 Only legends this code created (tagged ``_sdata_column``) are touched, so fill/outline and
1884- channel legends keep their own placement. Each legend is titled by its source column (matching
1885- colorbars). When 2+ legends share an axis they are stacked top-to-bottom — the right-margin
1886- convention — so wide legends grow the figure height on save instead of overflowing its right edge.
1884+ channel legends keep their own placement. Each legend is titled by the column passed to its
1885+ render call (an explicit title wins). A lone legend keeps scanpy's placement (just titled); 2+
1886+ are stacked top-to-bottom along the axes' right edge.
18871887 """
18881888 legends = [c for c in ax .get_children () if isinstance (c , Legend ) and hasattr (c , "_sdata_column" )]
18891889 if not legends :
18901890 return
1891- # Title each legend by its source column so it reads like the colorbars (an explicit title set
1892- # earlier stays as-is). A lone legend keeps scanpy's placement; only its title is added here.
18931891 for leg in legends :
18941892 if not leg .get_title ().get_text ():
18951893 leg .set_title (leg ._sdata_column )
18961894 if len (legends ) < 2 :
18971895 return
1898- # 2+ legends share the axis. Let constrained_layout settle, then freeze it: otherwise it shrinks
1899- # the axes to "make room" for the margin legends (squashing the plot, leaving a gap). Frozen, the
1900- # legends still count for `bbox_inches="tight"` on save.
1901- fig .canvas .draw ()
1902- fig .set_layout_engine ("none" )
1903- invf = fig .transFigure .inverted ()
1904- ax_bb = ax .get_window_extent ().transformed (invf )
1905- left , top = ax_bb .x1 + gap , ax_bb .y1
1906- # Anchor all at one point and settle, so heights are measured in a single consistent layout state.
1896+ # Anchor in axes-fraction so the legends stay glued to the axes' right edge: when
1897+ # constrained_layout reserves margin space and rescales the axes, axes-anchored legends move with
1898+ # it (figure-fraction anchoring would leave a gap and squash the plot). Each is placed just below
1899+ # the previous one's measured bottom.
1900+ inv = ax .transAxes .inverted ()
1901+ y = 1.0
19071902 for leg in legends :
1908- leg .set_bbox_to_anchor ((left , top ), transform = fig . transFigure )
1903+ leg .set_bbox_to_anchor ((1.02 , y ), transform = ax . transAxes )
19091904 if hasattr (leg , "set_loc" ):
19101905 leg .set_loc ("upper left" )
1911- fig .canvas .draw ()
1912- heights = [leg .get_window_extent ().transformed (invf ).height for leg in legends ]
1913- y = top
1914- for leg , h in zip (legends , heights , strict = True ):
1915- leg .set_bbox_to_anchor ((left , y ), transform = fig .transFigure )
1916- y -= h + gap
1906+ fig .canvas .draw ()
1907+ y = leg .get_window_extent ().transformed (inv ).y0 - gap
19171908
19181909
19191910def _layout_pending_colorbars (
0 commit comments