Skip to content

Commit 28fccc3

Browse files
FBumannclaude
andauthored
fix: legend, style persistence, and axis ranges in combined figures (#53)
* fix: legend visibility, style persistence, and axis ranges in combined figures Combined figures (overlay, add_secondary_y) had three issues: 1. Legends disappeared because Plotly Express sets showlegend=False on single-trace figures. Now unnamed traces get names derived from the source figure's y-axis title, and showlegend is fixed per legendgroup. 2. Colors and styles were lost during animation because frame traces carried PX defaults. Now marker, line, opacity and legend properties are propagated from fig.data into all animation frame traces. 3. Axis ranges were computed from fig.data only, so frames with different data ranges went off-screen during animation. Now global min/max is computed across all frames and set explicitly on the layout. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: add subplots() for composing figures into a grid New function to arrange independent figures in a subplot grid: grid = subplots(fig1, fig2, fig3, cols=2) - Subplot titles auto-derived from figure title or y-axis label - Axis config (titles, tick format, type) copied from source figures - Validates: rejects faceted or animated figures (not supported) - Empty cells via go.Figure() Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * docs: add subplots examples to combining notebook Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: support faceted figures in subplots() Rewrote subplots() to use manual axis domain management instead of make_subplots. Each figure's internal axes are remapped with scaled domains to fit within the grid cell, so faceted figures now work. Updated notebook with faceted subplots example. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: review findings — datetime axis, bar baseline, type narrowing, stale bullet - Skip datetime64/timedelta64 axes in _fix_animation_axis_ranges to prevent float epoch corruption; leave them on autorange - Include zero in axis range for bar traces so bars grow from baseline - Use isinstance(str) check in _get_figure_title for mypy type narrowing - Remove stale slider_to_dropdown bullet from notebook intro Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: disambiguation condition and stale comment - Only disambiguate labels when all are non-empty and identical, preventing spurious suffixes when mixing named and unnamed figures - Update stale make_subplots comment in test Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: unused variable lint error in test Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent fbca665 commit 28fccc3

File tree

4 files changed

+926
-37
lines changed

4 files changed

+926
-37
lines changed

docs/examples/combining.ipynb

Lines changed: 140 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@
1010
"xarray-plotly provides helper functions to combine multiple figures:\n",
1111
"\n",
1212
"- **`overlay`**: Overlay traces on the same axes\n",
13-
"- **`add_secondary_y`**: Plot with two independent y-axes"
13+
"- **`add_secondary_y`**: Plot with two independent y-axes\n",
14+
"- **`subplots`**: Arrange independent figures in a grid"
1415
]
1516
},
1617
{
@@ -23,7 +24,7 @@
2324
"import plotly.express as px\n",
2425
"import xarray as xr\n",
2526
"\n",
26-
"from xarray_plotly import add_secondary_y, config, overlay, xpx\n",
27+
"from xarray_plotly import add_secondary_y, config, overlay, subplots, xpx\n",
2728
"\n",
2829
"config.notebook()"
2930
]
@@ -440,6 +441,128 @@
440441
"cell_type": "markdown",
441442
"id": "27",
442443
"metadata": {},
444+
"source": [
445+
"## subplots\n",
446+
"\n",
447+
"Arrange independent figures side-by-side in a grid. Each figure gets its own\n",
448+
"subplot cell with axes and title automatically derived from the source figure."
449+
]
450+
},
451+
{
452+
"cell_type": "markdown",
453+
"id": "28",
454+
"metadata": {},
455+
"source": [
456+
"### Different Variables Side by Side"
457+
]
458+
},
459+
{
460+
"cell_type": "code",
461+
"execution_count": null,
462+
"id": "29",
463+
"metadata": {},
464+
"outputs": [],
465+
"source": [
466+
"# One figure per variable, arranged in a row\n",
467+
"us_pop = population.sel(country=\"United States\")\n",
468+
"us_gdp = gdp_per_capita.sel(country=\"United States\")\n",
469+
"us_life = life_expectancy.sel(country=\"United States\")\n",
470+
"\n",
471+
"pop_fig = xpx(us_pop).bar(title=\"Population\")\n",
472+
"gdp_fig = xpx(us_gdp).line(title=\"GDP per Capita\")\n",
473+
"life_fig = xpx(us_life).line(title=\"Life Expectancy\")\n",
474+
"\n",
475+
"grid = subplots(pop_fig, gdp_fig, life_fig, cols=3)\n",
476+
"grid.update_layout(title=\"United States Overview\", height=350, showlegend=False)\n",
477+
"grid"
478+
]
479+
},
480+
{
481+
"cell_type": "markdown",
482+
"id": "30",
483+
"metadata": {},
484+
"source": [
485+
"### 2x2 Grid\n",
486+
"\n",
487+
"Use `cols=2` and pass four figures for a 2x2 layout."
488+
]
489+
},
490+
{
491+
"cell_type": "code",
492+
"execution_count": null,
493+
"id": "31",
494+
"metadata": {},
495+
"outputs": [],
496+
"source": [
497+
"# One subplot per country\n",
498+
"fig_us = xpx(population.sel(country=\"United States\")).bar(title=\"United States\")\n",
499+
"fig_cn = xpx(population.sel(country=\"China\")).bar(title=\"China\")\n",
500+
"fig_de = xpx(population.sel(country=\"Germany\")).bar(title=\"Germany\")\n",
501+
"fig_br = xpx(population.sel(country=\"Brazil\")).bar(title=\"Brazil\")\n",
502+
"\n",
503+
"grid = subplots(fig_us, fig_cn, fig_de, fig_br, cols=2)\n",
504+
"grid.update_layout(height=500, showlegend=False)\n",
505+
"grid"
506+
]
507+
},
508+
{
509+
"cell_type": "markdown",
510+
"id": "32",
511+
"metadata": {},
512+
"source": [
513+
"### Mixed Chart Types\n",
514+
"\n",
515+
"Each cell can use a different chart type. Subplot titles fall back to the\n",
516+
"y-axis label when no explicit title is set."
517+
]
518+
},
519+
{
520+
"cell_type": "code",
521+
"execution_count": null,
522+
"id": "33",
523+
"metadata": {},
524+
"outputs": [],
525+
"source": [
526+
"# No explicit title — subplot titles come from the y-axis label (DataArray name)\n",
527+
"pop_bar = xpx(us_pop).bar()\n",
528+
"gdp_line = xpx(us_gdp).line()\n",
529+
"life_scatter = xpx(us_life).scatter()\n",
530+
"\n",
531+
"grid = subplots(pop_bar, gdp_line, life_scatter, cols=3)\n",
532+
"grid.update_layout(height=350, showlegend=False)\n",
533+
"grid"
534+
]
535+
},
536+
{
537+
"cell_type": "markdown",
538+
"id": "34",
539+
"metadata": {},
540+
"source": [
541+
"### With Facets\n",
542+
"\n",
543+
"Faceted figures can be composed — each figure's internal subplots are remapped into the grid cell."
544+
]
545+
},
546+
{
547+
"cell_type": "code",
548+
"execution_count": null,
549+
"id": "35",
550+
"metadata": {},
551+
"outputs": [],
552+
"source": [
553+
"# Faceted bar on top, faceted line below\n",
554+
"pop_faceted = xpx(population).bar(facet_col=\"country\")\n",
555+
"gdp_faceted = xpx(gdp_per_capita).line(facet_col=\"country\")\n",
556+
"\n",
557+
"grid = subplots(pop_faceted, gdp_faceted, cols=1)\n",
558+
"grid.update_layout(height=600, showlegend=False)\n",
559+
"grid"
560+
]
561+
},
562+
{
563+
"cell_type": "markdown",
564+
"id": "36",
565+
"metadata": {},
443566
"source": [
444567
"---\n",
445568
"\n",
@@ -450,7 +573,7 @@
450573
},
451574
{
452575
"cell_type": "markdown",
453-
"id": "28",
576+
"id": "37",
454577
"metadata": {},
455578
"source": [
456579
"### overlay: Mismatched Facet Structure\n",
@@ -461,7 +584,7 @@
461584
{
462585
"cell_type": "code",
463586
"execution_count": null,
464-
"id": "29",
587+
"id": "38",
465588
"metadata": {},
466589
"outputs": [],
467590
"source": [
@@ -479,7 +602,7 @@
479602
},
480603
{
481604
"cell_type": "markdown",
482-
"id": "30",
605+
"id": "39",
483606
"metadata": {},
484607
"source": [
485608
"### overlay: Animated Overlay on Static Base\n",
@@ -490,7 +613,7 @@
490613
{
491614
"cell_type": "code",
492615
"execution_count": null,
493-
"id": "31",
616+
"id": "40",
494617
"metadata": {},
495618
"outputs": [],
496619
"source": [
@@ -508,7 +631,7 @@
508631
},
509632
{
510633
"cell_type": "markdown",
511-
"id": "32",
634+
"id": "41",
512635
"metadata": {},
513636
"source": [
514637
"### overlay: Mismatched Animation Frames\n",
@@ -519,7 +642,7 @@
519642
{
520643
"cell_type": "code",
521644
"execution_count": null,
522-
"id": "33",
645+
"id": "42",
523646
"metadata": {},
524647
"outputs": [],
525648
"source": [
@@ -535,7 +658,7 @@
535658
},
536659
{
537660
"cell_type": "markdown",
538-
"id": "34",
661+
"id": "43",
539662
"metadata": {},
540663
"source": [
541664
"### add_secondary_y: Mismatched Facet Structure\n",
@@ -546,7 +669,7 @@
546669
{
547670
"cell_type": "code",
548671
"execution_count": null,
549-
"id": "35",
672+
"id": "44",
550673
"metadata": {},
551674
"outputs": [],
552675
"source": [
@@ -564,7 +687,7 @@
564687
},
565688
{
566689
"cell_type": "markdown",
567-
"id": "36",
690+
"id": "45",
568691
"metadata": {},
569692
"source": [
570693
"### add_secondary_y: Animated Secondary on Static Base\n",
@@ -575,7 +698,7 @@
575698
{
576699
"cell_type": "code",
577700
"execution_count": null,
578-
"id": "37",
701+
"id": "46",
579702
"metadata": {},
580703
"outputs": [],
581704
"source": [
@@ -593,7 +716,7 @@
593716
},
594717
{
595718
"cell_type": "markdown",
596-
"id": "38",
719+
"id": "47",
597720
"metadata": {},
598721
"source": [
599722
"### add_secondary_y: Mismatched Animation Frames"
@@ -602,7 +725,7 @@
602725
{
603726
"cell_type": "code",
604727
"execution_count": null,
605-
"id": "39",
728+
"id": "48",
606729
"metadata": {},
607730
"outputs": [],
608731
"source": [
@@ -618,15 +741,16 @@
618741
},
619742
{
620743
"cell_type": "markdown",
621-
"id": "40",
744+
"id": "49",
622745
"metadata": {},
623746
"source": [
624747
"## Summary\n",
625748
"\n",
626749
"| Function | Facets | Animation | Static + Animated |\n",
627750
"|----------|--------|-----------|-------------------|\n",
628751
"| `overlay` | Yes (must match) | Yes (frames must match) | Static overlay on animated base OK |\n",
629-
"| `add_secondary_y` | Yes (must match) | Yes (frames must match) | Static secondary on animated base OK |"
752+
"| `add_secondary_y` | Yes (must match) | Yes (frames must match) | Static secondary on animated base OK |\n",
753+
"| `subplots` | Yes (remapped into cells) | No | N/A |"
630754
]
631755
}
632756
],

0 commit comments

Comments
 (0)