aboutsummaryrefslogtreecommitdiff
path: root/venv/lib/python3.8/site-packages/plotly/basewidget.py
diff options
context:
space:
mode:
authorsotech117 <michael_foiani@brown.edu>2025-07-31 17:27:24 -0400
committersotech117 <michael_foiani@brown.edu>2025-07-31 17:27:24 -0400
commit5bf22fc7e3c392c8bd44315ca2d06d7dca7d084e (patch)
tree8dacb0f195df1c0788d36dd0064f6bbaa3143ede /venv/lib/python3.8/site-packages/plotly/basewidget.py
parentb832d364da8c2efe09e3f75828caf73c50d01ce3 (diff)
add code for analysis of data
Diffstat (limited to 'venv/lib/python3.8/site-packages/plotly/basewidget.py')
-rw-r--r--venv/lib/python3.8/site-packages/plotly/basewidget.py989
1 files changed, 989 insertions, 0 deletions
diff --git a/venv/lib/python3.8/site-packages/plotly/basewidget.py b/venv/lib/python3.8/site-packages/plotly/basewidget.py
new file mode 100644
index 0000000..08c655f
--- /dev/null
+++ b/venv/lib/python3.8/site-packages/plotly/basewidget.py
@@ -0,0 +1,989 @@
+from copy import deepcopy
+import pathlib
+from traitlets import List, Dict, observe, Integer
+from plotly.io._renderers import display_jupyter_version_warnings
+
+from .basedatatypes import BaseFigure, BasePlotlyType
+from .callbacks import BoxSelector, LassoSelector, InputDeviceState, Points
+from .serializers import custom_serializers
+import anywidget
+
+
+class BaseFigureWidget(BaseFigure, anywidget.AnyWidget):
+ """
+ Base class for FigureWidget. The FigureWidget class is code-generated as a
+ subclass
+ """
+
+ _esm = pathlib.Path(__file__).parent / "package_data" / "widgetbundle.js"
+
+ # ### _data and _layout ###
+ # These properties store the current state of the traces and
+ # layout as JSON-style dicts. These dicts do not store any subclasses of
+ # `BasePlotlyType`
+ #
+ # Note: These are only automatically synced with the frontend on full
+ # assignment, not on mutation. We use this fact to only directly sync
+ # them to the front-end on FigureWidget construction. All other updates
+ # are made using mutation, and they are manually synced to the frontend
+ # using the relayout/restyle/update/etc. messages.
+ _widget_layout = Dict().tag(sync=True, **custom_serializers)
+ _widget_data = List().tag(sync=True, **custom_serializers)
+ _config = Dict().tag(sync=True, **custom_serializers)
+
+ # ### Python -> JS message properties ###
+ # These properties are used to send messages from Python to the
+ # frontend. Messages are sent by assigning the message contents to the
+ # appropriate _py2js_* property and then immediatly assigning None to the
+ # property.
+ #
+ # See JSDoc comments in the FigureModel class in js/src/Figure.js for
+ # detailed descriptions of the messages.
+ _py2js_addTraces = Dict(allow_none=True).tag(sync=True, **custom_serializers)
+ _py2js_restyle = Dict(allow_none=True).tag(sync=True, **custom_serializers)
+ _py2js_relayout = Dict(allow_none=True).tag(sync=True, **custom_serializers)
+ _py2js_update = Dict(allow_none=True).tag(sync=True, **custom_serializers)
+ _py2js_animate = Dict(allow_none=True).tag(sync=True, **custom_serializers)
+
+ _py2js_deleteTraces = Dict(allow_none=True).tag(sync=True, **custom_serializers)
+ _py2js_moveTraces = Dict(allow_none=True).tag(sync=True, **custom_serializers)
+
+ _py2js_removeLayoutProps = Dict(allow_none=True).tag(
+ sync=True, **custom_serializers
+ )
+ _py2js_removeTraceProps = Dict(allow_none=True).tag(sync=True, **custom_serializers)
+
+ # ### JS -> Python message properties ###
+ # These properties are used to receive messages from the frontend.
+ # Messages are received by defining methods that observe changes to these
+ # properties. Receive methods are named `_handler_js2py_*` where '*' is
+ # the name of the corresponding message property. Receive methods are
+ # responsible for setting the message property to None after retreiving
+ # the message data.
+ #
+ # See JSDoc comments in the FigureModel class in js/src/Figure.js for
+ # detailed descriptions of the messages.
+ _js2py_traceDeltas = Dict(allow_none=True).tag(sync=True, **custom_serializers)
+ _js2py_layoutDelta = Dict(allow_none=True).tag(sync=True, **custom_serializers)
+ _js2py_restyle = Dict(allow_none=True).tag(sync=True, **custom_serializers)
+ _js2py_relayout = Dict(allow_none=True).tag(sync=True, **custom_serializers)
+ _js2py_update = Dict(allow_none=True).tag(sync=True, **custom_serializers)
+ _js2py_pointsCallback = Dict(allow_none=True).tag(sync=True, **custom_serializers)
+
+ # ### Message tracking properties ###
+ # The _last_layout_edit_id and _last_trace_edit_id properties are used
+ # to keep track of the edit id of the message that most recently
+ # requested an update to the Figures layout or traces respectively.
+ #
+ # We track this information because we don't want to update the Figure's
+ # default layout/trace properties (_layout_defaults, _data_defaults)
+ # while edits are in process. This can lead to inconsistent property
+ # states.
+ _last_layout_edit_id = Integer(0).tag(sync=True)
+ _last_trace_edit_id = Integer(0).tag(sync=True)
+
+ _set_trace_uid = True
+ _allow_disable_validation = False
+
+ # Constructor
+ # -----------
+ def __init__(
+ self, data=None, layout=None, frames=None, skip_invalid=False, **kwargs
+ ):
+ # Call superclass constructors
+ # ----------------------------
+ # Note: We rename layout to layout_plotly because to deconflict it
+ # with the `layout` constructor parameter of the `widgets.DOMWidget`
+ # ipywidgets class
+ super(BaseFigureWidget, self).__init__(
+ data=data,
+ layout_plotly=layout,
+ frames=frames,
+ skip_invalid=skip_invalid,
+ **kwargs,
+ )
+
+ # Validate Frames
+ # ---------------
+ # Frames are not supported by figure widget
+ if self._frame_objs:
+ BaseFigureWidget._display_frames_error()
+
+ # Message States
+ # --------------
+ # ### Layout ###
+
+ # _last_layout_edit_id is described above
+ self._last_layout_edit_id = 0
+
+ # _layout_edit_in_process is set to True if there are layout edit
+ # operations that have been sent to the frontend that haven't
+ # completed yet.
+ self._layout_edit_in_process = False
+
+ # _waiting_edit_callbacks is a list of callback functions that
+ # should be executed as soon as all pending edit operations are
+ # completed
+ self._waiting_edit_callbacks = []
+
+ # ### Trace ###
+ # _last_trace_edit_id: described above
+ self._last_trace_edit_id = 0
+
+ # _trace_edit_in_process is set to True if there are trace edit
+ # operations that have been sent to the frontend that haven't
+ # completed yet.
+ self._trace_edit_in_process = False
+
+ # View count
+ # ----------
+ # ipywidget property that stores the number of active frontend
+ # views of this widget
+ self._view_count = 0
+
+ # Initialize widget layout and data for third-party widget integration
+ # --------------------------------------------------------------------
+ self._widget_layout = deepcopy(self._layout_obj._props)
+ self._widget_data = deepcopy(self._data)
+
+ def show(self, *args, **kwargs):
+ return self
+
+ # Python -> JavaScript Messages
+ # -----------------------------
+ def _send_relayout_msg(self, layout_data, source_view_id=None):
+ """
+ Send Plotly.relayout message to the frontend
+
+ Parameters
+ ----------
+ layout_data : dict
+ Plotly.relayout layout data
+ source_view_id : str
+ UID of view that triggered this relayout operation
+ (e.g. By the user clicking 'zoom' in the toolbar). None if the
+ operation was not triggered by a frontend view
+ """
+ # Increment layout edit messages IDs
+ # ----------------------------------
+ layout_edit_id = self._last_layout_edit_id + 1
+ self._last_layout_edit_id = layout_edit_id
+ self._layout_edit_in_process = True
+
+ # Build message
+ # -------------
+ msg_data = {
+ "relayout_data": layout_data,
+ "layout_edit_id": layout_edit_id,
+ "source_view_id": source_view_id,
+ }
+
+ # Send message
+ # ------------
+ self._py2js_relayout = msg_data
+ self._py2js_relayout = None
+
+ def _send_restyle_msg(self, restyle_data, trace_indexes=None, source_view_id=None):
+ """
+ Send Plotly.restyle message to the frontend
+
+ Parameters
+ ----------
+ restyle_data : dict
+ Plotly.restyle restyle data
+ trace_indexes : list[int]
+ List of trace indexes that the restyle operation
+ applies to
+ source_view_id : str
+ UID of view that triggered this restyle operation
+ (e.g. By the user clicking the legend to hide a trace).
+ None if the operation was not triggered by a frontend view
+ """
+
+ # Validate / normalize inputs
+ # ---------------------------
+ trace_indexes = self._normalize_trace_indexes(trace_indexes)
+
+ # Increment layout/trace edit message IDs
+ # ---------------------------------------
+ layout_edit_id = self._last_layout_edit_id + 1
+ self._last_layout_edit_id = layout_edit_id
+ self._layout_edit_in_process = True
+
+ trace_edit_id = self._last_trace_edit_id + 1
+ self._last_trace_edit_id = trace_edit_id
+ self._trace_edit_in_process = True
+
+ # Build message
+ # -------------
+ restyle_msg = {
+ "restyle_data": restyle_data,
+ "restyle_traces": trace_indexes,
+ "trace_edit_id": trace_edit_id,
+ "layout_edit_id": layout_edit_id,
+ "source_view_id": source_view_id,
+ }
+
+ # Send message
+ # ------------
+ self._py2js_restyle = restyle_msg
+ self._py2js_restyle = None
+
+ def _send_addTraces_msg(self, new_traces_data):
+ """
+ Send Plotly.addTraces message to the frontend
+
+ Parameters
+ ----------
+ new_traces_data : list[dict]
+ List of trace data for new traces as accepted by Plotly.addTraces
+ """
+
+ # Increment layout/trace edit message IDs
+ # ---------------------------------------
+ layout_edit_id = self._last_layout_edit_id + 1
+ self._last_layout_edit_id = layout_edit_id
+ self._layout_edit_in_process = True
+
+ trace_edit_id = self._last_trace_edit_id + 1
+ self._last_trace_edit_id = trace_edit_id
+ self._trace_edit_in_process = True
+
+ # Build message
+ # -------------
+ add_traces_msg = {
+ "trace_data": new_traces_data,
+ "trace_edit_id": trace_edit_id,
+ "layout_edit_id": layout_edit_id,
+ }
+
+ # Send message
+ # ------------
+ self._py2js_addTraces = add_traces_msg
+ self._py2js_addTraces = None
+
+ def _send_moveTraces_msg(self, current_inds, new_inds):
+ """
+ Send Plotly.moveTraces message to the frontend
+
+ Parameters
+ ----------
+ current_inds : list[int]
+ List of current trace indexes
+ new_inds : list[int]
+ List of new trace indexes
+ """
+
+ # Build message
+ # -------------
+ move_msg = {"current_trace_inds": current_inds, "new_trace_inds": new_inds}
+
+ # Send message
+ # ------------
+ self._py2js_moveTraces = move_msg
+ self._py2js_moveTraces = None
+
+ def _send_update_msg(
+ self, restyle_data, relayout_data, trace_indexes=None, source_view_id=None
+ ):
+ """
+ Send Plotly.update message to the frontend
+
+ Parameters
+ ----------
+ restyle_data : dict
+ Plotly.update restyle data
+ relayout_data : dict
+ Plotly.update relayout data
+ trace_indexes : list[int]
+ List of trace indexes that the update operation applies to
+ source_view_id : str
+ UID of view that triggered this update operation
+ (e.g. By the user clicking a button).
+ None if the operation was not triggered by a frontend view
+ """
+
+ # Validate / normalize inputs
+ # ---------------------------
+ trace_indexes = self._normalize_trace_indexes(trace_indexes)
+
+ # Increment layout/trace edit message IDs
+ # ---------------------------------------
+ trace_edit_id = self._last_trace_edit_id + 1
+ self._last_trace_edit_id = trace_edit_id
+ self._trace_edit_in_process = True
+
+ layout_edit_id = self._last_layout_edit_id + 1
+ self._last_layout_edit_id = layout_edit_id
+ self._layout_edit_in_process = True
+
+ # Build message
+ # -------------
+ update_msg = {
+ "style_data": restyle_data,
+ "layout_data": relayout_data,
+ "style_traces": trace_indexes,
+ "trace_edit_id": trace_edit_id,
+ "layout_edit_id": layout_edit_id,
+ "source_view_id": source_view_id,
+ }
+
+ # Send message
+ # ------------
+ self._py2js_update = update_msg
+ self._py2js_update = None
+
+ def _send_animate_msg(
+ self, styles_data, relayout_data, trace_indexes, animation_opts
+ ):
+ """
+ Send Plotly.update message to the frontend
+
+ Note: there is no source_view_id parameter because animations
+ triggered by the fontend are not currently supported
+
+ Parameters
+ ----------
+ styles_data : list[dict]
+ Plotly.animate styles data
+ relayout_data : dict
+ Plotly.animate relayout data
+ trace_indexes : list[int]
+ List of trace indexes that the animate operation applies to
+ """
+
+ # Validate / normalize inputs
+ # ---------------------------
+ trace_indexes = self._normalize_trace_indexes(trace_indexes)
+
+ # Increment layout/trace edit message IDs
+ # ---------------------------------------
+ trace_edit_id = self._last_trace_edit_id + 1
+ self._last_trace_edit_id = trace_edit_id
+ self._trace_edit_in_process = True
+
+ layout_edit_id = self._last_layout_edit_id + 1
+ self._last_layout_edit_id = layout_edit_id
+ self._layout_edit_in_process = True
+
+ # Build message
+ # -------------
+ animate_msg = {
+ "style_data": styles_data,
+ "layout_data": relayout_data,
+ "style_traces": trace_indexes,
+ "animation_opts": animation_opts,
+ "trace_edit_id": trace_edit_id,
+ "layout_edit_id": layout_edit_id,
+ "source_view_id": None,
+ }
+
+ # Send message
+ # ------------
+ self._py2js_animate = animate_msg
+ self._py2js_animate = None
+
+ def _send_deleteTraces_msg(self, delete_inds):
+ """
+ Send Plotly.deleteTraces message to the frontend
+
+ Parameters
+ ----------
+ delete_inds : list[int]
+ List of trace indexes of traces to delete
+ """
+
+ # Increment layout/trace edit message IDs
+ # ---------------------------------------
+ trace_edit_id = self._last_trace_edit_id + 1
+ self._last_trace_edit_id = trace_edit_id
+ self._trace_edit_in_process = True
+
+ layout_edit_id = self._last_layout_edit_id + 1
+ self._last_layout_edit_id = layout_edit_id
+ self._layout_edit_in_process = True
+
+ # Build message
+ # -------------
+ delete_msg = {
+ "delete_inds": delete_inds,
+ "layout_edit_id": layout_edit_id,
+ "trace_edit_id": trace_edit_id,
+ }
+
+ # Send message
+ # ------------
+ self._py2js_deleteTraces = delete_msg
+ self._py2js_deleteTraces = None
+
+ # JavaScript -> Python Messages
+ # -----------------------------
+ @observe("_js2py_traceDeltas")
+ def _handler_js2py_traceDeltas(self, change):
+ """
+ Process trace deltas message from the frontend
+ """
+
+ # Receive message
+ # ---------------
+ msg_data = change["new"]
+ if not msg_data:
+ self._js2py_traceDeltas = None
+ return
+
+ trace_deltas = msg_data["trace_deltas"]
+ trace_edit_id = msg_data["trace_edit_id"]
+
+ # Apply deltas
+ # ------------
+ # We only apply the deltas if this message corresponds to the most
+ # recent trace edit operation
+ if trace_edit_id == self._last_trace_edit_id:
+ # ### Loop over deltas ###
+ for delta in trace_deltas:
+ # #### Find existing trace for uid ###
+ trace_uid = delta["uid"]
+ trace_uids = [trace.uid for trace in self.data]
+ trace_index = trace_uids.index(trace_uid)
+ uid_trace = self.data[trace_index]
+
+ # #### Transform defaults to delta ####
+ delta_transform = BaseFigureWidget._transform_data(
+ uid_trace._prop_defaults, delta
+ )
+
+ # #### Remove overlapping properties ####
+ # If a property is present in both _props and _prop_defaults
+ # then we remove the copy from _props
+ remove_props = self._remove_overlapping_props(
+ uid_trace._props, uid_trace._prop_defaults
+ )
+
+ # #### Notify frontend model of property removal ####
+ if remove_props:
+ remove_trace_props_msg = {
+ "remove_trace": trace_index,
+ "remove_props": remove_props,
+ }
+ self._py2js_removeTraceProps = remove_trace_props_msg
+ self._py2js_removeTraceProps = None
+
+ # #### Dispatch change callbacks ####
+ self._dispatch_trace_change_callbacks(delta_transform, [trace_index])
+
+ # ### Trace edits no longer in process ###
+ self._trace_edit_in_process = False
+
+ # ### Call any waiting trace edit callbacks ###
+ if not self._layout_edit_in_process:
+ while self._waiting_edit_callbacks:
+ self._waiting_edit_callbacks.pop()()
+
+ self._js2py_traceDeltas = None
+
+ @observe("_js2py_layoutDelta")
+ def _handler_js2py_layoutDelta(self, change):
+ """
+ Process layout delta message from the frontend
+ """
+
+ # Receive message
+ # ---------------
+ msg_data = change["new"]
+ if not msg_data:
+ self._js2py_layoutDelta = None
+ return
+
+ layout_delta = msg_data["layout_delta"]
+ layout_edit_id = msg_data["layout_edit_id"]
+
+ # Apply delta
+ # -----------
+ # We only apply the delta if this message corresponds to the most
+ # recent layout edit operation
+ if layout_edit_id == self._last_layout_edit_id:
+ # ### Transform defaults to delta ###
+ delta_transform = BaseFigureWidget._transform_data(
+ self._layout_defaults, layout_delta
+ )
+
+ # ### Remove overlapping properties ###
+ # If a property is present in both _layout and _layout_defaults
+ # then we remove the copy from _layout
+ removed_props = self._remove_overlapping_props(
+ self._widget_layout, self._layout_defaults
+ )
+
+ # ### Notify frontend model of property removal ###
+ if removed_props:
+ remove_props_msg = {"remove_props": removed_props}
+
+ self._py2js_removeLayoutProps = remove_props_msg
+ self._py2js_removeLayoutProps = None
+
+ # ### Create axis objects ###
+ # For example, when a SPLOM trace is created the layout defaults
+ # may include axes that weren't explicitly defined by the user.
+ for proppath in delta_transform:
+ prop = proppath[0]
+ match = self.layout._subplot_re_match(prop)
+ if match and prop not in self.layout:
+ # We need to create a subplotid object
+ self.layout[prop] = {}
+
+ # ### Dispatch change callbacks ###
+ self._dispatch_layout_change_callbacks(delta_transform)
+
+ # ### Layout edits no longer in process ###
+ self._layout_edit_in_process = False
+
+ # ### Call any waiting layout edit callbacks ###
+ if not self._trace_edit_in_process:
+ while self._waiting_edit_callbacks:
+ self._waiting_edit_callbacks.pop()()
+
+ self._js2py_layoutDelta = None
+
+ @observe("_js2py_restyle")
+ def _handler_js2py_restyle(self, change):
+ """
+ Process Plotly.restyle message from the frontend
+ """
+
+ # Receive message
+ # ---------------
+ restyle_msg = change["new"]
+
+ if not restyle_msg:
+ self._js2py_restyle = None
+ return
+
+ style_data = restyle_msg["style_data"]
+ style_traces = restyle_msg["style_traces"]
+ source_view_id = restyle_msg["source_view_id"]
+
+ # Perform restyle
+ # ---------------
+ self.plotly_restyle(
+ restyle_data=style_data,
+ trace_indexes=style_traces,
+ source_view_id=source_view_id,
+ )
+
+ self._js2py_restyle = None
+
+ @observe("_js2py_update")
+ def _handler_js2py_update(self, change):
+ """
+ Process Plotly.update message from the frontend
+ """
+
+ # Receive message
+ # ---------------
+ update_msg = change["new"]
+
+ if not update_msg:
+ self._js2py_update = None
+ return
+
+ style = update_msg["style_data"]
+ trace_indexes = update_msg["style_traces"]
+ layout = update_msg["layout_data"]
+ source_view_id = update_msg["source_view_id"]
+
+ # Perform update
+ # --------------
+ self.plotly_update(
+ restyle_data=style,
+ relayout_data=layout,
+ trace_indexes=trace_indexes,
+ source_view_id=source_view_id,
+ )
+
+ self._js2py_update = None
+
+ @observe("_js2py_relayout")
+ def _handler_js2py_relayout(self, change):
+ """
+ Process Plotly.relayout message from the frontend
+ """
+
+ # Receive message
+ # ---------------
+ relayout_msg = change["new"]
+
+ if not relayout_msg:
+ self._js2py_relayout = None
+ return
+
+ relayout_data = relayout_msg["relayout_data"]
+ source_view_id = relayout_msg["source_view_id"]
+
+ if "lastInputTime" in relayout_data:
+ # Remove 'lastInputTime'. Seems to be an internal plotly
+ # property that is introduced for some plot types, but it is not
+ # actually a property in the schema
+ relayout_data.pop("lastInputTime")
+
+ # Perform relayout
+ # ----------------
+ self.plotly_relayout(relayout_data=relayout_data, source_view_id=source_view_id)
+
+ self._js2py_relayout = None
+
+ @observe("_js2py_pointsCallback")
+ def _handler_js2py_pointsCallback(self, change):
+ """
+ Process points callback message from the frontend
+ """
+
+ # Receive message
+ # ---------------
+ callback_data = change["new"]
+
+ if not callback_data:
+ self._js2py_pointsCallback = None
+ return
+
+ # Get event type
+ # --------------
+ event_type = callback_data["event_type"]
+
+ # Build Selector Object
+ # ---------------------
+ if callback_data.get("selector", None):
+ selector_data = callback_data["selector"]
+ selector_type = selector_data["type"]
+ selector_state = selector_data["selector_state"]
+ if selector_type == "box":
+ selector = BoxSelector(**selector_state)
+ elif selector_type == "lasso":
+ selector = LassoSelector(**selector_state)
+ else:
+ raise ValueError("Unsupported selector type: %s" % selector_type)
+ else:
+ selector = None
+
+ # Build Input Device State Object
+ # -------------------------------
+ if callback_data.get("device_state", None):
+ device_state_data = callback_data["device_state"]
+ state = InputDeviceState(**device_state_data)
+ else:
+ state = None
+
+ # Build Trace Points Dictionary
+ # -----------------------------
+ points_data = callback_data["points"]
+ trace_points = {
+ trace_ind: {
+ "point_inds": [],
+ "xs": [],
+ "ys": [],
+ "trace_name": self._data_objs[trace_ind].name,
+ "trace_index": trace_ind,
+ }
+ for trace_ind in range(len(self._data_objs))
+ }
+
+ for x, y, point_ind, trace_ind in zip(
+ points_data["xs"],
+ points_data["ys"],
+ points_data["point_indexes"],
+ points_data["trace_indexes"],
+ ):
+ trace_dict = trace_points[trace_ind]
+ trace_dict["xs"].append(x)
+ trace_dict["ys"].append(y)
+ trace_dict["point_inds"].append(point_ind)
+
+ # Dispatch callbacks
+ # ------------------
+ for trace_ind, trace_points_data in trace_points.items():
+ points = Points(**trace_points_data)
+ trace = self.data[trace_ind]
+
+ if event_type == "plotly_click":
+ trace._dispatch_on_click(points, state)
+ elif event_type == "plotly_hover":
+ trace._dispatch_on_hover(points, state)
+ elif event_type == "plotly_unhover":
+ trace._dispatch_on_unhover(points, state)
+ elif event_type == "plotly_selected":
+ trace._dispatch_on_selection(points, selector)
+ elif event_type == "plotly_deselect":
+ trace._dispatch_on_deselect(points)
+
+ self._js2py_pointsCallback = None
+
+ # Display
+ # -------
+ def _repr_html_(self):
+ """
+ Customize html representation
+ """
+ raise NotImplementedError # Prefer _repr_mimebundle_
+
+ def _repr_mimebundle_(self, include=None, exclude=None, validate=True, **kwargs):
+ """
+ Return mimebundle corresponding to default renderer.
+ """
+ display_jupyter_version_warnings()
+
+ # Widget layout and data need to be set here in case there are
+ # changes made to the figure after the widget is created but before
+ # the cell is run.
+ self._widget_layout = deepcopy(self._layout_obj._props)
+ self._widget_data = deepcopy(self._data)
+ return {
+ "application/vnd.jupyter.widget-view+json": {
+ "version_major": 2,
+ "version_minor": 0,
+ "model_id": self._model_id,
+ },
+ }
+
+ def _ipython_display_(self):
+ """
+ Handle rich display of figures in ipython contexts
+ """
+ raise NotImplementedError # Prefer _repr_mimebundle_
+
+ # Callbacks
+ # ---------
+ def on_edits_completed(self, fn):
+ """
+ Register a function to be called after all pending trace and layout
+ edit operations have completed
+
+ If there are no pending edit operations then function is called
+ immediately
+
+ Parameters
+ ----------
+ fn : callable
+ Function of zero arguments to be called when all pending edit
+ operations have completed
+ """
+ if self._layout_edit_in_process or self._trace_edit_in_process:
+ self._waiting_edit_callbacks.append(fn)
+ else:
+ fn()
+
+ # Validate No Frames
+ # ------------------
+ @property
+ def frames(self):
+ # Note: This property getter is identical to that of the superclass,
+ # but it must be included here because we're overriding the setter
+ # below.
+ return self._frame_objs
+
+ @frames.setter
+ def frames(self, new_frames):
+ if new_frames:
+ BaseFigureWidget._display_frames_error()
+
+ @staticmethod
+ def _display_frames_error():
+ """
+ Display an informative error when user attempts to set frames on a
+ FigureWidget
+
+ Raises
+ ------
+ ValueError
+ always
+ """
+ msg = """
+Frames are not supported by the plotly.graph_objs.FigureWidget class.
+Note: Frames are supported by the plotly.graph_objs.Figure class"""
+ raise ValueError(msg)
+
+ # Static Helpers
+ # --------------
+ @staticmethod
+ def _remove_overlapping_props(input_data, delta_data, prop_path=()):
+ """
+ Remove properties in input_data that are also in delta_data, and do so
+ recursively.
+
+ Exception: Never remove 'uid' from input_data, this property is used
+ to align traces
+
+ Parameters
+ ----------
+ input_data : dict|list
+ delta_data : dict|list
+
+ Returns
+ -------
+ list[tuple[str|int]]
+ List of removed property path tuples
+ """
+
+ # Initialize removed
+ # ------------------
+ # This is the list of path tuples to the properties that were
+ # removed from input_data
+ removed = []
+
+ # Handle dict
+ # -----------
+ if isinstance(input_data, dict):
+ assert isinstance(delta_data, dict)
+
+ for p, delta_val in delta_data.items():
+ if isinstance(delta_val, dict) or BaseFigure._is_dict_list(delta_val):
+ if p in input_data:
+ # ### Recurse ###
+ input_val = input_data[p]
+ recur_prop_path = prop_path + (p,)
+ recur_removed = BaseFigureWidget._remove_overlapping_props(
+ input_val, delta_val, recur_prop_path
+ )
+ removed.extend(recur_removed)
+
+ # Check whether the last property in input_val
+ # has been removed. If so, remove it entirely
+ if not input_val:
+ input_data.pop(p)
+ removed.append(recur_prop_path)
+
+ elif p in input_data and p != "uid":
+ # ### Remove property ###
+ input_data.pop(p)
+ removed.append(prop_path + (p,))
+
+ # Handle list
+ # -----------
+ elif isinstance(input_data, list):
+ assert isinstance(delta_data, list)
+
+ for i, delta_val in enumerate(delta_data):
+ if i >= len(input_data):
+ break
+
+ input_val = input_data[i]
+ if (
+ input_val is not None
+ and isinstance(delta_val, dict)
+ or BaseFigure._is_dict_list(delta_val)
+ ):
+ # ### Recurse ###
+ recur_prop_path = prop_path + (i,)
+ recur_removed = BaseFigureWidget._remove_overlapping_props(
+ input_val, delta_val, recur_prop_path
+ )
+
+ removed.extend(recur_removed)
+
+ return removed
+
+ @staticmethod
+ def _transform_data(to_data, from_data, should_remove=True, relayout_path=()):
+ """
+ Transform to_data into from_data and return relayout-style
+ description of the transformation
+
+ Parameters
+ ----------
+ to_data : dict|list
+ from_data : dict|list
+
+ Returns
+ -------
+ dict
+ relayout-style description of the transformation
+ """
+
+ # Initialize relayout data
+ # ------------------------
+ relayout_data = {}
+
+ # Handle dict
+ # -----------
+ if isinstance(to_data, dict):
+ # ### Validate from_data ###
+ if not isinstance(from_data, dict):
+ raise ValueError(
+ "Mismatched data types: {to_dict} {from_data}".format(
+ to_dict=to_data, from_data=from_data
+ )
+ )
+
+ # ### Add/modify properties ###
+ # Loop over props/vals
+ for from_prop, from_val in from_data.items():
+ # #### Handle compound vals recursively ####
+ if isinstance(from_val, dict) or BaseFigure._is_dict_list(from_val):
+ # ##### Init property value if needed #####
+ if from_prop not in to_data:
+ to_data[from_prop] = {} if isinstance(from_val, dict) else []
+
+ # ##### Transform property val recursively #####
+ input_val = to_data[from_prop]
+ relayout_data.update(
+ BaseFigureWidget._transform_data(
+ input_val,
+ from_val,
+ should_remove=should_remove,
+ relayout_path=relayout_path + (from_prop,),
+ )
+ )
+
+ # #### Handle simple vals directly ####
+ else:
+ if from_prop not in to_data or not BasePlotlyType._vals_equal(
+ to_data[from_prop], from_val
+ ):
+ to_data[from_prop] = from_val
+ relayout_path_prop = relayout_path + (from_prop,)
+ relayout_data[relayout_path_prop] = from_val
+
+ # ### Remove properties ###
+ if should_remove:
+ for remove_prop in set(to_data.keys()).difference(
+ set(from_data.keys())
+ ):
+ to_data.pop(remove_prop)
+
+ # Handle list
+ # -----------
+ elif isinstance(to_data, list):
+ # ### Validate from_data ###
+ if not isinstance(from_data, list):
+ raise ValueError(
+ "Mismatched data types: to_data: {to_data} {from_data}".format(
+ to_data=to_data, from_data=from_data
+ )
+ )
+
+ # ### Add/modify properties ###
+ # Loop over indexes / elements
+ for i, from_val in enumerate(from_data):
+ # #### Initialize element if needed ####
+ if i >= len(to_data):
+ to_data.append(None)
+ input_val = to_data[i]
+
+ # #### Handle compound element recursively ####
+ if input_val is not None and (
+ isinstance(from_val, dict) or BaseFigure._is_dict_list(from_val)
+ ):
+ relayout_data.update(
+ BaseFigureWidget._transform_data(
+ input_val,
+ from_val,
+ should_remove=should_remove,
+ relayout_path=relayout_path + (i,),
+ )
+ )
+
+ # #### Handle simple elements directly ####
+ else:
+ if not BasePlotlyType._vals_equal(to_data[i], from_val):
+ to_data[i] = from_val
+ relayout_data[relayout_path + (i,)] = from_val
+
+ return relayout_data