@@ -517,7 +517,7 @@ def data(self, new_data):
517
517
518
518
# Restyle
519
519
# -------
520
- def _plotly_restyle (self , restyle_data , trace_indexes = None , ** kwargs ):
520
+ def plotly_restyle (self , restyle_data , trace_indexes = None , ** kwargs ):
521
521
"""
522
522
Perform a Plotly restyle operation on the figure's traces
523
523
@@ -543,7 +543,7 @@ def _plotly_restyle(self, restyle_data, trace_indexes=None, **kwargs):
543
543
example, the following command would be used to update the 'x'
544
544
property of the first trace to the list [1, 2, 3]
545
545
546
- >>> fig._plotly_restyle ({'x': [[1, 2, 3]]}, 0)
546
+ >>> fig.plotly_restyle ({'x': [[1, 2, 3]]}, 0)
547
547
548
548
trace_indexes : int or list of int
549
549
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):
552
552
Returns
553
553
-------
554
554
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.
567
555
"""
568
556
569
557
# Normalize trace indexes
@@ -581,15 +569,20 @@ def _plotly_restyle(self, restyle_data, trace_indexes=None, **kwargs):
581
569
582
570
# Perform restyle on trace dicts
583
571
# ------------------------------
584
- restyle_changes = self ._perform_plotly_restyle (restyle_data , trace_indexes )
572
+ restyle_changes = self ._perform_plotly_restyle (restyle_data ,
573
+ trace_indexes )
585
574
if restyle_changes :
586
575
# The restyle operation resulted in a change to some trace
587
576
# properties, so we dispatch change callbacks and send the
588
577
# 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
+
589
582
self ._send_restyle_msg (
590
583
restyle_changes ,
591
584
trace_indexes = trace_indexes ,
592
- source_view_id = source_view_id )
585
+ ** msg_kwargs )
593
586
594
587
self ._dispatch_trace_change_callbacks (
595
588
restyle_changes , trace_indexes )
@@ -633,17 +626,29 @@ def _perform_plotly_restyle(self, restyle_data, trace_indexes):
633
626
# Get new value for this particular trace
634
627
trace_v = v [i % len (v )] if isinstance (v , list ) else v
635
628
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 ):
640
637
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 )
643
650
644
651
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.
647
652
restyle_changes [key_path_str ] = v
648
653
649
654
return restyle_changes
@@ -1308,7 +1313,7 @@ def layout(self, new_layout):
1308
1313
# Notify JS side
1309
1314
self ._send_relayout_msg (new_layout_data )
1310
1315
1311
- def _plotly_relayout (self , relayout_data , ** kwargs ):
1316
+ def plotly_relayout (self , relayout_data , ** kwargs ):
1312
1317
"""
1313
1318
Perform a Plotly relayout operation on the figure's layout
1314
1319
@@ -1326,21 +1331,6 @@ def _plotly_relayout(self, relayout_data, **kwargs):
1326
1331
Returns
1327
1332
-------
1328
1333
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.
1344
1334
"""
1345
1335
1346
1336
# Handle source_view_id
@@ -1393,15 +1383,43 @@ def _perform_plotly_relayout(self, relayout_data):
1393
1383
# ----------------
1394
1384
for key_path_str , v in relayout_data .items ():
1395
1385
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
+
1396
1393
# Apply set operation on the layout dict
1397
1394
val_changed = BaseFigure ._set_in (self ._layout , key_path_str , v )
1398
1395
1399
1396
if val_changed :
1400
- # Save operation to changed dict
1401
1397
relayout_changes [key_path_str ] = v
1402
1398
1403
1399
return relayout_changes
1404
1400
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
+
1405
1423
def _relayout_child (self , child , key_path_str , val ):
1406
1424
"""
1407
1425
Process relayout operation on child layout object
@@ -1583,11 +1601,11 @@ def frames(self, new_frames):
1583
1601
1584
1602
# Update
1585
1603
# ------
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 ):
1591
1609
"""
1592
1610
Perform a Plotly update operation on the figure.
1593
1611
@@ -1609,12 +1627,6 @@ def _plotly_update(self,
1609
1627
-------
1610
1628
BaseFigure
1611
1629
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.
1618
1630
"""
1619
1631
1620
1632
# Handle source_view_id
@@ -1646,8 +1658,8 @@ def _plotly_update(self,
1646
1658
# Send a plotly_update message to the frontend (if any)
1647
1659
if restyle_changes or relayout_changes :
1648
1660
self ._send_update_msg (
1649
- style = restyle_changes ,
1650
- layout = relayout_changes ,
1661
+ restyle_data = restyle_changes ,
1662
+ relayout_data = relayout_changes ,
1651
1663
trace_indexes = trace_indexes ,
1652
1664
** msg_kwargs )
1653
1665
@@ -1713,13 +1725,17 @@ def _send_relayout_msg(self, layout, source_view_id=None):
1713
1725
pass
1714
1726
1715
1727
def _send_update_msg (self ,
1716
- style ,
1717
- layout ,
1728
+ restyle_data ,
1729
+ relayout_data ,
1718
1730
trace_indexes = None ,
1719
1731
source_view_id = None ):
1720
1732
pass
1721
1733
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 ):
1723
1739
pass
1724
1740
1725
1741
# Context managers
@@ -1780,7 +1796,7 @@ def batch_update(self):
1780
1796
trace_indexes ) = self ._build_update_params_from_batch ()
1781
1797
1782
1798
# ### Call plotly_update ###
1783
- self ._plotly_update (
1799
+ self .plotly_update (
1784
1800
restyle_data = restyle_data ,
1785
1801
relayout_data = relayout_data ,
1786
1802
trace_indexes = trace_indexes )
@@ -1974,8 +1990,8 @@ def _perform_batch_animate(self, animation_opts):
1974
1990
# --------------------
1975
1991
# Sends animate message to the front end (if any)
1976
1992
self ._send_animate_msg (
1977
- styles = list (animate_styles ),
1978
- layout = animate_layout ,
1993
+ styles_data = list (animate_styles ),
1994
+ relayout_data = animate_layout ,
1979
1995
trace_indexes = list (animate_trace_indexes ),
1980
1996
animation_opts = animation_opts )
1981
1997
@@ -3193,6 +3209,16 @@ def on_change(self, callback, *args, append=False):
3193
3209
None
3194
3210
"""
3195
3211
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
+
3196
3222
# Validate args not empty
3197
3223
# -----------------------
3198
3224
if len (args ) == 0 :
0 commit comments