Skip to content

Improve frames support in graph_objs.py. #656

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Jan 11, 2017
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 42 additions & 20 deletions plotly/graph_objs/graph_objs.py
Original file line number Diff line number Diff line change
Expand Up @@ -787,7 +787,7 @@ def create(object_name, *args, **kwargs):

# We patch Figure and Data, so they actually require the subclass.
class_name = graph_reference.OBJECT_NAME_TO_CLASS_NAME.get(object_name)
if class_name in ['Figure', 'Data']:
if class_name in ['Figure', 'Data', 'Frames']:
return globals()[class_name](*args, **kwargs)
else:
kwargs['_name'] = object_name
Expand Down Expand Up @@ -1097,7 +1097,7 @@ class Figure(PlotlyDict):
"""
Valid attributes for 'figure' at path [] under parents ():

['data', 'layout']
['data', 'frames', 'layout']

Run `<figure-object>.help('attribute')` on any of the above.
'<figure-object>' is the object at []
Expand All @@ -1108,22 +1108,7 @@ class Figure(PlotlyDict):
def __init__(self, *args, **kwargs):
super(Figure, self).__init__(*args, **kwargs)
if 'data' not in self:
self.data = GraphObjectFactory.create('data', _parent=self,
_parent_key='data')

# TODO better integrate frames into Figure - #604
def __setitem__(self, key, value, **kwargs):
if key == 'frames':
super(PlotlyDict, self).__setitem__(key, value)
else:
super(Figure, self).__setitem__(key, value, **kwargs)

def _get_valid_attributes(self):
super(Figure, self)._get_valid_attributes()
# TODO better integrate frames into Figure - #604
if 'frames' not in self._valid_attributes:
self._valid_attributes.add('frames')
return self._valid_attributes
self.data = Data(_parent=self, _parent_key='data')

def get_data(self, flatten=False):
"""
Expand Down Expand Up @@ -1241,8 +1226,45 @@ class Font(PlotlyDict):
_name = 'font'


class Frames(dict):
pass
class Frames(PlotlyList):
"""
Valid items for 'frames' at path [] under parents ():
['dict']

"""
_name = 'frames'

def _value_to_graph_object(self, index, value, _raise=True):
if isinstance(value, six.string_types):
return value
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a patch to allow a string in the frames array.

return super(Frames, self)._value_to_graph_object(index, value,
_raise=_raise)

def to_string(self, level=0, indent=4, eol='\n',
pretty=True, max_chars=80):
"""Get formatted string by calling `to_string` on children items."""
if not len(self):
return "{name}()".format(name=self._get_class_name())
string = "{name}([{eol}{indent}".format(
name=self._get_class_name(),
eol=eol,
indent=' ' * indent * (level + 1))
for index, entry in enumerate(self):
if isinstance(entry, six.string_types):
string += repr(entry)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another patch to allow a string in the frames array. This sorta sucks since it's not possible to super... but I'm not terribly concerned.

else:
string += entry.to_string(level=level+1,
indent=indent,
eol=eol,
pretty=pretty,
max_chars=max_chars)
if index < len(self) - 1:
string += ",{eol}{indent}".format(
eol=eol,
indent=' ' * indent * (level + 1))
string += (
"{eol}{indent}])").format(eol=eol, indent=' ' * indent * level)
return string


class Heatmap(PlotlyDict):
Expand Down
10 changes: 8 additions & 2 deletions plotly/graph_objs/graph_objs_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,14 @@ def get_help(object_name, path=(), parent_object_names=(), attribute=None):
def _list_help(object_name, path=(), parent_object_names=()):
"""See get_help()."""
items = graph_reference.ARRAYS[object_name]['items']
items_classes = [graph_reference.string_to_class_name(item)
for item in items]
items_classes = set()
for item in items:
if item in graph_reference.OBJECT_NAME_TO_CLASS_NAME:
items_classes.add(graph_reference.string_to_class_name(item))
else:
# There are no lists objects which can contain list entries.
items_classes.add('dict')
items_classes = list(items_classes)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This allows us to print dict in a list's to_string() return if the item isn't a publicly-available class.

items_classes.sort()
lines = textwrap.wrap(repr(items_classes), width=LINE_SIZE-TAB_SIZE-1)

Expand Down
89 changes: 84 additions & 5 deletions plotly/graph_reference.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
'ErrorZ': {'object_name': 'error_z', 'base_type': dict},
'Figure': {'object_name': 'figure', 'base_type': dict},
'Font': {'object_name': 'font', 'base_type': dict},
'Frames': {'object_name': 'frames', 'base_type': dict},
'Frames': {'object_name': 'frames', 'base_type': list},
'Heatmap': {'object_name': 'heatmap', 'base_type': dict},
'Histogram': {'object_name': 'histogram', 'base_type': dict},
'Histogram2d': {'object_name': 'histogram2d', 'base_type': dict},
Expand Down Expand Up @@ -68,9 +68,62 @@ def get_graph_reference():
"""
path = os.path.join('package_data', 'default-schema.json')
s = resource_string('plotly', path).decode('utf-8')
graph_reference = _json.loads(s)
graph_reference = utils.decode_unicode(_json.loads(s))

# TODO: Patch in frames info until it hits streambed. See #659
graph_reference['frames'] = {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just patching in what I'm expecting to see here. This means that we don't need to wait for any changes to land on Plotly prod and can get this into 1.X now. I'll remove this once that changes.

"items": {
"frames_entry": {
"baseframe": {
"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.",
"role": "info",
"valType": "string"
},
"data": {
"description": "A list of traces this frame modifies. "
"The format is identical to the normal "
"trace definition.",
"role": "object",
"valType": "any"
},
"group": {
"description": "An identifier that specifies the group "
"to which the frame belongs, used by "
"animate to select a subset of frames.",
"role": "info",
"valType": "string"
},
"layout": {
"role": "object",
"description": "Layout properties which this frame "
"modifies. The format is identical to "
"the normal layout definition.",
"valType": "any"
},
"name": {
"description": "A label by which to identify the frame",
"role": "info",
"valType": "string"
},
"role": "object",
"traces": {
"description": "A list of trace indices that identify "
"the respective traces in the data "
"attribute",
"role": "info",
"valType": "info_array"
}
}
},
"role": "object"
}

return utils.decode_unicode(graph_reference)
return graph_reference


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

# Map frame 'data' and 'layout' to previously-defined figure attributes.
# Examples of parent_object_names changes:
# ['figure', 'frames'] --> ['figure', 'frames']
# ['figure', 'frames', FRAME_NAME] --> ['figure']
# ['figure', 'frames', FRAME_NAME, 'data'] --> ['figure', 'data']
# ['figure', 'frames', FRAME_NAME, 'layout'] --> ['figure', 'layout']
# ['figure', 'frames', FRAME_NAME, 'foo'] -->
# ['figure', 'frames', FRAME_NAME, 'foo']
# [FRAME_NAME, 'layout'] --> ['figure', 'layout']
if FRAME_NAME in parent_object_names:
len_parent_object_names = len(parent_object_names)
index = parent_object_names.index(FRAME_NAME)
if len_parent_object_names == index + 1:
if object_name in ('data', 'layout'):
parent_object_names = ['figure', object_name]
elif len_parent_object_names > index + 1:
if parent_object_names[index + 1] in ('data', 'layout'):
parent_object_names = (
['figure'] + list(parent_object_names)[index + 1:]
)

# If we have parent_names, some of these attribute paths may be invalid.
for parent_object_name in reversed(parent_object_names):
if parent_object_name in ARRAYS:
Expand Down Expand Up @@ -410,8 +484,11 @@ def _patch_objects():
'attribute_paths': layout_attribute_paths,
'additional_attributes': {}}

figure_attributes = {'layout': {'role': 'object'},
'data': {'role': 'object', '_isLinkedToArray': True}}
figure_attributes = {
'layout': {'role': 'object'},
'data': {'role': 'object', '_isLinkedToArray': True},
'frames': {'role': 'object', '_isLinkedToArray': True}
}
OBJECTS['figure'] = {'meta_paths': [],
'attribute_paths': [],
'additional_attributes': figure_attributes}
Expand Down Expand Up @@ -479,6 +556,8 @@ def _get_classes():
# The ordering here is important.
GRAPH_REFERENCE = get_graph_reference()

FRAME_NAME = list(GRAPH_REFERENCE['frames']['items'].keys())[0]

# See http://blog.labix.org/2008/06/27/watch-out-for-listdictkeys-in-python-3
TRACE_NAMES = list(GRAPH_REFERENCE['traces'].keys())

Expand Down
37 changes: 37 additions & 0 deletions plotly/tests/test_core/test_graph_objs/test_figure.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
from __future__ import absolute_import

from unittest import TestCase

from plotly import exceptions
from plotly.graph_objs import Figure


class FigureTest(TestCase):

def test_instantiation(self):

native_figure = {
'data': [],
'layout': {},
'frames': []
}

Figure(native_figure)
Figure()

def test_access_top_level(self):

# Figure is special, we define top-level objects that always exist.

self.assertEqual(Figure().data, [])
self.assertEqual(Figure().layout, {})
self.assertEqual(Figure().frames, [])

def test_nested_frames(self):
with self.assertRaisesRegexp(exceptions.PlotlyDictKeyError, 'frames'):
Figure({'frames': [{'frames': []}]})

figure = Figure()
figure.frames = [{}]
with self.assertRaisesRegexp(exceptions.PlotlyDictKeyError, 'frames'):
figure.frames[0].frames = []
79 changes: 79 additions & 0 deletions plotly/tests/test_core/test_graph_objs/test_frames.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
from __future__ import absolute_import

from unittest import TestCase

from plotly import exceptions
from plotly.graph_objs import Bar, Frames


class FramesTest(TestCase):

def test_instantiation(self):

native_frames = [
{},
{'data': []},
'foo',
{'data': [], 'group': 'baz', 'layout': {}, 'name': 'hoopla'}
]

Frames(native_frames)
Frames()

def test_string_frame(self):
frames = Frames()
frames.append({'group': 'baz', 'data': []})
frames.append('foobar')
self.assertEqual(frames[1], 'foobar')
self.assertEqual(frames.to_string(),
"Frames([\n"
" dict(\n"
" data=Data(),\n"
" group='baz'\n"
" ),\n"
" 'foobar'\n"
"])")

def test_non_string_frame(self):
frames = Frames()
frames.append({})

with self.assertRaises(exceptions.PlotlyListEntryError):
frames.append([])

with self.assertRaises(exceptions.PlotlyListEntryError):
frames.append(0)

def test_deeply_nested_layout_attributes(self):
frames = Frames()
frames.append({})
frames[0].layout.xaxis.showexponent = 'all'

# It's OK if this needs to change, but we should check *something*.
self.assertEqual(
frames[0].layout.font._get_valid_attributes(),
{'color', 'family', 'size'}
)

def test_deeply_nested_data_attributes(self):
frames = Frames()
frames.append({})
frames[0].data = [Bar()]
frames[0].data[0].marker.color = 'red'

# It's OK if this needs to change, but we should check *something*.
self.assertEqual(
frames[0].data[0].marker.line._get_valid_attributes(),
{'colorsrc', 'autocolorscale', 'cmin', 'colorscale', 'color',
'reversescale', 'width', 'cauto', 'widthsrc', 'cmax'}
)

def test_frame_only_attrs(self):
frames = Frames()
frames.append({})

# It's OK if this needs to change, but we should check *something*.
self.assertEqual(
frames[0]._get_valid_attributes(),
{'group', 'name', 'data', 'layout', 'baseframe', 'traces'}
)
Loading