diff --git a/plotly/basedatatypes.py b/plotly/basedatatypes.py index 72f4cd25ca..e3545914b3 100644 --- a/plotly/basedatatypes.py +++ b/plotly/basedatatypes.py @@ -4093,6 +4093,8 @@ def _process_multiple_axis_spanning_shapes( augmented_annotation = shapeannotation.axis_spanning_shape_annotation( annotation, shape_type, shape_args, annotation_kwargs ) + if exclude_empty_subplots and len(self.data) == 0: + exclude_empty_subplots = False self.add_shape( row=row, col=col, diff --git a/tests/test_core/test_shapes/test_add_hline_empty_subplots.py b/tests/test_core/test_shapes/test_add_hline_empty_subplots.py new file mode 100644 index 0000000000..f14225a36d --- /dev/null +++ b/tests/test_core/test_shapes/test_add_hline_empty_subplots.py @@ -0,0 +1,31 @@ +import pytest +import plotly.graph_objects as go +from plotly.subplots import make_subplots + + +def test_add_hline_on_empty_subplots_creates_shape_default(): + fig = make_subplots(rows=1, cols=1) + fig.add_hline(y=0.25) + shapes = fig.layout.shapes + assert len(shapes) == 1, "Expected one shape for the horizontal line" + shape = shapes[0] + assert shape.type == "line" + assert shape.y0 == 0.25 and shape.y1 == 0.25 + # xref and yref should be set + assert getattr(shape, "xref", None) is not None + assert getattr(shape, "yref", None) is not None + + +@pytest.mark.parametrize("row, col", [(None, None), (1, 1)]) +def test_add_hline_with_explicit_row_col_on_empty_subplots(row, col): + fig = make_subplots(rows=1, cols=1) + # explicit row/col + fig.add_hline(y=0.5, row=row, col=col) + shapes = fig.layout.shapes + assert len(shapes) == 1, f"Expected one shape when row={row}, col={col}" + shape = shapes[0] + assert shape.y0 == 0.5 and shape.y1 == 0.5 + assert shape.type == "line" + # ensure references default + assert shape.xref is not None + assert shape.yref is not None diff --git a/tests/test_regression/test_hline_subplots_bug.py b/tests/test_regression/test_hline_subplots_bug.py new file mode 100644 index 0000000000..8d00b2cd15 --- /dev/null +++ b/tests/test_regression/test_hline_subplots_bug.py @@ -0,0 +1,61 @@ +import pytest +import plotly.graph_objects as go +from plotly.subplots import make_subplots + + +def _apply_line(fig, orientation): + if orientation == "h": + fig.add_hline(y=0.5) + elif orientation == "v": + fig.add_vline(x=0.3) + else: + raise ValueError("orientation must be 'h' or 'v'") + + +@pytest.mark.parametrize( + "orientation,kwargs", + [ + ( + "h", + dict( + line_coord_key="y0", coord=0.5, span_keys=("x0", "x1"), span_vals=(0, 1) + ), + ), + ( + "v", + dict( + line_coord_key="x0", coord=0.3, span_keys=("y0", "y1"), span_vals=(0, 1) + ), + ), + ], +) +@pytest.mark.parametrize( + "constructor", + [ + pytest.param(lambda: go.Figure(), id="plain-figure"), + pytest.param(lambda: make_subplots(rows=1, cols=1), id="make_subplots"), + ], +) +def test_add_line_presence(orientation, kwargs, constructor): + """Both add_hline and add_vline must create a shape, even on empty subplots.""" + fig = constructor() + + assert len(fig.data) == 0 + assert len(fig.layout.shapes) == 0 + + _apply_line(fig, orientation) + + # exactly one shape expected regardless of constructor and orientation + assert len(fig.layout.shapes) == 1, ( + f"add_{orientation}line must create a shape on an empty figure; " + "currently fails for make_subplots." + ) + + shape = fig.layout.shapes[0] + # validate coordinate of line + assert pytest.approx(shape[kwargs["line_coord_key"]]) == kwargs["coord"] + # validate full-span of the other axis + span_key0, span_key1 = kwargs["span_keys"] + expected0, expected1 = kwargs["span_vals"] + assert shape[span_key0] == expected0 + assert shape[span_key1] == expected1