Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit fa89dcc

Browse filesBrowse files
authored
Merge pull request plotly#656 from plotly/frames-support
Improve `frames` support in graph_objs.py.
2 parents 3df1290 + 0f4fb0e commit fa89dcc
Copy full SHA for fa89dcc

File tree

9 files changed

+309
-63
lines changed
Filter options

9 files changed

+309
-63
lines changed

‎CHANGELOG.md

Copy file name to clipboardExpand all lines: CHANGELOG.md
+1Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ This project adheres to [Semantic Versioning](http://semver.org/).
88

99
### Updated
1010
- `plotly.plotly.create_animations` and `plotly.plotly.icreate_animations` now return appropriate error messages if the response is not successful.
11+
- `frames` are now integrated into GRAPH_REFERENCE and figure validation.
1112

1213
### Changed
1314
- The plot-schema from `https://api.plot.ly/plot-schema` is no longer updated on import.

‎plotly/graph_objs/graph_objs.py

Copy file name to clipboardExpand all lines: plotly/graph_objs/graph_objs.py
+42-20Lines changed: 42 additions & 20 deletions
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

Copy file name to clipboardExpand all lines: plotly/graph_objs/graph_objs_tools.py
+8-2Lines changed: 8 additions & 2 deletions
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

Copy file name to clipboardExpand all lines: plotly/graph_reference.py
+84-5Lines changed: 84 additions & 5 deletions
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},
@@ -68,9 +68,62 @@ def get_graph_reference():
6868
"""
6969
path = os.path.join('package_data', 'default-schema.json')
7070
s = resource_string('plotly', path).decode('utf-8')
71-
graph_reference = _json.loads(s)
71+
graph_reference = utils.decode_unicode(_json.loads(s))
72+
73+
# TODO: Patch in frames info until it hits streambed. See #659
74+
graph_reference['frames'] = {
75+
"items": {
76+
"frames_entry": {
77+
"baseframe": {
78+
"description": "The name of the frame into which this "
79+
"frame's properties are merged before "
80+
"applying. This is used to unify "
81+
"properties and avoid needing to specify "
82+
"the same values for the same properties "
83+
"in multiple frames.",
84+
"role": "info",
85+
"valType": "string"
86+
},
87+
"data": {
88+
"description": "A list of traces this frame modifies. "
89+
"The format is identical to the normal "
90+
"trace definition.",
91+
"role": "object",
92+
"valType": "any"
93+
},
94+
"group": {
95+
"description": "An identifier that specifies the group "
96+
"to which the frame belongs, used by "
97+
"animate to select a subset of frames.",
98+
"role": "info",
99+
"valType": "string"
100+
},
101+
"layout": {
102+
"role": "object",
103+
"description": "Layout properties which this frame "
104+
"modifies. The format is identical to "
105+
"the normal layout definition.",
106+
"valType": "any"
107+
},
108+
"name": {
109+
"description": "A label by which to identify the frame",
110+
"role": "info",
111+
"valType": "string"
112+
},
113+
"role": "object",
114+
"traces": {
115+
"description": "A list of trace indices that identify "
116+
"the respective traces in the data "
117+
"attribute",
118+
"role": "info",
119+
"valType": "info_array"
120+
}
121+
}
122+
},
123+
"role": "object"
124+
}
72125

73-
return utils.decode_unicode(graph_reference)
126+
return graph_reference
74127

75128

76129
def string_to_class_name(string):
@@ -136,6 +189,27 @@ def get_attributes_dicts(object_name, parent_object_names=()):
136189
# We should also one or more paths where attributes are defined.
137190
attribute_paths = list(object_dict['attribute_paths']) # shallow copy
138191

192+
# Map frame 'data' and 'layout' to previously-defined figure attributes.
193+
# Examples of parent_object_names changes:
194+
# ['figure', 'frames'] --> ['figure', 'frames']
195+
# ['figure', 'frames', FRAME_NAME] --> ['figure']
196+
# ['figure', 'frames', FRAME_NAME, 'data'] --> ['figure', 'data']
197+
# ['figure', 'frames', FRAME_NAME, 'layout'] --> ['figure', 'layout']
198+
# ['figure', 'frames', FRAME_NAME, 'foo'] -->
199+
# ['figure', 'frames', FRAME_NAME, 'foo']
200+
# [FRAME_NAME, 'layout'] --> ['figure', 'layout']
201+
if FRAME_NAME in parent_object_names:
202+
len_parent_object_names = len(parent_object_names)
203+
index = parent_object_names.index(FRAME_NAME)
204+
if len_parent_object_names == index + 1:
205+
if object_name in ('data', 'layout'):
206+
parent_object_names = ['figure', object_name]
207+
elif len_parent_object_names > index + 1:
208+
if parent_object_names[index + 1] in ('data', 'layout'):
209+
parent_object_names = (
210+
['figure'] + list(parent_object_names)[index + 1:]
211+
)
212+
139213
# If we have parent_names, some of these attribute paths may be invalid.
140214
for parent_object_name in reversed(parent_object_names):
141215
if parent_object_name in ARRAYS:
@@ -410,8 +484,11 @@ def _patch_objects():
410484
'attribute_paths': layout_attribute_paths,
411485
'additional_attributes': {}}
412486

413-
figure_attributes = {'layout': {'role': 'object'},
414-
'data': {'role': 'object', '_isLinkedToArray': True}}
487+
figure_attributes = {
488+
'layout': {'role': 'object'},
489+
'data': {'role': 'object', '_isLinkedToArray': True},
490+
'frames': {'role': 'object', '_isLinkedToArray': True}
491+
}
415492
OBJECTS['figure'] = {'meta_paths': [],
416493
'attribute_paths': [],
417494
'additional_attributes': figure_attributes}
@@ -479,6 +556,8 @@ def _get_classes():
479556
# The ordering here is important.
480557
GRAPH_REFERENCE = get_graph_reference()
481558

559+
FRAME_NAME = list(GRAPH_REFERENCE['frames']['items'].keys())[0]
560+
482561
# See http://blog.labix.org/2008/06/27/watch-out-for-listdictkeys-in-python-3
483562
TRACE_NAMES = list(GRAPH_REFERENCE['traces'].keys())
484563

‎plotly/package_data/default-schema.json

Copy file name to clipboardExpand all lines: plotly/package_data/default-schema.json
+14-10Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9136,7 +9136,7 @@
91369136
]
91379137
},
91389138
"end": {
9139-
"description": "Sets the end contour level value.",
9139+
"description": "Sets the end contour level value. Must be more than `contours.start`",
91409140
"dflt": null,
91419141
"role": "style",
91429142
"valType": "number"
@@ -9149,13 +9149,14 @@
91499149
"valType": "boolean"
91509150
},
91519151
"size": {
9152-
"description": "Sets the step between each contour level.",
9152+
"description": "Sets the step between each contour level. Must be positive.",
91539153
"dflt": null,
9154+
"min": 0,
91549155
"role": "style",
91559156
"valType": "number"
91569157
},
91579158
"start": {
9158-
"description": "Sets the starting contour level value.",
9159+
"description": "Sets the starting contour level value. Must be less than `contours.end`",
91599160
"dflt": null,
91609161
"role": "style",
91619162
"valType": "number"
@@ -9240,8 +9241,9 @@
92409241
"valType": "string"
92419242
},
92429243
"ncontours": {
9243-
"description": "Sets the maximum number of contour levels. The actual number of contours will be chosen automatically to be less than or equal to the value of `ncontours`. Has an effect only if `autocontour` is *true*.",
9244-
"dflt": 0,
9244+
"description": "Sets the maximum number of contour levels. The actual number of contours will be chosen automatically to be less than or equal to the value of `ncontours`. Has an effect only if `autocontour` is *true* or if `contours.size` is missing.",
9245+
"dflt": 15,
9246+
"min": 1,
92459247
"role": "style",
92469248
"valType": "integer"
92479249
},
@@ -12754,7 +12756,7 @@
1275412756
]
1275512757
},
1275612758
"end": {
12757-
"description": "Sets the end contour level value.",
12759+
"description": "Sets the end contour level value. Must be more than `contours.start`",
1275812760
"dflt": null,
1275912761
"role": "style",
1276012762
"valType": "number"
@@ -12767,13 +12769,14 @@
1276712769
"valType": "boolean"
1276812770
},
1276912771
"size": {
12770-
"description": "Sets the step between each contour level.",
12772+
"description": "Sets the step between each contour level. Must be positive.",
1277112773
"dflt": null,
12774+
"min": 0,
1277212775
"role": "style",
1277312776
"valType": "number"
1277412777
},
1277512778
"start": {
12776-
"description": "Sets the starting contour level value.",
12779+
"description": "Sets the starting contour level value. Must be less than `contours.end`",
1277712780
"dflt": null,
1277812781
"role": "style",
1277912782
"valType": "number"
@@ -12899,8 +12902,9 @@
1289912902
"valType": "integer"
1290012903
},
1290112904
"ncontours": {
12902-
"description": "Sets the maximum number of contour levels. The actual number of contours will be chosen automatically to be less than or equal to the value of `ncontours`. Has an effect only if `autocontour` is *true*.",
12903-
"dflt": 0,
12905+
"description": "Sets the maximum number of contour levels. The actual number of contours will be chosen automatically to be less than or equal to the value of `ncontours`. Has an effect only if `autocontour` is *true* or if `contours.size` is missing.",
12906+
"dflt": 15,
12907+
"min": 1,
1290412908
"role": "style",
1290512909
"valType": "integer"
1290612910
},
+37Lines changed: 37 additions & 0 deletions
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 = []

0 commit comments

Comments
0 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.