Skip to content

Commit a6e64bf

Browse files
FBumannclaude
andcommitted
feat(add_secondary_y): legend kwarg for cross-source trace handling
Adds `legend: Literal["suffix", "merge", "separate"] = "suffix"` so the caller picks how same-named traces from the two source figures are presented: - "suffix" (default, current behavior): each trace gets its own legend entry with the source y-axis title appended ("Brazil (Population)", "Brazil (GDP per Capita)"). Each toggles independently. - "merge": same-named traces share a legendgroup, collapsing to a single legend entry that toggles both axes together (Plotly's default groupclick="togglegroup"). - "separate": PX legend output is left untouched; duplicate names across the two figures are accepted, each toggles alone. `_ensure_legend_visibility` was refactored from a `cross_source_dedup` bool to a `mode` parameter using the new `LegendMode` Literal in `xarray_plotly.common`. Tests cover all three modes plus an invalid mode error path. Notebook adds an example of `legend="merge"`. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 6c4328e commit a6e64bf

4 files changed

Lines changed: 167 additions & 61 deletions

File tree

docs/examples/combining.ipynb

Lines changed: 66 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -354,7 +354,7 @@
354354
"source": [
355355
"### Multi-Trace Figures\n",
356356
"\n",
357-
"When both base and secondary figures already split into multiple traces (e.g. a categorical xarray dimension), `add_secondary_y` keeps each side's traces visible in the legend. If the two figures share `legendgroup` names — common when they share a categorical dimension — those legendgroups are namespaced with each figure's y-axis title so every trace gets its own legend entry."
357+
"When both base and secondary figures already split into multiple traces (e.g. via a categorical xarray dimension), `add_secondary_y` keeps every trace visible in the legend. By default (`legend=\"suffix\"`), traces with the same name across the two figures are disambiguated by appending each figure's y-axis title — `\"Brazil (Population)\"` vs `\"Brazil (GDP per Capita)\"` — so each trace is its own legend entry and toggles independently."
358358
]
359359
},
360360
{
@@ -382,6 +382,39 @@
382382
"cell_type": "markdown",
383383
"id": "23",
384384
"metadata": {},
385+
"source": [
386+
"#### Choosing how the legend treats same-named traces\n",
387+
"\n",
388+
"`add_secondary_y` accepts a `legend=` argument controlling how same-named\n",
389+
"traces from the two figures are presented:\n",
390+
"\n",
391+
"- `\"suffix\"` (default): each trace gets its own entry with the source y-axis title appended.\n",
392+
"- `\"merge\"`: same-named traces share a `legendgroup`, collapsing to a single entry that toggles both axes together.\n",
393+
"- `\"separate\"`: PX entries are left as-is; duplicate names are accepted and each trace toggles alone."
394+
]
395+
},
396+
{
397+
"cell_type": "code",
398+
"execution_count": null,
399+
"id": "24",
400+
"metadata": {},
401+
"outputs": [],
402+
"source": [
403+
"# legend=\"merge\": clicking \"United States\" in the legend toggles both\n",
404+
"# the Population trace (left axis) and the GDP per Capita trace (right axis).\n",
405+
"pop_fig = xpx(population).line()\n",
406+
"gdp_fig = xpx(gdp_per_capita).line()\n",
407+
"combined_merge = add_secondary_y(\n",
408+
" pop_fig, gdp_fig, secondary_y_title=\"GDP per Capita ($)\", legend=\"merge\"\n",
409+
")\n",
410+
"combined_merge.update_layout(title='legend=\"merge\" — one entry per country, toggles both axes')\n",
411+
"combined_merge"
412+
]
413+
},
414+
{
415+
"cell_type": "markdown",
416+
"id": "25",
417+
"metadata": {},
385418
"source": [
386419
"### With Animation\n",
387420
"\n",
@@ -391,7 +424,7 @@
391424
{
392425
"cell_type": "code",
393426
"execution_count": null,
394-
"id": "24",
427+
"id": "26",
395428
"metadata": {},
396429
"outputs": [],
397430
"source": [
@@ -409,7 +442,7 @@
409442
},
410443
{
411444
"cell_type": "markdown",
412-
"id": "25",
445+
"id": "27",
413446
"metadata": {},
414447
"source": [
415448
"### Static Secondary on Animated Base\n",
@@ -420,7 +453,7 @@
420453
{
421454
"cell_type": "code",
422455
"execution_count": null,
423-
"id": "26",
456+
"id": "28",
424457
"metadata": {},
425458
"outputs": [],
426459
"source": [
@@ -441,7 +474,7 @@
441474
},
442475
{
443476
"cell_type": "markdown",
444-
"id": "27",
477+
"id": "29",
445478
"metadata": {},
446479
"source": [
447480
"### With Facets\n",
@@ -452,7 +485,7 @@
452485
{
453486
"cell_type": "code",
454487
"execution_count": null,
455-
"id": "28",
488+
"id": "30",
456489
"metadata": {},
457490
"outputs": [],
458491
"source": [
@@ -470,7 +503,7 @@
470503
},
471504
{
472505
"cell_type": "markdown",
473-
"id": "29",
506+
"id": "31",
474507
"metadata": {},
475508
"source": [
476509
"### Multi-Trace + Facets\n",
@@ -482,7 +515,7 @@
482515
{
483516
"cell_type": "code",
484517
"execution_count": null,
485-
"id": "30",
518+
"id": "32",
486519
"metadata": {},
487520
"outputs": [],
488521
"source": [
@@ -519,7 +552,7 @@
519552
},
520553
{
521554
"cell_type": "markdown",
522-
"id": "31",
555+
"id": "33",
523556
"metadata": {},
524557
"source": [
525558
"### Composing `overlay` with `add_secondary_y`\n",
@@ -530,7 +563,7 @@
530563
{
531564
"cell_type": "code",
532565
"execution_count": null,
533-
"id": "32",
566+
"id": "34",
534567
"metadata": {},
535568
"outputs": [],
536569
"source": [
@@ -558,7 +591,7 @@
558591
},
559592
{
560593
"cell_type": "markdown",
561-
"id": "33",
594+
"id": "35",
562595
"metadata": {},
563596
"source": [
564597
"## subplots\n",
@@ -569,7 +602,7 @@
569602
},
570603
{
571604
"cell_type": "markdown",
572-
"id": "34",
605+
"id": "36",
573606
"metadata": {},
574607
"source": [
575608
"### Different Variables Side by Side"
@@ -578,7 +611,7 @@
578611
{
579612
"cell_type": "code",
580613
"execution_count": null,
581-
"id": "35",
614+
"id": "37",
582615
"metadata": {},
583616
"outputs": [],
584617
"source": [
@@ -598,7 +631,7 @@
598631
},
599632
{
600633
"cell_type": "markdown",
601-
"id": "36",
634+
"id": "38",
602635
"metadata": {},
603636
"source": [
604637
"### 2x2 Grid\n",
@@ -609,7 +642,7 @@
609642
{
610643
"cell_type": "code",
611644
"execution_count": null,
612-
"id": "37",
645+
"id": "39",
613646
"metadata": {},
614647
"outputs": [],
615648
"source": [
@@ -626,7 +659,7 @@
626659
},
627660
{
628661
"cell_type": "markdown",
629-
"id": "38",
662+
"id": "40",
630663
"metadata": {},
631664
"source": [
632665
"### Mixed Chart Types\n",
@@ -638,7 +671,7 @@
638671
{
639672
"cell_type": "code",
640673
"execution_count": null,
641-
"id": "39",
674+
"id": "41",
642675
"metadata": {},
643676
"outputs": [],
644677
"source": [
@@ -654,7 +687,7 @@
654687
},
655688
{
656689
"cell_type": "markdown",
657-
"id": "40",
690+
"id": "42",
658691
"metadata": {},
659692
"source": [
660693
"### With Facets\n",
@@ -665,7 +698,7 @@
665698
{
666699
"cell_type": "code",
667700
"execution_count": null,
668-
"id": "41",
701+
"id": "43",
669702
"metadata": {},
670703
"outputs": [],
671704
"source": [
@@ -680,7 +713,7 @@
680713
},
681714
{
682715
"cell_type": "markdown",
683-
"id": "42",
716+
"id": "44",
684717
"metadata": {},
685718
"source": [
686719
"---\n",
@@ -692,7 +725,7 @@
692725
},
693726
{
694727
"cell_type": "markdown",
695-
"id": "43",
728+
"id": "45",
696729
"metadata": {},
697730
"source": [
698731
"### overlay: Mismatched Facet Structure\n",
@@ -703,7 +736,7 @@
703736
{
704737
"cell_type": "code",
705738
"execution_count": null,
706-
"id": "44",
739+
"id": "46",
707740
"metadata": {},
708741
"outputs": [],
709742
"source": [
@@ -721,7 +754,7 @@
721754
},
722755
{
723756
"cell_type": "markdown",
724-
"id": "45",
757+
"id": "47",
725758
"metadata": {},
726759
"source": [
727760
"### overlay: Animated Overlay on Static Base\n",
@@ -732,7 +765,7 @@
732765
{
733766
"cell_type": "code",
734767
"execution_count": null,
735-
"id": "46",
768+
"id": "48",
736769
"metadata": {},
737770
"outputs": [],
738771
"source": [
@@ -750,7 +783,7 @@
750783
},
751784
{
752785
"cell_type": "markdown",
753-
"id": "47",
786+
"id": "49",
754787
"metadata": {},
755788
"source": [
756789
"### overlay: Mismatched Animation Frames\n",
@@ -761,7 +794,7 @@
761794
{
762795
"cell_type": "code",
763796
"execution_count": null,
764-
"id": "48",
797+
"id": "50",
765798
"metadata": {},
766799
"outputs": [],
767800
"source": [
@@ -777,7 +810,7 @@
777810
},
778811
{
779812
"cell_type": "markdown",
780-
"id": "49",
813+
"id": "51",
781814
"metadata": {},
782815
"source": [
783816
"### add_secondary_y: Mismatched Facet Structure\n",
@@ -788,7 +821,7 @@
788821
{
789822
"cell_type": "code",
790823
"execution_count": null,
791-
"id": "50",
824+
"id": "52",
792825
"metadata": {},
793826
"outputs": [],
794827
"source": [
@@ -806,7 +839,7 @@
806839
},
807840
{
808841
"cell_type": "markdown",
809-
"id": "51",
842+
"id": "53",
810843
"metadata": {},
811844
"source": [
812845
"### add_secondary_y: Animated Secondary on Static Base\n",
@@ -817,7 +850,7 @@
817850
{
818851
"cell_type": "code",
819852
"execution_count": null,
820-
"id": "52",
853+
"id": "54",
821854
"metadata": {},
822855
"outputs": [],
823856
"source": [
@@ -835,7 +868,7 @@
835868
},
836869
{
837870
"cell_type": "markdown",
838-
"id": "53",
871+
"id": "55",
839872
"metadata": {},
840873
"source": [
841874
"### add_secondary_y: Mismatched Animation Frames"
@@ -844,7 +877,7 @@
844877
{
845878
"cell_type": "code",
846879
"execution_count": null,
847-
"id": "54",
880+
"id": "56",
848881
"metadata": {},
849882
"outputs": [],
850883
"source": [
@@ -860,7 +893,7 @@
860893
},
861894
{
862895
"cell_type": "markdown",
863-
"id": "55",
896+
"id": "57",
864897
"metadata": {},
865898
"source": [
866899
"## Summary\n",

0 commit comments

Comments
 (0)