Skip to content

Commit 1433a95

Browse files
FBumannclaude
andauthored
feat(plotting): add facet_titles kwarg to strip dim= prefix (#77)
Plotly Express renders faceted subplot titles as "<dim>=<value>" (e.g. "country=Brazil"). The new `facet_titles` keyword on every plotting/accessor method ("default" or "value") lets callers strip the prefix without a separate post-processing step. Default is "default" — no behavior change for existing users. Also adds a public `simplify_facet_titles(fig, mode)` helper for use on figures from any source (overlay/add_secondary_y outputs, raw PX figures, etc.). Both share the same Literal type alias `FacetTitlesMode` exported from `common`. Only annotations whose text starts with a Python-identifier prefix followed by `=` are touched, so user-added annotations are preserved. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent c3043e7 commit 1433a95

6 files changed

Lines changed: 183 additions & 11 deletions

File tree

tests/test_figures.py

Lines changed: 68 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,13 @@
99
import pytest
1010
import xarray as xr
1111

12-
from xarray_plotly import add_secondary_y, overlay, subplots, xpx
12+
from xarray_plotly import (
13+
add_secondary_y,
14+
overlay,
15+
simplify_facet_titles,
16+
subplots,
17+
xpx,
18+
)
1319

1420

1521
class TestOverlayBasic:
@@ -922,3 +928,64 @@ def test_source_not_modified(self) -> None:
922928
original_count = len(fig.data)
923929
_ = subplots(fig, fig, cols=2)
924930
assert len(fig.data) == original_count
931+
932+
933+
class TestSimplifyFacetTitles:
934+
"""Tests for the simplify_facet_titles helper and the `facet_titles` kwarg."""
935+
936+
@pytest.fixture(autouse=True)
937+
def setup(self) -> None:
938+
self.da = xr.DataArray(
939+
np.random.rand(10, 3),
940+
dims=["x", "country"],
941+
coords={"country": ["United States", "China", "Brazil"]},
942+
name="value",
943+
)
944+
945+
def test_helper_strips_dim_prefix(self) -> None:
946+
fig = xpx(self.da).line(facet_col="country")
947+
# PX writes annotations like "country=United States"
948+
original_texts = [a.text for a in fig.layout.annotations]
949+
assert any(t and t.startswith("country=") for t in original_texts)
950+
951+
simplify_facet_titles(fig)
952+
953+
for ann in fig.layout.annotations:
954+
if ann.text:
955+
assert "=" not in ann.text or ann.text.split("=", 1)[0] != "country"
956+
957+
def test_helper_full_is_noop(self) -> None:
958+
fig = xpx(self.da).line(facet_col="country")
959+
before = [a.text for a in fig.layout.annotations]
960+
simplify_facet_titles(fig, mode="default")
961+
after = [a.text for a in fig.layout.annotations]
962+
assert before == after
963+
964+
def test_helper_invalid_mode_raises(self) -> None:
965+
fig = xpx(self.da).line(facet_col="country")
966+
with pytest.raises(ValueError, match="facet_titles must be"):
967+
simplify_facet_titles(fig, mode="bogus") # type: ignore[arg-type]
968+
969+
def test_helper_leaves_user_annotations_alone(self) -> None:
970+
"""User-added annotations without a Python-identifier prefix are preserved."""
971+
fig = xpx(self.da).line(facet_col="country")
972+
fig.add_annotation(text="Some note", x=0, y=0, showarrow=False)
973+
simplify_facet_titles(fig)
974+
texts = [a.text for a in fig.layout.annotations]
975+
assert "Some note" in texts
976+
977+
def test_kwarg_default_keeps_px_format(self) -> None:
978+
fig = xpx(self.da).line(facet_col="country")
979+
# At least one annotation still carries the dim= prefix.
980+
assert any(a.text and a.text.startswith("country=") for a in fig.layout.annotations)
981+
982+
def test_kwarg_value_strips_prefix(self) -> None:
983+
fig = xpx(self.da).line(facet_col="country", facet_titles="value")
984+
for ann in fig.layout.annotations:
985+
if ann.text:
986+
# Should not start with "country="; the dim prefix is stripped.
987+
assert not ann.text.startswith("country=")
988+
989+
def test_kwarg_invalid_raises(self) -> None:
990+
with pytest.raises(ValueError, match="facet_titles must be"):
991+
xpx(self.da).line(facet_col="country", facet_titles="bogus") # type: ignore[arg-type]

xarray_plotly/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@
5656
from xarray_plotly.figures import (
5757
add_secondary_y,
5858
overlay,
59+
simplify_facet_titles,
5960
subplots,
6061
update_traces,
6162
)
@@ -67,6 +68,7 @@
6768
"auto",
6869
"config",
6970
"overlay",
71+
"simplify_facet_titles",
7072
"subplots",
7173
"update_traces",
7274
"xpx",

xarray_plotly/accessor.py

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from xarray import DataArray, Dataset
77

88
from xarray_plotly import plotting
9-
from xarray_plotly.common import Colors, SlotValue, auto
9+
from xarray_plotly.common import Colors, FacetTitlesMode, SlotValue, auto
1010
from xarray_plotly.config import _options
1111

1212

@@ -54,6 +54,7 @@ def line(
5454
facet_row: SlotValue = auto,
5555
animation_frame: SlotValue = auto,
5656
colors: Colors = None,
57+
facet_titles: FacetTitlesMode = "default",
5758
**px_kwargs: Any,
5859
) -> go.Figure:
5960
"""Create an interactive line plot.
@@ -84,6 +85,7 @@ def line(
8485
facet_row=facet_row,
8586
animation_frame=animation_frame,
8687
colors=colors,
88+
facet_titles=facet_titles,
8789
**px_kwargs,
8890
)
8991

@@ -97,6 +99,7 @@ def bar(
9799
facet_row: SlotValue = auto,
98100
animation_frame: SlotValue = auto,
99101
colors: Colors = None,
102+
facet_titles: FacetTitlesMode = "default",
100103
**px_kwargs: Any,
101104
) -> go.Figure:
102105
"""Create an interactive bar chart.
@@ -125,6 +128,7 @@ def bar(
125128
facet_row=facet_row,
126129
animation_frame=animation_frame,
127130
colors=colors,
131+
facet_titles=facet_titles,
128132
**px_kwargs,
129133
)
130134

@@ -138,6 +142,7 @@ def area(
138142
facet_row: SlotValue = auto,
139143
animation_frame: SlotValue = auto,
140144
colors: Colors = None,
145+
facet_titles: FacetTitlesMode = "default",
141146
**px_kwargs: Any,
142147
) -> go.Figure:
143148
"""Create an interactive stacked area chart.
@@ -166,6 +171,7 @@ def area(
166171
facet_row=facet_row,
167172
animation_frame=animation_frame,
168173
colors=colors,
174+
facet_titles=facet_titles,
169175
**px_kwargs,
170176
)
171177

@@ -178,6 +184,7 @@ def fast_bar(
178184
facet_row: SlotValue = auto,
179185
animation_frame: SlotValue = auto,
180186
colors: Colors = None,
187+
facet_titles: FacetTitlesMode = "default",
181188
**px_kwargs: Any,
182189
) -> go.Figure:
183190
"""Create a bar-like chart using stacked areas for better performance.
@@ -204,6 +211,7 @@ def fast_bar(
204211
facet_row=facet_row,
205212
animation_frame=animation_frame,
206213
colors=colors,
214+
facet_titles=facet_titles,
207215
**px_kwargs,
208216
)
209217

@@ -218,6 +226,7 @@ def scatter(
218226
facet_row: SlotValue = auto,
219227
animation_frame: SlotValue = auto,
220228
colors: Colors = None,
229+
facet_titles: FacetTitlesMode = "default",
221230
**px_kwargs: Any,
222231
) -> go.Figure:
223232
"""Create an interactive scatter plot.
@@ -252,6 +261,7 @@ def scatter(
252261
facet_row=facet_row,
253262
animation_frame=animation_frame,
254263
colors=colors,
264+
facet_titles=facet_titles,
255265
**px_kwargs,
256266
)
257267

@@ -264,6 +274,7 @@ def box(
264274
facet_row: SlotValue = None,
265275
animation_frame: SlotValue = None,
266276
colors: Colors = None,
277+
facet_titles: FacetTitlesMode = "default",
267278
**px_kwargs: Any,
268279
) -> go.Figure:
269280
"""Create an interactive box plot.
@@ -293,6 +304,7 @@ def box(
293304
facet_row=facet_row,
294305
animation_frame=animation_frame,
295306
colors=colors,
307+
facet_titles=facet_titles,
296308
**px_kwargs,
297309
)
298310

@@ -306,6 +318,7 @@ def imshow(
306318
animation_frame: SlotValue = auto,
307319
robust: bool = False,
308320
colors: Colors = None,
321+
facet_titles: FacetTitlesMode = "default",
309322
**px_kwargs: Any,
310323
) -> go.Figure:
311324
"""Create an interactive heatmap image.
@@ -342,6 +355,7 @@ def imshow(
342355
animation_frame=animation_frame,
343356
robust=robust,
344357
colors=colors,
358+
facet_titles=facet_titles,
345359
**px_kwargs,
346360
)
347361

@@ -353,6 +367,7 @@ def pie(
353367
facet_col: SlotValue = auto,
354368
facet_row: SlotValue = auto,
355369
colors: Colors = None,
370+
facet_titles: FacetTitlesMode = "default",
356371
**px_kwargs: Any,
357372
) -> go.Figure:
358373
"""Create an interactive pie chart.
@@ -377,6 +392,7 @@ def pie(
377392
facet_col=facet_col,
378393
facet_row=facet_row,
379394
colors=colors,
395+
facet_titles=facet_titles,
380396
**px_kwargs,
381397
)
382398

@@ -457,6 +473,7 @@ def line(
457473
facet_row: SlotValue = auto,
458474
animation_frame: SlotValue = auto,
459475
colors: Colors = None,
476+
facet_titles: FacetTitlesMode = "default",
460477
**px_kwargs: Any,
461478
) -> go.Figure:
462479
"""Create an interactive line plot.
@@ -487,6 +504,7 @@ def line(
487504
facet_row=facet_row,
488505
animation_frame=animation_frame,
489506
colors=colors,
507+
facet_titles=facet_titles,
490508
**px_kwargs,
491509
)
492510

@@ -501,6 +519,7 @@ def bar(
501519
facet_row: SlotValue = auto,
502520
animation_frame: SlotValue = auto,
503521
colors: Colors = None,
522+
facet_titles: FacetTitlesMode = "default",
504523
**px_kwargs: Any,
505524
) -> go.Figure:
506525
"""Create an interactive bar chart.
@@ -529,6 +548,7 @@ def bar(
529548
facet_row=facet_row,
530549
animation_frame=animation_frame,
531550
colors=colors,
551+
facet_titles=facet_titles,
532552
**px_kwargs,
533553
)
534554

@@ -543,6 +563,7 @@ def area(
543563
facet_row: SlotValue = auto,
544564
animation_frame: SlotValue = auto,
545565
colors: Colors = None,
566+
facet_titles: FacetTitlesMode = "default",
546567
**px_kwargs: Any,
547568
) -> go.Figure:
548569
"""Create an interactive stacked area chart.
@@ -571,6 +592,7 @@ def area(
571592
facet_row=facet_row,
572593
animation_frame=animation_frame,
573594
colors=colors,
595+
facet_titles=facet_titles,
574596
**px_kwargs,
575597
)
576598

@@ -584,6 +606,7 @@ def fast_bar(
584606
facet_row: SlotValue = auto,
585607
animation_frame: SlotValue = auto,
586608
colors: Colors = None,
609+
facet_titles: FacetTitlesMode = "default",
587610
**px_kwargs: Any,
588611
) -> go.Figure:
589612
"""Create a bar-like chart using stacked areas for better performance.
@@ -610,6 +633,7 @@ def fast_bar(
610633
facet_row=facet_row,
611634
animation_frame=animation_frame,
612635
colors=colors,
636+
facet_titles=facet_titles,
613637
**px_kwargs,
614638
)
615639

@@ -625,6 +649,7 @@ def scatter(
625649
facet_row: SlotValue = auto,
626650
animation_frame: SlotValue = auto,
627651
colors: Colors = None,
652+
facet_titles: FacetTitlesMode = "default",
628653
**px_kwargs: Any,
629654
) -> go.Figure:
630655
"""Create an interactive scatter plot.
@@ -655,6 +680,7 @@ def scatter(
655680
facet_row=facet_row,
656681
animation_frame=animation_frame,
657682
colors=colors,
683+
facet_titles=facet_titles,
658684
**px_kwargs,
659685
)
660686

@@ -668,6 +694,7 @@ def box(
668694
facet_row: SlotValue = None,
669695
animation_frame: SlotValue = None,
670696
colors: Colors = None,
697+
facet_titles: FacetTitlesMode = "default",
671698
**px_kwargs: Any,
672699
) -> go.Figure:
673700
"""Create an interactive box plot.
@@ -694,6 +721,7 @@ def box(
694721
facet_row=facet_row,
695722
animation_frame=animation_frame,
696723
colors=colors,
724+
facet_titles=facet_titles,
697725
**px_kwargs,
698726
)
699727

@@ -706,6 +734,7 @@ def pie(
706734
facet_col: SlotValue = auto,
707735
facet_row: SlotValue = auto,
708736
colors: Colors = None,
737+
facet_titles: FacetTitlesMode = "default",
709738
**px_kwargs: Any,
710739
) -> go.Figure:
711740
"""Create an interactive pie chart.
@@ -730,5 +759,6 @@ def pie(
730759
facet_col=facet_col,
731760
facet_row=facet_row,
732761
colors=colors,
762+
facet_titles=facet_titles,
733763
**px_kwargs,
734764
)

xarray_plotly/common.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import functools
66
import warnings
77
from collections.abc import Hashable, Mapping, Sequence
8-
from typing import TYPE_CHECKING, Any
8+
from typing import TYPE_CHECKING, Any, Literal
99

1010
import plotly.express as px
1111

@@ -39,6 +39,13 @@ def __repr__(self) -> str:
3939
- None: Use Plotly defaults
4040
"""
4141

42+
FacetTitlesMode = Literal["value", "default"]
43+
"""Type alias for facet_titles parameter.
44+
45+
- "default" (default): keep PX's ``"<dim>=<value>"`` subplot titles.
46+
- "value": strip the ``<dim>=`` prefix, leaving just the value.
47+
"""
48+
4249
# Re-export for backward compatibility
4350
SLOT_ORDERS = DEFAULT_SLOT_ORDERS
4451
"""Slot orders per plot type.

0 commit comments

Comments
 (0)