@@ -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 :
0 commit comments