Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions doc/_quartodoc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -520,6 +520,9 @@ quartodoc:
- strip_background_x
- strip_background_y
- strip_placement
- strip_switch_pad
- strip_switch_pad_x
- strip_switch_pad_y
- strip_text
- strip_text_x
- strip_text_x_bottom
Expand Down
159 changes: 99 additions & 60 deletions plotnine/_mpl/layout_manager/_plot_side_space.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
from functools import cached_property
from typing import TYPE_CHECKING, Literal

from plotnine._utils import MARGIN_SIDE
from plotnine.exceptions import PlotnineError
from plotnine.facets import facet_grid, facet_null, facet_wrap

Expand Down Expand Up @@ -152,6 +151,26 @@ def _axis_ticks_and_text(self) -> float:
"""
return self.sum_incl("axis_ticks") - self.sum_upto("axis_text")

def _strip_switch_pad(self, axis: Literal["x", "y"]) -> float:
"""
Reserved gap between a shared axis and a strip placed beyond it

The gap is non-zero only for `strip_placement="outside"` on a
side that carries both a strip and an axis; otherwise zero. The
themeable value is in points and is returned as a fraction of
the relevant figure dimension (height for x strips, width for y).
"""
theme = self.items.plot.theme
if theme.getp("strip_placement") != "outside":
return 0
strip_breadth: float = self.strip_text # pyright: ignore[reportAttributeAccessIssue]
if not (strip_breadth and self._axis_ticks_and_text):
return 0
pad_pt = theme.getp(f"strip_switch_pad_{axis}") or 0
W, H = theme.getp("figure_size")
dim = H if axis == "x" else W
return (pad_pt / 72) / dim

def strip_band_offset(self, member: Literal["strip", "axis"]) -> float:
"""
Outward offset for one member of a shared strip/axis band
Expand All @@ -176,8 +195,9 @@ def strip_band_offset(self, member: Literal["strip", "axis"]) -> float:
placement = self.items.plot.theme.getp("strip_placement")
if placement == "inside":
return 0 if member == "strip" else strip_breadth
# "outside"
return axis_breadth if member == "strip" else 0
# "outside": the strip clears the axis, plus the switch pad
pad: float = self.strip_switch_pad # pyright: ignore[reportAttributeAccessIssue]
return axis_breadth + pad if member == "strip" else 0


class left_space(_plot_side_space):
Expand Down Expand Up @@ -234,9 +254,11 @@ class left_space(_plot_side_space):
"""
legend: float = 0
legend_box_spacing: float = 0
axis_title_margin_left: float = 0
"""Outer (edge-facing) margin of the y-axis title"""
axis_title: float = 0
axis_title_margin: float = 0
"""Margin to the right of the y-axis title (panel-facing side)"""
axis_title_margin_right: float = 0
"""Panel-facing margin of the y-axis title"""
axis_title_alignment: float = 0
"""
Space added to align the axis title with others in a composition
Expand All @@ -245,10 +267,14 @@ class left_space(_plot_side_space):
the difference between the largest and smallest axis_title_clearance
among the items in the composition.
"""
axis_text_margin_left: float = 0
"""Outer (edge-facing) margin of the y-axis text"""
axis_text: float = 0
axis_text_margin: float = 0
"""Margin to the right of the y-axis text (panel-facing side)"""
axis_text_margin_right: float = 0
"""Panel-facing margin of the y-axis text"""
axis_ticks: float = 0
strip_switch_pad: float = 0
"""Gap between a shared axis and a strip beyond it (outside placement)"""
strip_text: float = 0
"""Outward extent of a left facet strip"""

Expand All @@ -269,24 +295,23 @@ def _calculate(self):
self.legend = self.legend_width
self.legend_box_spacing = theme.getp("legend_box_spacing")

# The text<->panel gap is the right margin of the y text/title; it
# sits on the panel-facing (right) side of the left axis.
# A left y-axis reads its left (outer) and right (panel-facing)
# margins directly.
if items.axis_title_y_left:
self.axis_title = geometry.width(items.axis_title_y_left)
self.axis_title_margin = getattr(
theme.get_margin("axis_title_y_left").fig,
MARGIN_SIDE["left"],
)
m = theme.get_margin("axis_title_y_left").fig
self.axis_title_margin_left = m.l
self.axis_title_margin_right = m.r

self.axis_text = items.axis_text_y_left
if self.axis_text:
self.axis_text_margin = getattr(
theme.get_margin("axis_text_y_left").fig,
MARGIN_SIDE["left"],
)
m = theme.get_margin("axis_text_y_left").fig
self.axis_text_margin_left = m.l
self.axis_text_margin_right = m.r

self.axis_ticks = items.axis_ticks_y_left
self.strip_text = items.strip_text_y("left")
self.strip_switch_pad = self._strip_switch_pad("y")

# Adjust plot_margin to make room for ylabels that protude well
# beyond the axes
Expand Down Expand Up @@ -372,14 +397,20 @@ class right_space(_plot_side_space):
margin_alignment: float = 0
legend: float = 0
legend_box_spacing: float = 0
axis_title_margin_right: float = 0
"""Outer (edge-facing) margin of the y-axis title"""
axis_title: float = 0
axis_title_margin: float = 0
"""Margin to the left of the y-axis title (panel-facing side)"""
axis_title_margin_left: float = 0
"""Panel-facing margin of the y-axis title"""
axis_title_alignment: float = 0
axis_text_margin_right: float = 0
"""Outer (edge-facing) margin of the y-axis text"""
axis_text: float = 0
axis_text_margin: float = 0
"""Margin to the left of the y-axis text (panel-facing side)"""
axis_text_margin_left: float = 0
"""Panel-facing margin of the y-axis text"""
axis_ticks: float = 0
strip_switch_pad: float = 0
"""Gap between a shared axis and a strip beyond it (outside placement)"""
strip_text: float = 0
"""Outward extent of a right facet strip (next to the panel by default)"""

Expand All @@ -402,23 +433,21 @@ def _calculate(self):

self.strip_text = items.strip_text_y("right")

# Space consumed by a y-axis on the right. The text<->panel gap is the
# left margin of the y text/title (the edge facing the panel to the
# left).
# A right y-axis reads its right (outer) and left (panel-facing)
# margins directly.
if items.axis_title_y_right:
self.axis_title = geometry.width(items.axis_title_y_right)
self.axis_title_margin = getattr(
theme.get_margin("axis_title_y_right").fig,
MARGIN_SIDE["right"],
)
m = theme.get_margin("axis_title_y_right").fig
self.axis_title_margin_right = m.r
self.axis_title_margin_left = m.l

self.axis_text = items.axis_text_y_right
if self.axis_text:
self.axis_text_margin = getattr(
theme.get_margin("axis_text_y_right").fig,
MARGIN_SIDE["right"],
)
m = theme.get_margin("axis_text_y_right").fig
self.axis_text_margin_right = m.r
self.axis_text_margin_left = m.l
self.axis_ticks = items.axis_ticks_y_right
self.strip_switch_pad = self._strip_switch_pad("y")

# Adjust plot_margin to make room for ylabels that protude well
# beyond the axes
Expand Down Expand Up @@ -520,14 +549,20 @@ class top_space(_plot_side_space):
plot_subtitle_margin_bottom: float = 0
legend: float = 0
legend_box_spacing: float = 0
axis_title_margin_top: float = 0
"""Outer (edge-facing) margin of the x-axis title"""
axis_title: float = 0
axis_title_margin: float = 0
"""Margin below the x-axis title (panel-facing side)"""
axis_title_margin_bottom: float = 0
"""Panel-facing margin of the x-axis title"""
axis_title_alignment: float = 0
axis_text_margin_top: float = 0
"""Outer (edge-facing) margin of the x-axis text"""
axis_text: float = 0
axis_text_margin: float = 0
"""Margin below the x-axis text (panel-facing side)"""
axis_text_margin_bottom: float = 0
"""Panel-facing margin of the x-axis text"""
axis_ticks: float = 0
strip_switch_pad: float = 0
"""Gap between a shared axis and a strip beyond it (outside placement)"""
strip_text: float = 0
"""Outward extent of a top facet strip (next to the panel by default)"""

Expand Down Expand Up @@ -564,22 +599,21 @@ def _calculate(self):

self.strip_text = items.strip_text_x("top")

# Space consumed by an x-axis on the top. The text<->panel gap is the
# bottom margin of the x text/title (the edge facing the panel below).
# A top x-axis reads its top (outer) and bottom (panel-facing)
# margins directly.
if items.axis_title_x_top:
self.axis_title = geometry.height(items.axis_title_x_top)
self.axis_title_margin = getattr(
theme.get_margin("axis_title_x_top").fig,
MARGIN_SIDE["top"],
)
m = theme.get_margin("axis_title_x_top").fig
self.axis_title_margin_top = m.t
self.axis_title_margin_bottom = m.b

self.axis_text = items.axis_text_x_top
if self.axis_text:
self.axis_text_margin = getattr(
theme.get_margin("axis_text_x_top").fig,
MARGIN_SIDE["top"],
)
m = theme.get_margin("axis_text_x_top").fig
self.axis_text_margin_top = m.t
self.axis_text_margin_bottom = m.b
self.axis_ticks = items.axis_ticks_x_top
self.strip_switch_pad = self._strip_switch_pad("x")

# Adjust plot_margin to make room for ylabels that protude well
# beyond the axes
Expand Down Expand Up @@ -684,9 +718,11 @@ class bottom_space(_plot_side_space):
plot_caption_margin_top: float = 0
legend: float = 0
legend_box_spacing: float = 0
axis_title_margin_bottom: float = 0
"""Outer (edge-facing) margin of the x-axis title"""
axis_title: float = 0
axis_title_margin: float = 0
"""Margin above the x-axis title (panel-facing side)"""
axis_title_margin_top: float = 0
"""Panel-facing margin of the x-axis title"""
axis_title_alignment: float = 0
"""
Space added to align the axis title with others in a composition
Expand All @@ -696,10 +732,14 @@ class bottom_space(_plot_side_space):
composition. It's amount is the difference in height between this axis
text (and it's margins) and the tallest axis text (and it's margin).
"""
axis_text_margin_bottom: float = 0
"""Outer (edge-facing) margin of the x-axis text"""
axis_text: float = 0
axis_text_margin: float = 0
"""Margin above the x-axis text (panel-facing side)"""
axis_text_margin_top: float = 0
"""Panel-facing margin of the x-axis text"""
axis_ticks: float = 0
strip_switch_pad: float = 0
"""Gap between a shared axis and a strip beyond it (outside placement)"""
strip_text: float = 0
"""Outward extent of a bottom facet strip"""

Expand Down Expand Up @@ -734,23 +774,22 @@ def _calculate(self):
self.legend = self.legend_height
self.legend_box_spacing = theme.getp("legend_box_spacing") * F

# The text<->panel gap is the top margin of the x text/title; it
# sits on the panel-facing (top) side of the bottom axis.
# A bottom x-axis reads its bottom (outer) and top (panel-facing)
# margins directly.
if items.axis_title_x_bottom:
self.axis_title = geometry.height(items.axis_title_x_bottom)
self.axis_title_margin = getattr(
theme.get_margin("axis_title_x_bottom").fig,
MARGIN_SIDE["bottom"],
)
m = theme.get_margin("axis_title_x_bottom").fig
self.axis_title_margin_bottom = m.b
self.axis_title_margin_top = m.t

self.axis_text = items.axis_text_x_bottom
if self.axis_text:
self.axis_text_margin = getattr(
theme.get_margin("axis_text_x_bottom").fig,
MARGIN_SIDE["bottom"],
)
m = theme.get_margin("axis_text_x_bottom").fig
self.axis_text_margin_bottom = m.b
self.axis_text_margin_top = m.t
self.axis_ticks = items.axis_ticks_x_bottom
self.strip_text = items.strip_text_x("bottom")
self.strip_switch_pad = self._strip_switch_pad("x")

# Adjust plot_margin to make room for ylabels that protude well
# beyond the axes
Expand Down
3 changes: 3 additions & 0 deletions plotnine/themes/theme.py
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,9 @@ def __init__(
strip_align_x=None,
strip_align_y=None,
strip_align=None,
strip_switch_pad_x=None,
strip_switch_pad_y=None,
strip_switch_pad=None,
strip_placement=None,
svg_usefonts=None,
**kwargs,
Expand Down
34 changes: 23 additions & 11 deletions plotnine/themes/theme_gray.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,25 +56,36 @@ def __init__(self, base_size=11, base_family=None):
axis_line_x=element_blank(),
axis_line_y=element_blank(),
axis_text=element_text(size=base_size * 0.8, color="#4D4D4D"),
axis_text_x=element_text(
va="top", margin=margin(t=fifth_line, b=fifth_line)
axis_text_x_bottom=element_text(
va="top", margin=margin(t=fifth_line)
),
axis_text_y=element_text(
ha="right", margin=margin(r=fifth_line, l=fifth_line)
axis_text_x_top=element_text(
va="bottom", margin=margin(b=fifth_line)
),
axis_text_y_left=element_text(
ha="right", margin=margin(r=fifth_line)
),
axis_text_y_right=element_text(
ha="left", margin=margin(l=fifth_line)
),
axis_ticks=element_line(color="#333333"),
axis_ticks_length=0,
axis_ticks_length_major=quarter_line,
axis_ticks_length_minor=eighth_line,
axis_ticks_minor=element_blank(),
axis_title_x=element_text(
va="bottom", ha="center", margin=margin(t=m, b=m, unit="fig")
axis_title_x=element_text(ha="center"),
axis_title_x_bottom=element_text(
va="bottom", margin=margin(t=m, unit="fig")
),
axis_title_y=element_text(
angle=90,
va="center",
ha="left",
margin=margin(r=m, l=m, unit="fig"),
axis_title_x_top=element_text(
va="bottom", margin=margin(b=m, unit="fig")
),
axis_title_y=element_text(angle=90, va="center"),
axis_title_y_left=element_text(
ha="left", margin=margin(r=m, unit="fig")
),
axis_title_y_right=element_text(
ha="left", margin=margin(l=m, unit="fig")
),
dpi=get_option("dpi"),
figure_size=get_option("figure_size"),
Expand Down Expand Up @@ -145,6 +156,7 @@ def __init__(self, base_size=11, base_family=None):
plot_tag_location="margin",
plot_tag_position="topleft",
strip_align=0,
strip_switch_pad=base_size / 4,
strip_placement="inside",
strip_background=element_rect(color="none", fill="#D9D9D9"),
strip_background_x=element_rect(width=1),
Expand Down
Loading
Loading