Skip to content

Commit dd9d2e5

Browse files
authored
Merge pull request #5508 from mosh3eb/fix/add-vline-datetime-annotation
Fix add_vline and add_hline with datetime axes
2 parents b5b1062 + 8c18945 commit dd9d2e5

2 files changed

Lines changed: 119 additions & 1 deletion

File tree

plotly/shapeannotation.py

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,52 @@
11
# some functions defined here to avoid numpy import
22

3+
import datetime
4+
5+
6+
def _is_date_string(val):
7+
"""Check if a value is a date/datetime string."""
8+
if not isinstance(val, str):
9+
return False
10+
try:
11+
datetime.datetime.fromisoformat(val.replace("Z", "+00:00"))
12+
return True
13+
except (ValueError, AttributeError):
14+
return False
15+
16+
17+
def _datetime_str_to_ms(val):
18+
"""Convert a datetime string to milliseconds since epoch."""
19+
dt = datetime.datetime.fromisoformat(val.replace("Z", "+00:00"))
20+
if dt.tzinfo is None:
21+
dt = dt.replace(tzinfo=datetime.timezone.utc)
22+
return dt.timestamp() * 1000
23+
24+
25+
def _ms_to_datetime_str(ms):
26+
"""Convert milliseconds since epoch back to a datetime string."""
27+
dt = datetime.datetime.fromtimestamp(ms / 1000, tz=datetime.timezone.utc)
28+
return dt.strftime("%Y-%m-%d %H:%M:%S")
29+
330

431
def _mean(x):
532
if len(x) == 0:
633
raise ValueError("x must have positive length")
7-
return float(sum(x)) / len(x)
34+
try:
35+
return float(sum(x)) / len(x)
36+
except TypeError:
37+
# Handle non-numeric types like datetime strings or datetime objects
38+
if all(_is_date_string(v) for v in x):
39+
ms_values = [_datetime_str_to_ms(v) for v in x]
40+
mean_ms = sum(ms_values) / len(ms_values)
41+
return _ms_to_datetime_str(mean_ms)
42+
# Handle datetime.datetime, pd.Timestamp, or similar objects
43+
if all(hasattr(v, "timestamp") for v in x):
44+
ts_values = [v.timestamp() * 1000 for v in x]
45+
mean_ms = sum(ts_values) / len(ts_values)
46+
return datetime.datetime.fromtimestamp(
47+
mean_ms / 1000, tz=datetime.timezone.utc
48+
).isoformat()
49+
raise
850

951

1052
def _argmin(x):

tests/test_optional/test_autoshapes/test_annotated_shapes.py

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -427,3 +427,79 @@ def test_all_annotation_positions():
427427

428428
if __name__ == "__main__":
429429
draw_all_annotation_positions()
430+
431+
432+
# Tests for datetime axis annotation support (issue #3065)
433+
import datetime
434+
435+
436+
def test_vline_datetime_string_annotation():
437+
"""add_vline with annotation_text on datetime x-axis should not crash."""
438+
fig = go.Figure()
439+
fig.add_trace(go.Scatter(x=["2018-01-01", "2018-06-01", "2018-12-31"], y=[1, 2, 3]))
440+
fig.add_vline(x="2018-09-24", annotation_text="test")
441+
assert len(fig.layout.annotations) == 1
442+
assert fig.layout.annotations[0].text == "test"
443+
assert fig.layout.annotations[0].x == "2018-09-24"
444+
445+
446+
def test_hline_with_datetime_xaxis():
447+
"""numeric add_hline should still work with datetime x-axis."""
448+
fig = go.Figure()
449+
fig.add_trace(go.Scatter(x=["2018-01-01", "2018-06-01", "2018-12-31"], y=[1, 2, 3]))
450+
fig.add_hline(y=2, annotation_text="hline test")
451+
assert len(fig.layout.annotations) == 1
452+
assert fig.layout.annotations[0].text == "hline test"
453+
assert fig.layout.annotations[0].y == 2
454+
455+
456+
def test_vrect_datetime_string_annotation():
457+
"""add_vrect with annotation_text on datetime x-axis should not crash."""
458+
fig = go.Figure()
459+
fig.add_trace(go.Scatter(x=["2018-01-01", "2018-06-01", "2018-12-31"], y=[1, 2, 3]))
460+
fig.add_vrect(x0="2018-03-01", x1="2018-09-01", annotation_text="rect test")
461+
assert len(fig.layout.annotations) == 1
462+
assert fig.layout.annotations[0].text == "rect test"
463+
assert fig.layout.annotations[0].x == "2018-09-01"
464+
465+
466+
def test_vline_datetime_object_annotation():
467+
"""add_vline with datetime.datetime object should not crash."""
468+
fig = go.Figure()
469+
fig.add_trace(
470+
go.Scatter(
471+
x=[
472+
datetime.datetime(2018, 1, 1),
473+
datetime.datetime(2018, 6, 1),
474+
datetime.datetime(2018, 12, 31),
475+
],
476+
y=[1, 2, 3],
477+
)
478+
)
479+
fig.add_vline(x=datetime.datetime(2018, 9, 24), annotation_text="dt test")
480+
assert len(fig.layout.annotations) == 1
481+
assert fig.layout.annotations[0].text == "dt test"
482+
assert fig.layout.annotations[0].x == datetime.datetime(2018, 9, 24, 0, 0)
483+
484+
485+
def test_vrect_datetime_object_annotation():
486+
"""add_vrect with datetime.datetime objects should compute correct mean."""
487+
fig = go.Figure()
488+
fig.add_trace(
489+
go.Scatter(
490+
x=[
491+
datetime.datetime(2018, 1, 1),
492+
datetime.datetime(2018, 6, 1),
493+
datetime.datetime(2018, 12, 31),
494+
],
495+
y=[1, 2, 3],
496+
)
497+
)
498+
fig.add_vrect(
499+
x0=datetime.datetime(2018, 3, 1),
500+
x1=datetime.datetime(2018, 9, 1),
501+
annotation_text="rect dt test",
502+
)
503+
assert len(fig.layout.annotations) == 1
504+
assert fig.layout.annotations[0].text == "rect dt test"
505+
assert fig.layout.annotations[0].x == datetime.datetime(2018, 9, 1)

0 commit comments

Comments
 (0)