diff --git a/CHANGELOG.md b/CHANGELOG.md index 8378fcf3477..9e8374d4c01 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ This project adheres to [Semantic Versioning](http://semver.org/). - Add support for converting matplotlib reference lines (`axhline`, `axvline`, and horizontal/vertical `axline`) as well as axes-coordinate lines into Plotly layout shapes in `mpl_to_plotly`, including support for date axes [[#5719](https://github.com/plotly/plotly.py/pull/5719)], with thanks to @robertoffmoura for the contribution! ### Fixed +- Fix `add_hline` and `add_vline` dropping lines on empty subplots when the target is unambiguous (the grid has a single subplot, or both `row` and `col` are given) [[#5136](https://github.com/plotly/plotly.py/issues/5136)] - Fix `mpl_to_plotly` tick marker mirroring, axis positioning, and trace visibility for twinned and overlaid subplots (such as `twinx` and `twiny`) [[#5310](https://github.com/plotly/plotly.py/pull/5310)], with thanks to @robertoffmoura for the contribution! - Fix `mpl_to_plotly` losing matplotlib axis line colors by exporting the color of each axis's displayed spine to the plotly x and y axis `linecolor` [[#5311](https://github.com/plotly/plotly.py/pull/5311)], with thanks to @robertoffmoura for the contribution! - Fix concurrent first access to lazily initialized graph object properties, which could raise `ValueError("Invalid value")` [[#5691](https://github.com/plotly/plotly.py/pull/5691)], with thanks to @hb1915 for the contribution! diff --git a/plotly/basedatatypes.py b/plotly/basedatatypes.py index a7326da8e5a..3292c35bfd6 100644 --- a/plotly/basedatatypes.py +++ b/plotly/basedatatypes.py @@ -4050,6 +4050,20 @@ def _process_multiple_axis_spanning_shapes( self.layout[layout_obj][:n_layout_objs_before] + new_layout_objs ) + def _axis_spanning_line_target_is_explicit(self, row, col): + """ + Returns True when the subplot targeted by add_hline/add_vline is + unambiguous: the grid has a single subplot, or both row and col were + given explicitly. In these cases the line is kept even if the + subplot does not contain any data yet. + """ + if not self._has_subplots(): + return False + rows, cols = self._get_subplot_rows_columns() + if len(rows) * len(cols) == 1: + return True + return all(rc is not None and rc != "all" for rc in (row, col)) + def add_vline( self, x, @@ -4064,7 +4078,8 @@ def add_vline( row, col, "vline", - exclude_empty_subplots=exclude_empty_subplots, + exclude_empty_subplots=exclude_empty_subplots + and not self._axis_spanning_line_target_is_explicit(row, col), annotation=annotation, **kwargs, ) @@ -4092,7 +4107,8 @@ def add_hline( row, col, "hline", - exclude_empty_subplots=exclude_empty_subplots, + exclude_empty_subplots=exclude_empty_subplots + and not self._axis_spanning_line_target_is_explicit(row, col), annotation=annotation, **kwargs, ) diff --git a/tests/test_core/test_update_objects/test_update_annotations.py b/tests/test_core/test_update_objects/test_update_annotations.py index 773ca603b3a..95b022c99b8 100644 --- a/tests/test_core/test_update_objects/test_update_annotations.py +++ b/tests/test_core/test_update_objects/test_update_annotations.py @@ -428,3 +428,38 @@ def test_select_annotations_integer(select_annotations_integer): assert (len(anns) == 1) and anns[0]["text"] == "B" with pytest.raises(IndexError): fig.select_annotations(row=2, col=2, selector=3) + + +@pytest.mark.parametrize("method", ["add_hline", "add_vline"]) +def test_axis_spanning_line_kept_on_single_empty_subplot(method): + fig = make_subplots(rows=1, cols=1) + getattr(fig, method)(1) + assert len(fig.layout.shapes) == 1 + + +@pytest.mark.parametrize("method", ["add_hline", "add_vline"]) +def test_axis_spanning_line_kept_on_explicit_empty_subplot(method): + fig = make_subplots(rows=2, cols=2) + getattr(fig, method)(1, row=2, col=1) + assert len(fig.layout.shapes) == 1 + shape = fig.layout.shapes[0] + assert (shape.xref, shape.yref) in [("x3", "y3 domain"), ("x3 domain", "y3")] + + +@pytest.mark.parametrize("method", ["add_hline", "add_vline"]) +def test_axis_spanning_line_excludes_unspecified_empty_subplots(method): + fig = make_subplots(rows=2, cols=2) + getattr(fig, method)(1) + getattr(fig, method)(1, row=1) + assert len(fig.layout.shapes) == 0 + + fig.add_trace(go.Scatter(x=[1, 2], y=[1, 2]), row=1, col=2) + getattr(fig, method)(1) + assert len(fig.layout.shapes) == 1 + + +@pytest.mark.parametrize("method", ["add_hrect", "add_vrect"]) +def test_axis_spanning_rect_still_excluded_on_explicit_empty_subplot(method): + fig = make_subplots(rows=2, cols=2) + getattr(fig, method)(0, 1, row=2, col=1) + assert len(fig.layout.shapes) == 0