diff --git a/CHANGELOG.md b/CHANGELOG.md index cbbaf3cb89..454d2210ab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ This project adheres to [Semantic Versioning](http://semver.org/). - Fixed another compatibility issue with Pandas 2.0, just affecting `px.*(line_close=True)` [[#4190](https://github.com/plotly/plotly.py/pull/4190)] - Added some rounding to the `make_subplots` function to handle situations where the user-input specs cause the domain to exceed 1 by small amounts [[#4153](https://github.com/plotly/plotly.py/pull/4153)] - Sanitize JSON output to prevent an XSS vector when graphs are inserted directly into HTML [[#4196](https://github.com/plotly/plotly.py/pull/4196)] + - Fixed issue with shapes and annotations plotting on the wrong y axis when supplied with a specific axis in the `yref` parameter [[#4177](https://github.com/plotly/plotly.py/pull/4177)] - Remove `use_2to3` setuptools arg, which is invalid in the latest Python and setuptools versions [[#4206](https://github.com/plotly/plotly.py/pull/4206)] - Fix [#4066](https://github.com/plotly/plotly.py/issues/4066) JupyterLab v4 giving tiny default graph height [[#4227](https://github.com/plotly/plotly.py/pull/4227)] diff --git a/packages/python/plotly/plotly/basedatatypes.py b/packages/python/plotly/plotly/basedatatypes.py index bf5eac3dfa..8fa70e0ada 100644 --- a/packages/python/plotly/plotly/basedatatypes.py +++ b/packages/python/plotly/plotly/basedatatypes.py @@ -1559,17 +1559,31 @@ def _add_annotation_like( subplot_type=refs[0].subplot_type, ) ) - if len(refs) == 1 and secondary_y: - raise ValueError( - """ -Cannot add {prop_singular} to secondary y-axis of subplot at position ({r}, {c}) -because subplot does not have a secondary y-axis""" - ) - if secondary_y: - xaxis, yaxis = refs[1].layout_keys + + # If the new_object was created with a yref specified that did not include paper or domain, the specified yref should be used otherwise assign the xref and yref from the layout_keys + if ( + new_obj.yref is None + or new_obj.yref == "y" + or "paper" in new_obj.yref + or "domain" in new_obj.yref + ): + if len(refs) == 1 and secondary_y: + raise ValueError( + """ + Cannot add {prop_singular} to secondary y-axis of subplot at position ({r}, {c}) + because subplot does not have a secondary y-axis""".format( + prop_singular=prop_singular, r=row, c=col + ) + ) + if secondary_y: + xaxis, yaxis = refs[1].layout_keys + else: + xaxis, yaxis = refs[0].layout_keys + xref, yref = xaxis.replace("axis", ""), yaxis.replace("axis", "") else: - xaxis, yaxis = refs[0].layout_keys - xref, yref = xaxis.replace("axis", ""), yaxis.replace("axis", "") + yref = new_obj.yref + xaxis = refs[0].layout_keys[0] + xref = xaxis.replace("axis", "") # if exclude_empty_subplots is True, check to see if subplot is # empty and return if it is if exclude_empty_subplots and ( @@ -1591,6 +1605,11 @@ def _add_domain(ax_letter, new_axref): new_obj.update(xref=xref, yref=yref) self.layout[prop_plural] += (new_obj,) + # The 'new_obj.xref' and 'new_obj.yref' parameters need to be reset otherwise it + # will appear as if user supplied yref params when looping through subplots and + # will force annotation to be on the axis of the last drawn annotation + # i.e. they all end up on the same axis. + new_obj.update(xref=None, yref=None) return self @@ -4034,6 +4053,7 @@ def _process_multiple_axis_spanning_shapes( row=row, col=col, exclude_empty_subplots=exclude_empty_subplots, + yref=shape_kwargs.get("yref", "y"), ) # update xref and yref for the new shapes and annotations for layout_obj, n_layout_objs_before in zip( @@ -4045,10 +4065,13 @@ def _process_multiple_axis_spanning_shapes( ): # this was called intending to add to a single plot (and # self.add_{layout_obj} succeeded) - # however, in the case of a single plot, xref and yref are not - # specified, so we specify them here so the following routines can work - # (they need to append " domain" to xref or yref) - self.layout[layout_obj][-1].update(xref="x", yref="y") + # however, in the case of a single plot, xref and yref MAY not be + # specified, IF they are not specified we specify them here so the following routines can work + # (they need to append " domain" to xref or yref). If they are specified, we leave them alone. + if self.layout[layout_obj][-1].xref is None: + self.layout[layout_obj][-1].update(xref="x") + if self.layout[layout_obj][-1].yref is None: + self.layout[layout_obj][-1].update(yref="y") new_layout_objs = tuple( filter( lambda x: x is not None, diff --git a/packages/python/plotly/plotly/tests/test_core/test_update_objects/test_update_annotations.py b/packages/python/plotly/plotly/tests/test_core/test_update_objects/test_update_annotations.py index 23c3af2796..d1c81a3113 100644 --- a/packages/python/plotly/plotly/tests/test_core/test_update_objects/test_update_annotations.py +++ b/packages/python/plotly/plotly/tests/test_core/test_update_objects/test_update_annotations.py @@ -5,6 +5,7 @@ import plotly.graph_objs as go from plotly.subplots import make_subplots + import pytest @@ -351,6 +352,64 @@ def test_no_exclude_empty_subplots(): assert fig.layout[k][3]["xref"] == "x4" and fig.layout[k][3]["yref"] == "y4" +def test_supplied_yref_on_single_plot_subplot(): + ### test a (1,1) subplot figure object + fig = make_subplots(1, 1) + fig.add_trace(go.Scatter(x=[1, 2, 3, 4], y=[1, 2, 2, 1])) + fig.add_trace(go.Scatter(x=[1, 2, 3, 4], y=[4, 3, 2, 1], yaxis="y2")) + fig.update_layout( + yaxis=dict(title="yaxis1 title"), + yaxis2=dict(title="yaxis2 title", overlaying="y", side="right"), + ) + # add horizontal line on y2. Secondary_y can be True or False when yref is supplied + fig.add_hline(y=3, yref="y2", secondary_y=True) + assert fig.layout["shapes"][0]["yref"] == "y2" + + +def test_supplied_yref_on_non_subplot_figure_object(): + ### test a non-subplot figure object from go.Figure + trace1 = go.Scatter(x=[1, 2, 3, 4], y=[1, 2, 2, 1]) + trace2 = go.Scatter(x=[1, 2, 3, 4], y=[4, 3, 2, 1], yaxis="y2") + data = [trace1, trace2] + layout = go.Layout( + yaxis=dict(title="yaxis1 title"), + yaxis2=dict(title="yaxis2 title", overlaying="y", side="right"), + ) + fig = go.Figure(data=data, layout=layout) + # add horizontal line on y2. Secondary_y can be True or False when yref is supplied + fig.add_hline(y=3, yref="y2", secondary_y=False) + assert fig.layout["shapes"][0]["yref"] == "y2" + + +def test_supplied_yref_on_multi_plot_subplot(): + ### test multiple subploted figure object with subplots.make_subplots + fig = make_subplots( + rows=1, + cols=2, + shared_yaxes=False, + specs=[[{"secondary_y": True}, {"secondary_y": True}]], + ) + ### Add traces to the first subplot + fig.add_trace(go.Scatter(x=[1, 2, 3], y=[1, 2, 3]), row=1, col=1) + fig.add_trace( + go.Scatter(x=[1, 2, 3], y=[3, 2, 1], yaxis="y2"), row=1, col=1, secondary_y=True + ) + ### Add traces to the second subplot + fig.add_trace(go.Scatter(x=[1, 2, 3], y=[1, 2, 3], yaxis="y"), row=1, col=2) + fig.add_trace( + go.Scatter(x=[1, 2, 3], y=[1, 1, 2], yaxis="y2"), row=1, col=2, secondary_y=True + ) + # add a horizontal line on both subplots on their respective secondary y. + # When using the subplots.make_subplots() method yref parameter should NOT be supplied per docstring instructions. + # Instead secondary_y specs and secondary_y parameter MUST be True to plot on secondary y + fig.add_hline(y=2, row=1, col=1, secondary_y=True) + fig.add_hline(y=1, row=1, col=2, secondary_y=True) + assert fig.layout["shapes"][0]["yref"] == "y2" + assert fig.layout["shapes"][0]["xref"] == "x domain" + assert fig.layout["shapes"][1]["yref"] == "y4" + assert fig.layout["shapes"][1]["xref"] == "x2 domain" + + @pytest.fixture def select_annotations_integer(): fig = make_subplots(2, 3)