Skip to content

Commit 789656f

Browse files
committed
fix(layout): order strip before axis title; keep tick styling on spine offset
For strip_placement=outside the strip now clears only the axis ticks and labels, sitting before the axis title rather than beyond it; the title shifts out past the strip. The shared band is ordered ticks+labels, strip, title from the panel outward. The moved axis is shifted by repositioning its spine outward. This avoids Spine.set_position's axis.reset_ticks(), which regenerates the ticks and drops the theme's per-tick styling (tick label colour and font, blanked minor marks). Instead the existing tick marks are re-pointed at the moved spine transform; the labels are positioned by the layout.
1 parent 6f846cd commit 789656f

2 files changed

Lines changed: 56 additions & 24 deletions

File tree

plotnine/_mpl/layout_manager/_plot_layout_items.py

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,11 @@
2929
)
3030

3131
from matplotlib.axes import Axes
32-
from matplotlib.axis import Tick
32+
from matplotlib.axis import Axis, Tick
3333
from matplotlib.figure import Figure
3434
from matplotlib.lines import Line2D
3535
from matplotlib.patches import Rectangle
36+
from matplotlib.spines import Spine
3637
from matplotlib.transforms import Bbox, Transform
3738

3839
from plotnine import ggplot
@@ -508,7 +509,7 @@ def _move_artists(self, spaces: PlotSideSpaces):
508509

509510
if self.axis_title_x_top:
510511
ha = theme.getp(("axis_title_x_top", "ha"), "center")
511-
offset = spaces.t.strip_band_offset("axis")
512+
offset = spaces.t.strip_band_offset("title")
512513
self.axis_title_x_top.set_y(spaces.t.y1("axis_title_x") + offset)
513514
justify.horizontally_about(self.axis_title_x_top, ha, "panel")
514515

@@ -519,7 +520,7 @@ def _move_artists(self, spaces: PlotSideSpaces):
519520

520521
if self.axis_title_y_right:
521522
va = theme.getp(("axis_title_y_right", "va"), "center")
522-
offset = spaces.r.strip_band_offset("axis")
523+
offset = spaces.r.strip_band_offset("title")
523524
self.axis_title_y_right.set_x(spaces.r.x1("axis_title_y") + offset)
524525
justify.vertically_about(self.axis_title_y_right, va, "panel")
525526

@@ -662,9 +663,11 @@ def _place_moved_axes(self, spaces: PlotSideSpaces):
662663
right = spaces.r.strip_band_offset("axis") * fig.bbox.width * to_points
663664
for ax in self.plot.axs:
664665
if top:
665-
ax.spines["top"].set_position(("outward", top))
666+
_spine_set_position_outward(ax.spines["top"], ax.xaxis, top)
666667
if right:
667-
ax.spines["right"].set_position(("outward", right))
668+
_spine_set_position_outward(
669+
ax.spines["right"], ax.yaxis, right
670+
)
668671

669672
def _strip_breadth_scales(
670673
self, group: list[StripText], breadth: Literal["height", "width"]
@@ -788,6 +791,26 @@ def _place_strip_text(self, st: StripText):
788791
st.set_position((x, y))
789792

790793

794+
def _spine_set_position_outward(spine: Spine, axis: Axis, distance: float):
795+
"""
796+
Move a spine and its tick marks outward, keeping the theme's tick styling
797+
798+
This mirrors `Spine.set_position(("outward", distance))` but skips its
799+
`axis.reset_ticks()`, which would regenerate the ticks and drop the
800+
per-tick styling the theme applied (tick label colour and font, blanked
801+
minor tick marks, ...). The reset is avoided by re-pointing the existing
802+
tick marks at the moved spine's transform; the tick labels are
803+
positioned separately by the layout.
804+
"""
805+
spine._position = ("outward", distance) # pyright: ignore[reportAttributeAccessIssue]
806+
transform = spine.get_spine_transform()
807+
spine.set_transform(transform)
808+
# The outward-facing marks on a top or right spine are tick2.
809+
for tick in (*axis.get_major_ticks(), *axis.get_minor_ticks()):
810+
tick.tick2line.set_transform(transform)
811+
spine.stale = True
812+
813+
791814
def _text_is_visible(text: Text) -> bool:
792815
"""
793816
Return True if text is visible and is not empty

plotnine/_mpl/layout_manager/_plot_side_space.py

Lines changed: 28 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -152,34 +152,43 @@ def _strip_band_extent(self) -> float:
152152
return 0
153153

154154
@property
155-
def _axis_group_extent(self) -> float:
155+
def _axis_primary_extent(self) -> float:
156156
"""
157-
Outward extent of a moved axis on this side, figure space
157+
Outward extent of a moved axis's ticks and tick labels, figure space
158158
159-
The block is the axis ticks, text and title together. Zero on
160-
sides whose axis is in its default position.
159+
This is the part of a moved axis that sits next to the panel; the
160+
axis title is excluded. Zero on sides whose axis is in its default
161+
position.
161162
"""
162163
return 0
163164

164-
def strip_band_offset(self, member: Literal["strip", "axis"]) -> float:
165+
def strip_band_offset(
166+
self, member: Literal["strip", "axis", "title"]
167+
) -> float:
165168
"""
166169
Outward offset for one member of a shared strip/axis band
167170
168-
When a moved axis and a facet strip occupy the same side, one of
169-
them shifts outward so they do not overlap. `strip_placement`
170-
decides which: `"inside"` keeps the strip against the panel and
171-
shifts the axis; `"outside"` keeps the axis against the panel and
172-
shifts the strip. The offset is zero when the side has no such
173-
collision.
171+
When a moved axis and a facet strip occupy the same side, the band
172+
is ordered, from the panel outward, as the axis ticks and labels,
173+
the strip, and the axis title. `strip_placement` decides whether
174+
the strip comes before or after the ticks and labels:
175+
176+
- `"inside"`: panel, strip, ticks and labels, title.
177+
- `"outside"`: panel, ticks and labels, strip, title.
178+
179+
`member` is `"strip"`, `"axis"` (the ticks and labels) or
180+
`"title"`. The offset is zero when the side has no such collision.
174181
"""
175182
strip = self._strip_band_extent
176-
axis = self._axis_group_extent
177-
if not (strip and axis):
183+
primary = self._axis_primary_extent
184+
if not (strip and primary):
178185
return 0
179186
placement = self.items.plot.theme.getp("strip_placement")
180187
if placement == "inside":
181-
return strip if member == "axis" else 0
182-
return axis if member == "strip" else 0
188+
return 0 if member == "strip" else strip
189+
if member == "axis":
190+
return 0
191+
return primary if member == "strip" else strip
183192

184193

185194
class left_space(_plot_side_space):
@@ -427,8 +436,8 @@ def _strip_band_extent(self) -> float:
427436
return self.strip_text_y_extra_width
428437

429438
@property
430-
def _axis_group_extent(self) -> float:
431-
return self.sum_incl("axis_ticks_y") - self.sum_upto("axis_title_y")
439+
def _axis_primary_extent(self) -> float:
440+
return self.sum_incl("axis_ticks_y") - self.sum_upto("axis_text_y")
432441

433442
@property
434443
def offset(self):
@@ -583,8 +592,8 @@ def _strip_band_extent(self) -> float:
583592
return self.strip_text_x_extra_height
584593

585594
@property
586-
def _axis_group_extent(self) -> float:
587-
return self.sum_incl("axis_ticks_x") - self.sum_upto("axis_title_x")
595+
def _axis_primary_extent(self) -> float:
596+
return self.sum_incl("axis_ticks_x") - self.sum_upto("axis_text_x")
588597

589598
@property
590599
def offset(self) -> float:

0 commit comments

Comments
 (0)