diff --git a/_plotly_utils/basevalidators.py b/_plotly_utils/basevalidators.py index 0d7e387bbc3..c4d40f9e178 100644 --- a/_plotly_utils/basevalidators.py +++ b/_plotly_utils/basevalidators.py @@ -22,6 +22,20 @@ def fullmatch(regex, string, flags=0): return re.match("(?:" + regex_string + r")\Z", string, flags=flags) +def to_non_numpy_type(np, v): + """ + Convert a numpy scalar value to a native Python type. + Calling .item() on a datetime64[ns] value returns an integer, since + Python datetimes only support microsecond precision. So we cast + datetime64[ns] to datetime64[us] to ensure it remains a datetime. + + Should only be used in contexts where we already know `np` is defined. + """ + if hasattr(v, "dtype") and v.dtype == np.dtype("datetime64[ns]"): + return v.astype("datetime64[us]").item() + return v.item() + + # Utility functions # ----------------- def to_scalar_or_list(v): @@ -35,12 +49,12 @@ def to_scalar_or_list(v): np = get_module("numpy", should_load=False) pd = get_module("pandas", should_load=False) if np and np.isscalar(v) and hasattr(v, "item"): - return v.item() + return to_non_numpy_type(np, v) if isinstance(v, (list, tuple)): return [to_scalar_or_list(e) for e in v] elif np and isinstance(v, np.ndarray): if v.ndim == 0: - return v.item() + return to_non_numpy_type(np, v) return [to_scalar_or_list(e) for e in v] elif pd and isinstance(v, (pd.Series, pd.Index)): return [to_scalar_or_list(e) for e in v] diff --git a/tests/test_optional/test_graph_objs/test_numpy.py b/tests/test_optional/test_graph_objs/test_numpy.py new file mode 100644 index 00000000000..a234dc47830 --- /dev/null +++ b/tests/test_optional/test_graph_objs/test_numpy.py @@ -0,0 +1,16 @@ +from datetime import datetime + +import numpy as np + +import plotly.graph_objs as go + + +def test_np_ns_datetime(): + x = [np.datetime64("2025-09-26").astype("datetime64[ns]")] + y = [1.23] + scatter = go.Scatter(x=x, y=y, mode="markers") + + # x value should be converted to native datetime + assert isinstance(scatter.x[0], datetime) + # x value should match original numpy value at microsecond precision + assert x[0].astype("datetime64[us]").item() == scatter.x[0]