diff --git a/plotly/shapeannotation.py b/plotly/shapeannotation.py index a2323ed02d..a3498973fa 100644 --- a/plotly/shapeannotation.py +++ b/plotly/shapeannotation.py @@ -1,10 +1,32 @@ # some functions defined here to avoid numpy import +from datetime import datetime +from numbers import Number + def _mean(x): if len(x) == 0: raise ValueError("x must have positive length") - return float(sum(x)) / len(x) + + # Numeric sequence: default behaviour + if all(isinstance(v, Number) for v in x): + return float(sum(x)) / len(x) + + # Datetime sequence: delegate to _mean_datetime + if all(isinstance(v, datetime) for v in x): + return _mean_datetime(x) + + # Fallback for non-numeric and non-datetime types: return first element + return x[0] + + +def _mean_datetime(x): + """Return midpoint of a sequence of datetime objects (assumes homogenous).""" + + timestamps = [v.timestamp() for v in x] # convert to POSIX timestamps + avg = sum(timestamps) / len(timestamps) + tzinfo = x[0].tzinfo # extract timezone info from first element + return datetime.fromtimestamp(avg, tz=tzinfo) def _argmin(x): diff --git a/tests/test_optional/test_autoshapes/test_date_axis_annotated_shapes.py b/tests/test_optional/test_autoshapes/test_date_axis_annotated_shapes.py new file mode 100644 index 0000000000..5f4f7df6ed --- /dev/null +++ b/tests/test_optional/test_autoshapes/test_date_axis_annotated_shapes.py @@ -0,0 +1,48 @@ +"""Tests for annotated axis-spanning shapes with datetime coordinates. + +These tests cover the regression described in GitHub issue #3065: +https://github.com/plotly/plotly.py/issues/3065 + +""" + +from datetime import datetime + +import plotly.express as px +import plotly.graph_objects as go + + +def test_add_vline_with_date_annotation_express(): + """MWE from Github issue https://github.com/plotly/plotly.py/issues/3065""" + df = px.data.stocks(indexed=True) + fig = px.line(df) + fig.add_vline(x="2018-09-24", annotation_text="test") + + +def test_add_vline_with_date_annotation(): + fig = go.Figure() + # Provide a couple of traces so axis type is inferred as date from data + dates = [datetime(2025, 9, 23), datetime(2025, 9, 24), datetime(2025, 9, 25)] + fig.add_scatter(x=dates, y=[1, 2, 3]) + fig.add_vline(x=dates[1], annotation_text="Test") + + # Ensure one annotation was added and x coordinate preserved + annotations = getattr(fig.layout, "annotations", []) + assert len(annotations) == 1 + ann = annotations[0] + assert ann.x == dates[1] + assert ann.text == "Test" + + +def test_add_hline_with_date_annotation(): + fig = go.Figure() + # Provide a couple of traces so axis type is inferred as date from data + dates = [datetime(2025, 9, 23), datetime(2025, 9, 24), datetime(2025, 9, 25)] + fig.add_scatter(y=dates, x=[1, 2, 3]) + fig.add_hline(y=dates[1], annotation_text="Test") + + # Ensure one annotation was added and x coordinate preserved + annotations = getattr(fig.layout, "annotations", []) + assert len(annotations) == 1 + ann = annotations[0] + assert ann.y == dates[1] + assert ann.text == "Test"