Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit d309589

Browse files
committed
Merge pull request plotly#159 from plotly/jsonify-masked-numbers
Add encoder to handle masked numbers.
2 parents cf49b6c + 6ba5371 commit d309589

File tree

3 files changed

+171
-67
lines changed

3 files changed

+171
-67
lines changed

plotly/tests/test_optional/test_utils/test_utils.py

Lines changed: 60 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,23 @@
66
import numpy as np
77
import json
88
import pandas as pd
9+
from pandas.util.testing import assert_series_equal
910

1011
from plotly import utils
1112
from plotly.grid_objs import Column
1213
from plotly.graph_objs import Scatter, Scatter3d, Figure, Data
1314

1415
## JSON encoding
1516
numeric_list = [1, 2, 3]
16-
np_list = np.array([1, 2, 3])
17-
mixed_list = [1, 'A', dt(2014, 1, 5)]
18-
pd = pd.DataFrame(columns=['col 1'], data=[1, 2, 3])
17+
np_list = np.array([1, 2, 3, np.NaN, np.NAN, np.Inf, dt(2014, 1, 5)])
18+
mixed_list = [1, 'A', dt(2014, 1, 5), dt(2014, 1, 5, 1, 1, 1),
19+
dt(2014, 1, 5, 1, 1, 1, 1)]
20+
21+
df = pd.DataFrame(columns=['col 1'],
22+
data=[1, 2, 3, dt(2014, 1, 5), pd.NaT, np.NaN, np.Inf])
23+
24+
rng = pd.date_range('1/1/2011', periods=2, freq='H')
25+
ts = pd.Series([1.5, 2.5], index=rng)
1926

2027

2128
def test_column_json_encoding():
@@ -28,21 +35,66 @@ def test_column_json_encoding():
2835
columns, cls=utils._plotlyJSONEncoder, sort_keys=True
2936
)
3037
assert('[{"data": [1, 2, 3], "name": "col 1"}, '
31-
'{"data": [1, "A", "2014-01-05"], "name": "col 2"}, '
32-
'{"data": [1, 2, 3], "name": "col 3"}]' == json_columns)
38+
'{"data": [1, "A", "2014-01-05", '
39+
'"2014-01-05 01:01:01", '
40+
'"2014-01-05 01:01:01.000001"], '
41+
'"name": "col 2"}, '
42+
'{"data": [1, 2, 3, NaN, NaN, Infinity, '
43+
'"2014-01-05"], "name": "col 3"}]' == json_columns)
3344

3445

3546
def test_figure_json_encoding():
47+
df = pd.DataFrame(columns=['col 1'], data=[1, 2, 3])
3648
s1 = Scatter3d(x=numeric_list, y=np_list, z=mixed_list)
37-
s2 = Scatter(x=pd['col 1'])
49+
s2 = Scatter(x=df['col 1'])
3850
data = Data([s1, s2])
3951
figure = Figure(data=data)
4052

4153
js1 = json.dumps(s1, cls=utils._plotlyJSONEncoder, sort_keys=True)
4254
js2 = json.dumps(s2, cls=utils._plotlyJSONEncoder, sort_keys=True)
4355

44-
assert(js1 == '{"type": "scatter3d", "x": [1, 2, 3], '+
45-
'"y": [1, 2, 3], "z": [1, "A", "2014-01-05"]}')
56+
assert(js1 == '{"type": "scatter3d", "x": [1, 2, 3], '
57+
'"y": [1, 2, 3, NaN, NaN, Infinity, "2014-01-05"], '
58+
'"z": [1, "A", "2014-01-05", '
59+
'"2014-01-05 01:01:01", "2014-01-05 01:01:01.000001"]}')
4660
assert(js2 == '{"type": "scatter", "x": [1, 2, 3]}')
61+
62+
# Test JSON encoding works
4763
json.dumps(data, cls=utils._plotlyJSONEncoder, sort_keys=True)
4864
json.dumps(figure, cls=utils._plotlyJSONEncoder, sort_keys=True)
65+
66+
# Test data wasn't mutated
67+
assert(bool(np.asarray(np_list ==
68+
np.array([1, 2, 3, np.NaN,
69+
np.NAN, np.Inf, dt(2014, 1, 5)])).all()))
70+
assert(set(data[0]['z']) ==
71+
set([1, 'A', dt(2014, 1, 5), dt(2014, 1, 5, 1, 1, 1),
72+
dt(2014, 1, 5, 1, 1, 1, 1)]))
73+
74+
75+
def test_pandas_json_encoding():
76+
j1 = json.dumps(df['col 1'], cls=utils._plotlyJSONEncoder)
77+
assert(j1 == '[1, 2, 3, "2014-01-05", null, NaN, Infinity]')
78+
79+
# Test that data wasn't mutated
80+
assert_series_equal(df['col 1'],
81+
pd.Series([1, 2, 3, dt(2014, 1, 5),
82+
pd.NaT, np.NaN, np.Inf]))
83+
84+
j2 = json.dumps(df.index, cls=utils._plotlyJSONEncoder)
85+
assert(j2 == '[0, 1, 2, 3, 4, 5, 6]')
86+
87+
nat = [pd.NaT]
88+
j3 = json.dumps(nat, cls=utils._plotlyJSONEncoder)
89+
assert(j3 == '[null]')
90+
assert(nat[0] is pd.NaT)
91+
92+
j4 = json.dumps(rng, cls=utils._plotlyJSONEncoder)
93+
assert(j4 == '["2011-01-01", "2011-01-01 01:00:00"]')
94+
95+
j5 = json.dumps(ts, cls=utils._plotlyJSONEncoder)
96+
assert(j5 == '[1.5, 2.5]')
97+
assert_series_equal(ts, pd.Series([1.5, 2.5], index=rng))
98+
99+
j6 = json.dumps(ts.index, cls=utils._plotlyJSONEncoder)
100+
assert(j6 == '["2011-01-01", "2011-01-01 01:00:00"]')

plotly/utils.py

Lines changed: 110 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,26 @@
1111
import sys
1212
import threading
1313
import re
14+
import datetime
15+
16+
try:
17+
import numpy
18+
_numpy_imported = True
19+
except ImportError:
20+
_numpy_imported = False
21+
22+
try:
23+
import pandas
24+
_pandas_imported = True
25+
except ImportError:
26+
_pandas_imported = False
27+
28+
try:
29+
from sage.all import RR, ZZ
30+
_sage_imported = True
31+
except ImportError:
32+
_sage_imported = False
33+
1434

1535
### incase people are using threading, we lock file reads
1636
lock = threading.Lock()
@@ -72,79 +92,111 @@ def ensure_dir_exists(directory):
7292

7393

7494
### Custom JSON encoders ###
95+
class NotEncodable(Exception):
96+
pass
97+
98+
7599
class _plotlyJSONEncoder(json.JSONEncoder):
76100
def numpyJSONEncoder(self, obj):
77-
try:
78-
import numpy
79-
if type(obj).__module__.split('.')[0] == numpy.__name__:
80-
l = obj.tolist()
81-
d = self.datetimeJSONEncoder(l)
82-
return d if d is not None else l
83-
except:
84-
pass
85-
return None
101+
if not _numpy_imported:
102+
raise NotEncodable
103+
104+
if type(obj).__module__.split('.')[0] == numpy.__name__:
105+
l = obj.tolist()
106+
try:
107+
return self.datetimeJSONEncoder(l)
108+
except NotEncodable:
109+
return l
110+
else:
111+
raise NotEncodable
86112

87113
def datetimeJSONEncoder(self, obj):
88-
# if datetime or iterable of datetimes, convert to a string that plotly understands
89-
# format as %Y-%m-%d %H:%M:%S.%f, %Y-%m-%d %H:%M:%S, or %Y-%m-%d depending on what non-zero resolution was provided
90-
import datetime
91-
try:
92-
if isinstance(obj, (datetime.datetime, datetime.date)):
93-
if obj.microsecond != 0:
94-
return obj.strftime('%Y-%m-%d %H:%M:%S.%f')
95-
elif obj.second != 0 or obj.minute != 0 or obj.hour != 0:
96-
return obj.strftime('%Y-%m-%d %H:%M:%S')
97-
else:
98-
return obj.strftime('%Y-%m-%d')
99-
elif isinstance(obj[0], (datetime.datetime, datetime.date)):
100-
return [o.strftime(
101-
'%Y-%m-%d %H:%M:%S.%f') if o.microsecond != 0 else
102-
o.strftime('%Y-%m-%d %H:%M:%S') if o.second != 0 or o.minute != 0 or o.hour != 0 else
103-
o.strftime('%Y-%m-%d')
104-
for o in obj]
105-
except:
106-
pass
107-
return None
114+
"""
115+
if datetime or iterable of datetimes,
116+
convert to a string that plotly understands
117+
format as %Y-%m-%d %H:%M:%S.%f,
118+
%Y-%m-%d %H:%M:%S, or
119+
%Y-%m-%d
120+
depending on what non-zero resolution was provided
121+
"""
122+
123+
if _pandas_imported and obj is pandas.NaT:
124+
return None
125+
126+
if isinstance(obj, (datetime.datetime, datetime.date)):
127+
if obj.microsecond:
128+
return obj.strftime('%Y-%m-%d %H:%M:%S.%f')
129+
elif any((obj.second, obj.minute, obj.hour)):
130+
return obj.strftime('%Y-%m-%d %H:%M:%S')
131+
else:
132+
return obj.strftime('%Y-%m-%d')
133+
elif isinstance(obj[0], (datetime.datetime, datetime.date)):
134+
return [o.strftime(
135+
'%Y-%m-%d %H:%M:%S.%f') if o.microsecond else
136+
o.strftime('%Y-%m-%d %H:%M:%S') if any((o.second, o.minute, o.hour)) else
137+
o.strftime('%Y-%m-%d')
138+
for o in obj]
139+
else:
140+
raise NotEncodable
108141

109142
def pandasJSONEncoder(self, obj):
110-
try:
111-
import pandas
112-
if isinstance(obj, pandas.Series):
113-
return obj.tolist()
114-
except:
115-
pass
116-
return None
143+
if not _pandas_imported:
144+
raise NotEncodable
145+
146+
if isinstance(obj, pandas.Series):
147+
serializable_list = []
148+
for li in list(obj):
149+
try:
150+
json.dumps(li)
151+
serializable_list.append(li)
152+
except TypeError:
153+
serializable_list.append(self.default(li))
154+
155+
return serializable_list
156+
elif isinstance(obj, pandas.Index):
157+
return obj.tolist()
158+
elif obj is pandas.NaT:
159+
return None
160+
else:
161+
raise NotEncodable
117162

118163
def sageJSONEncoder(self, obj):
119-
try:
120-
from sage.all import RR, ZZ
121-
if obj in RR:
122-
return float(obj)
123-
elif obj in ZZ:
124-
return int(obj)
125-
except:
126-
pass
127-
return None
164+
if not _sage_imported:
165+
raise NotEncodable
166+
167+
if obj in RR:
168+
return float(obj)
169+
elif obj in ZZ:
170+
return int(obj)
171+
else:
172+
raise NotEncodable
128173

129174
def builtinJSONEncoder(self, obj):
175+
'''
176+
Provide an API for folks to write their own
177+
JSON encoders for their objects that they wanna
178+
send to Plotly: this is an object's `to_plotly_json` method
179+
180+
Used for grid_objs.Column objects.
181+
'''
130182
try:
131183
return obj.to_plotly_json()
132184
except AttributeError:
133-
return None
185+
raise NotEncodable
134186

135187
def default(self, obj):
136-
try:
137-
return json.dumps(obj)
138-
except TypeError as e:
139-
encoders = (self.builtinJSONEncoder, self.datetimeJSONEncoder,
140-
self.numpyJSONEncoder, self.pandasJSONEncoder,
141-
self.sageJSONEncoder)
142-
for encoder in encoders:
143-
s = encoder(obj)
144-
if s is not None:
145-
return s
146-
raise e
147-
return json.JSONEncoder.default(self, obj)
188+
encoders = (self.builtinJSONEncoder,
189+
self.datetimeJSONEncoder,
190+
self.numpyJSONEncoder,
191+
self.pandasJSONEncoder,
192+
self.sageJSONEncoder)
193+
for encoder in encoders:
194+
try:
195+
return encoder(obj)
196+
except NotEncodable:
197+
pass
198+
199+
raise TypeError(repr(obj) + " is not JSON serializable")
148200

149201

150202
### unicode stuff ###

plotly/version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__version__ = '1.4.9'
1+
__version__ = '1.4.10'

0 commit comments

Comments
 (0)