Skip to content

Commit 5c3419b

Browse files
committed
Fix for GH1072. Empty array properties are replaced during update
1 parent d670bb0 commit 5c3419b

File tree

3 files changed

+160
-62
lines changed

3 files changed

+160
-62
lines changed

plotly/basedatatypes.py

+19-8
Original file line numberDiff line numberDiff line change
@@ -378,7 +378,16 @@ def update(self, dict1=None, **kwargs):
378378
for d in [dict1, kwargs]:
379379
if d:
380380
for k, v in d.items():
381-
BaseFigure._perform_update(self[k], v)
381+
if self[k] == ():
382+
# existing data or frames property is empty
383+
# In this case we accept the v as is.
384+
if k == 'data':
385+
self.add_traces(v)
386+
else:
387+
# Accept v
388+
self[k] = v
389+
else:
390+
BaseFigure._perform_update(self[k], v)
382391

383392
return self
384393

@@ -1059,11 +1068,6 @@ def add_traces(self, data, rows=None, cols=None):
10591068
... rows=[1, 2], cols=[1, 1])
10601069
"""
10611070

1062-
if self._in_batch_mode:
1063-
self._batch_layout_edits.clear()
1064-
self._batch_trace_edits.clear()
1065-
raise ValueError('Traces may not be added in a batch context')
1066-
10671071
# Validate traces
10681072
data = self._data_validator.validate_coerce(data)
10691073

@@ -2133,8 +2137,15 @@ def _perform_update(plotly_obj, update_obj):
21332137
BaseFigure._perform_update(
21342138
plotly_obj[key], val)
21352139
elif isinstance(validator, CompoundArrayValidator):
2136-
BaseFigure._perform_update(
2137-
plotly_obj[key], val)
2140+
if plotly_obj[key]:
2141+
# plotly_obj has an existing non-empty array for key
2142+
# In this case we merge val into the existing elements
2143+
BaseFigure._perform_update(
2144+
plotly_obj[key], val)
2145+
else:
2146+
# plotly_obj is an empty or uninitialized list for key
2147+
# In this case we accept val as is
2148+
plotly_obj[key] = val
21382149
else:
21392150
# Assign non-compound value
21402151
plotly_obj[key] = val

plotly/tests/test_core/test_graph_objs/test_figure_properties.py

+25-1
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@
22
import plotly.graph_objs as go
33
from nose.tools import raises
44

5+
from plotly.tests.test_optional.optional_utils import NumpyTestUtilsMixin
56

6-
class TestFigureProperties(TestCase):
7+
8+
class TestFigureProperties(TestCase, NumpyTestUtilsMixin):
79

810
def setUp(self):
911
# Construct initial scatter object
@@ -121,6 +123,28 @@ def test_update_data(self):
121123
self.figure.update({'data': {0: {'marker': {'color': 'yellow'}}}})
122124
self.assertEqual(self.figure.data[0].marker.color, 'yellow')
123125

126+
def test_update_data_empty(self):
127+
# Create figure with empty data (no traces)
128+
figure = go.Figure(layout={'width': 1000})
129+
130+
# Update data with new traces
131+
figure.update(data=[go.Scatter(y=[2, 1, 3]), go.Bar(y=[1, 2, 3])])
132+
133+
# Build expected dict
134+
expected = {
135+
'data': [{'y': [2, 1, 3], 'type': 'scatter'},
136+
{'y': [1, 2, 3], 'type': 'bar'}],
137+
'layout': {'width': 1000}
138+
}
139+
140+
# Compute expected figure dict (pop uids for comparison)
141+
result = figure.to_dict()
142+
del result['data'][0]['uid']
143+
del result['data'][1]['uid']
144+
145+
# Perform comparison
146+
self.assertEqual(result, expected)
147+
124148
def test_update_frames(self):
125149
# Check initial frame axis title
126150
self.assertEqual(self.figure.frames[0].layout.yaxis.title, 'f1')
Original file line numberDiff line numberDiff line change
@@ -1,60 +1,123 @@
11
from __future__ import absolute_import
22
from unittest import skip
33

4+
import plotly.graph_objs as go
45
from plotly.graph_objs import Data, Figure, Layout, Line, Scatter, scatter, XAxis
56
from plotly.tests.utils import strip_dict_params
67

8+
from unittest import TestCase
79

8-
def test_update_dict():
9-
title = 'this'
10-
fig = Figure()
11-
fig.update(layout=Layout(title=title))
12-
assert fig == Figure(layout=Layout(title=title))
13-
fig['layout'].update(xaxis=XAxis())
14-
assert fig == Figure(layout=Layout(title=title, xaxis=XAxis()))
15-
16-
17-
def test_update_list():
18-
trace1 = Scatter(x=[1, 2, 3], y=[2, 1, 2])
19-
trace2 = Scatter(x=[1, 2, 3], y=[3, 2, 1])
20-
fig = Figure([trace1, trace2])
21-
update = dict(x=[2, 3, 4], y=[1, 2, 3])
22-
fig.data[0].update(update)
23-
fig.data[1].update(update)
24-
25-
d1, d2 = strip_dict_params(fig.data[0], Scatter(x=[2, 3, 4], y=[1, 2, 3]))
26-
assert d1 == d2
27-
d1, d2 = strip_dict_params(fig.data[1], Scatter(x=[2, 3, 4], y=[1, 2, 3]))
28-
assert d1 == d2
29-
30-
31-
def test_update_dict_empty():
32-
trace1 = Scatter(x=[1, 2, 3], y=[2, 1, 2])
33-
trace2 = Scatter(x=[1, 2, 3], y=[3, 2, 1])
34-
fig = Figure([trace1, trace2])
35-
fig.update({})
36-
d1, d2 = strip_dict_params(fig.data[0], Scatter(x=[1, 2, 3], y=[2, 1, 2]))
37-
assert d1 == d2
38-
d1, d2 = strip_dict_params(fig.data[1], Scatter(x=[1, 2, 3], y=[3, 2, 1]))
39-
assert d1 == d2
40-
41-
42-
def test_update_list_empty():
43-
trace1 = Scatter(x=[1, 2, 3], y=[2, 1, 2])
44-
trace2 = Scatter(x=[1, 2, 3], y=[3, 2, 1])
45-
fig = Figure([trace1, trace2])
46-
fig.update([])
47-
d1, d2 = strip_dict_params(fig.data[0], Scatter(x=[1, 2, 3], y=[2, 1, 2]))
48-
assert d1 == d2
49-
d1, d2 = strip_dict_params(fig.data[1], Scatter(x=[1, 2, 3], y=[3, 2, 1]))
50-
assert d1 == d2
51-
52-
53-
@skip('See https://github.com/plotly/python-api/issues/291')
54-
def test_update_list_make_copies_false():
55-
trace1 = Scatter(x=[1, 2, 3], y=[2, 1, 2])
56-
trace2 = Scatter(x=[1, 2, 3], y=[3, 2, 1])
57-
data = Data([trace1, trace2])
58-
update = dict(x=[2, 3, 4], y=[1, 2, 3], line=Line())
59-
data.update(update, make_copies=False)
60-
assert data[0]['line'] is data[1]['line']
10+
11+
class TestUpdateMethod(TestCase):
12+
def setUp(self):
13+
print('Setup!')
14+
15+
def test_update_dict(self):
16+
title = 'this'
17+
fig = Figure()
18+
fig.update(layout=Layout(title=title))
19+
assert fig == Figure(layout=Layout(title=title))
20+
fig['layout'].update(xaxis=XAxis())
21+
assert fig == Figure(layout=Layout(title=title, xaxis=XAxis()))
22+
23+
24+
def test_update_list(self):
25+
trace1 = Scatter(x=[1, 2, 3], y=[2, 1, 2])
26+
trace2 = Scatter(x=[1, 2, 3], y=[3, 2, 1])
27+
fig = Figure([trace1, trace2])
28+
update = dict(x=[2, 3, 4], y=[1, 2, 3])
29+
fig.data[0].update(update)
30+
fig.data[1].update(update)
31+
32+
d1, d2 = strip_dict_params(fig.data[0], Scatter(x=[2, 3, 4], y=[1, 2, 3]))
33+
assert d1 == d2
34+
d1, d2 = strip_dict_params(fig.data[1], Scatter(x=[2, 3, 4], y=[1, 2, 3]))
35+
assert d1 == d2
36+
37+
38+
def test_update_dict_empty(self):
39+
trace1 = Scatter(x=[1, 2, 3], y=[2, 1, 2])
40+
trace2 = Scatter(x=[1, 2, 3], y=[3, 2, 1])
41+
fig = Figure([trace1, trace2])
42+
fig.update({})
43+
d1, d2 = strip_dict_params(fig.data[0], Scatter(x=[1, 2, 3], y=[2, 1, 2]))
44+
assert d1 == d2
45+
d1, d2 = strip_dict_params(fig.data[1], Scatter(x=[1, 2, 3], y=[3, 2, 1]))
46+
assert d1 == d2
47+
48+
49+
def test_update_list_empty(self):
50+
trace1 = Scatter(x=[1, 2, 3], y=[2, 1, 2])
51+
trace2 = Scatter(x=[1, 2, 3], y=[3, 2, 1])
52+
fig = Figure([trace1, trace2])
53+
fig.update([])
54+
d1, d2 = strip_dict_params(fig.data[0], Scatter(x=[1, 2, 3], y=[2, 1, 2]))
55+
assert d1 == d2
56+
d1, d2 = strip_dict_params(fig.data[1], Scatter(x=[1, 2, 3], y=[3, 2, 1]))
57+
assert d1 == d2
58+
59+
60+
@skip('See https://github.com/plotly/python-api/issues/291')
61+
def test_update_list_make_copies_false(self):
62+
trace1 = Scatter(x=[1, 2, 3], y=[2, 1, 2])
63+
trace2 = Scatter(x=[1, 2, 3], y=[3, 2, 1])
64+
data = Data([trace1, trace2])
65+
update = dict(x=[2, 3, 4], y=[1, 2, 3], line=Line())
66+
data.update(update, make_copies=False)
67+
assert data[0]['line'] is data[1]['line']
68+
69+
def test_update_uninitialized_list_with_list(self):
70+
"""
71+
If the original list is undefined, the updated list should be
72+
accepted in full.
73+
74+
See GH1072
75+
"""
76+
layout = go.Layout()
77+
layout.update(annotations=[
78+
go.layout.Annotation(text='one'),
79+
go.layout.Annotation(text='two'),
80+
])
81+
82+
expected = {'annotations': [{'text': 'one'}, {'text': 'two'}]}
83+
84+
self.assertEqual(len(layout.annotations), 2)
85+
self.assertEqual(layout.to_plotly_json(), expected)
86+
87+
def test_update_initialized_empty_list_with_list(self):
88+
"""
89+
If the original list is empty, treat is just as if it's undefined.
90+
This is a change in behavior from version 2
91+
(where the input list would just be completly ignored), because
92+
in version 3 the difference between an uninitialized and empty list
93+
is not obvious to the user.
94+
"""
95+
layout = go.Layout(annotations=[])
96+
layout.update(annotations=[
97+
go.layout.Annotation(text='one'),
98+
go.layout.Annotation(text='two'),
99+
])
100+
101+
expected = {'annotations': [{'text': 'one'}, {'text': 'two'}]}
102+
103+
self.assertEqual(len(layout.annotations), 2)
104+
self.assertEqual(layout.to_plotly_json(), expected)
105+
106+
def test_update_initialized_nonempty_list_with_dict(self):
107+
"""
108+
If the original list is defined, a dict from
109+
index numbers to property dicts may be used to update select
110+
elements of the existing list
111+
"""
112+
layout = go.Layout(annotations=[
113+
go.layout.Annotation(text='one'),
114+
go.layout.Annotation(text='two'),
115+
])
116+
117+
layout.update(annotations={1: go.layout.Annotation(width=30)})
118+
119+
expected = {'annotations': [{'text': 'one'},
120+
{'text': 'two', 'width': 30}]}
121+
122+
self.assertEqual(len(layout.annotations), 2)
123+
self.assertEqual(layout.to_plotly_json(), expected)

0 commit comments

Comments
 (0)