Skip to content

Commit a750909

Browse files
committed
Improve frames support in graph_objs.py.
This does the following: * Allow object-or-string in frames array * Refuse `frames` in frame object if nested * Generates a non-public `FramesEntry` graph obj * Tests this functionality
1 parent 3df1290 commit a750909

File tree

7 files changed

+265
-80
lines changed

7 files changed

+265
-80
lines changed

plotly/graph_objs/graph_objs.py

+42-20
Original file line numberDiff line numberDiff line change
@@ -787,7 +787,7 @@ def create(object_name, *args, **kwargs):
787787

788788
# We patch Figure and Data, so they actually require the subclass.
789789
class_name = graph_reference.OBJECT_NAME_TO_CLASS_NAME.get(object_name)
790-
if class_name in ['Figure', 'Data']:
790+
if class_name in ['Figure', 'Data', 'Frames']:
791791
return globals()[class_name](*args, **kwargs)
792792
else:
793793
kwargs['_name'] = object_name
@@ -1097,7 +1097,7 @@ class Figure(PlotlyDict):
10971097
"""
10981098
Valid attributes for 'figure' at path [] under parents ():
10991099
1100-
['data', 'layout']
1100+
['data', 'frames', 'layout']
11011101
11021102
Run `<figure-object>.help('attribute')` on any of the above.
11031103
'<figure-object>' is the object at []
@@ -1108,22 +1108,7 @@ class Figure(PlotlyDict):
11081108
def __init__(self, *args, **kwargs):
11091109
super(Figure, self).__init__(*args, **kwargs)
11101110
if 'data' not in self:
1111-
self.data = GraphObjectFactory.create('data', _parent=self,
1112-
_parent_key='data')
1113-
1114-
# TODO better integrate frames into Figure - #604
1115-
def __setitem__(self, key, value, **kwargs):
1116-
if key == 'frames':
1117-
super(PlotlyDict, self).__setitem__(key, value)
1118-
else:
1119-
super(Figure, self).__setitem__(key, value, **kwargs)
1120-
1121-
def _get_valid_attributes(self):
1122-
super(Figure, self)._get_valid_attributes()
1123-
# TODO better integrate frames into Figure - #604
1124-
if 'frames' not in self._valid_attributes:
1125-
self._valid_attributes.add('frames')
1126-
return self._valid_attributes
1111+
self.data = Data(_parent=self, _parent_key='data')
11271112

11281113
def get_data(self, flatten=False):
11291114
"""
@@ -1241,8 +1226,45 @@ class Font(PlotlyDict):
12411226
_name = 'font'
12421227

12431228

1244-
class Frames(dict):
1245-
pass
1229+
class Frames(PlotlyList):
1230+
"""
1231+
Valid items for 'frames' at path [] under parents ():
1232+
['dict']
1233+
1234+
"""
1235+
_name = 'frames'
1236+
1237+
def _value_to_graph_object(self, index, value, _raise=True):
1238+
if isinstance(value, six.string_types):
1239+
return value
1240+
return super(Frames, self)._value_to_graph_object(index, value,
1241+
_raise=_raise)
1242+
1243+
def to_string(self, level=0, indent=4, eol='\n',
1244+
pretty=True, max_chars=80):
1245+
"""Get formatted string by calling `to_string` on children items."""
1246+
if not len(self):
1247+
return "{name}()".format(name=self._get_class_name())
1248+
string = "{name}([{eol}{indent}".format(
1249+
name=self._get_class_name(),
1250+
eol=eol,
1251+
indent=' ' * indent * (level + 1))
1252+
for index, entry in enumerate(self):
1253+
if isinstance(entry, six.string_types):
1254+
string += repr(entry)
1255+
else:
1256+
string += entry.to_string(level=level+1,
1257+
indent=indent,
1258+
eol=eol,
1259+
pretty=pretty,
1260+
max_chars=max_chars)
1261+
if index < len(self) - 1:
1262+
string += ",{eol}{indent}".format(
1263+
eol=eol,
1264+
indent=' ' * indent * (level + 1))
1265+
string += (
1266+
"{eol}{indent}])").format(eol=eol, indent=' ' * indent * level)
1267+
return string
12461268

12471269

12481270
class Heatmap(PlotlyDict):

plotly/graph_objs/graph_objs_tools.py

+8-2
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,14 @@ def get_help(object_name, path=(), parent_object_names=(), attribute=None):
3535
def _list_help(object_name, path=(), parent_object_names=()):
3636
"""See get_help()."""
3737
items = graph_reference.ARRAYS[object_name]['items']
38-
items_classes = [graph_reference.string_to_class_name(item)
39-
for item in items]
38+
items_classes = set()
39+
for item in items:
40+
if item in graph_reference.OBJECT_NAME_TO_CLASS_NAME:
41+
items_classes.add(graph_reference.string_to_class_name(item))
42+
else:
43+
# There are no lists objects which can contain list entries.
44+
items_classes.add('dict')
45+
items_classes = list(items_classes)
4046
items_classes.sort()
4147
lines = textwrap.wrap(repr(items_classes), width=LINE_SIZE-TAB_SIZE-1)
4248

plotly/graph_reference.py

+29-3
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333
'ErrorZ': {'object_name': 'error_z', 'base_type': dict},
3434
'Figure': {'object_name': 'figure', 'base_type': dict},
3535
'Font': {'object_name': 'font', 'base_type': dict},
36-
'Frames': {'object_name': 'frames', 'base_type': dict},
36+
'Frames': {'object_name': 'frames', 'base_type': list},
3737
'Heatmap': {'object_name': 'heatmap', 'base_type': dict},
3838
'Histogram': {'object_name': 'histogram', 'base_type': dict},
3939
'Histogram2d': {'object_name': 'histogram2d', 'base_type': dict},
@@ -136,6 +136,27 @@ def get_attributes_dicts(object_name, parent_object_names=()):
136136
# We should also one or more paths where attributes are defined.
137137
attribute_paths = list(object_dict['attribute_paths']) # shallow copy
138138

139+
# Map frame 'data' and 'layout' to previously-defined figure attributes.
140+
# Examples of parent_object_names changes:
141+
# ['figure', 'frames'] --> ['figure', 'frames']
142+
# ['figure', 'frames', FRAME_NAME] --> ['figure']
143+
# ['figure', 'frames', FRAME_NAME, 'data'] --> ['figure', 'data']
144+
# ['figure', 'frames', FRAME_NAME, 'layout'] --> ['figure', 'layout']
145+
# ['figure', 'frames', FRAME_NAME, 'foo'] -->
146+
# ['figure', 'frames', FRAME_NAME, 'foo']
147+
# [FRAME_NAME, 'layout'] --> ['figure', 'layout']
148+
if FRAME_NAME in parent_object_names:
149+
len_parent_object_names = len(parent_object_names)
150+
index = parent_object_names.index(FRAME_NAME)
151+
if len_parent_object_names == index + 1:
152+
if object_name in ('data', 'layout'):
153+
parent_object_names = ['figure', object_name]
154+
elif len_parent_object_names > index + 1:
155+
if parent_object_names[index + 1] in ('data', 'layout'):
156+
parent_object_names = (
157+
['figure'] + list(parent_object_names)[index + 1:]
158+
)
159+
139160
# If we have parent_names, some of these attribute paths may be invalid.
140161
for parent_object_name in reversed(parent_object_names):
141162
if parent_object_name in ARRAYS:
@@ -410,8 +431,11 @@ def _patch_objects():
410431
'attribute_paths': layout_attribute_paths,
411432
'additional_attributes': {}}
412433

413-
figure_attributes = {'layout': {'role': 'object'},
414-
'data': {'role': 'object', '_isLinkedToArray': True}}
434+
figure_attributes = {
435+
'layout': {'role': 'object'},
436+
'data': {'role': 'object', '_isLinkedToArray': True},
437+
'frames': {'role': 'object', '_isLinkedToArray': True}
438+
}
415439
OBJECTS['figure'] = {'meta_paths': [],
416440
'attribute_paths': [],
417441
'additional_attributes': figure_attributes}
@@ -479,6 +503,8 @@ def _get_classes():
479503
# The ordering here is important.
480504
GRAPH_REFERENCE = get_graph_reference()
481505

506+
FRAME_NAME = list(GRAPH_REFERENCE['frames']['items'].keys())[0]
507+
482508
# See http://blog.labix.org/2008/06/27/watch-out-for-listdictkeys-in-python-3
483509
TRACE_NAMES = list(GRAPH_REFERENCE['traces'].keys())
484510

plotly/package_data/default-schema.json

+36-39
Original file line numberDiff line numberDiff line change
@@ -223,45 +223,42 @@
223223
}
224224
},
225225
"frames": {
226-
"baseframe": {
227-
"description": "The name of the frame into which this frame's properties are merged before applying. This is used to unify properties and avoid needing to specify the same values for the same properties in multiple frames.",
228-
"role": "info",
229-
"valType": "string"
230-
},
231-
"data": {
232-
"description": "A list of traces this frame modifies. The format is identical to the normal trace definition.",
233-
"role": "data",
234-
"valType": "data_array"
235-
},
236-
"datasrc": {
237-
"description": "Sets the source reference on plot.ly for data .",
238-
"role": "info",
239-
"valType": "string"
240-
},
241-
"group": {
242-
"description": "An identifier that specifies the group to which the frame belongs, used by animate to select a subset of frames.",
243-
"role": "info",
244-
"valType": "string"
245-
},
246-
"layout": {
247-
"description": "Layout properties which this frame modifies. The format is identical to the normal layout definition.",
248-
"valType": "any"
249-
},
250-
"name": {
251-
"description": "A label by which to identify the frame",
252-
"role": "info",
253-
"valType": "string"
254-
},
255-
"traces": {
256-
"description": "A list of trace indices that identify the respective traces in the data attribute",
257-
"role": "data",
258-
"valType": "data_array"
226+
"items": {
227+
"frames_entry": {
228+
"baseframe": {
229+
"description": "The name of the frame into which this frame's properties are merged before applying. This is used to unify properties and avoid needing to specify the same values for the same properties in multiple frames.",
230+
"role": "info",
231+
"valType": "string"
232+
},
233+
"data": {
234+
"description": "A list of traces this frame modifies. The format is identical to the normal trace definition.",
235+
"role": "object",
236+
"valType": "any"
237+
},
238+
"group": {
239+
"description": "An identifier that specifies the group to which the frame belongs, used by animate to select a subset of frames.",
240+
"role": "info",
241+
"valType": "string"
242+
},
243+
"layout": {
244+
"role": "object",
245+
"description": "Layout properties which this frame modifies. The format is identical to the normal layout definition.",
246+
"valType": "any"
247+
},
248+
"name": {
249+
"description": "A label by which to identify the frame",
250+
"role": "info",
251+
"valType": "string"
252+
},
253+
"role": "object",
254+
"traces": {
255+
"description": "A list of trace indices that identify the respective traces in the data attribute",
256+
"role": "info",
257+
"valType": "info_array"
258+
}
259+
}
259260
},
260-
"tracessrc": {
261-
"description": "Sets the source reference on plot.ly for traces .",
262-
"role": "info",
263-
"valType": "string"
264-
}
261+
"role": "object"
265262
},
266263
"layout": {
267264
"layoutAttributes": {
@@ -22363,4 +22360,4 @@
2236322360
"attributes": {}
2236422361
}
2236522362
}
22366-
}
22363+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
from __future__ import absolute_import
2+
3+
from unittest import TestCase
4+
5+
from plotly import exceptions
6+
from plotly.graph_objs import Figure
7+
8+
9+
class FigureTest(TestCase):
10+
11+
def test_instantiation(self):
12+
13+
native_figure = {
14+
'data': [],
15+
'layout': {},
16+
'frames': []
17+
}
18+
19+
Figure(native_figure)
20+
Figure()
21+
22+
def test_access_top_level(self):
23+
24+
# Figure is special, we define top-level objects that always exist.
25+
26+
self.assertEqual(Figure().data, [])
27+
self.assertEqual(Figure().layout, {})
28+
self.assertEqual(Figure().frames, [])
29+
30+
def test_nested_frames(self):
31+
with self.assertRaisesRegexp(exceptions.PlotlyDictKeyError, 'frames'):
32+
Figure({'frames': [{'frames': []}]})
33+
34+
figure = Figure()
35+
figure.frames = [{}]
36+
with self.assertRaisesRegexp(exceptions.PlotlyDictKeyError, 'frames'):
37+
figure.frames[0].frames = []
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
from __future__ import absolute_import
2+
3+
from unittest import TestCase
4+
5+
from plotly import exceptions
6+
from plotly.graph_objs import Bar, Frames
7+
8+
9+
class FramesTest(TestCase):
10+
11+
def test_instantiation(self):
12+
13+
native_frames = [
14+
{},
15+
{'data': []},
16+
'foo',
17+
{'data': [], 'group': 'baz', 'layout': {}, 'name': 'hoopla'}
18+
]
19+
20+
Frames(native_frames)
21+
Frames()
22+
23+
def test_string_frame(self):
24+
frames = Frames()
25+
frames.append({'group': 'baz', 'data': []})
26+
frames.append('foobar')
27+
self.assertEqual(frames[1], 'foobar')
28+
self.assertEqual(frames.to_string(),
29+
"Frames([\n"
30+
" dict(\n"
31+
" data=Data(),\n"
32+
" group='baz'\n"
33+
" ),\n"
34+
" 'foobar'\n"
35+
"])")
36+
37+
def test_non_string_frame(self):
38+
frames = Frames()
39+
frames.append({})
40+
41+
with self.assertRaises(exceptions.PlotlyListEntryError):
42+
frames.append([])
43+
44+
with self.assertRaises(exceptions.PlotlyListEntryError):
45+
frames.append(0)
46+
47+
def test_deeply_nested_layout_attributes(self):
48+
frames = Frames()
49+
frames.append({})
50+
frames[0].layout.xaxis.showexponent = 'all'
51+
52+
# It's OK if this needs to change, but we should check *something*.
53+
self.assertEqual(
54+
frames[0].layout.font._get_valid_attributes(),
55+
{'color', 'family', 'size'}
56+
)
57+
58+
def test_deeply_nested_data_attributes(self):
59+
frames = Frames()
60+
frames.append({})
61+
frames[0].data = [Bar()]
62+
frames[0].data[0].marker.color = 'red'
63+
64+
# It's OK if this needs to change, but we should check *something*.
65+
self.assertEqual(
66+
frames[0].data[0].marker.line._get_valid_attributes(),
67+
{'colorsrc', 'autocolorscale', 'cmin', 'colorscale', 'color',
68+
'reversescale', 'width', 'cauto', 'widthsrc', 'cmax'}
69+
)

0 commit comments

Comments
 (0)