Skip to content

Commit 2630a5a

Browse files
committed
fixup time zone handling
1 parent e47827e commit 2630a5a

19 files changed

+45
-62
lines changed

packages/python/plotly/_plotly_utils/basevalidators.py

Lines changed: 12 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -95,44 +95,21 @@ def copy_to_readonly_numpy_array(v, kind=None, force_numeric=False):
9595
}
9696

9797
if isinstance(v, nw.Series):
98-
if nw.dependencies.is_pandas_like_series(v_native := v.to_native()):
99-
v = v_native
98+
if v.dtype == nw.Datetime and v.dtype.time_zone is not None:
99+
# Remove time zone so that local time is displayed
100+
v = v.dt.replace_time_zone(None).to_numpy()
100101
else:
101102
v = v.to_numpy()
102103
elif isinstance(v, nw.DataFrame):
103-
if nw.dependencies.is_pandas_like_dataframe(v_native := v.to_native()):
104-
v = v_native
105-
else:
106-
v = v.to_numpy()
107-
108-
if pd and isinstance(v, (pd.Series, pd.Index)):
109-
# Handle pandas Series and Index objects
110-
if v.dtype.kind in numeric_kinds:
111-
# Get the numeric numpy array so we use fast path below
112-
v = v.values
113-
elif v.dtype.kind == "M":
114-
# Convert datetime Series/Index to numpy array of datetimes
115-
if isinstance(v, pd.Series):
116-
with warnings.catch_warnings():
117-
warnings.simplefilter("ignore", FutureWarning)
118-
# Series.dt.to_pydatetime will return Index[object]
119-
# https://github.com/pandas-dev/pandas/pull/52459
120-
v = np.array(v.dt.to_pydatetime())
121-
else:
122-
# DatetimeIndex
123-
v = v.to_pydatetime()
124-
elif pd and isinstance(v, pd.DataFrame) and len(set(v.dtypes)) == 1:
125-
dtype = v.dtypes.tolist()[0]
126-
if dtype.kind in numeric_kinds:
127-
v = v.values
128-
elif dtype.kind == "M":
129-
with warnings.catch_warnings():
130-
warnings.simplefilter("ignore", FutureWarning)
131-
# Series.dt.to_pydatetime will return Index[object]
132-
# https://github.com/pandas-dev/pandas/pull/52459
133-
v = [
134-
np.array(row.dt.to_pydatetime()).tolist() for i, row in v.iterrows()
135-
]
104+
schema = v.schema
105+
overrides = {}
106+
for key, val in schema.items():
107+
if val == nw.Datetime and val.time_zone is not None:
108+
# Remove time zone so that local time is displayed
109+
overrides[key] = nw.col(key).dt.replace_time_zone(None)
110+
if overrides:
111+
v = v.with_columns(**overrides)
112+
v = v.to_numpy()
136113

137114
if not isinstance(v, np.ndarray):
138115
# v has its own logic on how to convert itself into a numpy array

packages/python/plotly/optional-requirements.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ ipython
3939

4040
## pandas deps for some matplotlib functionality ##
4141
pandas
42-
narwhals>=1.12.0
42+
narwhals>=1.13.1
4343

4444
## scipy deps for some FigureFactory functions ##
4545
scipy

packages/python/plotly/plotly/express/_core.py

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -413,12 +413,20 @@ def make_trace_kwargs(args, trace_spec, trace_data, mapping_labels, sizeref):
413413
# i.e. we can't do resampling, because then the X values might not line up!
414414
non_missing = ~(x.is_null() | y.is_null())
415415
trace_patch["x"] = (
416-
sorted_trace_data.filter(non_missing)
417-
.get_column(args["x"])
418-
.to_numpy()
416+
sorted_trace_data.filter(non_missing).get_column(args["x"])
419417
# FIXME: Converting to numpy is needed to pass `test_trendline_on_timeseries`
420418
# test, but I wonder if it is the right way to do it in the first place.
421419
)
420+
if (
421+
trace_patch["x"].dtype == nw.Datetime
422+
and trace_patch["x"].dtype.time_zone is not None
423+
):
424+
# Remove time zone so that local time is displayed
425+
trace_patch["x"] = (
426+
trace_patch["x"].dt.replace_time_zone(None).to_numpy()
427+
)
428+
else:
429+
trace_patch["x"] = trace_patch["x"].to_numpy()
422430

423431
trendline_function = trendline_functions[attr_value]
424432
y_out, hover_header, fit_results = trendline_function(

packages/python/plotly/plotly/tests/test_optional/test_px/test_px_hover.py

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -190,14 +190,12 @@ def test_sunburst_hoverdict_color(constructor):
190190

191191

192192
def test_date_in_hover(request, constructor):
193-
if "pyarrow_table" in str(constructor) or "polars_eager" in str(constructor):
194-
# fig.data[0].customdata[0][0] is a numpy.datetime64 for non pandas
195-
# input, and it does not keep the timezone when converting to py scalar
196-
request.applymarker(pytest.mark.xfail)
197-
198193
df = nw.from_native(
199194
constructor({"date": ["2015-04-04 19:31:30+01:00"], "value": [3]})
200195
).with_columns(date=nw.col("date").str.to_datetime(format="%Y-%m-%d %H:%M:%S%z"))
201196
fig = px.scatter(df.to_native(), x="value", y="value", hover_data=["date"])
202197

203-
assert fig.data[0].customdata[0][0] == df.item(row=0, column="date")
198+
# Check that what gets displayed is the local datetime
199+
assert nw.to_py_scalar(fig.data[0].customdata[0][0]) == nw.to_py_scalar(
200+
df.item(row=0, column="date")
201+
).replace(tzinfo=None)

packages/python/plotly/plotly/tests/test_optional/test_utils/test_utils.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -270,7 +270,7 @@ def test_encode_customdata_datetime_series(self):
270270
)
271271
self.assertTrue(
272272
fig_json.startswith(
273-
'{"data":[{"customdata":["2010-01-01T00:00:00","2010-01-02T00:00:00"]'
273+
'{"data":[{"customdata":["2010-01-01T00:00:00.000000000","2010-01-02T00:00:00.000000000"]'
274274
)
275275
)
276276

@@ -292,8 +292,8 @@ def test_encode_customdata_datetime_homogeneous_dataframe(self):
292292
self.assertTrue(
293293
fig_json.startswith(
294294
'{"data":[{"customdata":'
295-
'[["2010-01-01T00:00:00","2011-01-01T00:00:00"],'
296-
'["2010-01-02T00:00:00","2011-01-02T00:00:00"]'
295+
'[["2010-01-01T00:00:00.000000000","2011-01-01T00:00:00.000000000"],'
296+
'["2010-01-02T00:00:00.000000000","2011-01-02T00:00:00.000000000"]'
297297
)
298298
)
299299

packages/python/plotly/requirements.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,4 @@
66
###################################################
77

88
## dataframe agnostic layer ##
9-
narwhals>=1.12.0
9+
narwhals>=1.13.1

packages/python/plotly/setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -603,7 +603,7 @@ def run(self):
603603
data_files=[
604604
("etc/jupyter/nbconfig/notebook.d", ["jupyterlab-plotly.json"]),
605605
],
606-
install_requires=["narwhals>=1.12.0", "packaging"],
606+
install_requires=["narwhals>=1.13.1", "packaging"],
607607
zip_safe=False,
608608
cmdclass=dict(
609609
build_py=js_prerelease(versioneer_cmds["build_py"]),
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
requests==2.25.1
22
pytest==7.4.4
3-
narwhals>=1.12.0
3+
narwhals>=1.13.1

packages/python/plotly/test_requirements/requirements_310_optional.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,4 +21,4 @@ kaleido
2121
orjson==3.8.12
2222
polars[timezone]
2323
pyarrow
24-
narwhals>=1.12.0
24+
narwhals>=1.13.1
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
requests==2.25.1
22
pytest==7.4.4
3-
narwhals>=1.12.0
3+
narwhals>=1.13.1

0 commit comments

Comments
 (0)