Skip to content

Commit 03979d1

Browse files
Merge pull request #3022 from plotly/numpy_date_serialization
Pandas and Numpy datetime serialization fixes
2 parents 7540acf + 7d5023b commit 03979d1

File tree

4 files changed

+101
-4
lines changed

4 files changed

+101
-4
lines changed

Diff for: packages/python/plotly/_plotly_utils/basevalidators.py

+8-1
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,13 @@ def copy_to_readonly_numpy_array(v, kind=None, force_numeric=False):
103103
else:
104104
# DatetimeIndex
105105
v = v.to_pydatetime()
106+
elif pd and isinstance(v, pd.DataFrame) and len(set(v.dtypes)) == 1:
107+
dtype = v.dtypes.tolist()[0]
108+
if dtype.kind in numeric_kinds:
109+
v = v.values
110+
elif dtype.kind == "M":
111+
v = [row.dt.to_pydatetime().tolist() for i, row in v.iterrows()]
112+
106113
if not isinstance(v, np.ndarray):
107114
# v has its own logic on how to convert itself into a numpy array
108115
if is_numpy_convertable(v):
@@ -146,7 +153,7 @@ def copy_to_readonly_numpy_array(v, kind=None, force_numeric=False):
146153
# datatype. This works around cases like np.array([1, 2, '3']) where
147154
# numpy converts the integers to strings and returns array of dtype
148155
# '<U21'
149-
if new_v.dtype.kind not in ["u", "i", "f", "O"]:
156+
if new_v.dtype.kind not in ["u", "i", "f", "O", "M"]:
150157
new_v = np.array(v, dtype="object")
151158

152159
# Set new array to be read-only

Diff for: packages/python/plotly/_plotly_utils/tests/validators/test_pandas_series_input.py

+20-1
Original file line numberDiff line numberDiff line change
@@ -173,7 +173,9 @@ def test_color_validator_categorical(color_validator, color_categorical_pandas):
173173
np.testing.assert_array_equal(res, np.array(color_categorical_pandas))
174174

175175

176-
def test_data_array_validator_dates(data_array_validator, datetime_pandas, dates_array):
176+
def test_data_array_validator_dates_series(
177+
data_array_validator, datetime_pandas, dates_array
178+
):
177179

178180
res = data_array_validator.validate_coerce(datetime_pandas)
179181

@@ -185,3 +187,20 @@ def test_data_array_validator_dates(data_array_validator, datetime_pandas, dates
185187

186188
# Check values
187189
np.testing.assert_array_equal(res, dates_array)
190+
191+
192+
def test_data_array_validator_dates_dataframe(
193+
data_array_validator, datetime_pandas, dates_array
194+
):
195+
196+
df = pd.DataFrame({"d": datetime_pandas})
197+
res = data_array_validator.validate_coerce(df)
198+
199+
# Check type
200+
assert isinstance(res, np.ndarray)
201+
202+
# Check dtype
203+
assert res.dtype == "object"
204+
205+
# Check values
206+
np.testing.assert_array_equal(res, dates_array.reshape(len(dates_array), 1))

Diff for: packages/python/plotly/_plotly_utils/utils.py

+7-2
Original file line numberDiff line numberDiff line change
@@ -184,8 +184,13 @@ def encode_as_numpy(obj):
184184

185185
if obj is numpy.ma.core.masked:
186186
return float("nan")
187-
else:
188-
raise NotEncodable
187+
elif isinstance(obj, numpy.ndarray) and obj.dtype.kind == "M":
188+
try:
189+
return numpy.datetime_as_string(obj).tolist()
190+
except TypeError:
191+
pass
192+
193+
raise NotEncodable
189194

190195
@staticmethod
191196
def encode_as_datetime(obj):

Diff for: packages/python/plotly/plotly/tests/test_optional/test_utils/test_utils.py

+66
Original file line numberDiff line numberDiff line change
@@ -257,6 +257,63 @@ def test_pandas_json_encoding(self):
257257
j6 = _json.dumps(ts.index, cls=utils.PlotlyJSONEncoder)
258258
assert j6 == '["2011-01-01T00:00:00", "2011-01-01T01:00:00"]'
259259

260+
def test_encode_customdata_datetime_series(self):
261+
df = pd.DataFrame(dict(t=pd.to_datetime(["2010-01-01", "2010-01-02"])))
262+
263+
# 1D customdata
264+
fig = Figure(
265+
Scatter(x=df["t"], customdata=df["t"]), layout=dict(template="none")
266+
)
267+
fig_json = _json.dumps(
268+
fig, cls=utils.PlotlyJSONEncoder, separators=(",", ":"), sort_keys=True
269+
)
270+
self.assertTrue(
271+
fig_json.startswith(
272+
'{"data":[{"customdata":["2010-01-01T00:00:00","2010-01-02T00:00:00"]'
273+
)
274+
)
275+
276+
def test_encode_customdata_datetime_homogenous_dataframe(self):
277+
df = pd.DataFrame(
278+
dict(
279+
t1=pd.to_datetime(["2010-01-01", "2010-01-02"]),
280+
t2=pd.to_datetime(["2011-01-01", "2011-01-02"]),
281+
)
282+
)
283+
# 2D customdata
284+
fig = Figure(
285+
Scatter(x=df["t1"], customdata=df[["t1", "t2"]]),
286+
layout=dict(template="none"),
287+
)
288+
fig_json = _json.dumps(
289+
fig, cls=utils.PlotlyJSONEncoder, separators=(",", ":"), sort_keys=True
290+
)
291+
self.assertTrue(
292+
fig_json.startswith(
293+
'{"data":[{"customdata":'
294+
'[["2010-01-01T00:00:00","2011-01-01T00:00:00"],'
295+
'["2010-01-02T00:00:00","2011-01-02T00:00:00"]'
296+
)
297+
)
298+
299+
def test_encode_customdata_datetime_inhomogenous_dataframe(self):
300+
df = pd.DataFrame(
301+
dict(t=pd.to_datetime(["2010-01-01", "2010-01-02"]), v=np.arange(2),)
302+
)
303+
# 2D customdata
304+
fig = Figure(
305+
Scatter(x=df["t"], customdata=df[["t", "v"]]), layout=dict(template="none")
306+
)
307+
fig_json = _json.dumps(
308+
fig, cls=utils.PlotlyJSONEncoder, separators=(",", ":"), sort_keys=True
309+
)
310+
self.assertTrue(
311+
fig_json.startswith(
312+
'{"data":[{"customdata":'
313+
'[["2010-01-01T00:00:00",0],["2010-01-02T00:00:00",1]]'
314+
)
315+
)
316+
260317
def test_numpy_masked_json_encoding(self):
261318
l = [1, 2, np.ma.core.masked]
262319
j1 = _json.dumps(l, cls=utils.PlotlyJSONEncoder)
@@ -277,6 +334,15 @@ def test_datetime_dot_date(self):
277334
j1 = _json.dumps(a, cls=utils.PlotlyJSONEncoder)
278335
assert j1 == '["2014-01-01", "2014-01-02"]'
279336

337+
def test_numpy_datetime64(self):
338+
a = pd.date_range("2011-07-11", "2011-07-13", freq="D").values
339+
j1 = _json.dumps(a, cls=utils.PlotlyJSONEncoder)
340+
assert (
341+
j1 == '["2011-07-11T00:00:00.000000000", '
342+
'"2011-07-12T00:00:00.000000000", '
343+
'"2011-07-13T00:00:00.000000000"]'
344+
)
345+
280346
def test_pil_image_encoding(self):
281347
import _plotly_utils
282348

0 commit comments

Comments
 (0)