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
1 change: 0 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,6 @@ Planned additions (contributions welcome):
- WebGL rendering option for `line()` and `scatter()` (large datasets)

**Figure utilities** (facet/animation-aware)
- `add_trace()` — add a trace to base figure and all animation frames
- `fill_between()` — fill area between two traces (uncertainty bands)
- `sync_axes()` — consistent axis ranges across facets and animation frames
- `add_secondary_x()` — secondary x-axis (like `add_secondary_y()`)
Expand Down
4 changes: 2 additions & 2 deletions tests/test_figures.py
Original file line number Diff line number Diff line change
Expand Up @@ -505,8 +505,8 @@ def test_facets_with_custom_title(self) -> None:

combined = add_secondary_y(base, secondary, secondary_y_title="Custom Title")

# Title should be on the first secondary axis
assert combined.layout.yaxis4.title.text == "Custom Title"
# Title should be on the rightmost secondary axis (yaxis6 for 3 facets)
assert combined.layout.yaxis6.title.text == "Custom Title"


class TestAddSecondaryYAnimation:
Expand Down
31 changes: 23 additions & 8 deletions xarray_plotly/figures.py
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,13 @@ def add_secondary_y(
# Build mapping from primary y-axes to secondary y-axes
y_mapping = _build_secondary_y_mapping(base_axes)

# Build x-y correspondence from base_axes (which x-axis pairs with which y-axis)
x_for_y = {yaxis: xaxis for xaxis, yaxis in base_axes}

# Find the rightmost x-axis (highest number) to determine which secondary axis shows ticks
rightmost_x = max(x_for_y.values(), key=lambda x: int(x[1:]) if x != "x" else 1)
rightmost_primary_y = next(y for y, x in x_for_y.items() if x == rightmost_x)

# Create new figure with base's layout
combined = go.Figure(layout=copy.deepcopy(base.layout))

Expand All @@ -322,24 +329,32 @@ def add_secondary_y(
trace_copy.yaxis = y_mapping[original_yaxis]
combined.add_trace(trace_copy)

# Get the rightmost secondary y-axis name for linking
rightmost_secondary_y = y_mapping[rightmost_primary_y]

# Configure secondary y-axes
for primary_yaxis, secondary_yaxis in y_mapping.items():
# Get title - only set on first secondary axis or use provided title
is_rightmost = primary_yaxis == rightmost_primary_y

# Get title - only set on rightmost secondary axis
title = None
if secondary_y_title is not None:
# Only set title on the first secondary axis to avoid repetition
if primary_yaxis == "y":
if is_rightmost:
if secondary_y_title is not None:
title = secondary_y_title
elif primary_yaxis == "y" and secondary.layout.yaxis and secondary.layout.yaxis.title:
# Try to get from secondary's layout
title = secondary.layout.yaxis.title.text
elif secondary.layout.yaxis and secondary.layout.yaxis.title:
title = secondary.layout.yaxis.title.text

# Configure the secondary axis
# Anchor to the corresponding x-axis so it appears on the right side of its subplot
axis_config = {
"title": title,
"overlaying": primary_yaxis,
"side": "right",
"anchor": "free" if primary_yaxis != "y" else None,
"anchor": x_for_y[primary_yaxis],
# Only show ticks on the rightmost secondary axis
"showticklabels": is_rightmost,
# Link non-rightmost axes to the rightmost for consistent scaling
"matches": None if is_rightmost else rightmost_secondary_y,
}
# Remove None values
axis_config = {k: v for k, v in axis_config.items() if v is not None}
Expand Down