Skip to content

Commit 0cb6e6d

Browse files
FBumannclaude
andcommitted
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>
1 parent a9d6247 commit 0cb6e6d

File tree

3 files changed

+67
-17
lines changed

3 files changed

+67
-17
lines changed

docs/examples/combining.ipynb

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,7 @@
1111
"\n",
1212
"- **`overlay`**: Overlay traces on the same axes\n",
1313
"- **`add_secondary_y`**: Plot with two independent y-axes\n",
14-
"- **`subplots`**: Arrange independent figures in a grid\n",
15-
"- **`slider_to_dropdown`**: Convert animation slider to a dropdown menu"
14+
"- **`subplots`**: Arrange independent figures in a grid"
1615
]
1716
},
1817
{

tests/test_figures.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -757,6 +757,40 @@ def test_overlay_animation_frames_preserve_style(self) -> None:
757757
assert frame.data[1].line.color == "red"
758758

759759

760+
class TestAnimationAxisRanges:
761+
"""Tests for _fix_animation_axis_ranges."""
762+
763+
def test_datetime_x_axis_not_corrupted(self) -> None:
764+
"""datetime64 x-axis should be left on autorange, not cast to float epochs."""
765+
dates = np.array(["2020-01-01", "2020-06-01", "2021-01-01"], dtype="datetime64[ns]")
766+
da = xr.DataArray(
767+
np.random.rand(3, 2),
768+
dims=["date", "cat"],
769+
coords={"date": dates, "cat": ["A", "B"]},
770+
name="value",
771+
)
772+
fig1 = xpx(da).line(animation_frame="cat")
773+
fig2 = xpx(da).scatter(animation_frame="cat")
774+
combined = overlay(fig1, fig2)
775+
776+
# x-axis range should NOT be set (dates left to autorange)
777+
assert combined.layout.xaxis.range is None
778+
779+
def test_bar_zero_baseline(self) -> None:
780+
"""Bar chart y-axis range should include zero."""
781+
da = xr.DataArray(
782+
np.array([[100, 200], [150, 250]]),
783+
dims=["x", "frame"],
784+
name="val",
785+
)
786+
fig = xpx(da).bar(animation_frame="frame")
787+
# After overlay (which triggers _fix_animation_axis_ranges)
788+
combined = overlay(fig, xpx(da).line(animation_frame="frame"))
789+
790+
lo, hi = combined.layout.yaxis.range
791+
assert lo <= 0, f"Bar y-axis range should include 0, got lo={lo}"
792+
793+
760794
class TestSubplotsBasic:
761795
"""Basic tests for subplots function."""
762796

xarray_plotly/figures.py

Lines changed: 32 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -136,35 +136,49 @@ def _fix_animation_axis_ranges(fig: go.Figure) -> None:
136136
y_by_axis: dict[str, list[float]] = defaultdict(list)
137137
x_by_axis: dict[str, list[float]] = defaultdict(list)
138138

139+
# Track which axes have bar traces (for zero-baseline clamping)
140+
y_has_vbar: set[str] = set() # vertical bars → y-axis includes 0
141+
x_has_hbar: set[str] = set() # horizontal bars → x-axis includes 0
142+
139143
for trace in _iter_all_traces(fig):
140144
yaxis = getattr(trace, "yaxis", None) or "y"
141145
xaxis = getattr(trace, "xaxis", None) or "x"
142146

143-
y = getattr(trace, "y", None)
144-
if y is not None:
147+
# Track bar orientations
148+
if getattr(trace, "type", None) == "bar":
149+
orientation = getattr(trace, "orientation", None) or "v"
150+
if orientation == "h":
151+
x_has_hbar.add(xaxis)
152+
else:
153+
y_has_vbar.add(yaxis)
154+
155+
for data_attr, axis_ref, by_axis in [
156+
("y", yaxis, y_by_axis),
157+
("x", xaxis, x_by_axis),
158+
]:
159+
vals = getattr(trace, data_attr, None)
160+
if vals is None:
161+
continue
162+
arr = np.asarray(vals)
163+
# Skip datetime/timedelta — leave those axes on autorange
164+
if np.issubdtype(arr.dtype, np.datetime64) or np.issubdtype(arr.dtype, np.timedelta64):
165+
continue
145166
try:
146-
arr = np.asarray(y, dtype=float)
167+
arr = arr.astype(float)
147168
finite = arr[np.isfinite(arr)]
148169
if len(finite):
149-
y_by_axis[yaxis].extend(finite.tolist())
170+
by_axis[axis_ref].extend(finite.tolist())
150171
except (ValueError, TypeError):
151172
pass # Non-numeric (categorical) — skip
152173

153-
x = getattr(trace, "x", None)
154-
if x is not None:
155-
try:
156-
arr = np.asarray(x, dtype=float)
157-
finite = arr[np.isfinite(arr)]
158-
if len(finite):
159-
x_by_axis[xaxis].extend(finite.tolist())
160-
except (ValueError, TypeError):
161-
pass
162-
163174
# Apply ranges to layout
164175
for axis_ref, values in y_by_axis.items():
165176
if not values:
166177
continue
167178
lo, hi = min(values), max(values)
179+
if axis_ref in y_has_vbar:
180+
lo = min(lo, 0.0)
181+
hi = max(hi, 0.0)
168182
pad = (hi - lo) * 0.05 or 1 # 5% padding
169183
layout_prop = "yaxis" if axis_ref == "y" else f"yaxis{axis_ref[1:]}"
170184
fig.layout[layout_prop].range = [lo - pad, hi + pad]
@@ -173,6 +187,9 @@ def _fix_animation_axis_ranges(fig: go.Figure) -> None:
173187
if not values:
174188
continue
175189
lo, hi = min(values), max(values)
190+
if axis_ref in x_has_hbar:
191+
lo = min(lo, 0.0)
192+
hi = max(hi, 0.0)
176193
pad = (hi - lo) * 0.05 or 1
177194
layout_prop = "xaxis" if axis_ref == "x" else f"xaxis{axis_ref[1:]}"
178195
fig.layout[layout_prop].range = [lo - pad, hi + pad]
@@ -613,7 +630,7 @@ def _get_figure_title(fig: go.Figure) -> str:
613630
"""
614631
try:
615632
title = fig.layout.title.text
616-
if title:
633+
if isinstance(title, str) and title:
617634
return title
618635
except AttributeError:
619636
pass

0 commit comments

Comments
 (0)