Skip to content

Commit

Permalink
Merge pull request #4177 from zfoltz/axis_spanning_layout_object_xref…
Browse files Browse the repository at this point in the history
…_yref_bug

Issue #3755 Bug Fix for: Shapes and Annotations With yref Parameter Not Drawing on Correct Axis
  • Loading branch information
alexcjohnson authored Jun 3, 2023
2 parents 58075f4 + f5d2900 commit 42266b0
Show file tree
Hide file tree
Showing 3 changed files with 97 additions and 14 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)]

Expand Down
51 changes: 37 additions & 14 deletions packages/python/plotly/plotly/basedatatypes.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand All @@ -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

Expand Down Expand Up @@ -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(
Expand All @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

import plotly.graph_objs as go
from plotly.subplots import make_subplots

import pytest


Expand Down Expand Up @@ -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)
Expand Down

0 comments on commit 42266b0

Please sign in to comment.