wg-backend-django/dell-env/lib/python3.11/site-packages/plotly/basewidget.py
2023-10-30 14:40:43 +07:00

997 lines
34 KiB
Python

import ipywidgets as widgets
from traitlets import List, Unicode, Dict, observe, Integer
from .basedatatypes import BaseFigure, BasePlotlyType
from .callbacks import BoxSelector, LassoSelector, InputDeviceState, Points
from .serializers import custom_serializers
from .version import __frontend_version__
@widgets.register
class BaseFigureWidget(BaseFigure, widgets.DOMWidget):
"""
Base class for FigureWidget. The FigureWidget class is code-generated as a
subclass
"""
# Widget Traits
# -------------
# Widget traitlets are automatically synchronized with the FigureModel
# JavaScript object
_view_name = Unicode("FigureView").tag(sync=True)
_view_module = Unicode("jupyterlab-plotly").tag(sync=True)
_view_module_version = Unicode(__frontend_version__).tag(sync=True)
_model_name = Unicode("FigureModel").tag(sync=True)
_model_module = Unicode("jupyterlab-plotly").tag(sync=True)
_model_module_version = Unicode(__frontend_version__).tag(sync=True)
# ### _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.
_layout = Dict().tag(sync=True, **custom_serializers)
_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
# 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._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.
"""
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