Skip to content

Commit dbbc6f6

Browse files
committed
Merge branch 'ipyplotly_integration' of https://github.com/plotly/plotly.py into ipyplotly_integration
2 parents b6a6f80 + 61f4795 commit dbbc6f6

File tree

11 files changed

+642
-120
lines changed

11 files changed

+642
-120
lines changed

plotly/basedatatypes.py

Lines changed: 86 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -517,7 +517,7 @@ def data(self, new_data):
517517

518518
# Restyle
519519
# -------
520-
def _plotly_restyle(self, restyle_data, trace_indexes=None, **kwargs):
520+
def plotly_restyle(self, restyle_data, trace_indexes=None, **kwargs):
521521
"""
522522
Perform a Plotly restyle operation on the figure's traces
523523
@@ -543,7 +543,7 @@ def _plotly_restyle(self, restyle_data, trace_indexes=None, **kwargs):
543543
example, the following command would be used to update the 'x'
544544
property of the first trace to the list [1, 2, 3]
545545
546-
>>> fig._plotly_restyle({'x': [[1, 2, 3]]}, 0)
546+
>>> fig.plotly_restyle({'x': [[1, 2, 3]]}, 0)
547547
548548
trace_indexes : int or list of int
549549
Trace index, or list of trace indexes, that the restyle operation
@@ -552,18 +552,6 @@ def _plotly_restyle(self, restyle_data, trace_indexes=None, **kwargs):
552552
Returns
553553
-------
554554
None
555-
556-
Notes
557-
-----
558-
This method is does not create new graph_obj objects in the figure
559-
hierarchy. Some things that can go wrong...
560-
561-
1) ``_plotly_restyle({'dimensions[2].values': [0, 1, 2]})``
562-
For a ``parcoords`` trace that has not been intialized with at
563-
least 3 dimensions.
564-
565-
This isn't a problem for style operations originating from the
566-
front-end, but should be addressed before making this method public.
567555
"""
568556

569557
# Normalize trace indexes
@@ -581,15 +569,20 @@ def _plotly_restyle(self, restyle_data, trace_indexes=None, **kwargs):
581569

582570
# Perform restyle on trace dicts
583571
# ------------------------------
584-
restyle_changes = self._perform_plotly_restyle(restyle_data, trace_indexes)
572+
restyle_changes = self._perform_plotly_restyle(restyle_data,
573+
trace_indexes)
585574
if restyle_changes:
586575
# The restyle operation resulted in a change to some trace
587576
# properties, so we dispatch change callbacks and send the
588577
# restyle message to the frontend (if any)
578+
msg_kwargs = ({'source_view_id': source_view_id}
579+
if source_view_id is not None
580+
else {})
581+
589582
self._send_restyle_msg(
590583
restyle_changes,
591584
trace_indexes=trace_indexes,
592-
source_view_id=source_view_id)
585+
**msg_kwargs)
593586

594587
self._dispatch_trace_change_callbacks(
595588
restyle_changes, trace_indexes)
@@ -633,17 +626,29 @@ def _perform_plotly_restyle(self, restyle_data, trace_indexes):
633626
# Get new value for this particular trace
634627
trace_v = v[i % len(v)] if isinstance(v, list) else v
635628

636-
# Apply set operation for this trace and thist value
637-
val_changed = BaseFigure._set_in(self._data[trace_ind],
638-
key_path_str,
639-
trace_v)
629+
if trace_v is not Undefined:
630+
631+
# Get trace being updated
632+
trace_obj = self.data[trace_ind]
633+
634+
# Validate key_path_str
635+
if not BaseFigure._is_key_path_compatible(
636+
key_path_str, trace_obj):
640637

641-
# Update any_vals_changed status
642-
any_vals_changed = (any_vals_changed or val_changed)
638+
trace_class = trace_obj.__class__.__name__
639+
raise ValueError("""
640+
Invalid property path '{key_path_str}' for trace class {trace_class}
641+
""".format(key_path_str=key_path_str, trace_class=trace_class))
642+
643+
# Apply set operation for this trace and thist value
644+
val_changed = BaseFigure._set_in(self._data[trace_ind],
645+
key_path_str,
646+
trace_v)
647+
648+
# Update any_vals_changed status
649+
any_vals_changed = (any_vals_changed or val_changed)
643650

644651
if any_vals_changed:
645-
# At lease one of the values for one of the traces has
646-
# changed for the current key_path_str.
647652
restyle_changes[key_path_str] = v
648653

649654
return restyle_changes
@@ -1308,7 +1313,7 @@ def layout(self, new_layout):
13081313
# Notify JS side
13091314
self._send_relayout_msg(new_layout_data)
13101315

1311-
def _plotly_relayout(self, relayout_data, **kwargs):
1316+
def plotly_relayout(self, relayout_data, **kwargs):
13121317
"""
13131318
Perform a Plotly relayout operation on the figure's layout
13141319
@@ -1326,21 +1331,6 @@ def _plotly_relayout(self, relayout_data, **kwargs):
13261331
Returns
13271332
-------
13281333
None
1329-
1330-
Notes
1331-
-----
1332-
This method is does not create new graph_obj objects in the figure
1333-
hierarchy. Some things that can go wrong...
1334-
1335-
1) ``_plotly_relayout({'xaxis2.range': [0, 1]})``
1336-
If xaxis2 has not been initialized
1337-
1338-
2) ``_plotly_relayout({'images[2].source': 'http://...'})``
1339-
If the images array has not been initialized with at least 3
1340-
elements
1341-
1342-
This isn't a problem for relayout operations originating from the
1343-
front-end, but should be addressed before making this method public.
13441334
"""
13451335

13461336
# Handle source_view_id
@@ -1393,15 +1383,43 @@ def _perform_plotly_relayout(self, relayout_data):
13931383
# ----------------
13941384
for key_path_str, v in relayout_data.items():
13951385

1386+
if not BaseFigure._is_key_path_compatible(
1387+
key_path_str, self.layout):
1388+
1389+
raise ValueError("""
1390+
Invalid property path '{key_path_str}' for layout
1391+
""".format(key_path_str=key_path_str))
1392+
13961393
# Apply set operation on the layout dict
13971394
val_changed = BaseFigure._set_in(self._layout, key_path_str, v)
13981395

13991396
if val_changed:
1400-
# Save operation to changed dict
14011397
relayout_changes[key_path_str] = v
14021398

14031399
return relayout_changes
14041400

1401+
@staticmethod
1402+
def _is_key_path_compatible(key_path_str, plotly_obj):
1403+
"""
1404+
Return whether the specifieid key path string is compatible with
1405+
the specified plotly object for the purpose of relayout/restyle
1406+
operation
1407+
"""
1408+
1409+
# Convert string to tuple of path components
1410+
# e.g. 'foo[0].bar[1]' -> ('foo', 0, 'bar', 1)
1411+
key_path_tuple = BaseFigure._str_to_dict_path(key_path_str)
1412+
1413+
# Remove trailing integer component
1414+
# e.g. ('foo', 0, 'bar', 1) -> ('foo', 0, 'bar')
1415+
# We do this because it's fine for relayout/restyle to create new
1416+
# elements in the final array in the path.
1417+
if isinstance(key_path_tuple[-1], int):
1418+
key_path_tuple = key_path_tuple[:-1]
1419+
1420+
# Test whether modified key path tuple is in plotly_obj
1421+
return key_path_tuple in plotly_obj
1422+
14051423
def _relayout_child(self, child, key_path_str, val):
14061424
"""
14071425
Process relayout operation on child layout object
@@ -1583,11 +1601,11 @@ def frames(self, new_frames):
15831601

15841602
# Update
15851603
# ------
1586-
def _plotly_update(self,
1587-
restyle_data=None,
1588-
relayout_data=None,
1589-
trace_indexes=None,
1590-
**kwargs):
1604+
def plotly_update(self,
1605+
restyle_data=None,
1606+
relayout_data=None,
1607+
trace_indexes=None,
1608+
**kwargs):
15911609
"""
15921610
Perform a Plotly update operation on the figure.
15931611
@@ -1609,12 +1627,6 @@ def _plotly_update(self,
16091627
-------
16101628
BaseFigure
16111629
None
1612-
1613-
Notes
1614-
-----
1615-
This method is does not create new graph_obj objects in the figure
1616-
hierarchy. See notes for ``_plotly_relayout`` and
1617-
``_plotly_restyle`` for examples.
16181630
"""
16191631

16201632
# Handle source_view_id
@@ -1646,8 +1658,8 @@ def _plotly_update(self,
16461658
# Send a plotly_update message to the frontend (if any)
16471659
if restyle_changes or relayout_changes:
16481660
self._send_update_msg(
1649-
style=restyle_changes,
1650-
layout=relayout_changes,
1661+
restyle_data=restyle_changes,
1662+
relayout_data=relayout_changes,
16511663
trace_indexes=trace_indexes,
16521664
**msg_kwargs)
16531665

@@ -1713,13 +1725,17 @@ def _send_relayout_msg(self, layout, source_view_id=None):
17131725
pass
17141726

17151727
def _send_update_msg(self,
1716-
style,
1717-
layout,
1728+
restyle_data,
1729+
relayout_data,
17181730
trace_indexes=None,
17191731
source_view_id=None):
17201732
pass
17211733

1722-
def _send_animate_msg(self, styles, layout, trace_indexes, animation_opts):
1734+
def _send_animate_msg(self,
1735+
styles_data,
1736+
relayout_data,
1737+
trace_indexes,
1738+
animation_opts):
17231739
pass
17241740

17251741
# Context managers
@@ -1780,7 +1796,7 @@ def batch_update(self):
17801796
trace_indexes) = self._build_update_params_from_batch()
17811797

17821798
# ### Call plotly_update ###
1783-
self._plotly_update(
1799+
self.plotly_update(
17841800
restyle_data=restyle_data,
17851801
relayout_data=relayout_data,
17861802
trace_indexes=trace_indexes)
@@ -1974,8 +1990,8 @@ def _perform_batch_animate(self, animation_opts):
19741990
# --------------------
19751991
# Sends animate message to the front end (if any)
19761992
self._send_animate_msg(
1977-
styles=list(animate_styles),
1978-
layout=animate_layout,
1993+
styles_data=list(animate_styles),
1994+
relayout_data=animate_layout,
19791995
trace_indexes=list(animate_trace_indexes),
19801996
animation_opts=animation_opts)
19811997

@@ -3193,6 +3209,16 @@ def on_change(self, callback, *args, append=False):
31933209
None
31943210
"""
31953211

3212+
# Warn if object not descendent of a figure
3213+
# -----------------------------------------
3214+
if not self.figure:
3215+
class_name = self.__class__.__name__
3216+
msg = """
3217+
{class_name} object is not a descendant of a Figure.
3218+
on_change callbacks are not supported in this case.
3219+
""".format(class_name=class_name)
3220+
raise ValueError(msg)
3221+
31963222
# Validate args not empty
31973223
# -----------------------
31983224
if len(args) == 0:

plotly/basewidget.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -751,9 +751,9 @@ def _handler_js2py_restyle(self, change):
751751

752752
# Perform restyle
753753
# ---------------
754-
self._plotly_restyle(restyle_data=style_data,
755-
trace_indexes=style_traces,
756-
source_view_id=source_view_id)
754+
self.plotly_restyle(restyle_data=style_data,
755+
trace_indexes=style_traces,
756+
source_view_id=source_view_id)
757757

758758
@observe('_js2py_update')
759759
def _handler_js2py_update(self, change):
@@ -775,9 +775,9 @@ def _handler_js2py_update(self, change):
775775

776776
# Perform update
777777
# --------------
778-
self._plotly_update(restyle_data=style, relayout_data=layout,
779-
trace_indexes=trace_indexes,
780-
source_view_id=source_view_id)
778+
self.plotly_update(restyle_data=style, relayout_data=layout,
779+
trace_indexes=trace_indexes,
780+
source_view_id=source_view_id)
781781

782782
@observe('_js2py_relayout')
783783
def _handler_js2py_relayout(self, change):
@@ -803,8 +803,8 @@ def _handler_js2py_relayout(self, change):
803803

804804
# Perform relayout
805805
# ----------------
806-
self._plotly_relayout(relayout_data=relayout_data,
807-
source_view_id=source_view_id)
806+
self.plotly_relayout(relayout_data=relayout_data,
807+
source_view_id=source_view_id)
808808

809809
@observe('_js2py_pointsCallback')
810810
def _handler_js2py_pointsCallback(self, change):

plotly/tests/test_core/test_figure_messages/test_batch_animate.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,8 @@ def test_batch_animate(self):
4747

4848
# Check that update message was sent
4949
self.figure._send_animate_msg.assert_called_once_with(
50-
styles=[{'marker.color': 'yellow'}, {'marker.opacity': 0.9}],
51-
layout={'xaxis.range': [10, 20]},
50+
styles_data=[{'marker.color': 'yellow'}, {'marker.opacity': 0.9}],
51+
relayout_data={'xaxis.range': [10, 20]},
5252
trace_indexes=[0, 1],
5353
animation_opts={'transition': {'easing': 'elastic',
5454
'duration': 1200},

plotly/tests/test_core/test_figure_messages/test_move_delete_traces.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from unittest import TestCase
22
from mock import MagicMock
33
import plotly.graph_objs as go
4-
4+
from nose.tools import raises
55

66
class TestMoveDeleteTracesMessages(TestCase):
77
def setUp(self):
@@ -73,3 +73,17 @@ def test_move_and_delete_traces(self):
7373
self.figure._send_deleteTraces_msg.assert_called_once_with([1])
7474
self.figure._send_moveTraces_msg.assert_called_once_with(
7575
[0, 1], [1, 0])
76+
77+
@raises(ValueError)
78+
def test_validate_assigned_traces_are_subset(self):
79+
traces = self.figure.data
80+
self.figure.data = [traces[2],
81+
go.Scatter(y=[3, 2, 1]),
82+
traces[1]]
83+
84+
@raises(ValueError)
85+
def test_validate_assigned_traces_are_not_duplicates(self):
86+
traces = self.figure.data
87+
self.figure.data = [traces[2],
88+
traces[1],
89+
traces[1]]

0 commit comments

Comments
 (0)