Skip to content

Commit d94cbf7

Browse files
authored
fixup timezones
fixup timezone handling, by Marco
2 parents e47827e + 18cc11c commit d94cbf7

20 files changed

+57
-72
lines changed

packages/python/plotly/_plotly_utils/basevalidators.py

Lines changed: 16 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -73,8 +73,6 @@ def copy_to_readonly_numpy_array(v, kind=None, force_numeric=False):
7373
"""
7474
np = get_module("numpy")
7575

76-
# Don't force pandas to be loaded, we only want to know if it's already loaded
77-
pd = get_module("pandas", should_load=False)
7876
assert np is not None
7977

8078
# ### Process kind ###
@@ -94,45 +92,26 @@ def copy_to_readonly_numpy_array(v, kind=None, force_numeric=False):
9492
"O": "object",
9593
}
9694

95+
# With `pass_through=True``, the original object will be returned if unable to convert
96+
# to a Narwhals DataFrame or Series.
97+
v = nw.from_native(v, allow_series=True, pass_through=True)
98+
9799
if isinstance(v, nw.Series):
98-
if nw.dependencies.is_pandas_like_series(v_native := v.to_native()):
99-
v = v_native
100+
if v.dtype == nw.Datetime and v.dtype.time_zone is not None:
101+
# Remove time zone so that local time is displayed
102+
v = v.dt.replace_time_zone(None).to_numpy()
100103
else:
101104
v = v.to_numpy()
102105
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-
]
106+
schema = v.schema
107+
overrides = {}
108+
for key, val in schema.items():
109+
if val == nw.Datetime and val.time_zone is not None:
110+
# Remove time zone so that local time is displayed
111+
overrides[key] = nw.col(key).dt.replace_time_zone(None)
112+
if overrides:
113+
v = v.with_columns(**overrides)
114+
v = v.to_numpy()
136115

137116
if not isinstance(v, np.ndarray):
138117
# 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: 18 additions & 10 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(
@@ -1166,7 +1174,7 @@ def to_unindexed_series(x, name=None, native_namespace=None):
11661174
its index reset if pandas-like). Stripping the index from existing pd.Series is
11671175
required to get things to match up right in the new DataFrame we're building.
11681176
"""
1169-
x = nw.from_native(x, series_only=True, strict=False)
1177+
x = nw.from_native(x, series_only=True, pass_through=True)
11701178
if isinstance(x, nw.Series):
11711179
return nw.maybe_reset_index(x).rename(name)
11721180
elif native_namespace is not None:
@@ -1372,7 +1380,7 @@ def process_args_into_dataframe(
13721380
)
13731381

13741382
df_output[str(col_name)] = to_unindexed_series(
1375-
x=nw.from_native(argument, series_only=True, strict=False),
1383+
x=nw.from_native(argument, series_only=True, pass_through=True),
13761384
name=str(col_name),
13771385
native_namespace=native_namespace,
13781386
)
@@ -1500,11 +1508,11 @@ def build_dataframe(args, constructor):
15001508
is_pd_like = True
15011509

15021510
# data_frame is any other DataFrame object natively supported via Narwhals.
1503-
# With strict=False, the original object will be returned if unable to convert
1511+
# With pass_through=True, the original object will be returned if unable to convert
15041512
# to a Narwhals DataFrame, making this condition False.
15051513
elif isinstance(
15061514
data_frame := nw.from_native(
1507-
args["data_frame"], eager_or_interchange_only=True, strict=False
1515+
args["data_frame"], eager_or_interchange_only=True, pass_through=True
15081516
),
15091517
nw.DataFrame,
15101518
):
@@ -1513,11 +1521,11 @@ def build_dataframe(args, constructor):
15131521
columns = args["data_frame"].columns
15141522

15151523
# data_frame is any other Series object natively supported via Narwhals.
1516-
# With strict=False, the original object will be returned if unable to convert
1517-
# to a Narwhals DataFrame, making this condition False.
1524+
# With pass_through=True, the original object will be returned if unable to convert
1525+
# to a Narwhals Series, making this condition False.
15181526
elif isinstance(
15191527
series := nw.from_native(
1520-
args["data_frame"], series_only=True, strict=False
1528+
args["data_frame"], series_only=True, pass_through=True
15211529
),
15221530
nw.Series,
15231531
):

packages/python/plotly/plotly/express/_imshow.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -321,7 +321,7 @@ def imshow(
321321
aspect = "equal"
322322

323323
# --- Set the value of binary_string (forbidden for pandas)
324-
img = nw.from_native(img, strict=False)
324+
img = nw.from_native(img, pass_through=True)
325325
if isinstance(img, nw.DataFrame):
326326
if binary_string:
327327
raise ValueError("Binary strings cannot be used with pandas arrays")

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

0 commit comments

Comments
 (0)