Skip to content

Commit febe6f4

Browse files
authored
Merge pull request #584 from plotly/animations-for-iqt2
Animations/Frames in Python API
2 parents 4fbe779 + dd20177 commit febe6f4

File tree

8 files changed

+412
-79
lines changed

8 files changed

+412
-79
lines changed

plotly/graph_objs/graph_objs.py

+16
Original file line numberDiff line numberDiff line change
@@ -836,6 +836,22 @@ def __init__(self, *args, **kwargs):
836836
_parent_key='data')
837837
figure_class.__init__ = __init__
838838

839+
# TODO better integrate frames into Figure - #604
840+
def __setitem__(self, key, value, **kwargs):
841+
if key == 'frames':
842+
super(PlotlyDict, self).__setitem__(key, value)
843+
else:
844+
super(figure_class, self).__setitem__(key, value, **kwargs)
845+
figure_class.__setitem__ = __setitem__
846+
847+
def _get_valid_attributes(self):
848+
super(figure_class, self)._get_valid_attributes()
849+
# TODO better integrate frames into Figure - #604
850+
if 'frames' not in self._valid_attributes:
851+
self._valid_attributes.add('frames')
852+
return self._valid_attributes
853+
figure_class._get_valid_attributes = _get_valid_attributes
854+
839855
def get_data(self, flatten=False):
840856
"""
841857
Returns the JSON for the plot with non-data elements stripped.

plotly/graph_reference.py

+1-3
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
'ErrorZ': {'object_name': 'error_z', 'base_type': dict},
3939
'Figure': {'object_name': 'figure', 'base_type': dict},
4040
'Font': {'object_name': 'font', 'base_type': dict},
41+
'Frames': {'object_name': 'frames', 'base_type': dict},
4142
'Heatmap': {'object_name': 'heatmap', 'base_type': dict},
4243
'Histogram': {'object_name': 'histogram', 'base_type': dict},
4344
'Histogram2d': {'object_name': 'histogram2d', 'base_type': dict},
@@ -87,7 +88,6 @@ def get_graph_reference():
8788

8889
graph_reference_url = '{}{}?sha1={}'.format(plotly_api_domain,
8990
GRAPH_REFERENCE_PATH, sha1)
90-
9191
try:
9292
response = requests.get(graph_reference_url,
9393
timeout=GRAPH_REFERENCE_DOWNLOAD_TIMEOUT)
@@ -196,11 +196,9 @@ def get_valid_attributes(object_name, parent_object_names=()):
196196
# These are for documentation and quick lookups. They're just strings.
197197
valid_attributes = set()
198198
for attributes_dict in attributes.values():
199-
200199
for key, val in attributes_dict.items():
201200
if key not in GRAPH_REFERENCE['defs']['metaKeys']:
202201
valid_attributes.add(key)
203-
204202
deprecated_attributes = attributes_dict.get('_deprecated', {})
205203
for key, val in deprecated_attributes.items():
206204
if key not in GRAPH_REFERENCE['defs']['metaKeys']:

plotly/grid_objs/grid_objs.py

+101-12
Original file line numberDiff line numberDiff line change
@@ -118,29 +118,95 @@ class Grid(MutableSequence):
118118
py.plot([trace], filename='graph from grid')
119119
```
120120
"""
121-
def __init__(self, iterable_of_columns):
121+
def __init__(self, columns_or_json, fid=None):
122122
"""
123-
Initialize a grid with an iterable of
124-
`plotly.grid_objs.Column objects
123+
Initialize a grid with an iterable of `plotly.grid_objs.Column`
124+
objects or a json/dict describing a grid. See second usage example
125+
below for the necessary structure of the dict.
125126
126-
Usage example:
127+
:param (str|bool) fid: should not be accessible to users. Default
128+
is 'None' but if a grid is retrieved via `py.get_grid()` then the
129+
retrieved grid response will contain the fid which will be
130+
necessary to set `self.id` and `self._columns.id` below.
131+
132+
Example from iterable of columns:
127133
```
128134
column_1 = Column([1, 2, 3], 'time')
129135
column_2 = Column([4, 2, 5], 'voltage')
130136
grid = Grid([column_1, column_2])
131137
```
138+
Example from json grid
139+
```
140+
grid_json = {
141+
'cols': {
142+
'time': {'data': [1, 2, 3], 'order': 0, 'uid': '4cd7fc'},
143+
'voltage': {'data': [4, 2, 5], 'order': 1, 'uid': u'2744be'}
144+
}
145+
}
146+
grid = Grid(grid_json)
147+
```
132148
"""
133-
134149
# TODO: verify that columns are actually columns
150+
if isinstance(columns_or_json, dict):
151+
# check that fid is entered
152+
if fid is None:
153+
raise exceptions.PlotlyError(
154+
"If you are manually converting a raw json/dict grid "
155+
"into a Grid instance, you must ensure that 'fid' is "
156+
"set to your file ID. This looks like 'username:187'."
157+
)
158+
159+
self.id = fid
160+
161+
# check if 'cols' is a root key
162+
if 'cols' not in columns_or_json:
163+
raise exceptions.PlotlyError(
164+
"'cols' must be a root key in your json grid."
165+
)
166+
167+
# check if 'data', 'order' and 'uid' are not in columns
168+
grid_col_keys = ['data', 'order', 'uid']
169+
170+
for column_name in columns_or_json['cols']:
171+
for key in grid_col_keys:
172+
if key not in columns_or_json['cols'][column_name]:
173+
raise exceptions.PlotlyError(
174+
"Each column name of your dictionary must have "
175+
"'data', 'order' and 'uid' as keys."
176+
)
177+
# collect and sort all orders in case orders do not start
178+
# at zero or there are jump discontinuities between them
179+
all_orders = []
180+
for column_name in columns_or_json['cols'].keys():
181+
all_orders.append(columns_or_json['cols'][column_name]['order'])
182+
all_orders.sort()
183+
184+
# put columns in order in a list
185+
ordered_columns = []
186+
for order in all_orders:
187+
for column_name in columns_or_json['cols'].keys():
188+
if columns_or_json['cols'][column_name]['order'] == order:
189+
break
190+
191+
ordered_columns.append(Column(
192+
columns_or_json['cols'][column_name]['data'],
193+
column_name)
194+
)
195+
self._columns = ordered_columns
196+
197+
# fill in column_ids
198+
for column in self:
199+
column.id = self.id + ':' + columns_or_json['cols'][column.name]['uid']
135200

136-
column_names = [column.name for column in iterable_of_columns]
137-
duplicate_name = utils.get_first_duplicate(column_names)
138-
if duplicate_name:
139-
err = exceptions.NON_UNIQUE_COLUMN_MESSAGE.format(duplicate_name)
140-
raise exceptions.InputError(err)
201+
else:
202+
column_names = [column.name for column in columns_or_json]
203+
duplicate_name = utils.get_first_duplicate(column_names)
204+
if duplicate_name:
205+
err = exceptions.NON_UNIQUE_COLUMN_MESSAGE.format(duplicate_name)
206+
raise exceptions.InputError(err)
141207

142-
self._columns = list(iterable_of_columns)
143-
self.id = ''
208+
self._columns = list(columns_or_json)
209+
self.id = ''
144210

145211
def __repr__(self):
146212
return self._columns.__repr__()
@@ -187,3 +253,26 @@ def get_column(self, column_name):
187253
for column in self._columns:
188254
if column.name == column_name:
189255
return column
256+
257+
def get_column_reference(self, column_name):
258+
"""
259+
Returns the column reference of given column in the grid by its name.
260+
261+
Raises an error if the column name is not in the grid. Otherwise,
262+
returns the fid:uid pair, which may be the empty string.
263+
"""
264+
column_id = None
265+
for column in self._columns:
266+
if column.name == column_name:
267+
column_id = column.id
268+
break
269+
270+
if column_id is None:
271+
col_names = []
272+
for column in self._columns:
273+
col_names.append(column.name)
274+
raise exceptions.PlotlyError(
275+
"Whoops, that column name doesn't match any of the column "
276+
"names in your grid. You must pick from {cols}".format(cols=col_names)
277+
)
278+
return column_id

plotly/offline/offline.py

+36-7
Original file line numberDiff line numberDiff line change
@@ -162,8 +162,16 @@ def init_notebook_mode(connected=False):
162162

163163
def _plot_html(figure_or_data, config, validate, default_width,
164164
default_height, global_requirejs):
165-
166-
figure = tools.return_figure_from_figure_or_data(figure_or_data, validate)
165+
# force no validation if frames is in the call
166+
# TODO - add validation for frames in call - #605
167+
if 'frames' in figure_or_data:
168+
figure = tools.return_figure_from_figure_or_data(
169+
figure_or_data, False
170+
)
171+
else:
172+
figure = tools.return_figure_from_figure_or_data(
173+
figure_or_data, validate
174+
)
167175

168176
width = figure.get('layout', {}).get('width', default_width)
169177
height = figure.get('layout', {}).get('height', default_height)
@@ -185,6 +193,8 @@ def _plot_html(figure_or_data, config, validate, default_width,
185193
plotdivid = uuid.uuid4()
186194
jdata = json.dumps(figure.get('data', []), cls=utils.PlotlyJSONEncoder)
187195
jlayout = json.dumps(figure.get('layout', {}), cls=utils.PlotlyJSONEncoder)
196+
if 'frames' in figure_or_data:
197+
jframes = json.dumps(figure.get('frames', {}), cls=utils.PlotlyJSONEncoder)
188198

189199
configkeys = (
190200
'editable',
@@ -225,11 +235,30 @@ def _plot_html(figure_or_data, config, validate, default_width,
225235
link_text = link_text.replace('plot.ly', link_domain)
226236
config['linkText'] = link_text
227237

228-
script = 'Plotly.newPlot("{id}", {data}, {layout}, {config})'.format(
229-
id=plotdivid,
230-
data=jdata,
231-
layout=jlayout,
232-
config=jconfig)
238+
if 'frames' in figure_or_data:
239+
script = '''
240+
Plotly.plot(
241+
'{id}',
242+
{data},
243+
{layout},
244+
{config}
245+
).then(function () {add_frames}).then(function(){animate})
246+
'''.format(
247+
id=plotdivid,
248+
data=jdata,
249+
layout=jlayout,
250+
config=jconfig,
251+
add_frames="{" + "return Plotly.addFrames('{id}',{frames}".format(
252+
id=plotdivid, frames=jframes
253+
) + ");}",
254+
animate="{" + "Plotly.animate('{id}');".format(id=plotdivid) + "}"
255+
)
256+
else:
257+
script = 'Plotly.newPlot("{id}", {data}, {layout}, {config})'.format(
258+
id=plotdivid,
259+
data=jdata,
260+
layout=jlayout,
261+
config=jconfig)
233262

234263
optional_line1 = ('require(["plotly"], function(Plotly) {{ '
235264
if global_requirejs else '')

plotly/offline/plotly.min.js

+58-54
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

plotly/plotly/__init__.py

+4-1
Original file line numberDiff line numberDiff line change
@@ -21,5 +21,8 @@
2121
grid_ops,
2222
meta_ops,
2323
file_ops,
24-
get_config
24+
get_config,
25+
get_grid,
26+
create_animations,
27+
icreate_animations
2528
)

0 commit comments

Comments
 (0)