From 5bf22fc7e3c392c8bd44315ca2d06d7dca7d084e Mon Sep 17 00:00:00 2001 From: sotech117 Date: Thu, 31 Jul 2025 17:27:24 -0400 Subject: add code for analysis of data --- .../python3.8/site-packages/plotly/basewidget.py | 989 +++++++++++++++++++++ 1 file changed, 989 insertions(+) create mode 100644 venv/lib/python3.8/site-packages/plotly/basewidget.py (limited to 'venv/lib/python3.8/site-packages/plotly/basewidget.py') 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 -- cgit v1.2.3-70-g09d2