from __future__ import absolute_import import collections from collections import OrderedDict import re import six from six import string_types import warnings from contextlib import contextmanager from copy import deepcopy, copy import itertools from functools import reduce from _plotly_utils.utils import ( _natural_sort_strings, _get_int_type, split_multichar, split_string_positions, display_string_positions, chomp_empty_strings, find_closest_string, ) from _plotly_utils.exceptions import PlotlyKeyError from .optional_imports import get_module from . import shapeannotation from . import subplots # Create Undefined sentinel value # - Setting a property to None removes any existing value # - Setting a property to Undefined leaves existing value unmodified Undefined = object() def _len_dict_item(item): """ Because a parsed dict path is a tuple containings strings or integers, to know the length of the resulting string when printing we might need to convert to a string before calling len on it. """ try: l = len(item) except TypeError: try: l = len("%d" % (item,)) except TypeError: raise ValueError( "Cannot find string length of an item that is not string-like nor an integer." ) return l def _str_to_dict_path_full(key_path_str): """ Convert a key path string into a tuple of key path elements and also return a tuple of indices marking the beginning of each element in the string. Parameters ---------- key_path_str : str Key path string, where nested keys are joined on '.' characters and array indexes are specified using brackets (e.g. 'foo.bar[1]') Returns ------- tuple[str | int] tuple [int] """ # skip all the parsing if the string is empty if len(key_path_str): # split string on ".[]" and filter out empty strings key_path2 = split_multichar([key_path_str], list(".[]")) # Split out underscore # e.g. ['foo', 'bar_baz', '1'] -> ['foo', 'bar', 'baz', '1'] key_path3 = [] underscore_props = BaseFigure._valid_underscore_properties def _make_hyphen_key(key): if "_" in key[1:]: # For valid properties that contain underscores (error_x) # replace the underscores with hyphens to protect them # from being split up for under_prop, hyphen_prop in underscore_props.items(): key = key.replace(under_prop, hyphen_prop) return key def _make_underscore_key(key): return key.replace("-", "_") key_path2b = list(map(_make_hyphen_key, key_path2)) # Here we want to split up each non-empty string in the list at # underscores and recombine the strings using chomp_empty_strings so # that leading, trailing and multiple _ will be preserved def _split_and_chomp(s): if not len(s): return s s_split = split_multichar([s], list("_")) # handle key paths like "a_path_", "_another_path", or # "yet__another_path" by joining extra "_" to the string to the right or # the empty string if at the end s_chomped = chomp_empty_strings(s_split, "_", reverse=True) return s_chomped # after running _split_and_chomp on key_path2b, it will be a list # containing strings and lists of strings; concatenate the sublists with # the list ("lift" the items out of the sublists) key_path2c = list( reduce( lambda x, y: x + y if type(y) == type(list()) else x + [y], map(_split_and_chomp, key_path2b), [], ) ) key_path2d = list(map(_make_underscore_key, key_path2c)) all_elem_idcs = tuple(split_string_positions(list(key_path2d))) # remove empty strings, and indices pointing to them key_elem_pairs = list(filter(lambda t: len(t[1]), enumerate(key_path2d))) key_path3 = [x for _, x in key_elem_pairs] elem_idcs = [all_elem_idcs[i] for i, _ in key_elem_pairs] # Convert elements to ints if possible. # e.g. ['foo', 'bar', '0'] -> ['foo', 'bar', 0] for i in range(len(key_path3)): try: key_path3[i] = int(key_path3[i]) except ValueError as _: pass else: key_path3 = [] elem_idcs = [] return (tuple(key_path3), elem_idcs) def _remake_path_from_tuple(props): """ try to remake a path using the properties in props """ if len(props) == 0: return "" def _add_square_brackets_to_number(n): if type(n) == type(int()): return "[%d]" % (n,) return n def _prepend_dot_if_not_number(s): if not s.startswith("["): return "." + s return s props_all_str = list(map(_add_square_brackets_to_number, props)) props_w_underscore = props_all_str[:1] + list( map(_prepend_dot_if_not_number, props_all_str[1:]) ) return "".join(props_w_underscore) def _check_path_in_prop_tree(obj, path, error_cast=None): """ obj: the object in which the first property is looked up path: the path that will be split into properties to be looked up path can also be a tuple. In this case, it is combined using . and [] because it is impossible to reconstruct the string fully in order to give a decent error message. error_cast: this function walks down the property tree by looking up values in objects. So this will throw exceptions that are thrown by __getitem__, but in some cases we are checking the path for a different reason and would prefer throwing a more relevant exception (e.g., __getitem__ throws KeyError but __setitem__ throws ValueError for subclasses of BasePlotlyType and BaseFigure). So the resulting error can be "casted" to the passed in type, if not None. returns an Exception object or None. The caller can raise this exception to see where the lookup error occurred. """ if isinstance(path, tuple): path = _remake_path_from_tuple(path) prop, prop_idcs = _str_to_dict_path_full(path) prev_objs = [] for i, p in enumerate(prop): arg = "" prev_objs.append(obj) try: obj = obj[p] except (ValueError, KeyError, IndexError, TypeError) as e: arg = e.args[0] if issubclass(e.__class__, TypeError): # If obj doesn't support subscripting, state that and show the # (valid) property that gives the object that doesn't support # subscripting. if i > 0: validator = prev_objs[i - 1]._get_validator(prop[i - 1]) arg += """ Invalid value received for the '{plotly_name}' property of {parent_name} {description}""".format( parent_name=validator.parent_name, plotly_name=validator.plotly_name, description=validator.description(), ) # In case i is 0, the best we can do is indicate the first # property in the string as having caused the error disp_i = max(i - 1, 0) dict_item_len = _len_dict_item(prop[disp_i]) # if the path has trailing underscores, the prop string will start with "_" trailing_underscores = "" if prop[i][0] == "_": trailing_underscores = " and path has trailing underscores" # if the path has trailing underscores and the display index is # one less than the prop index (see above), then we can also # indicate the offending underscores if (trailing_underscores != "") and (disp_i != i): dict_item_len += _len_dict_item(prop[i]) arg += """ Property does not support subscripting%s: %s %s""" % ( trailing_underscores, path, display_string_positions( prop_idcs, disp_i, length=dict_item_len, char="^" ), ) else: # State that the property for which subscripting was attempted # is bad and indicate the start of the bad property. arg += """ Bad property path: %s %s""" % ( path, display_string_positions( prop_idcs, i, length=_len_dict_item(prop[i]), char="^" ), ) # Make KeyError more pretty by changing it to a PlotlyKeyError, # because the Python interpreter has a special way of printing # KeyError if isinstance(e, KeyError): e = PlotlyKeyError() if error_cast is not None: e = error_cast() e.args = (arg,) return e return None def _combine_dicts(dicts): all_args = dict() for d in dicts: for k in d: all_args[k] = d[k] return all_args def _indexing_combinations(dims, alls, product=False): """ Gives indexing tuples specified by the coordinates in dims. If a member of dims is 'all' then it is replaced by the corresponding member in alls. If product is True, then the cartesian product of all the indices is returned, otherwise the zip (that means index lists of mis-matched length will yield a list of tuples whose length is the length of the shortest list). """ if len(dims) == 0: # this is because list(itertools.product(*[])) returns [()] which has non-zero # length! return [] if len(dims) != len(alls): raise ValueError( "Must have corresponding values in alls for each value of dims. Got dims=%s and alls=%s." % (str(dims), str(alls)) ) r = [] for d, a in zip(dims, alls): if d == "all": d = a elif not isinstance(d, list): d = [d] r.append(d) if product: return itertools.product(*r) else: return zip(*r) def _is_select_subplot_coordinates_arg(*args): """ Returns true if any args are lists or the string 'all' """ return any((a == "all") or isinstance(a, list) for a in args) def _axis_spanning_shapes_docstr(shape_type): docstr = "" if shape_type == "hline": docstr = """ Add a horizontal line to a plot or subplot that extends infinitely in the x-dimension. Parameters ---------- y: float or int A number representing the y coordinate of the horizontal line.""" elif shape_type == "vline": docstr = """ Add a vertical line to a plot or subplot that extends infinitely in the y-dimension. Parameters ---------- x: float or int A number representing the x coordinate of the vertical line.""" elif shape_type == "hrect": docstr = """ Add a rectangle to a plot or subplot that extends infinitely in the x-dimension. Parameters ---------- y0: float or int A number representing the y coordinate of one side of the rectangle. y1: float or int A number representing the y coordinate of the other side of the rectangle.""" elif shape_type == "vrect": docstr = """ Add a rectangle to a plot or subplot that extends infinitely in the y-dimension. Parameters ---------- x0: float or int A number representing the x coordinate of one side of the rectangle. x1: float or int A number representing the x coordinate of the other side of the rectangle.""" docstr += """ exclude_empty_subplots: Boolean If True (default) do not place the shape on subplots that have no data plotted on them. row: None, int or 'all' Subplot row for shape indexed starting at 1. If 'all', addresses all rows in the specified column(s). If both row and col are None, addresses the first subplot if subplots exist, or the only plot. By default is "all". col: None, int or 'all' Subplot column for shape indexed starting at 1. If 'all', addresses all rows in the specified column(s). If both row and col are None, addresses the first subplot if subplots exist, or the only plot. By default is "all". annotation: dict or plotly.graph_objects.layout.Annotation. If dict(), it is interpreted as describing an annotation. The annotation is placed relative to the shape based on annotation_position (see below) unless its x or y value has been specified for the annotation passed here. xref and yref are always the same as for the added shape and cannot be overridden.""" if shape_type in ["hline", "vline"]: docstr += """ annotation_position: a string containing optionally ["top", "bottom"] and ["left", "right"] specifying where the text should be anchored to on the line. Example positions are "bottom left", "right top", "right", "bottom". If an annotation is added but annotation_position is not specified, this defaults to "top right".""" elif shape_type in ["hrect", "vrect"]: docstr += """ annotation_position: a string containing optionally ["inside", "outside"], ["top", "bottom"] and ["left", "right"] specifying where the text should be anchored to on the rectangle. Example positions are "outside top left", "inside bottom", "right", "inside left", "inside" ("outside" is not supported). If an annotation is added but annotation_position is not specified this defaults to "inside top right".""" docstr += """ annotation_*: any parameters to go.layout.Annotation can be passed as keywords by prefixing them with "annotation_". For example, to specify the annotation text "example" you can pass annotation_text="example" as a keyword argument. **kwargs: Any named function parameters that can be passed to 'add_shape', except for x0, x1, y0, y1 or type.""" return docstr def _generator(i): """ "cast" an iterator to a generator """ for x in i: yield x class BaseFigure(object): """ Base class for all figure types (both widget and non-widget) """ _bracket_re = re.compile(r"^(.*)\[(\d+)\]$") _valid_underscore_properties = { "error_x": "error-x", "error_y": "error-y", "error_z": "error-z", "copy_xstyle": "copy-xstyle", "copy_ystyle": "copy-ystyle", "copy_zstyle": "copy-zstyle", "paper_bgcolor": "paper-bgcolor", "plot_bgcolor": "plot-bgcolor", } _set_trace_uid = False _allow_disable_validation = True # Constructor # ----------- def __init__( self, data=None, layout_plotly=None, frames=None, skip_invalid=False, **kwargs ): """ Construct a BaseFigure object Parameters ---------- data One of: - A list or tuple of trace objects (or dicts that can be coerced into trace objects) - If `data` is a dict that contains a 'data', 'layout', or 'frames' key then these values are used to construct the figure. - If `data` is a `BaseFigure` instance then the `data`, `layout`, and `frames` properties are extracted from the input figure layout_plotly The plotly layout dict. Note: this property is named `layout_plotly` rather than `layout` to deconflict it with the `layout` constructor parameter of the `widgets.DOMWidget` ipywidgets class, as the `BaseFigureWidget` class is a subclass of both BaseFigure and widgets.DOMWidget. If the `data` property is a BaseFigure instance, or a dict that contains a 'layout' key, then this property is ignored. frames A list or tuple of `plotly.graph_objs.Frame` objects (or dicts that can be coerced into Frame objects) If the `data` property is a BaseFigure instance, or a dict that contains a 'frames' key, then this property is ignored. skip_invalid: bool If True, invalid properties in the figure specification will be skipped silently. If False (default) invalid properties in the figure specification will result in a ValueError Raises ------ ValueError if a property in the specification of data, layout, or frames is invalid AND skip_invalid is False """ from .validators import DataValidator, LayoutValidator, FramesValidator super(BaseFigure, self).__init__() # Initialize validation self._validate = kwargs.pop("_validate", True) # Assign layout_plotly to layout # ------------------------------ # See docstring note for explanation layout = layout_plotly # Subplot properties # ------------------ # These properties are used by the tools.make_subplots logic. # We initialize them to None here, before checking if the input data # object is a BaseFigure, or a dict with _grid_str and _grid_ref # properties, in which case we bring over the _grid* properties of # the input self._grid_str = None self._grid_ref = None # Handle case where data is a Figure or Figure-like dict # ------------------------------------------------------ if isinstance(data, BaseFigure): # Bring over subplot fields self._grid_str = data._grid_str self._grid_ref = data._grid_ref # Extract data, layout, and frames data, layout, frames = data.data, data.layout, data.frames elif isinstance(data, dict) and ( "data" in data or "layout" in data or "frames" in data ): # Bring over subplot fields self._grid_str = data.get("_grid_str", None) self._grid_ref = data.get("_grid_ref", None) # Extract data, layout, and frames data, layout, frames = ( data.get("data", None), data.get("layout", None), data.get("frames", None), ) # Handle data (traces) # -------------------- # ### Construct data validator ### # This is the validator that handles importing sequences of trace # objects self._data_validator = DataValidator(set_uid=self._set_trace_uid) # ### Import traces ### data = self._data_validator.validate_coerce( data, skip_invalid=skip_invalid, _validate=self._validate ) # ### Save tuple of trace objects ### self._data_objs = data # ### Import clone of trace properties ### # The _data property is a list of dicts containing the properties # explicitly set by the user for each trace. self._data = [deepcopy(trace._props) for trace in data] # ### Create data defaults ### # _data_defaults is a tuple of dicts, one for each trace. When # running in a widget context, these defaults are populated with # all property values chosen by the Plotly.js library that # aren't explicitly specified by the user. # # Note: No property should exist in both the _data and # _data_defaults for the same trace. self._data_defaults = [{} for _ in data] # ### Reparent trace objects ### for trace_ind, trace in enumerate(data): # By setting the trace's parent to be this figure, we tell the # trace object to use the figure's _data and _data_defaults # dicts to get/set it's properties, rather than using the trace # object's internal _orphan_props dict. trace._parent = self # We clear the orphan props since the trace no longer needs then trace._orphan_props.clear() # Set trace index trace._trace_ind = trace_ind # Layout # ------ # ### Construct layout validator ### # This is the validator that handles importing Layout objects self._layout_validator = LayoutValidator() # ### Import Layout ### self._layout_obj = self._layout_validator.validate_coerce( layout, skip_invalid=skip_invalid, _validate=self._validate ) # ### Import clone of layout properties ### self._layout = deepcopy(self._layout_obj._props) # ### Initialize layout defaults dict ### self._layout_defaults = {} # ### Reparent layout object ### self._layout_obj._orphan_props.clear() self._layout_obj._parent = self # Config # ------ # Pass along default config to the front end. For now this just # ensures that the plotly domain url gets passed to the front end. # In the future we can extend this to allow the user to supply # arbitrary config options like in plotly.offline.plot/iplot. But # this will require a fair amount of testing to determine which # options are compatible with FigureWidget. from plotly.offline.offline import _get_jconfig self._config = _get_jconfig(None) # Frames # ------ # ### Construct frames validator ### # This is the validator that handles importing sequences of frame # objects self._frames_validator = FramesValidator() # ### Import frames ### self._frame_objs = self._frames_validator.validate_coerce( frames, skip_invalid=skip_invalid ) # Note: Because frames are not currently supported in the widget # context, we don't need to follow the pattern above and create # _frames and _frame_defaults properties and then reparent the # frames. The figure doesn't need to be notified of # changes to the properties in the frames object hierarchy. # Context manager # --------------- # ### batch mode indicator ### # Flag that indicates whether we're currently inside a batch_*() # context self._in_batch_mode = False # ### Batch trace edits ### # Dict from trace indexes to trace edit dicts. These trace edit dicts # are suitable as `data` elements of Plotly.animate, but not # the Plotly.update (See `_build_update_params_from_batch`) self._batch_trace_edits = OrderedDict() # ### Batch layout edits ### # Dict from layout properties to new layout values. This dict is # directly suitable for use in Plotly.animate and Plotly.update self._batch_layout_edits = OrderedDict() # Animation property validators # ----------------------------- from . import animation self._animation_duration_validator = animation.DurationValidator() self._animation_easing_validator = animation.EasingValidator() # Template # -------- # ### Check for default template ### self._initialize_layout_template() # Process kwargs # -------------- for k, v in kwargs.items(): err = _check_path_in_prop_tree(self, k) if err is None: self[k] = v elif not skip_invalid: type_err = TypeError("invalid Figure property: {}".format(k)) type_err.args = ( type_err.args[0] + """ %s""" % (err.args[0],), ) raise type_err # Magic Methods # ------------- def __reduce__(self): """ Custom implementation of reduce is used to support deep copying and pickling """ props = self.to_dict() props["_grid_str"] = self._grid_str props["_grid_ref"] = self._grid_ref return (self.__class__, (props,)) def __setitem__(self, prop, value): # Normalize prop # -------------- # Convert into a property tuple orig_prop = prop prop = BaseFigure._str_to_dict_path(prop) # Handle empty case # ----------------- if len(prop) == 0: raise KeyError(orig_prop) # Handle scalar case # ------------------ # e.g. ('foo',) elif len(prop) == 1: # ### Unwrap scalar tuple ### prop = prop[0] if prop == "data": self.data = value elif prop == "layout": self.layout = value elif prop == "frames": self.frames = value else: raise KeyError(prop) # Handle non-scalar case # ---------------------- # e.g. ('foo', 1) else: err = _check_path_in_prop_tree(self, orig_prop, error_cast=ValueError) if err is not None: raise err res = self for p in prop[:-1]: res = res[p] res._validate = self._validate res[prop[-1]] = value def __setattr__(self, prop, value): """ Parameters ---------- prop : str The name of a direct child of this object value New property value Returns ------- None """ if prop.startswith("_") or hasattr(self, prop): # Let known properties and private properties through super(BaseFigure, self).__setattr__(prop, value) else: # Raise error on unknown public properties raise AttributeError(prop) def __getitem__(self, prop): # Normalize prop # -------------- # Convert into a property tuple orig_prop = prop prop = BaseFigure._str_to_dict_path(prop) # Handle scalar case # ------------------ # e.g. ('foo',) if len(prop) == 1: # Unwrap scalar tuple prop = prop[0] if prop == "data": return self._data_validator.present(self._data_objs) elif prop == "layout": return self._layout_validator.present(self._layout_obj) elif prop == "frames": return self._frames_validator.present(self._frame_objs) else: raise KeyError(orig_prop) # Handle non-scalar case # ---------------------- # e.g. ('foo', 1) else: err = _check_path_in_prop_tree(self, orig_prop, error_cast=PlotlyKeyError) if err is not None: raise err res = self for p in prop: res = res[p] return res def __iter__(self): return iter(("data", "layout", "frames")) def __contains__(self, prop): prop = BaseFigure._str_to_dict_path(prop) if prop[0] not in ("data", "layout", "frames"): return False elif len(prop) == 1: return True else: return prop[1:] in self[prop[0]] def __eq__(self, other): if not isinstance(other, BaseFigure): # Require objects to both be BaseFigure instances return False else: # Compare plotly_json representations # Use _vals_equal instead of `==` to handle cases where # underlying dicts contain numpy arrays return BasePlotlyType._vals_equal( self.to_plotly_json(), other.to_plotly_json() ) def __repr__(self): """ Customize Figure representation when displayed in the terminal/notebook """ props = self.to_plotly_json() # Elide template template_props = props.get("layout", {}).get("template", {}) if template_props: props["layout"]["template"] = "..." repr_str = BasePlotlyType._build_repr_for_class( props=props, class_name=self.__class__.__name__ ) return repr_str def _repr_html_(self): """ Customize html representation """ bundle = self._repr_mimebundle_() if "text/html" in bundle: return bundle["text/html"] else: return self.to_html(full_html=False, include_plotlyjs="cdn") def _repr_mimebundle_(self, include=None, exclude=None, validate=True, **kwargs): """ Return mimebundle corresponding to default renderer. """ import plotly.io as pio renderer_str = pio.renderers.default renderers = pio._renderers.renderers renderer_names = renderers._validate_coerce_renderers(renderer_str) renderers_list = [renderers[name] for name in renderer_names] from plotly.io._utils import validate_coerce_fig_to_dict from plotly.io._renderers import MimetypeRenderer fig_dict = validate_coerce_fig_to_dict(self, validate) # Mimetype renderers bundle = {} for renderer in renderers_list: if isinstance(renderer, MimetypeRenderer): bundle.update(renderer.to_mimebundle(fig_dict)) return bundle def _ipython_display_(self): """ Handle rich display of figures in ipython contexts """ import plotly.io as pio if pio.renderers.render_on_display and pio.renderers.default: pio.show(self) else: print(repr(self)) def update(self, dict1=None, overwrite=False, **kwargs): """ Update the properties of the figure with a dict and/or with keyword arguments. This recursively updates the structure of the figure object with the values in the input dict / keyword arguments. Parameters ---------- dict1 : dict Dictionary of properties to be updated overwrite: bool If True, overwrite existing properties. If False, apply updates to existing properties recursively, preserving existing properties that are not specified in the update operation. kwargs : Keyword/value pair of properties to be updated Examples -------- >>> import plotly.graph_objs as go >>> fig = go.Figure(data=[{'y': [1, 2, 3]}]) >>> fig.update(data=[{'y': [4, 5, 6]}]) # doctest: +ELLIPSIS Figure(...) >>> fig.to_plotly_json() # doctest: +SKIP {'data': [{'type': 'scatter', 'uid': 'e86a7c7a-346a-11e8-8aa8-a0999b0c017b', 'y': array([4, 5, 6], dtype=int32)}], 'layout': {}} >>> fig = go.Figure(layout={'xaxis': ... {'color': 'green', ... 'range': [0, 1]}}) >>> fig.update({'layout': {'xaxis': {'color': 'pink'}}}) # doctest: +ELLIPSIS Figure(...) >>> fig.to_plotly_json() # doctest: +SKIP {'data': [], 'layout': {'xaxis': {'color': 'pink', 'range': [0, 1]}}} Returns ------- BaseFigure Updated figure """ with self.batch_update(): for d in [dict1, kwargs]: if d: for k, v in d.items(): update_target = self[k] if update_target == () or overwrite: if k == "data": # Overwrite all traces as special due to # restrictions on trace assignment self.data = () self.add_traces(v) else: # Accept v self[k] = v elif ( isinstance(update_target, BasePlotlyType) and isinstance(v, (dict, BasePlotlyType)) ) or ( isinstance(update_target, tuple) and isinstance(update_target[0], BasePlotlyType) ): BaseFigure._perform_update(self[k], v) else: self[k] = v return self def pop(self, key, *args): """ Remove the value associated with the specified key and return it Parameters ---------- key: str Property name dflt The default value to return if key was not found in figure Returns ------- value The removed value that was previously associated with key Raises ------ KeyError If key is not in object and no dflt argument specified """ # Handle default if key not in self and args: return args[0] elif key in self: val = self[key] self[key] = None return val else: raise KeyError(key) # Data # ---- @property def data(self): """ The `data` property is a tuple of the figure's trace objects Returns ------- tuple[BaseTraceType] """ return self["data"] @data.setter def data(self, new_data): # Validate new_data # ----------------- err_header = ( "The data property of a figure may only be assigned \n" "a list or tuple that contains a permutation of a " "subset of itself.\n" ) # ### Treat None as empty ### if new_data is None: new_data = () # ### Check valid input type ### if not isinstance(new_data, (list, tuple)): err_msg = err_header + " Received value with type {typ}".format( typ=type(new_data) ) raise ValueError(err_msg) # ### Check valid element types ### for trace in new_data: if not isinstance(trace, BaseTraceType): err_msg = ( err_header + " Received element value of type {typ}".format(typ=type(trace)) ) raise ValueError(err_msg) # ### Check trace objects ### # Require that no new traces are introduced orig_uids = [id(trace) for trace in self.data] new_uids = [id(trace) for trace in new_data] invalid_uids = set(new_uids).difference(set(orig_uids)) if invalid_uids: err_msg = err_header raise ValueError(err_msg) # ### Check for duplicates in assignment ### uid_counter = collections.Counter(new_uids) duplicate_uids = [uid for uid, count in uid_counter.items() if count > 1] if duplicate_uids: err_msg = err_header + " Received duplicated traces" raise ValueError(err_msg) # Remove traces # ------------- remove_uids = set(orig_uids).difference(set(new_uids)) delete_inds = [] # ### Unparent removed traces ### for i, trace in enumerate(self.data): if id(trace) in remove_uids: delete_inds.append(i) # Unparent trace object to be removed old_trace = self.data[i] old_trace._orphan_props.update(deepcopy(old_trace._props)) old_trace._parent = None old_trace._trace_ind = None # ### Compute trace props / defaults after removal ### traces_props_post_removal = [t for t in self._data] traces_prop_defaults_post_removal = [t for t in self._data_defaults] uids_post_removal = [id(trace_data) for trace_data in self.data] for i in reversed(delete_inds): del traces_props_post_removal[i] del traces_prop_defaults_post_removal[i] del uids_post_removal[i] # Modify in-place so we don't trigger serialization del self._data[i] if delete_inds: # Update widget, if any self._send_deleteTraces_msg(delete_inds) # Move traces # ----------- # ### Compute new index for each remaining trace ### new_inds = [] for uid in uids_post_removal: new_inds.append(new_uids.index(uid)) # ### Compute current index for each remaining trace ### current_inds = list(range(len(traces_props_post_removal))) # ### Check whether a move is needed ### if not all([i1 == i2 for i1, i2 in zip(new_inds, current_inds)]): # #### Save off index lists for moveTraces message #### msg_current_inds = current_inds msg_new_inds = new_inds # #### Reorder trace elements #### # We do so in-place so we don't trigger traitlet property # serialization for the FigureWidget case # ##### Remove by curr_inds in reverse order ##### moving_traces_data = [] for ci in reversed(current_inds): # Push moving traces data to front of list moving_traces_data.insert(0, self._data[ci]) del self._data[ci] # #### Sort new_inds and moving_traces_data by new_inds #### new_inds, moving_traces_data = zip( *sorted(zip(new_inds, moving_traces_data)) ) # #### Insert by new_inds in forward order #### for ni, trace_data in zip(new_inds, moving_traces_data): self._data.insert(ni, trace_data) # #### Update widget, if any #### self._send_moveTraces_msg(msg_current_inds, msg_new_inds) # ### Update data defaults ### # There is to front-end syncronization to worry about so this # operations doesn't need to be in-place self._data_defaults = [ _trace for i, _trace in sorted(zip(new_inds, traces_prop_defaults_post_removal)) ] # Update trace objects tuple self._data_objs = list(new_data) # Update trace indexes for trace_ind, trace in enumerate(self._data_objs): trace._trace_ind = trace_ind def select_traces(self, selector=None, row=None, col=None, secondary_y=None): """ Select traces from a particular subplot cell and/or traces that satisfy custom selection criteria. Parameters ---------- selector: dict, function, int, str or None (default None) Dict to use as selection criteria. Traces will be selected if they contain properties corresponding to all of the dictionary's keys, with values that exactly match the supplied values. If None (the default), all traces are selected. If a function, it must be a function accepting a single argument and returning a boolean. The function will be called on each trace and those for which the function returned True will be in the selection. If an int N, the Nth trace matching row and col will be selected (N can be negative). If a string S, the selector is equivalent to dict(type=S). row, col: int or None (default None) Subplot row and column index of traces to select. To select traces by row and column, the Figure must have been created using plotly.subplots.make_subplots. If None (the default), all traces are selected. secondary_y: boolean or None (default None) * If True, only select traces associated with the secondary y-axis of the subplot. * If False, only select traces associated with the primary y-axis of the subplot. * If None (the default), do not filter traces based on secondary y-axis. To select traces by secondary y-axis, the Figure must have been created using plotly.subplots.make_subplots. See the docstring for the specs argument to make_subplots for more info on creating subplots with secondary y-axes. Returns ------- generator Generator that iterates through all of the traces that satisfy all of the specified selection criteria """ if not selector: selector = {} if row is not None or col is not None or secondary_y is not None: grid_ref = self._validate_get_grid_ref() filter_by_subplot = True if row is None and col is not None: # All rows for column grid_subplot_ref_tuples = [ref_row[col - 1] for ref_row in grid_ref] elif col is None and row is not None: # All columns for row grid_subplot_ref_tuples = grid_ref[row - 1] elif col is not None and row is not None: # Single grid cell grid_subplot_ref_tuples = [grid_ref[row - 1][col - 1]] else: # row and col are None, secondary_y not None grid_subplot_ref_tuples = [ refs for refs_row in grid_ref for refs in refs_row ] # Collect list of subplot refs, taking secondary_y into account grid_subplot_refs = [] for refs in grid_subplot_ref_tuples: if not refs: continue if secondary_y is not True: grid_subplot_refs.append(refs[0]) if secondary_y is not False and len(refs) > 1: grid_subplot_refs.append(refs[1]) else: filter_by_subplot = False grid_subplot_refs = None return self._perform_select_traces( filter_by_subplot, grid_subplot_refs, selector ) def _perform_select_traces(self, filter_by_subplot, grid_subplot_refs, selector): from plotly.subplots import _get_subplot_ref_for_trace # functions for filtering def _filter_by_subplot_ref(trace): trace_subplot_ref = _get_subplot_ref_for_trace(trace) return trace_subplot_ref in grid_subplot_refs funcs = [] if filter_by_subplot: funcs.append(_filter_by_subplot_ref) return _generator(self._filter_by_selector(self.data, funcs, selector)) @staticmethod def _selector_matches(obj, selector): if selector is None: return True # If selector is a string then put it at the 'type' key of a dictionary # to select objects where "type":selector if isinstance(selector, six.string_types): selector = dict(type=selector) # If selector is a dict, compare the fields if isinstance(selector, dict) or isinstance(selector, BasePlotlyType): # This returns True if selector is an empty dict for k in selector: if k not in obj: return False obj_val = obj[k] selector_val = selector[k] if isinstance(obj_val, BasePlotlyType): obj_val = obj_val.to_plotly_json() if isinstance(selector_val, BasePlotlyType): selector_val = selector_val.to_plotly_json() if obj_val != selector_val: return False return True # If selector is a function, call it with the obj as the argument elif six.callable(selector): return selector(obj) else: raise TypeError( "selector must be dict or a function " "accepting a graph object returning a boolean." ) def _filter_by_selector(self, objects, funcs, selector): """ objects is a sequence of objects, funcs a list of functions that return True if the object should be included in the selection and False otherwise and selector is an argument to the self._selector_matches function. If selector is an integer, the resulting sequence obtained after sucessively filtering by each function in funcs is indexed by this integer. Otherwise selector is used as the selector argument to self._selector_matches which is used to filter down the sequence. The function returns the sequence (an iterator). """ # if selector is not an int, we call it on each trace to test it for selection if not isinstance(selector, int): funcs.append(lambda obj: self._selector_matches(obj, selector)) def _filt(last, f): return filter(f, last) filtered_objects = reduce(_filt, funcs, objects) if isinstance(selector, int): return iter([list(filtered_objects)[selector]]) return filtered_objects def for_each_trace(self, fn, selector=None, row=None, col=None, secondary_y=None): """ Apply a function to all traces that satisfy the specified selection criteria Parameters ---------- fn: Function that inputs a single trace object. selector: dict, function, int, str or None (default None) Dict to use as selection criteria. Traces will be selected if they contain properties corresponding to all of the dictionary's keys, with values that exactly match the supplied values. If None (the default), all traces are selected. If a function, it must be a function accepting a single argument and returning a boolean. The function will be called on each trace and those for which the function returned True will be in the selection. If an int N, the Nth trace matching row and col will be selected (N can be negative). If a string S, the selector is equivalent to dict(type=S). row, col: int or None (default None) Subplot row and column index of traces to select. To select traces by row and column, the Figure must have been created using plotly.subplots.make_subplots. If None (the default), all traces are selected. secondary_y: boolean or None (default None) * If True, only select traces associated with the secondary y-axis of the subplot. * If False, only select traces associated with the primary y-axis of the subplot. * If None (the default), do not filter traces based on secondary y-axis. To select traces by secondary y-axis, the Figure must have been created using plotly.subplots.make_subplots. See the docstring for the specs argument to make_subplots for more info on creating subplots with secondary y-axes. Returns ------- self Returns the Figure object that the method was called on """ for trace in self.select_traces( selector=selector, row=row, col=col, secondary_y=secondary_y ): fn(trace) return self def update_traces( self, patch=None, selector=None, row=None, col=None, secondary_y=None, overwrite=False, **kwargs ): """ Perform a property update operation on all traces that satisfy the specified selection criteria Parameters ---------- patch: dict or None (default None) Dictionary of property updates to be applied to all traces that satisfy the selection criteria. selector: dict, function, int, str or None (default None) Dict to use as selection criteria. Traces will be selected if they contain properties corresponding to all of the dictionary's keys, with values that exactly match the supplied values. If None (the default), all traces are selected. If a function, it must be a function accepting a single argument and returning a boolean. The function will be called on each trace and those for which the function returned True will be in the selection. If an int N, the Nth trace matching row and col will be selected (N can be negative). If a string S, the selector is equivalent to dict(type=S). row, col: int or None (default None) Subplot row and column index of traces to select. To select traces by row and column, the Figure must have been created using plotly.subplots.make_subplots. If None (the default), all traces are selected. secondary_y: boolean or None (default None) * If True, only select traces associated with the secondary y-axis of the subplot. * If False, only select traces associated with the primary y-axis of the subplot. * If None (the default), do not filter traces based on secondary y-axis. To select traces by secondary y-axis, the Figure must have been created using plotly.subplots.make_subplots. See the docstring for the specs argument to make_subplots for more info on creating subplots with secondary y-axes. overwrite: bool If True, overwrite existing properties. If False, apply updates to existing properties recursively, preserving existing properties that are not specified in the update operation. **kwargs Additional property updates to apply to each selected trace. If a property is specified in both patch and in **kwargs then the one in **kwargs takes precedence. Returns ------- self Returns the Figure object that the method was called on """ for trace in self.select_traces( selector=selector, row=row, col=col, secondary_y=secondary_y ): trace.update(patch, overwrite=overwrite, **kwargs) return self def update_layout(self, dict1=None, overwrite=False, **kwargs): """ Update the properties of the figure's layout with a dict and/or with keyword arguments. This recursively updates the structure of the original layout with the values in the input dict / keyword arguments. Parameters ---------- dict1 : dict Dictionary of properties to be updated overwrite: bool If True, overwrite existing properties. If False, apply updates to existing properties recursively, preserving existing properties that are not specified in the update operation. kwargs : Keyword/value pair of properties to be updated Returns ------- BaseFigure The Figure object that the update_layout method was called on """ self.layout.update(dict1, overwrite=overwrite, **kwargs) return self def _select_layout_subplots_by_prefix( self, prefix, selector=None, row=None, col=None, secondary_y=None ): """ Helper called by code generated select_* methods """ if row is not None or col is not None or secondary_y is not None: # Build mapping from container keys ('xaxis2', 'scene4', etc.) # to (row, col, secondary_y) triplets grid_ref = self._validate_get_grid_ref() container_to_row_col = {} for r, subplot_row in enumerate(grid_ref): for c, subplot_refs in enumerate(subplot_row): if not subplot_refs: continue # collect primary keys for i, subplot_ref in enumerate(subplot_refs): for layout_key in subplot_ref.layout_keys: if layout_key.startswith(prefix): is_secondary_y = i == 1 container_to_row_col[layout_key] = ( r + 1, c + 1, is_secondary_y, ) else: container_to_row_col = None layout_keys_filters = [ lambda k: k.startswith(prefix) and self.layout[k] is not None, lambda k: row is None or container_to_row_col.get(k, (None, None, None))[0] == row, lambda k: col is None or container_to_row_col.get(k, (None, None, None))[1] == col, lambda k: ( secondary_y is None or container_to_row_col.get(k, (None, None, None))[2] == secondary_y ), ] layout_keys = reduce( lambda last, f: filter(f, last), layout_keys_filters, # Natural sort keys so that xaxis20 is after xaxis3 _natural_sort_strings(list(self.layout)), ) layout_objs = [self.layout[k] for k in layout_keys] return _generator(self._filter_by_selector(layout_objs, [], selector)) def _select_annotations_like( self, prop, selector=None, row=None, col=None, secondary_y=None ): """ Helper to select annotation-like elements from a layout object array. Compatible with layout.annotations, layout.shapes, and layout.images """ xref_to_col = {} yref_to_row = {} yref_to_secondary_y = {} if isinstance(row, int) or isinstance(col, int) or secondary_y is not None: grid_ref = self._validate_get_grid_ref() for r, subplot_row in enumerate(grid_ref): for c, subplot_refs in enumerate(subplot_row): if not subplot_refs: continue for i, subplot_ref in enumerate(subplot_refs): if subplot_ref.subplot_type == "xy": is_secondary_y = i == 1 xaxis, yaxis = subplot_ref.layout_keys xref = xaxis.replace("axis", "") yref = yaxis.replace("axis", "") xref_to_col[xref] = c + 1 yref_to_row[yref] = r + 1 yref_to_secondary_y[yref] = is_secondary_y # filter down (select) which graph objects, by applying the filters # successively def _filter_row(obj): """ Filter objects in rows by column """ return (col is None) or (xref_to_col.get(obj.xref, None) == col) def _filter_col(obj): """ Filter objects in columns by row """ return (row is None) or (yref_to_row.get(obj.yref, None) == row) def _filter_sec_y(obj): """ Filter objects on secondary y axes """ return (secondary_y is None) or ( yref_to_secondary_y.get(obj.yref, None) == secondary_y ) funcs = [_filter_row, _filter_col, _filter_sec_y] return _generator(self._filter_by_selector(self.layout[prop], funcs, selector)) def _add_annotation_like( self, prop_singular, prop_plural, new_obj, row=None, col=None, secondary_y=None, exclude_empty_subplots=False, ): # Make sure we have both row and col or neither if row is not None and col is None: raise ValueError( "Received row parameter but not col.\n" "row and col must be specified together" ) elif col is not None and row is None: raise ValueError( "Received col parameter but not row.\n" "row and col must be specified together" ) # Address multiple subplots if row is not None and _is_select_subplot_coordinates_arg(row, col): # TODO product argument could be added rows_cols = self._select_subplot_coordinates(row, col) for r, c in rows_cols: self._add_annotation_like( prop_singular, prop_plural, new_obj, row=r, col=c, secondary_y=secondary_y, exclude_empty_subplots=exclude_empty_subplots, ) return self # Get grid_ref if specific row or column requested if row is not None: grid_ref = self._validate_get_grid_ref() if row > len(grid_ref): raise IndexError( "row index %d out-of-bounds, row index must be between 1 and %d, inclusive." % (row, len(grid_ref)) ) if col > len(grid_ref[row - 1]): raise IndexError( "column index %d out-of-bounds, " "column index must be between 1 and %d, inclusive." % (row, len(grid_ref[row - 1])) ) refs = grid_ref[row - 1][col - 1] if not refs: raise ValueError( "No subplot found at position ({r}, {c})".format(r=row, c=col) ) if refs[0].subplot_type != "xy": raise ValueError( """ Cannot add {prop_singular} to subplot at position ({r}, {c}) because subplot is of type {subplot_type}.""".format( prop_singular=prop_singular, r=row, c=col, subplot_type=refs[0].subplot_type, ) ) if len(refs) == 1 and secondary_y: raise ValueError( """ Cannot add {prop_singular} to secondary y-axis of subplot at position ({r}, {c}) because subplot does not have a secondary y-axis""" ) if secondary_y: xaxis, yaxis = refs[1].layout_keys else: xaxis, yaxis = refs[0].layout_keys xref, yref = xaxis.replace("axis", ""), yaxis.replace("axis", "") # if exclude_empty_subplots is True, check to see if subplot is # empty and return if it is if exclude_empty_subplots and ( not self._subplot_not_empty( xref, yref, selector=bool(exclude_empty_subplots) ) ): return self # in case the user specified they wanted an axis to refer to the # domain of that axis and not the data, append ' domain' to the # computed axis accordingly def _add_domain(ax_letter, new_axref): axref = ax_letter + "ref" if axref in new_obj._props.keys() and "domain" in new_obj[axref]: new_axref += " domain" return new_axref xref, yref = map(lambda t: _add_domain(*t), zip(["x", "y"], [xref, yref])) new_obj.update(xref=xref, yref=yref) self.layout[prop_plural] += (new_obj,) return self # Restyle # ------- def plotly_restyle(self, restyle_data, trace_indexes=None, **kwargs): """ Perform a Plotly restyle operation on the figure's traces Parameters ---------- restyle_data : dict Dict of trace style updates. Keys are strings that specify the properties to be updated. Nested properties are expressed by joining successive keys on '.' characters (e.g. 'marker.color'). Values may be scalars or lists. When values are scalars, that scalar value is applied to all traces specified by the `trace_indexes` parameter. When values are lists, the restyle operation will cycle through the elements of the list as it cycles through the traces specified by the `trace_indexes` parameter. Caution: To use plotly_restyle to update a list property (e.g. the `x` property of the scatter trace), the property value should be a scalar list containing the list to update with. For example, the following command would be used to update the 'x' property of the first trace to the list [1, 2, 3] >>> import plotly.graph_objects as go >>> fig = go.Figure(go.Scatter(x=[2, 4, 6])) >>> fig.plotly_restyle({'x': [[1, 2, 3]]}, 0) trace_indexes : int or list of int Trace index, or list of trace indexes, that the restyle operation applies to. Defaults to all trace indexes. Returns ------- None """ # Normalize trace indexes # ----------------------- trace_indexes = self._normalize_trace_indexes(trace_indexes) # Handle source_view_id # --------------------- # If not None, the source_view_id is the UID of the frontend # Plotly.js view that initially triggered this restyle operation # (e.g. the user clicked on the legend to hide a trace). We pass # this UID along so that the frontend views can determine whether # they need to apply the restyle operation on themselves. source_view_id = kwargs.get("source_view_id", None) # Perform restyle on trace dicts # ------------------------------ restyle_changes = self._perform_plotly_restyle(restyle_data, trace_indexes) if restyle_changes: # The restyle operation resulted in a change to some trace # properties, so we dispatch change callbacks and send the # restyle message to the frontend (if any) msg_kwargs = ( {"source_view_id": source_view_id} if source_view_id is not None else {} ) self._send_restyle_msg( restyle_changes, trace_indexes=trace_indexes, **msg_kwargs ) self._dispatch_trace_change_callbacks(restyle_changes, trace_indexes) def _perform_plotly_restyle(self, restyle_data, trace_indexes): """ Perform a restyle operation on the figure's traces data and return the changes that were applied Parameters ---------- restyle_data : dict[str, any] See docstring for plotly_restyle trace_indexes : list[int] List of trace indexes that restyle operation applies to Returns ------- restyle_changes: dict[str, any] Subset of restyle_data including only the keys / values that resulted in a change to the figure's traces data """ # Initialize restyle changes # -------------------------- # This will be a subset of the restyle_data including only the # keys / values that are changed in the figure's trace data restyle_changes = {} # Process each key # ---------------- for key_path_str, v in restyle_data.items(): # Track whether any of the new values are cause a change in # self._data any_vals_changed = False for i, trace_ind in enumerate(trace_indexes): if trace_ind >= len(self._data): raise ValueError( "Trace index {trace_ind} out of range".format( trace_ind=trace_ind ) ) # Get new value for this particular trace trace_v = v[i % len(v)] if isinstance(v, list) else v if trace_v is not Undefined: # Get trace being updated trace_obj = self.data[trace_ind] # Validate key_path_str if not BaseFigure._is_key_path_compatible(key_path_str, trace_obj): trace_class = trace_obj.__class__.__name__ raise ValueError( """ Invalid property path '{key_path_str}' for trace class {trace_class} """.format( key_path_str=key_path_str, trace_class=trace_class ) ) # Apply set operation for this trace and thist value val_changed = BaseFigure._set_in( self._data[trace_ind], key_path_str, trace_v ) # Update any_vals_changed status any_vals_changed = any_vals_changed or val_changed if any_vals_changed: restyle_changes[key_path_str] = v return restyle_changes def _restyle_child(self, child, key_path_str, val): """ Process restyle operation on a child trace object Note: This method name/signature must match the one in BasePlotlyType. BasePlotlyType objects call their parent's _restyle_child method without knowing whether their parent is a BasePlotlyType or a BaseFigure. Parameters ---------- child : BaseTraceType Child being restyled key_path_str : str A key path string (e.g. 'foo.bar[0]') val Restyle value Returns ------- None """ # Compute trace index # ------------------- trace_index = child._trace_ind # Not in batch mode # ----------------- # Dispatch change callbacks and send restyle message if not self._in_batch_mode: send_val = [val] restyle = {key_path_str: send_val} self._send_restyle_msg(restyle, trace_indexes=trace_index) self._dispatch_trace_change_callbacks(restyle, [trace_index]) # In batch mode # ------------- # Add key_path_str/val to saved batch edits else: if trace_index not in self._batch_trace_edits: self._batch_trace_edits[trace_index] = OrderedDict() self._batch_trace_edits[trace_index][key_path_str] = val def _normalize_trace_indexes(self, trace_indexes): """ Input trace index specification and return list of the specified trace indexes Parameters ---------- trace_indexes : None or int or list[int] Returns ------- list[int] """ if trace_indexes is None: trace_indexes = list(range(len(self.data))) if not isinstance(trace_indexes, (list, tuple)): trace_indexes = [trace_indexes] return list(trace_indexes) @staticmethod def _str_to_dict_path(key_path_str): """ Convert a key path string into a tuple of key path elements. Parameters ---------- key_path_str : str Key path string, where nested keys are joined on '.' characters and array indexes are specified using brackets (e.g. 'foo.bar[1]') Returns ------- tuple[str | int] """ if ( isinstance(key_path_str, string_types) and "." not in key_path_str and "[" not in key_path_str and "_" not in key_path_str ): # Fast path for common case that avoids regular expressions return (key_path_str,) elif isinstance(key_path_str, tuple): # Nothing to do return key_path_str else: ret = _str_to_dict_path_full(key_path_str)[0] return ret @staticmethod def _set_in(d, key_path_str, v): """ Set a value in a nested dict using a key path string (e.g. 'foo.bar[0]') Parameters ---------- d : dict Input dict to set property in key_path_str : str Key path string, where nested keys are joined on '.' characters and array indexes are specified using brackets (e.g. 'foo.bar[1]') v New value Returns ------- bool True if set resulted in modification of dict (i.e. v was not already present at the specified location), False otherwise. """ # Validate inputs # --------------- assert isinstance(d, dict) # Compute key path # ---------------- # Convert the key_path_str into a tuple of key paths # e.g. 'foo.bar[0]' -> ('foo', 'bar', 0) key_path = BaseFigure._str_to_dict_path(key_path_str) # Initialize val_parent # --------------------- # This variable will be assigned to the parent of the next key path # element currently being processed val_parent = d # Initialize parent dict or list of value to be assigned # ----------------------------------------------------- for kp, key_path_el in enumerate(key_path[:-1]): # Extend val_parent list if needed if isinstance(val_parent, list) and isinstance(key_path_el, int): while len(val_parent) <= key_path_el: val_parent.append(None) elif isinstance(val_parent, dict) and key_path_el not in val_parent: if isinstance(key_path[kp + 1], int): val_parent[key_path_el] = [] else: val_parent[key_path_el] = {} val_parent = val_parent[key_path_el] # Assign value to to final parent dict or list # -------------------------------------------- # ### Get reference to final key path element ### last_key = key_path[-1] # ### Track whether assignment alters parent ### val_changed = False # v is Undefined # -------------- # Don't alter val_parent if v is Undefined: pass # v is None # --------- # Check whether we can remove key from parent elif v is None: if isinstance(val_parent, dict): if last_key in val_parent: # Parent is a dict and has last_key as a current key so # we can pop the key, which alters parent val_parent.pop(last_key) val_changed = True elif isinstance(val_parent, list): if isinstance(last_key, int) and 0 <= last_key < len(val_parent): # Parent is a list and last_key is a valid index so we # can set the element value to None val_parent[last_key] = None val_changed = True else: # Unsupported parent type (numpy array for example) raise ValueError( """ Cannot remove element of type {typ} at location {raw_key}""".format( typ=type(val_parent), raw_key=key_path_str ) ) # v is a valid value # ------------------ # Check whether parent should be updated else: if isinstance(val_parent, dict): if last_key not in val_parent or not BasePlotlyType._vals_equal( val_parent[last_key], v ): # Parent is a dict and does not already contain the # value v at key last_key val_parent[last_key] = v val_changed = True elif isinstance(val_parent, list): if isinstance(last_key, int): # Extend list with Nones if needed so that last_key is # in bounds while len(val_parent) <= last_key: val_parent.append(None) if not BasePlotlyType._vals_equal(val_parent[last_key], v): # Parent is a list and does not already contain the # value v at index last_key val_parent[last_key] = v val_changed = True else: # Unsupported parent type (numpy array for example) raise ValueError( """ Cannot set element of type {typ} at location {raw_key}""".format( typ=type(val_parent), raw_key=key_path_str ) ) return val_changed # Add traces # ---------- @staticmethod def _raise_invalid_rows_cols(name, n, invalid): rows_err_msg = """ If specified, the {name} parameter must be a list or tuple of integers of length {n} (The number of traces being added) Received: {invalid} """.format( name=name, n=n, invalid=invalid ) raise ValueError(rows_err_msg) @staticmethod def _validate_rows_cols(name, n, vals): if vals is None: pass elif isinstance(vals, (list, tuple)): if len(vals) != n: BaseFigure._raise_invalid_rows_cols(name=name, n=n, invalid=vals) int_type = _get_int_type() if [r for r in vals if not isinstance(r, int_type)]: BaseFigure._raise_invalid_rows_cols(name=name, n=n, invalid=vals) else: BaseFigure._raise_invalid_rows_cols(name=name, n=n, invalid=vals) def add_trace( self, trace, row=None, col=None, secondary_y=None, exclude_empty_subplots=False ): """ Add a trace to the figure Parameters ---------- trace : BaseTraceType or dict Either: - An instances of a trace classe from the plotly.graph_objs package (e.g plotly.graph_objs.Scatter, plotly.graph_objs.Bar) - or a dicts where: - The 'type' property specifies the trace type (e.g. 'scatter', 'bar', 'area', etc.). If the dict has no 'type' property then 'scatter' is assumed. - All remaining properties are passed to the constructor of the specified trace type. row : 'all', int or None (default) Subplot row index (starting from 1) for the trace to be added. Only valid if figure was created using `plotly.tools.make_subplots`. If 'all', addresses all rows in the specified column(s). col : 'all', int or None (default) Subplot col index (starting from 1) for the trace to be added. Only valid if figure was created using `plotly.tools.make_subplots`. If 'all', addresses all columns in the specified row(s). secondary_y: boolean or None (default None) If True, associate this trace with the secondary y-axis of the subplot at the specified row and col. Only valid if all of the following conditions are satisfied: * The figure was created using `plotly.subplots.make_subplots`. * The row and col arguments are not None * The subplot at the specified row and col has type xy (which is the default) and secondary_y True. These properties are specified in the specs argument to make_subplots. See the make_subplots docstring for more info. * The trace argument is a 2D cartesian trace (scatter, bar, etc.) exclude_empty_subplots: boolean If True, the trace will not be added to subplots that don't already have traces. Returns ------- BaseFigure The Figure that add_trace was called on Examples -------- >>> from plotly import subplots >>> import plotly.graph_objs as go Add two Scatter traces to a figure >>> fig = go.Figure() >>> fig.add_trace(go.Scatter(x=[1,2,3], y=[2,1,2])) # doctest: +ELLIPSIS Figure(...) >>> fig.add_trace(go.Scatter(x=[1,2,3], y=[2,1,2])) # doctest: +ELLIPSIS Figure(...) Add two Scatter traces to vertically stacked subplots >>> fig = subplots.make_subplots(rows=2) >>> fig.add_trace(go.Scatter(x=[1,2,3], y=[2,1,2]), row=1, col=1) # doctest: +ELLIPSIS Figure(...) >>> fig.add_trace(go.Scatter(x=[1,2,3], y=[2,1,2]), row=2, col=1) # doctest: +ELLIPSIS Figure(...) """ # Make sure we have both row and col or neither if row is not None and col is None: raise ValueError( "Received row parameter but not col.\n" "row and col must be specified together" ) elif col is not None and row is None: raise ValueError( "Received col parameter but not row.\n" "row and col must be specified together" ) # Address multiple subplots if row is not None and _is_select_subplot_coordinates_arg(row, col): # TODO add product argument rows_cols = self._select_subplot_coordinates(row, col) for r, c in rows_cols: self.add_trace( trace, row=r, col=c, secondary_y=secondary_y, exclude_empty_subplots=exclude_empty_subplots, ) return self return self.add_traces( data=[trace], rows=[row] if row is not None else None, cols=[col] if col is not None else None, secondary_ys=[secondary_y] if secondary_y is not None else None, exclude_empty_subplots=exclude_empty_subplots, ) def add_traces( self, data, rows=None, cols=None, secondary_ys=None, exclude_empty_subplots=False, ): """ Add traces to the figure Parameters ---------- data : list[BaseTraceType or dict] A list of trace specifications to be added. Trace specifications may be either: - Instances of trace classes from the plotly.graph_objs package (e.g plotly.graph_objs.Scatter, plotly.graph_objs.Bar) - Dicts where: - The 'type' property specifies the trace type (e.g. 'scatter', 'bar', 'area', etc.). If the dict has no 'type' property then 'scatter' is assumed. - All remaining properties are passed to the constructor of the specified trace type. rows : None, list[int], or int (default None) List of subplot row indexes (starting from 1) for the traces to be added. Only valid if figure was created using `plotly.tools.make_subplots` If a single integer is passed, all traces will be added to row number cols : None or list[int] (default None) List of subplot column indexes (starting from 1) for the traces to be added. Only valid if figure was created using `plotly.tools.make_subplots` If a single integer is passed, all traces will be added to column number secondary_ys: None or list[boolean] (default None) List of secondary_y booleans for traces to be added. See the docstring for `add_trace` for more info. exclude_empty_subplots: boolean If True, the trace will not be added to subplots that don't already have traces. Returns ------- BaseFigure The Figure that add_traces was called on Examples -------- >>> from plotly import subplots >>> import plotly.graph_objs as go Add two Scatter traces to a figure >>> fig = go.Figure() >>> fig.add_traces([go.Scatter(x=[1,2,3], y=[2,1,2]), ... go.Scatter(x=[1,2,3], y=[2,1,2])]) # doctest: +ELLIPSIS Figure(...) Add two Scatter traces to vertically stacked subplots >>> fig = subplots.make_subplots(rows=2) >>> fig.add_traces([go.Scatter(x=[1,2,3], y=[2,1,2]), ... go.Scatter(x=[1,2,3], y=[2,1,2])], ... rows=[1, 2], cols=[1, 1]) # doctest: +ELLIPSIS Figure(...) """ # Validate traces data = self._data_validator.validate_coerce(data) # Set trace indexes for ind, new_trace in enumerate(data): new_trace._trace_ind = ind + len(self.data) # Allow integers as inputs to subplots int_type = _get_int_type() if isinstance(rows, int_type): rows = [rows] * len(data) if isinstance(cols, int_type): cols = [cols] * len(data) # Validate rows / cols n = len(data) BaseFigure._validate_rows_cols("rows", n, rows) BaseFigure._validate_rows_cols("cols", n, cols) # Make sure we have both rows and cols or neither if rows is not None and cols is None: raise ValueError( "Received rows parameter but not cols.\n" "rows and cols must be specified together" ) elif cols is not None and rows is None: raise ValueError( "Received cols parameter but not rows.\n" "rows and cols must be specified together" ) # Process secondary_ys defaults if secondary_ys is not None and rows is None: # Default rows/cols to 1s if secondary_ys specified but not rows # or cols rows = [1] * len(secondary_ys) cols = rows elif secondary_ys is None and rows is not None: # Default secondary_ys to Nones if secondary_ys is not specified # but not rows and cols are specified secondary_ys = [None] * len(rows) # Apply rows / cols if rows is not None: for trace, row, col, secondary_y in zip(data, rows, cols, secondary_ys): self._set_trace_grid_position(trace, row, col, secondary_y) if exclude_empty_subplots: data = list( filter( lambda trace: self._subplot_not_empty( trace["xaxis"], trace["yaxis"], bool(exclude_empty_subplots) ), data, ) ) # Make deep copy of trace data (Optimize later if needed) new_traces_data = [deepcopy(trace._props) for trace in data] # Update trace parent for trace in data: trace._parent = self trace._orphan_props.clear() # Update python side # Use extend instead of assignment so we don't trigger serialization self._data.extend(new_traces_data) self._data_defaults = self._data_defaults + [{} for _ in data] self._data_objs = self._data_objs + data # Update messages self._send_addTraces_msg(new_traces_data) return self # Subplots # -------- def print_grid(self): """ Print a visual layout of the figure's axes arrangement. This is only valid for figures that are created with plotly.tools.make_subplots. """ if self._grid_str is None: raise Exception( "Use plotly.tools.make_subplots " "to create a subplot grid." ) print(self._grid_str) def append_trace(self, trace, row, col): """ Add a trace to the figure bound to axes at the specified row, col index. A row, col index grid is generated for figures created with plotly.tools.make_subplots, and can be viewed with the `print_grid` method Parameters ---------- trace The data trace to be bound row: int Subplot row index (see Figure.print_grid) col: int Subplot column index (see Figure.print_grid) Examples -------- >>> from plotly import tools >>> import plotly.graph_objs as go >>> # stack two subplots vertically >>> fig = tools.make_subplots(rows=2) This is the format of your plot grid: [ (1,1) x1,y1 ] [ (2,1) x2,y2 ] >>> fig.append_trace(go.Scatter(x=[1,2,3], y=[2,1,2]), row=1, col=1) >>> fig.append_trace(go.Scatter(x=[1,2,3], y=[2,1,2]), row=2, col=1) """ warnings.warn( """\ The append_trace method is deprecated and will be removed in a future version. Please use the add_trace method with the row and col parameters. """, DeprecationWarning, ) self.add_trace(trace=trace, row=row, col=col) def _set_trace_grid_position(self, trace, row, col, secondary_y=False): from plotly.subplots import _set_trace_grid_reference grid_ref = self._validate_get_grid_ref() return _set_trace_grid_reference( trace, self.layout, grid_ref, row, col, secondary_y ) def _validate_get_grid_ref(self): try: grid_ref = self._grid_ref if grid_ref is None: raise AttributeError("_grid_ref") except AttributeError: raise Exception( "In order to reference traces by row and column, " "you must first use " "plotly.tools.make_subplots " "to create the figure with a subplot grid." ) return grid_ref def _get_subplot_rows_columns(self): """ Returns a pair of lists, the first containing all the row indices and the second all the column indices. """ # currently, this just iterates over all the rows and columns (because # self._grid_ref is currently always rectangular) grid_ref = self._validate_get_grid_ref() nrows = len(grid_ref) ncols = len(grid_ref[0]) return (range(1, nrows + 1), range(1, ncols + 1)) def _get_subplot_coordinates(self): """ Returns an iterator over (row,col) pairs representing all the possible subplot coordinates. """ return itertools.product(*self._get_subplot_rows_columns()) def _select_subplot_coordinates(self, rows, cols, product=False): """ Allows selecting all or a subset of the subplots. If any of rows or columns is 'all', product is set to True. This is probably the expected behaviour, so that rows=1,cols='all' selects all the columns in row 1 (otherwise it would just select the subplot in the first row and first column). """ product |= any([s == "all" for s in [rows, cols]]) # TODO: If grid_ref ever becomes non-rectangular, then t should be the # set-intersection of the result of _indexing_combinations and # _get_subplot_coordinates, because some coordinates given by # the _indexing_combinations function might be invalid. t = _indexing_combinations( [rows, cols], list(self._get_subplot_rows_columns()), product=product, ) t = list(t) # remove rows and cols where the subplot is "None" grid_ref = self._validate_get_grid_ref() t = list(filter(lambda u: grid_ref[u[0] - 1][u[1] - 1] is not None, t)) return t def get_subplot(self, row, col, secondary_y=False): """ Return an object representing the subplot at the specified row and column. May only be used on Figures created using plotly.tools.make_subplots Parameters ---------- row: int 1-based index of subplot row col: int 1-based index of subplot column secondary_y: bool If True, select the subplot that consists of the x-axis and the secondary y-axis at the specified row/col. Only valid if the subplot at row/col is an 2D cartesian subplot that was created with a secondary y-axis. See the docstring for the specs argument to make_subplots for more info on creating a subplot with a secondary y-axis. Returns ------- subplot * None: if subplot is empty * plotly.graph_objs.layout.Scene: if subplot type is 'scene' * plotly.graph_objs.layout.Polar: if subplot type is 'polar' * plotly.graph_objs.layout.Ternary: if subplot type is 'ternary' * plotly.graph_objs.layout.Mapbox: if subplot type is 'ternary' * SubplotDomain namedtuple with `x` and `y` fields: if subplot type is 'domain'. - x: length 2 list of the subplot start and stop width - y: length 2 list of the subplot start and stop height * SubplotXY namedtuple with `xaxis` and `yaxis` fields: if subplot type is 'xy'. - xaxis: plotly.graph_objs.layout.XAxis instance for subplot - yaxis: plotly.graph_objs.layout.YAxis instance for subplot """ from plotly.subplots import _get_grid_subplot return _get_grid_subplot(self, row, col, secondary_y) # Child property operations # ------------------------- def _get_child_props(self, child): """ Return the properties dict for a child trace or child layout Note: this method must match the name/signature of one on BasePlotlyType Parameters ---------- child : BaseTraceType | BaseLayoutType Returns ------- dict """ # Try to find index of child as a trace # ------------------------------------- if isinstance(child, BaseTraceType): # ### Child is a trace ### trace_index = child._trace_ind return self._data[trace_index] # Child is the layout # ------------------- elif child is self.layout: return self._layout # Unknown child # ------------- else: raise ValueError("Unrecognized child: %s" % child) def _get_child_prop_defaults(self, child): """ Return the default properties dict for a child trace or child layout Note: this method must match the name/signature of one on BasePlotlyType Parameters ---------- child : BaseTraceType | BaseLayoutType Returns ------- dict """ # Child is a trace # ---------------- if isinstance(child, BaseTraceType): trace_index = child._trace_ind return self._data_defaults[trace_index] # Child is the layout # ------------------- elif child is self.layout: return self._layout_defaults # Unknown child # ------------- else: raise ValueError("Unrecognized child: %s" % child) def _init_child_props(self, child): """ Initialize the properites dict for a child trace or layout Note: this method must match the name/signature of one on BasePlotlyType Parameters ---------- child : BaseTraceType | BaseLayoutType Returns ------- None """ # layout and traces dict are initialize when figure is constructed # and when new traces are added to the figure pass # Layout # ------ def _initialize_layout_template(self): import plotly.io as pio if self._layout_obj._props.get("template", None) is None: if pio.templates.default is not None: # Assume default template is already validated if self._allow_disable_validation: self._layout_obj._validate = False try: if isinstance(pio.templates.default, BasePlotlyType): # Template object. Don't want to actually import `Template` # here for performance so we check against `BasePlotlyType` template_object = pio.templates.default else: # Name of registered template object template_object = pio.templates[pio.templates.default] self._layout_obj.template = template_object finally: self._layout_obj._validate = self._validate @property def layout(self): """ The `layout` property of the figure Returns ------- plotly.graph_objs.Layout """ return self["layout"] @layout.setter def layout(self, new_layout): # Validate new layout # ------------------- new_layout = self._layout_validator.validate_coerce(new_layout) new_layout_data = deepcopy(new_layout._props) # Unparent current layout # ----------------------- if self._layout_obj: old_layout_data = deepcopy(self._layout_obj._props) self._layout_obj._orphan_props.update(old_layout_data) self._layout_obj._parent = None # Parent new layout # ----------------- self._layout = new_layout_data new_layout._parent = self new_layout._orphan_props.clear() self._layout_obj = new_layout # Initialize template object # -------------------------- self._initialize_layout_template() # Notify JS side self._send_relayout_msg(new_layout_data) def plotly_relayout(self, relayout_data, **kwargs): """ Perform a Plotly relayout operation on the figure's layout Parameters ---------- relayout_data : dict Dict of layout updates dict keys are strings that specify the properties to be updated. Nested properties are expressed by joining successive keys on '.' characters (e.g. 'xaxis.range') dict values are the values to use to update the layout. Returns ------- None """ # Handle source_view_id # --------------------- # If not None, the source_view_id is the UID of the frontend # Plotly.js view that initially triggered this relayout operation # (e.g. the user clicked on the toolbar to change the drag mode # from zoom to pan). We pass this UID along so that the frontend # views can determine whether they need to apply the relayout # operation on themselves. if "source_view_id" in kwargs: msg_kwargs = {"source_view_id": kwargs["source_view_id"]} else: msg_kwargs = {} # Perform relayout operation on layout dict # ----------------------------------------- relayout_changes = self._perform_plotly_relayout(relayout_data) if relayout_changes: # The relayout operation resulted in a change to some layout # properties, so we dispatch change callbacks and send the # relayout message to the frontend (if any) self._send_relayout_msg(relayout_changes, **msg_kwargs) self._dispatch_layout_change_callbacks(relayout_changes) def _perform_plotly_relayout(self, relayout_data): """ Perform a relayout operation on the figure's layout data and return the changes that were applied Parameters ---------- relayout_data : dict[str, any] See the docstring for plotly_relayout Returns ------- relayout_changes: dict[str, any] Subset of relayout_data including only the keys / values that resulted in a change to the figure's layout data """ # Initialize relayout changes # --------------------------- # This will be a subset of the relayout_data including only the # keys / values that are changed in the figure's layout data relayout_changes = {} # Process each key # ---------------- for key_path_str, v in relayout_data.items(): if not BaseFigure._is_key_path_compatible(key_path_str, self.layout): raise ValueError( """ Invalid property path '{key_path_str}' for layout """.format( key_path_str=key_path_str ) ) # Apply set operation on the layout dict val_changed = BaseFigure._set_in(self._layout, key_path_str, v) if val_changed: relayout_changes[key_path_str] = v return relayout_changes @staticmethod def _is_key_path_compatible(key_path_str, plotly_obj): """ Return whether the specifieid key path string is compatible with the specified plotly object for the purpose of relayout/restyle operation """ # Convert string to tuple of path components # e.g. 'foo[0].bar[1]' -> ('foo', 0, 'bar', 1) key_path_tuple = BaseFigure._str_to_dict_path(key_path_str) # Remove trailing integer component # e.g. ('foo', 0, 'bar', 1) -> ('foo', 0, 'bar') # We do this because it's fine for relayout/restyle to create new # elements in the final array in the path. if isinstance(key_path_tuple[-1], int): key_path_tuple = key_path_tuple[:-1] # Test whether modified key path tuple is in plotly_obj return key_path_tuple in plotly_obj def _relayout_child(self, child, key_path_str, val): """ Process relayout operation on child layout object Parameters ---------- child : BaseLayoutType The figure's layout key_path_str : A key path string (e.g. 'foo.bar[0]') val Relayout value Returns ------- None """ # Validate input # -------------- assert child is self.layout # Not in batch mode # ------------- # Dispatch change callbacks and send relayout message if not self._in_batch_mode: relayout_msg = {key_path_str: val} self._send_relayout_msg(relayout_msg) self._dispatch_layout_change_callbacks(relayout_msg) # In batch mode # ------------- # Add key_path_str/val to saved batch edits else: self._batch_layout_edits[key_path_str] = val # Dispatch change callbacks # ------------------------- @staticmethod def _build_dispatch_plan(key_path_strs): """ Build a dispatch plan for a list of key path strings A dispatch plan is a dict: - *from* path tuples that reference an object that has descendants that are referenced in `key_path_strs`. - *to* sets of tuples that correspond to descendants of the object above. Parameters ---------- key_path_strs : list[str] List of key path strings. For example: ['xaxis.rangeselector.font.color', 'xaxis.rangeselector.bgcolor'] Returns ------- dispatch_plan: dict[tuple[str|int], set[tuple[str|int]]] Examples -------- >>> key_path_strs = ['xaxis.rangeselector.font.color', ... 'xaxis.rangeselector.bgcolor'] >>> BaseFigure._build_dispatch_plan(key_path_strs) # doctest: +SKIP {(): {'xaxis', ('xaxis', 'rangeselector'), ('xaxis', 'rangeselector', 'bgcolor'), ('xaxis', 'rangeselector', 'font'), ('xaxis', 'rangeselector', 'font', 'color')}, ('xaxis',): {('rangeselector',), ('rangeselector', 'bgcolor'), ('rangeselector', 'font'), ('rangeselector', 'font', 'color')}, ('xaxis', 'rangeselector'): {('bgcolor',), ('font',), ('font', 'color')}, ('xaxis', 'rangeselector', 'font'): {('color',)}} """ dispatch_plan = {} for key_path_str in key_path_strs: key_path = BaseFigure._str_to_dict_path(key_path_str) key_path_so_far = () keys_left = key_path # Iterate down the key path for next_key in key_path: if key_path_so_far not in dispatch_plan: dispatch_plan[key_path_so_far] = set() to_add = [keys_left[: i + 1] for i in range(len(keys_left))] dispatch_plan[key_path_so_far].update(to_add) key_path_so_far = key_path_so_far + (next_key,) keys_left = keys_left[1:] return dispatch_plan def _dispatch_layout_change_callbacks(self, relayout_data): """ Dispatch property change callbacks given relayout_data Parameters ---------- relayout_data : dict[str, any] See docstring for plotly_relayout. Returns ------- None """ # Build dispatch plan # ------------------- key_path_strs = list(relayout_data.keys()) dispatch_plan = BaseFigure._build_dispatch_plan(key_path_strs) # Dispatch changes to each layout objects # --------------------------------------- for path_tuple, changed_paths in dispatch_plan.items(): if path_tuple in self.layout: dispatch_obj = self.layout[path_tuple] if isinstance(dispatch_obj, BasePlotlyType): dispatch_obj._dispatch_change_callbacks(changed_paths) def _dispatch_trace_change_callbacks(self, restyle_data, trace_indexes): """ Dispatch property change callbacks given restyle_data Parameters ---------- restyle_data : dict[str, any] See docstring for plotly_restyle. trace_indexes : list[int] List of trace indexes that restyle operation applied to Returns ------- None """ # Build dispatch plan # ------------------- key_path_strs = list(restyle_data.keys()) dispatch_plan = BaseFigure._build_dispatch_plan(key_path_strs) # Dispatch changes to each object in each trace # --------------------------------------------- for path_tuple, changed_paths in dispatch_plan.items(): for trace_ind in trace_indexes: trace = self.data[trace_ind] if path_tuple in trace: dispatch_obj = trace[path_tuple] if isinstance(dispatch_obj, BasePlotlyType): dispatch_obj._dispatch_change_callbacks(changed_paths) # Frames # ------ @property def frames(self): """ The `frames` property is a tuple of the figure's frame objects Returns ------- tuple[plotly.graph_objs.Frame] """ return self["frames"] @frames.setter def frames(self, new_frames): # Note: Frames are not supported by the FigureWidget subclass so we # only validate coerce the frames. We don't emit any events on frame # changes, and we don't reparent the frames. # Validate frames self._frame_objs = self._frames_validator.validate_coerce(new_frames) # Update # ------ def plotly_update( self, restyle_data=None, relayout_data=None, trace_indexes=None, **kwargs ): """ Perform a Plotly update operation on the figure. Note: This operation both mutates and returns the figure Parameters ---------- restyle_data : dict Traces update specification. See the docstring for the `plotly_restyle` method for details relayout_data : dict Layout update specification. See the docstring for the `plotly_relayout` method for details trace_indexes : Trace index, or list of trace indexes, that the update operation applies to. Defaults to all trace indexes. Returns ------- BaseFigure None """ # Handle source_view_id # --------------------- # If not None, the source_view_id is the UID of the frontend # Plotly.js view that initially triggered this update operation # (e.g. the user clicked a button that triggered an update # operation). We pass this UID along so that the frontend views can # determine whether they need to apply the update operation on # themselves. if "source_view_id" in kwargs: msg_kwargs = {"source_view_id": kwargs["source_view_id"]} else: msg_kwargs = {} # Perform update operation # ------------------------ # This updates the _data and _layout dicts, and returns the changes # to the traces (restyle_changes) and layout (relayout_changes) ( restyle_changes, relayout_changes, trace_indexes, ) = self._perform_plotly_update( restyle_data=restyle_data, relayout_data=relayout_data, trace_indexes=trace_indexes, ) # Send update message # ------------------- # Send a plotly_update message to the frontend (if any) if restyle_changes or relayout_changes: self._send_update_msg( restyle_data=restyle_changes, relayout_data=relayout_changes, trace_indexes=trace_indexes, **msg_kwargs ) # Dispatch changes # ---------------- # ### Dispatch restyle changes ### if restyle_changes: self._dispatch_trace_change_callbacks(restyle_changes, trace_indexes) # ### Dispatch relayout changes ### if relayout_changes: self._dispatch_layout_change_callbacks(relayout_changes) def _perform_plotly_update( self, restyle_data=None, relayout_data=None, trace_indexes=None ): # Check for early exist # --------------------- if not restyle_data and not relayout_data: # Nothing to do return None, None, None # Normalize input # --------------- if restyle_data is None: restyle_data = {} if relayout_data is None: relayout_data = {} trace_indexes = self._normalize_trace_indexes(trace_indexes) # Perform relayout # ---------------- relayout_changes = self._perform_plotly_relayout(relayout_data) # Perform restyle # --------------- restyle_changes = self._perform_plotly_restyle(restyle_data, trace_indexes) # Return changes # -------------- return restyle_changes, relayout_changes, trace_indexes # Plotly message stubs # -------------------- # send-message stubs that may be overridden by the widget subclass def _send_addTraces_msg(self, new_traces_data): pass def _send_moveTraces_msg(self, current_inds, new_inds): pass def _send_deleteTraces_msg(self, delete_inds): pass def _send_restyle_msg(self, style, trace_indexes=None, source_view_id=None): pass def _send_relayout_msg(self, layout, source_view_id=None): pass def _send_update_msg( self, restyle_data, relayout_data, trace_indexes=None, source_view_id=None ): pass def _send_animate_msg( self, styles_data, relayout_data, trace_indexes, animation_opts ): pass # Context managers # ---------------- @contextmanager def batch_update(self): """ A context manager that batches up trace and layout assignment operations into a singe plotly_update message that is executed when the context exits. Examples -------- For example, suppose we have a figure widget, `fig`, with a single trace. >>> import plotly.graph_objs as go >>> fig = go.FigureWidget(data=[{'y': [3, 4, 2]}]) If we want to update the xaxis range, the yaxis range, and the marker color, we could do so using a series of three property assignments as follows: >>> fig.layout.xaxis.range = [0, 5] >>> fig.layout.yaxis.range = [0, 10] >>> fig.data[0].marker.color = 'green' This will work, however it will result in three messages being sent to the front end (two relayout messages for the axis range updates followed by one restyle message for the marker color update). This can cause the plot to appear to stutter as the three updates are applied incrementally. We can avoid this problem by performing these three assignments in a `batch_update` context as follows: >>> with fig.batch_update(): ... fig.layout.xaxis.range = [0, 5] ... fig.layout.yaxis.range = [0, 10] ... fig.data[0].marker.color = 'green' Now, these three property updates will be sent to the frontend in a single update message, and they will be applied by the front end simultaneously. """ if self._in_batch_mode is True: yield else: try: self._in_batch_mode = True yield finally: # ### Disable batch mode ### self._in_batch_mode = False # ### Build plotly_update params ### ( restyle_data, relayout_data, trace_indexes, ) = self._build_update_params_from_batch() # ### Call plotly_update ### self.plotly_update( restyle_data=restyle_data, relayout_data=relayout_data, trace_indexes=trace_indexes, ) # ### Clear out saved batch edits ### self._batch_layout_edits.clear() self._batch_trace_edits.clear() def _build_update_params_from_batch(self): """ Convert `_batch_trace_edits` and `_batch_layout_edits` into the `restyle_data`, `relayout_data`, and `trace_indexes` params accepted by the `plotly_update` method. Returns ------- (dict, dict, list[int]) """ # Handle Style / Trace Indexes # ---------------------------- batch_style_commands = self._batch_trace_edits trace_indexes = sorted(set([trace_ind for trace_ind in batch_style_commands])) all_props = sorted( set( [ prop for trace_style in self._batch_trace_edits.values() for prop in trace_style ] ) ) # Initialize restyle_data dict with all values undefined restyle_data = { prop: [Undefined for _ in range(len(trace_indexes))] for prop in all_props } # Fill in values for trace_ind, trace_style in batch_style_commands.items(): for trace_prop, trace_val in trace_style.items(): restyle_trace_index = trace_indexes.index(trace_ind) restyle_data[trace_prop][restyle_trace_index] = trace_val # Handle Layout # ------------- relayout_data = self._batch_layout_edits # Return plotly_update params # --------------------------- return restyle_data, relayout_data, trace_indexes @contextmanager def batch_animate(self, duration=500, easing="cubic-in-out"): """ Context manager to animate trace / layout updates Parameters ---------- duration : number The duration of the transition, in milliseconds. If equal to zero, updates are synchronous. easing : string The easing function used for the transition. One of: - linear - quad - cubic - sin - exp - circle - elastic - back - bounce - linear-in - quad-in - cubic-in - sin-in - exp-in - circle-in - elastic-in - back-in - bounce-in - linear-out - quad-out - cubic-out - sin-out - exp-out - circle-out - elastic-out - back-out - bounce-out - linear-in-out - quad-in-out - cubic-in-out - sin-in-out - exp-in-out - circle-in-out - elastic-in-out - back-in-out - bounce-in-out Examples -------- Suppose we have a figure widget, `fig`, with a single trace. >>> import plotly.graph_objs as go >>> fig = go.FigureWidget(data=[{'y': [3, 4, 2]}]) 1) Animate a change in the xaxis and yaxis ranges using default duration and easing parameters. >>> with fig.batch_animate(): ... fig.layout.xaxis.range = [0, 5] ... fig.layout.yaxis.range = [0, 10] 2) Animate a change in the size and color of the trace's markers over 2 seconds using the elastic-in-out easing method >>> with fig.batch_animate(duration=2000, easing='elastic-in-out'): ... fig.data[0].marker.color = 'green' ... fig.data[0].marker.size = 20 """ # Validate inputs # --------------- duration = self._animation_duration_validator.validate_coerce(duration) easing = self._animation_easing_validator.validate_coerce(easing) if self._in_batch_mode is True: yield else: try: self._in_batch_mode = True yield finally: # Exit batch mode # --------------- self._in_batch_mode = False # Apply batch animate # ------------------- self._perform_batch_animate( { "transition": {"duration": duration, "easing": easing}, "frame": {"duration": duration}, } ) def _perform_batch_animate(self, animation_opts): """ Perform the batch animate operation This method should be called with the batch_animate() context manager exits. Parameters ---------- animation_opts : dict Animation options as accepted by frontend Plotly.animation command Returns ------- None """ # Apply commands to internal dictionaries as an update # ---------------------------------------------------- ( restyle_data, relayout_data, trace_indexes, ) = self._build_update_params_from_batch() ( restyle_changes, relayout_changes, trace_indexes, ) = self._perform_plotly_update(restyle_data, relayout_data, trace_indexes) # Convert style / trace_indexes into animate form # ----------------------------------------------- if self._batch_trace_edits: animate_styles, animate_trace_indexes = zip( *[ (trace_style, trace_index) for trace_index, trace_style in self._batch_trace_edits.items() ] ) else: animate_styles, animate_trace_indexes = {}, [] animate_layout = copy(self._batch_layout_edits) # Send animate message # -------------------- # Sends animate message to the front end (if any) self._send_animate_msg( styles_data=list(animate_styles), relayout_data=animate_layout, trace_indexes=list(animate_trace_indexes), animation_opts=animation_opts, ) # Clear batched commands # ---------------------- self._batch_layout_edits.clear() self._batch_trace_edits.clear() # Dispatch callbacks # ------------------ # ### Dispatch restyle changes ### if restyle_changes: self._dispatch_trace_change_callbacks(restyle_changes, trace_indexes) # ### Dispatch relayout changes ### if relayout_changes: self._dispatch_layout_change_callbacks(relayout_changes) # Exports # ------- def to_dict(self): """ Convert figure to a dictionary Note: the dictionary includes the properties explicitly set by the user, it does not include default values of unspecified properties Returns ------- dict """ # Handle data # ----------- data = deepcopy(self._data) # Handle layout # ------------- layout = deepcopy(self._layout) # Handle frames # ------------- # Frame key is only added if there are any frames res = {"data": data, "layout": layout} frames = deepcopy([frame._props for frame in self._frame_objs]) if frames: res["frames"] = frames return res def to_plotly_json(self): """ Convert figure to a JSON representation as a Python dict Returns ------- dict """ return self.to_dict() @staticmethod def _to_ordered_dict(d, skip_uid=False): """ Static helper for converting dict or list to structure of ordered dictionaries """ if isinstance(d, dict): # d is a dict result = collections.OrderedDict() for key in sorted(d.keys()): if skip_uid and key == "uid": continue else: result[key] = BaseFigure._to_ordered_dict(d[key], skip_uid=skip_uid) elif isinstance(d, list) and d and isinstance(d[0], dict): # d is a list of dicts result = [BaseFigure._to_ordered_dict(el, skip_uid=skip_uid) for el in d] else: result = d return result def to_ordered_dict(self, skip_uid=True): # Initialize resulting OrderedDict # -------------------------------- result = collections.OrderedDict() # Handle data # ----------- result["data"] = BaseFigure._to_ordered_dict(self._data, skip_uid=skip_uid) # Handle layout # ------------- result["layout"] = BaseFigure._to_ordered_dict(self._layout) # Handle frames # ------------- if self._frame_objs: frames_props = [frame._props for frame in self._frame_objs] result["frames"] = BaseFigure._to_ordered_dict(frames_props) return result # plotly.io methods # ----------------- # Note that docstrings are auto-generated in plotly/_docstring_gen.py def show(self, *args, **kwargs): """ Show a figure using either the default renderer(s) or the renderer(s) specified by the renderer argument Parameters ---------- renderer: str or None (default None) A string containing the names of one or more registered renderers (separated by '+' characters) or None. If None, then the default renderers specified in plotly.io.renderers.default are used. validate: bool (default True) True if the figure should be validated before being shown, False otherwise. width: int or float An integer or float that determines the number of pixels wide the plot is. The default is set in plotly.js. height: int or float An integer or float that determines the number of pixels wide the plot is. The default is set in plotly.js. config: dict A dict of parameters to configure the figure. The defaults are set in plotly.js. Returns ------- None """ import plotly.io as pio return pio.show(self, *args, **kwargs) def to_json(self, *args, **kwargs): """ Convert a figure to a JSON string representation Parameters ---------- validate: bool (default True) True if the figure should be validated before being converted to JSON, False otherwise. pretty: bool (default False) True if JSON representation should be pretty-printed, False if representation should be as compact as possible. remove_uids: bool (default True) True if trace UIDs should be omitted from the JSON representation engine: str (default None) The JSON encoding engine to use. One of: - "json" for an encoder based on the built-in Python json module - "orjson" for a fast encoder the requires the orjson package If not specified, the default encoder is set to the current value of plotly.io.json.config.default_encoder. Returns ------- str Representation of figure as a JSON string """ import plotly.io as pio return pio.to_json(self, *args, **kwargs) def full_figure_for_development(self, warn=True, as_dict=False): """ Compute default values for all attributes not specified in the input figure and returns the output as a "full" figure. This function calls Plotly.js via Kaleido to populate unspecified attributes. This function is intended for interactive use during development to learn more about how Plotly.js computes default values and is not generally necessary or recommended for production use. Parameters ---------- fig: Figure object or dict representing a figure warn: bool If False, suppress warnings about not using this in production. as_dict: bool If True, output is a dict with some keys that go.Figure can't parse. If False, output is a go.Figure with unparseable keys skipped. Returns ------- plotly.graph_objects.Figure or dict The full figure """ import plotly.io as pio return pio.full_figure_for_development(self, warn, as_dict) def write_json(self, *args, **kwargs): """ Convert a figure to JSON and write it to a file or writeable object Parameters ---------- file: str or writeable A string representing a local file path or a writeable object (e.g. an open file descriptor) pretty: bool (default False) True if JSON representation should be pretty-printed, False if representation should be as compact as possible. remove_uids: bool (default True) True if trace UIDs should be omitted from the JSON representation engine: str (default None) The JSON encoding engine to use. One of: - "json" for an encoder based on the built-in Python json module - "orjson" for a fast encoder the requires the orjson package If not specified, the default encoder is set to the current value of plotly.io.json.config.default_encoder. Returns ------- None """ import plotly.io as pio return pio.write_json(self, *args, **kwargs) def to_html(self, *args, **kwargs): """ Convert a figure to an HTML string representation. Parameters ---------- config: dict or None (default None) Plotly.js figure config options auto_play: bool (default=True) Whether to automatically start the animation sequence on page load if the figure contains frames. Has no effect if the figure does not contain frames. include_plotlyjs: bool or string (default True) Specifies how the plotly.js library is included/loaded in the output div string. If True, a script tag containing the plotly.js source code (~3MB) is included in the output. HTML files generated with this option are fully self-contained and can be used offline. If 'cdn', a script tag that references the plotly.js CDN is included in the output. HTML files generated with this option are about 3MB smaller than those generated with include_plotlyjs=True, but they require an active internet connection in order to load the plotly.js library. If 'directory', a script tag is included that references an external plotly.min.js bundle that is assumed to reside in the same directory as the HTML file. If 'require', Plotly.js is loaded using require.js. This option assumes that require.js is globally available and that it has been globally configured to know how to find Plotly.js as 'plotly'. This option is not advised when full_html=True as it will result in a non-functional html file. If a string that ends in '.js', a script tag is included that references the specified path. This approach can be used to point the resulting HTML file to an alternative CDN or local bundle. If False, no script tag referencing plotly.js is included. This is useful when the resulting div string will be placed inside an HTML document that already loads plotly.js. This option is not advised when full_html=True as it will result in a non-functional html file. include_mathjax: bool or string (default False) Specifies how the MathJax.js library is included in the output html div string. MathJax is required in order to display labels with LaTeX typesetting. If False, no script tag referencing MathJax.js will be included in the output. If 'cdn', a script tag that references a MathJax CDN location will be included in the output. HTML div strings generated with this option will be able to display LaTeX typesetting as long as internet access is available. If a string that ends in '.js', a script tag is included that references the specified path. This approach can be used to point the resulting HTML div string to an alternative CDN. post_script: str or list or None (default None) JavaScript snippet(s) to be included in the resulting div just after plot creation. The string(s) may include '{plot_id}' placeholders that will then be replaced by the `id` of the div element that the plotly.js figure is associated with. One application for this script is to install custom plotly.js event handlers. full_html: bool (default True) If True, produce a string containing a complete HTML document starting with an tag. If False, produce a string containing a single
element. animation_opts: dict or None (default None) dict of custom animation parameters to be passed to the function Plotly.animate in Plotly.js. See https://github.com/plotly/plotly.js/blob/master/src/plots/animation_attributes.js for available options. Has no effect if the figure does not contain frames, or auto_play is False. default_width, default_height: number or str (default '100%') The default figure width/height to use if the provided figure does not specify its own layout.width/layout.height property. May be specified in pixels as an integer (e.g. 500), or as a css width style string (e.g. '500px', '100%'). validate: bool (default True) True if the figure should be validated before being converted to JSON, False otherwise. div_id: str (default None) If provided, this is the value of the id attribute of the div tag. If None, the id attribute is a UUID. Returns ------- str Representation of figure as an HTML div string """ import plotly.io as pio return pio.to_html(self, *args, **kwargs) def write_html(self, *args, **kwargs): """ Write a figure to an HTML file representation Parameters ---------- file: str or writeable A string representing a local file path or a writeable object (e.g. a pathlib.Path object or an open file descriptor) config: dict or None (default None) Plotly.js figure config options auto_play: bool (default=True) Whether to automatically start the animation sequence on page load if the figure contains frames. Has no effect if the figure does not contain frames. include_plotlyjs: bool or string (default True) Specifies how the plotly.js library is included/loaded in the output div string. If True, a script tag containing the plotly.js source code (~3MB) is included in the output. HTML files generated with this option are fully self-contained and can be used offline. If 'cdn', a script tag that references the plotly.js CDN is included in the output. HTML files generated with this option are about 3MB smaller than those generated with include_plotlyjs=True, but they require an active internet connection in order to load the plotly.js library. If 'directory', a script tag is included that references an external plotly.min.js bundle that is assumed to reside in the same directory as the HTML file. If `file` is a string to a local file path and `full_html` is True then If 'directory', a script tag is included that references an external plotly.min.js bundle that is assumed to reside in the same directory as the HTML file. If `file` is a string to a local file path and `full_html` is True, then the plotly.min.js bundle is copied into the directory of the resulting HTML file. If a file named plotly.min.js already exists in the output directory then this file is left unmodified and no copy is performed. HTML files generated with this option can be used offline, but they require a copy of the plotly.min.js bundle in the same directory. This option is useful when many figures will be saved as HTML files in the same directory because the plotly.js source code will be included only once per output directory, rather than once per output file. If 'require', Plotly.js is loaded using require.js. This option assumes that require.js is globally available and that it has been globally configured to know how to find Plotly.js as 'plotly'. This option is not advised when full_html=True as it will result in a non-functional html file. If a string that ends in '.js', a script tag is included that references the specified path. This approach can be used to point the resulting HTML file to an alternative CDN or local bundle. If False, no script tag referencing plotly.js is included. This is useful when the resulting div string will be placed inside an HTML document that already loads plotly.js. This option is not advised when full_html=True as it will result in a non-functional html file. include_mathjax: bool or string (default False) Specifies how the MathJax.js library is included in the output html div string. MathJax is required in order to display labels with LaTeX typesetting. If False, no script tag referencing MathJax.js will be included in the output. If 'cdn', a script tag that references a MathJax CDN location will be included in the output. HTML div strings generated with this option will be able to display LaTeX typesetting as long as internet access is available. If a string that ends in '.js', a script tag is included that references the specified path. This approach can be used to point the resulting HTML div string to an alternative CDN. post_script: str or list or None (default None) JavaScript snippet(s) to be included in the resulting div just after plot creation. The string(s) may include '{plot_id}' placeholders that will then be replaced by the `id` of the div element that the plotly.js figure is associated with. One application for this script is to install custom plotly.js event handlers. full_html: bool (default True) If True, produce a string containing a complete HTML document starting with an tag. If False, produce a string containing a single
element. animation_opts: dict or None (default None) dict of custom animation parameters to be passed to the function Plotly.animate in Plotly.js. See https://github.com/plotly/plotly.js/blob/master/src/plots/animation_attributes.js for available options. Has no effect if the figure does not contain frames, or auto_play is False. default_width, default_height: number or str (default '100%') The default figure width/height to use if the provided figure does not specify its own layout.width/layout.height property. May be specified in pixels as an integer (e.g. 500), or as a css width style string (e.g. '500px', '100%'). validate: bool (default True) True if the figure should be validated before being converted to JSON, False otherwise. auto_open: bool (default True) If True, open the saved file in a web browser after saving. This argument only applies if `full_html` is True. div_id: str (default None) If provided, this is the value of the id attribute of the div tag. If None, the id attribute is a UUID. Returns ------- str Representation of figure as an HTML div string """ import plotly.io as pio return pio.write_html(self, *args, **kwargs) def to_image(self, *args, **kwargs): """ Convert a figure to a static image bytes string Parameters ---------- format: str or None The desired image format. One of - 'png' - 'jpg' or 'jpeg' - 'webp' - 'svg' - 'pdf' - 'eps' (Requires the poppler library to be installed) If not specified, will default to `plotly.io.config.default_format` width: int or None The width of the exported image in layout pixels. If the `scale` property is 1.0, this will also be the width of the exported image in physical pixels. If not specified, will default to `plotly.io.config.default_width` height: int or None The height of the exported image in layout pixels. If the `scale` property is 1.0, this will also be the height of the exported image in physical pixels. If not specified, will default to `plotly.io.config.default_height` scale: int or float or None The scale factor to use when exporting the figure. A scale factor larger than 1.0 will increase the image resolution with respect to the figure's layout pixel dimensions. Whereas as scale factor of less than 1.0 will decrease the image resolution. If not specified, will default to `plotly.io.config.default_scale` validate: bool True if the figure should be validated before being converted to an image, False otherwise. engine: str Image export engine to use: - "kaleido": Use Kaleido for image export - "orca": Use Orca for image export - "auto" (default): Use Kaleido if installed, otherwise use orca Returns ------- bytes The image data """ import plotly.io as pio return pio.to_image(self, *args, **kwargs) def write_image(self, *args, **kwargs): """ Convert a figure to a static image and write it to a file or writeable object Parameters ---------- file: str or writeable A string representing a local file path or a writeable object (e.g. a pathlib.Path object or an open file descriptor) format: str or None The desired image format. One of - 'png' - 'jpg' or 'jpeg' - 'webp' - 'svg' - 'pdf' - 'eps' (Requires the poppler library to be installed) If not specified and `file` is a string then this will default to the file extension. If not specified and `file` is not a string then this will default to `plotly.io.config.default_format` width: int or None The width of the exported image in layout pixels. If the `scale` property is 1.0, this will also be the width of the exported image in physical pixels. If not specified, will default to `plotly.io.config.default_width` height: int or None The height of the exported image in layout pixels. If the `scale` property is 1.0, this will also be the height of the exported image in physical pixels. If not specified, will default to `plotly.io.config.default_height` scale: int or float or None The scale factor to use when exporting the figure. A scale factor larger than 1.0 will increase the image resolution with respect to the figure's layout pixel dimensions. Whereas as scale factor of less than 1.0 will decrease the image resolution. If not specified, will default to `plotly.io.config.default_scale` validate: bool True if the figure should be validated before being converted to an image, False otherwise. engine: str Image export engine to use: - "kaleido": Use Kaleido for image export - "orca": Use Orca for image export - "auto" (default): Use Kaleido if installed, otherwise use orca Returns ------- None """ import plotly.io as pio return pio.write_image(self, *args, **kwargs) # Static helpers # -------------- @staticmethod def _is_dict_list(v): """ Return true of the input object is a list of dicts """ return isinstance(v, list) and len(v) > 0 and isinstance(v[0], dict) @staticmethod def _perform_update(plotly_obj, update_obj, overwrite=False): """ Helper to support the update() methods on :class:`BaseFigure` and :class:`BasePlotlyType` Parameters ---------- plotly_obj : BasePlotlyType|tuple[BasePlotlyType] Object to up updated update_obj : dict|list[dict]|tuple[dict] When ``plotly_obj`` is an instance of :class:`BaseFigure`, ``update_obj`` should be a dict When ``plotly_obj`` is a tuple of instances of :class:`BasePlotlyType`, ``update_obj`` should be a tuple or list of dicts """ from _plotly_utils.basevalidators import ( CompoundValidator, CompoundArrayValidator, ) if update_obj is None: # Nothing to do return elif isinstance(plotly_obj, BasePlotlyType): # Handle initializing subplot ids # ------------------------------- # This should be valid even if xaxis2 hasn't been initialized: # >>> layout.update(xaxis2={'title': 'xaxis 2'}) for key in update_obj: err = _check_path_in_prop_tree(plotly_obj, key, error_cast=ValueError) if err is not None: if isinstance(plotly_obj, BaseLayoutType): # try _subplot_re_match match = plotly_obj._subplot_re_match(key) if match: # We need to create a subplotid object plotly_obj[key] = {} continue # If no match, raise the error, which should already # contain the _raise_on_invalid_property_error # generated message raise err # Convert update_obj to dict # -------------------------- if isinstance(update_obj, BasePlotlyType): update_obj = update_obj.to_plotly_json() # Process valid properties # ------------------------ for key in update_obj: val = update_obj[key] if overwrite: # Don't recurse and assign property as-is plotly_obj[key] = val continue validator = plotly_obj._get_prop_validator(key) if isinstance(validator, CompoundValidator) and isinstance(val, dict): # Update compound objects recursively # plotly_obj[key].update(val) BaseFigure._perform_update(plotly_obj[key], val) elif isinstance(validator, CompoundArrayValidator): if plotly_obj[key]: # plotly_obj has an existing non-empty array for key # In this case we merge val into the existing elements BaseFigure._perform_update(plotly_obj[key], val) # If update tuple is longer that current tuple, append the # extra elements to the end if isinstance(val, (list, tuple)) and len(val) > len( plotly_obj[key] ): plotly_obj[key] = plotly_obj[key] + tuple( val[len(plotly_obj[key]) :] ) else: # plotly_obj is an empty or uninitialized list for key # In this case we accept val as is plotly_obj[key] = val else: # Assign non-compound value plotly_obj[key] = val elif isinstance(plotly_obj, tuple): if len(update_obj) == 0: # Nothing to do return else: for i, plotly_element in enumerate(plotly_obj): if isinstance(update_obj, dict): if i in update_obj: update_element = update_obj[i] else: continue else: update_element = update_obj[i % len(update_obj)] BaseFigure._perform_update(plotly_element, update_element) else: raise ValueError( "Unexpected plotly object with type {typ}".format(typ=type(plotly_obj)) ) @staticmethod def _index_is(iterable, val): """ Return the index of a value in an iterable using object identity (not object equality as is the case for list.index) """ index_list = [i for i, curr_val in enumerate(iterable) if curr_val is val] if not index_list: raise ValueError("Invalid value") return index_list[0] def _make_axis_spanning_layout_object(self, direction, shape): """ Convert a shape drawn on a plot or a subplot into one whose yref or xref ends with " domain" and has coordinates so that the shape will seem to extend infinitely in that dimension. This is useful for drawing lines or boxes on a plot where one dimension of the shape will not move out of bounds when moving the plot's view. Note that the shape already added to the (sub)plot must have the corresponding axis reference referring to an actual axis (e.g., 'x', 'y2' etc. are accepted, but not 'paper'). This will be the case if the shape was added with "add_shape". Shape must have the x0, x1, y0, y1 fields already initialized. """ if direction == "vertical": # fix y points to top and bottom of subplot ref = "yref" elif direction == "horizontal": # fix x points to left and right of subplot ref = "xref" else: raise ValueError( "Bad direction: %s. Permissible values are 'vertical' and 'horizontal'." % (direction,) ) # set the ref to " domain" so that its size is based on the # axis's size shape[ref] += " domain" return shape def _process_multiple_axis_spanning_shapes( self, shape_args, row, col, shape_type, exclude_empty_subplots=True, annotation=None, **kwargs ): """ Add a shape or multiple shapes and call _make_axis_spanning_layout_object on all the new shapes. """ if shape_type in ["vline", "vrect"]: direction = "vertical" elif shape_type in ["hline", "hrect"]: direction = "horizontal" else: raise ValueError( "Bad shape_type %s, needs to be one of 'vline', 'hline', 'vrect', 'hrect'" % (shape_type,) ) if (row is not None or col is not None) and (not self._has_subplots()): # this has no subplots to address, so we force row and col to be None row = None col = None n_shapes_before = len(self.layout["shapes"]) n_annotations_before = len(self.layout["annotations"]) # shapes are always added at the end of the tuple of shapes, so we see # how long the tuple is before the call and after the call, and adjust # the new shapes that were added at the end # extract annotation prefixed kwargs # annotation with extra parameters based on the annotation_position # argument and other annotation_ prefixed kwargs shape_kwargs, annotation_kwargs = shapeannotation.split_dict_by_key_prefix( kwargs, "annotation_" ) augmented_annotation = shapeannotation.axis_spanning_shape_annotation( annotation, shape_type, shape_args, annotation_kwargs ) self.add_shape( row=row, col=col, exclude_empty_subplots=exclude_empty_subplots, **_combine_dicts([shape_args, shape_kwargs]) ) if augmented_annotation is not None: self.add_annotation( augmented_annotation, row=row, col=col, exclude_empty_subplots=exclude_empty_subplots, ) # update xref and yref for the new shapes and annotations for layout_obj, n_layout_objs_before in zip( ["shapes", "annotations"], [n_shapes_before, n_annotations_before] ): n_layout_objs_after = len(self.layout[layout_obj]) if (n_layout_objs_after > n_layout_objs_before) and ( row is None and col is None ): # this was called intending to add to a single plot (and # self.add_{layout_obj} succeeded) # however, in the case of a single plot, xref and yref are not # specified, so we specify them here so the following routines can work # (they need to append " domain" to xref or yref) self.layout[layout_obj][-1].update(xref="x", yref="y") new_layout_objs = tuple( filter( lambda x: x is not None, [ self._make_axis_spanning_layout_object( direction, self.layout[layout_obj][n], ) for n in range(n_layout_objs_before, n_layout_objs_after) ], ) ) self.layout[layout_obj] = ( self.layout[layout_obj][:n_layout_objs_before] + new_layout_objs ) def add_vline( self, x, row="all", col="all", exclude_empty_subplots=True, annotation=None, **kwargs ): self._process_multiple_axis_spanning_shapes( dict(type="line", x0=x, x1=x, y0=0, y1=1), row, col, "vline", exclude_empty_subplots=exclude_empty_subplots, annotation=annotation, **kwargs ) return self add_vline.__doc__ = _axis_spanning_shapes_docstr("vline") def add_hline(self, y, row="all", col="all", exclude_empty_subplots=True, **kwargs): self._process_multiple_axis_spanning_shapes( dict(type="line", x0=0, x1=1, y0=y, y1=y,), row, col, "hline", exclude_empty_subplots=exclude_empty_subplots, **kwargs ) return self add_hline.__doc__ = _axis_spanning_shapes_docstr("hline") def add_vrect( self, x0, x1, row="all", col="all", exclude_empty_subplots=True, **kwargs ): self._process_multiple_axis_spanning_shapes( dict(type="rect", x0=x0, x1=x1, y0=0, y1=1), row, col, "vrect", exclude_empty_subplots=exclude_empty_subplots, **kwargs ) return self add_vrect.__doc__ = _axis_spanning_shapes_docstr("vrect") def add_hrect( self, y0, y1, row="all", col="all", exclude_empty_subplots=True, **kwargs ): self._process_multiple_axis_spanning_shapes( dict(type="rect", x0=0, x1=1, y0=y0, y1=y1), row, col, "hrect", exclude_empty_subplots=exclude_empty_subplots, **kwargs ) return self add_hrect.__doc__ = _axis_spanning_shapes_docstr("hrect") def _has_subplots(self): """ Returns True if figure contains subplots, otherwise it contains a single plot and so this returns False. """ return self._grid_ref is not None def _subplot_not_empty(self, xref, yref, selector="all"): """ xref: string representing the axis. Objects in the plot will be checked for this xref (for layout objects) or xaxis (for traces) to determine if they lie in a certain subplot. yref: string representing the axis. Objects in the plot will be checked for this yref (for layout objects) or yaxis (for traces) to determine if they lie in a certain subplot. selector: can be "all" or an iterable containing some combination of "traces", "shapes", "annotations", "images". Only the presence of objects specified in selector will be checked. So if ["traces","shapes"] is passed then a plot we be considered non-empty if it contains traces or shapes. If bool(selector) returns False, no checking is performed and this function returns True. If selector is True, it is converted to "all". """ if not selector: # If nothing to select was specified then a subplot is always deemed non-empty return True if selector is True: selector = "all" if selector == "all": selector = ["traces", "shapes", "annotations", "images"] ret = False for s in selector: if s == "traces": obj = self.data xaxiskw = "xaxis" yaxiskw = "yaxis" elif s in ["shapes", "annotations", "images"]: obj = self.layout[s] xaxiskw = "xref" yaxiskw = "yref" else: obj = None if obj: ret |= any( t == (xref, yref) for t in [ # if a object exists but has no xaxis or yaxis keys, then it # is plotted with xaxis/xref 'x' and yaxis/yref 'y' ( "x" if d[xaxiskw] is None else d[xaxiskw], "y" if d[yaxiskw] is None else d[yaxiskw], ) for d in obj ] ) return ret def set_subplots(self, rows=None, cols=None, **make_subplots_args): """ Add subplots to this figure. If the figure already contains subplots, then this throws an error. Accepts any keyword arguments that plotly.subplots.make_subplots accepts. """ # rows, cols provided so that this can be called like # fig.set_subplots(2,3), say if rows is not None: make_subplots_args["rows"] = rows if cols is not None: make_subplots_args["cols"] = cols if self._has_subplots(): raise ValueError("This figure already has subplots.") return subplots.make_subplots(figure=self, **make_subplots_args) class BasePlotlyType(object): """ BasePlotlyType is the base class for all objects in the trace, layout, and frame object hierarchies """ # ### Mapped (deprecated) properties ### # dict for deprecated property name (e.g. 'titlefont') to tuple # of relative path to new property (e.g. ('title', 'font') _mapped_properties = {} _parent_path_str = "" _path_str = "" _valid_props = set() def __init__(self, plotly_name, **kwargs): """ Construct a new BasePlotlyType Parameters ---------- plotly_name : str The lowercase name of the plotly object kwargs : dict Invalid props/values to raise on """ # ### _skip_invalid ## # If True, then invalid properties should be skipped, if False then # invalid properties will result in an exception self._skip_invalid = False self._validate = True # Validate inputs # --------------- self._process_kwargs(**kwargs) # Store params # ------------ self._plotly_name = plotly_name # Initialize properties # --------------------- # ### _compound_props ### # A dict from compound property names to compound objects self._compound_props = {} # ### _compound_array_props ### # A dict from compound array property names to tuples of compound # objects self._compound_array_props = {} # ### _orphan_props ### # A dict of properties for use while object has no parent. When # object has a parent, it requests its properties dict from its # parent and doesn't use this. self._orphan_props = {} # ### _parent ### # The parent of the object. May be another BasePlotlyType or it may # be a BaseFigure (as is the case for the Layout and Trace objects) self._parent = None # ### _change_callbacks ### # A dict from tuples of child property path tuples to lists # of callbacks that should be executed whenever any of these # properties is modified self._change_callbacks = {} # ### Backing property for backward compatible _validator property ## self.__validators = None # @property # def _validate(self): # fig = self.figure # if fig is None: # return True # else: # return fig._validate def _get_validator(self, prop): from .validator_cache import ValidatorCache return ValidatorCache.get_validator(self._path_str, prop) @property def _validators(self): """ Validators used to be stored in a private _validators property. This was eliminated when we switched to building validators on demand using the _get_validator method. This property returns a simple object that Returns ------- dict-like interface for accessing the object's validators """ obj = self if self.__validators is None: class ValidatorCompat(object): def __getitem__(self, item): return obj._get_validator(item) def __contains__(self, item): return obj.__contains__(item) def __iter__(self): return iter(obj) def items(self): return [(k, self[k]) for k in self] self.__validators = ValidatorCompat() return self.__validators def _process_kwargs(self, **kwargs): """ Process any extra kwargs that are not predefined as constructor params """ for k, v in kwargs.items(): err = _check_path_in_prop_tree(self, k, error_cast=ValueError) if err is None: # e.g. underscore kwargs like marker_line_color self[k] = v elif not self._validate: # Set extra property as-is self[k] = v elif not self._skip_invalid: raise err # No need to call _raise_on_invalid_property_error here, # because we have it set up so that the singular case of calling # __setitem__ will raise this. If _check_path_in_prop_tree # raised that in its travels, it will already be in the error # message. @property def plotly_name(self): """ The plotly name of the object Returns ------- str """ return self._plotly_name @property def _prop_descriptions(self): """ Formatted string containing all of this obejcts child properties and their descriptions Returns ------- str """ raise NotImplementedError @property def _props(self): """ Dictionary used to store this object properties. When the object has a parent, this dict is retreived from the parent. When the object does not have a parent, this dict is the object's `_orphan_props` property Note: Property will return None if the object has a parent and the object's properties have not been initialized using the `_init_props` method. Returns ------- dict|None """ if self.parent is None: # Use orphan data return self._orphan_props else: # Get data from parent's dict return self.parent._get_child_props(self) def _get_child_props(self, child): """ Return properties dict for child Parameters ---------- child : BasePlotlyType Returns ------- dict """ if self._props is None: # If this node's properties are uninitialized then so are its # child's return None else: # ### Child a compound property ### if child.plotly_name in self: from _plotly_utils.basevalidators import ( CompoundValidator, CompoundArrayValidator, ) validator = self._get_validator(child.plotly_name) if isinstance(validator, CompoundValidator): return self._props.get(child.plotly_name, None) # ### Child an element of a compound array property ### elif isinstance(validator, CompoundArrayValidator): children = self[child.plotly_name] child_ind = BaseFigure._index_is(children, child) assert child_ind is not None children_props = self._props.get(child.plotly_name, None) return ( children_props[child_ind] if children_props is not None and len(children_props) > child_ind else None ) # ### Invalid child ### else: raise ValueError("Invalid child with name: %s" % child.plotly_name) def _init_props(self): """ Ensure that this object's properties dict has been initialized. When the object has a parent, this ensures that the parent has an initialized properties dict with this object's plotly_name as a key. Returns ------- None """ # Ensure that _data is initialized. if self._props is not None: pass else: self._parent._init_child_props(self) def _init_child_props(self, child): """ Ensure that a properties dict has been initialized for a child object Parameters ---------- child : BasePlotlyType Returns ------- None """ # Init our own properties # ----------------------- self._init_props() # Child a compound property # ------------------------- if child.plotly_name in self._compound_props: if child.plotly_name not in self._props: self._props[child.plotly_name] = {} # Child an element of a compound array property # --------------------------------------------- elif child.plotly_name in self._compound_array_props: children = self._compound_array_props[child.plotly_name] child_ind = BaseFigure._index_is(children, child) assert child_ind is not None if child.plotly_name not in self._props: # Initialize list self._props[child.plotly_name] = [] # Make sure list is long enough for child children_list = self._props[child.plotly_name] while len(children_list) <= child_ind: children_list.append({}) # Invalid child # ------------- else: raise ValueError("Invalid child with name: %s" % child.plotly_name) def _get_child_prop_defaults(self, child): """ Return default properties dict for child Parameters ---------- child : BasePlotlyType Returns ------- dict """ if self._prop_defaults is None: # If this node's default properties are uninitialized then so are # its child's return None else: # ### Child a compound property ### if child.plotly_name in self._compound_props: return self._prop_defaults.get(child.plotly_name, None) # ### Child an element of a compound array property ### elif child.plotly_name in self._compound_array_props: children = self._compound_array_props[child.plotly_name] child_ind = BaseFigure._index_is(children, child) assert child_ind is not None children_props = self._prop_defaults.get(child.plotly_name, None) return ( children_props[child_ind] if children_props is not None and len(children_props) > child_ind else None ) # ### Invalid child ### else: raise ValueError("Invalid child with name: %s" % child.plotly_name) @property def _prop_defaults(self): """ Return default properties dict Returns ------- dict """ if self.parent is None: return None else: return self.parent._get_child_prop_defaults(self) def _get_prop_validator(self, prop): """ Return the validator associated with the specified property Parameters ---------- prop: str A property that exists in this object Returns ------- BaseValidator """ # Handle remapping # ---------------- if prop in self._mapped_properties: prop_path = self._mapped_properties[prop] plotly_obj = self[prop_path[:-1]] prop = prop_path[-1] else: prop_path = BaseFigure._str_to_dict_path(prop) plotly_obj = self[prop_path[:-1]] prop = prop_path[-1] # Return validator # ---------------- return plotly_obj._get_validator(prop) @property def parent(self): """ Return the object's parent, or None if the object has no parent Returns ------- BasePlotlyType|BaseFigure """ return self._parent @property def figure(self): """ Reference to the top-level Figure or FigureWidget that this object belongs to. None if the object does not belong to a Figure Returns ------- Union[BaseFigure, None] """ top_parent = self while top_parent is not None: if isinstance(top_parent, BaseFigure): break else: top_parent = top_parent.parent return top_parent # Magic Methods # ------------- def __reduce__(self): """ Custom implementation of reduce is used to support deep copying and pickling """ props = self.to_plotly_json() return (self.__class__, (props,)) def __getitem__(self, prop): """ Get item or nested item from object Parameters ---------- prop : str|tuple If prop is the name of a property of this object, then the property is returned. If prop is a nested property path string (e.g. 'foo[1].bar'), then a nested property is returned (e.g. obj['foo'][1]['bar']) If prop is a path tuple (e.g. ('foo', 1, 'bar')), then a nested property is returned (e.g. obj['foo'][1]['bar']). Returns ------- Any """ from _plotly_utils.basevalidators import ( CompoundValidator, CompoundArrayValidator, BaseDataValidator, ) # Normalize prop # -------------- # Convert into a property tuple orig_prop = prop prop = BaseFigure._str_to_dict_path(prop) # Handle remapping # ---------------- if prop and prop[0] in self._mapped_properties: prop = self._mapped_properties[prop[0]] + prop[1:] orig_prop = _remake_path_from_tuple(prop) # Handle scalar case # ------------------ # e.g. ('foo',) if len(prop) == 1: # Unwrap scalar tuple prop = prop[0] if prop not in self._valid_props: self._raise_on_invalid_property_error(_error_to_raise=PlotlyKeyError)( prop ) validator = self._get_validator(prop) if isinstance(validator, CompoundValidator): if self._compound_props.get(prop, None) is None: # Init compound objects self._compound_props[prop] = validator.data_class( _parent=self, plotly_name=prop ) # Update plotly_name value in case the validator applies # non-standard name (e.g. imagedefaults instead of image) self._compound_props[prop]._plotly_name = prop return validator.present(self._compound_props[prop]) elif isinstance(validator, (CompoundArrayValidator, BaseDataValidator)): if self._compound_array_props.get(prop, None) is None: # Init list of compound objects if self._props is not None: self._compound_array_props[prop] = [ validator.data_class(_parent=self) for _ in self._props.get(prop, []) ] else: self._compound_array_props[prop] = [] return validator.present(self._compound_array_props[prop]) elif self._props is not None and prop in self._props: return validator.present(self._props[prop]) elif self._prop_defaults is not None: return validator.present(self._prop_defaults.get(prop, None)) else: return None # Handle non-scalar case # ---------------------- # e.g. ('foo', 1), () else: err = _check_path_in_prop_tree(self, orig_prop, error_cast=PlotlyKeyError) if err is not None: raise err res = self for p in prop: res = res[p] return res def __contains__(self, prop): """ Determine whether object contains a property or nested property Parameters ---------- prop : str|tuple If prop is a simple string (e.g. 'foo'), then return true of the object contains an element named 'foo' If prop is a property path string (e.g. 'foo[0].bar'), then return true if the obejct contains the nested elements for each entry in the path string (e.g. 'bar' in obj['foo'][0]) If prop is a property path tuple (e.g. ('foo', 0, 'bar')), then return true if the object contains the nested elements for each entry in the path string (e.g. 'bar' in obj['foo'][0]) Returns ------- bool """ prop = BaseFigure._str_to_dict_path(prop) # Handle remapping if prop and prop[0] in self._mapped_properties: prop = self._mapped_properties[prop[0]] + prop[1:] obj = self for p in prop: if isinstance(p, int): if isinstance(obj, tuple) and 0 <= p < len(obj): obj = obj[p] else: return False else: if obj is not None and p in obj._valid_props: obj = obj[p] else: return False return True def __setitem__(self, prop, value): """ Parameters ---------- prop : str The name of a direct child of this object Note: Setting nested properties using property path string or property path tuples is not supported. value New property value Returns ------- None """ from _plotly_utils.basevalidators import ( CompoundValidator, CompoundArrayValidator, BaseDataValidator, ) # Normalize prop # -------------- # Convert into a property tuple orig_prop = prop prop = BaseFigure._str_to_dict_path(prop) # Handle empty case # ----------------- if len(prop) == 0: raise KeyError(orig_prop) # Handle remapping # ---------------- if prop[0] in self._mapped_properties: prop = self._mapped_properties[prop[0]] + prop[1:] # Handle scalar case # ------------------ # e.g. ('foo',) if len(prop) == 1: # ### Unwrap scalar tuple ### prop = prop[0] if self._validate: if prop not in self._valid_props: self._raise_on_invalid_property_error()(prop) # ### Get validator for this property ### validator = self._get_validator(prop) # ### Handle compound property ### if isinstance(validator, CompoundValidator): self._set_compound_prop(prop, value) # ### Handle compound array property ### elif isinstance(validator, (CompoundArrayValidator, BaseDataValidator)): self._set_array_prop(prop, value) # ### Handle simple property ### else: self._set_prop(prop, value) else: # Make sure properties dict is initialized self._init_props() if isinstance(value, BasePlotlyType): # Extract json from graph objects value = value.to_plotly_json() # Check for list/tuple of graph objects if ( isinstance(value, (list, tuple)) and value and isinstance(value[0], BasePlotlyType) ): value = [ v.to_plotly_json() if isinstance(v, BasePlotlyType) else v for v in value ] self._props[prop] = value # Remove any already constructed graph object so that it will be # reconstructed on property access self._compound_props.pop(prop, None) self._compound_array_props.pop(prop, None) # Handle non-scalar case # ---------------------- # e.g. ('foo', 1), () else: err = _check_path_in_prop_tree(self, orig_prop, error_cast=ValueError) if err is not None: raise err res = self for p in prop[:-1]: res = res[p] res._validate = self._validate res[prop[-1]] = value def __setattr__(self, prop, value): """ Parameters ---------- prop : str The name of a direct child of this object value New property value Returns ------- None """ if prop.startswith("_") or hasattr(self, prop) or prop in self._valid_props: # Let known properties and private properties through super(BasePlotlyType, self).__setattr__(prop, value) else: # Raise error on unknown public properties self._raise_on_invalid_property_error()(prop) def __iter__(self): """ Return an iterator over the object's properties """ res = list(self._valid_props) for prop in self._mapped_properties: res.append(prop) return iter(res) def __eq__(self, other): """ Test for equality To be considered equal, `other` must have the same type as this object and their `to_plotly_json` representaitons must be identical. Parameters ---------- other The object to compare against Returns ------- bool """ if not isinstance(other, self.__class__): # Require objects to be of the same plotly type return False else: # Compare plotly_json representations # Use _vals_equal instead of `==` to handle cases where # underlying dicts contain numpy arrays return BasePlotlyType._vals_equal( self._props if self._props is not None else {}, other._props if other._props is not None else {}, ) @staticmethod def _build_repr_for_class(props, class_name, parent_path_str=None): """ Helper to build representation string for a class Parameters ---------- class_name : str Name of the class being represented parent_path_str : str of None (default) Name of the class's parent package to display props : dict Properties to unpack into the constructor Returns ------- str The representation string """ from plotly.utils import ElidedPrettyPrinter if parent_path_str: class_name = parent_path_str + "." + class_name if len(props) == 0: repr_str = class_name + "()" else: pprinter = ElidedPrettyPrinter(threshold=200, width=120) pprint_res = pprinter.pformat(props) # pprint_res is indented by 1 space. Add extra 3 spaces for PEP8 # complaint indent body = " " + pprint_res[1:-1].replace("\n", "\n ") repr_str = class_name + "({\n " + body + "\n})" return repr_str def __repr__(self): """ Customize object representation when displayed in the terminal/notebook """ from _plotly_utils.basevalidators import LiteralValidator # Get all properties props = self._props if self._props is not None else {} # Remove literals (These can't be specified in the constructor) props = { p: v for p, v in props.items() if p in self._valid_props and not isinstance(self._get_validator(p), LiteralValidator) } # Elide template if "template" in props: props["template"] = "..." # Build repr string repr_str = BasePlotlyType._build_repr_for_class( props=props, class_name=self.__class__.__name__, parent_path_str=self._parent_path_str, ) return repr_str def _raise_on_invalid_property_error(self, _error_to_raise=None): """ Returns a function that raises informative exception when invalid property names are encountered. The _error_to_raise argument allows specifying the exception to raise, which is ValueError if None. Parameters ---------- args : list[str] List of property names that have already been determined to be invalid Raises ------ ValueError by default, or _error_to_raise if not None """ if _error_to_raise is None: _error_to_raise = ValueError def _ret(*args): invalid_props = args if invalid_props: if len(invalid_props) == 1: prop_str = "property" invalid_str = repr(invalid_props[0]) else: prop_str = "properties" invalid_str = repr(invalid_props) module_root = "plotly.graph_objs." if self._parent_path_str: full_obj_name = ( module_root + self._parent_path_str + "." + self.__class__.__name__ ) else: full_obj_name = module_root + self.__class__.__name__ guessed_prop = None if len(invalid_props) == 1: try: guessed_prop = find_closest_string( invalid_props[0], self._valid_props ) except Exception: pass guessed_prop_suggestion = "" if guessed_prop is not None: guessed_prop_suggestion = 'Did you mean "%s"?' % (guessed_prop,) raise _error_to_raise( "Invalid {prop_str} specified for object of type " "{full_obj_name}: {invalid_str}\n" "\n{guessed_prop_suggestion}\n" "\n Valid properties:\n" "{prop_descriptions}" "\n{guessed_prop_suggestion}\n".format( prop_str=prop_str, full_obj_name=full_obj_name, invalid_str=invalid_str, prop_descriptions=self._prop_descriptions, guessed_prop_suggestion=guessed_prop_suggestion, ) ) return _ret def update(self, dict1=None, overwrite=False, **kwargs): """ Update the properties of an object with a dict and/or with keyword arguments. This recursively updates the structure of the original object with the values in the input dict / keyword arguments. Parameters ---------- dict1 : dict Dictionary of properties to be updated overwrite: bool If True, overwrite existing properties. If False, apply updates to existing properties recursively, preserving existing properties that are not specified in the update operation. kwargs : Keyword/value pair of properties to be updated Returns ------- BasePlotlyType Updated plotly object """ if self.figure: with self.figure.batch_update(): BaseFigure._perform_update(self, dict1, overwrite=overwrite) BaseFigure._perform_update(self, kwargs, overwrite=overwrite) else: BaseFigure._perform_update(self, dict1, overwrite=overwrite) BaseFigure._perform_update(self, kwargs, overwrite=overwrite) return self def pop(self, key, *args): """ Remove the value associated with the specified key and return it Parameters ---------- key: str Property name dflt The default value to return if key was not found in object Returns ------- value The removed value that was previously associated with key Raises ------ KeyError If key is not in object and no dflt argument specified """ # Handle default if key not in self and args: return args[0] elif key in self: val = self[key] self[key] = None return val else: raise KeyError(key) @property def _in_batch_mode(self): """ True if the object belongs to a figure that is currently in batch mode Returns ------- bool """ return self.parent and self.parent._in_batch_mode def _set_prop(self, prop, val): """ Set the value of a simple property Parameters ---------- prop : str Name of a simple (non-compound, non-array) property val The new property value Returns ------- Any The coerced assigned value """ # val is Undefined # ---------------- if val is Undefined: # Do nothing return # Import value # ------------ validator = self._get_validator(prop) try: val = validator.validate_coerce(val) except ValueError as err: if self._skip_invalid: return else: raise err # val is None # ----------- if val is None: # Check if we should send null update if self._props and prop in self._props: # Remove property if not in batch mode if not self._in_batch_mode: self._props.pop(prop) # Send property update message self._send_prop_set(prop, val) # val is valid value # ------------------ else: # Make sure properties dict is initialized self._init_props() # Check whether the value is a change if prop not in self._props or not BasePlotlyType._vals_equal( self._props[prop], val ): # Set property value if not in batch mode if not self._in_batch_mode: self._props[prop] = val # Send property update message self._send_prop_set(prop, val) return val def _set_compound_prop(self, prop, val): """ Set the value of a compound property Parameters ---------- prop : str Name of a compound property val The new property value Returns ------- BasePlotlyType The coerced assigned object """ # val is Undefined # ---------------- if val is Undefined: # Do nothing return # Import value # ------------ validator = self._get_validator(prop) val = validator.validate_coerce(val, skip_invalid=self._skip_invalid) # Save deep copies of current and new states # ------------------------------------------ curr_val = self._compound_props.get(prop, None) if curr_val is not None: curr_dict_val = deepcopy(curr_val._props) else: curr_dict_val = None if val is not None: new_dict_val = deepcopy(val._props) else: new_dict_val = None # Update _props dict # ------------------ if not self._in_batch_mode: if not new_dict_val: if self._props and prop in self._props: self._props.pop(prop) else: self._init_props() self._props[prop] = new_dict_val # Send update if there was a change in value # ------------------------------------------ if not BasePlotlyType._vals_equal(curr_dict_val, new_dict_val): self._send_prop_set(prop, new_dict_val) # Reparent # -------- # ### Reparent new value and clear orphan data ### if isinstance(val, BasePlotlyType): val._parent = self val._orphan_props.clear() # ### Unparent old value and update orphan data ### if curr_val is not None: if curr_dict_val is not None: curr_val._orphan_props.update(curr_dict_val) curr_val._parent = None # Update _compound_props # ---------------------- self._compound_props[prop] = val return val def _set_array_prop(self, prop, val): """ Set the value of a compound property Parameters ---------- prop : str Name of a compound property val The new property value Returns ------- tuple[BasePlotlyType] The coerced assigned object """ # val is Undefined # ---------------- if val is Undefined: # Do nothing return # Import value # ------------ validator = self._get_validator(prop) val = validator.validate_coerce(val, skip_invalid=self._skip_invalid) # Save deep copies of current and new states # ------------------------------------------ curr_val = self._compound_array_props.get(prop, None) if curr_val is not None: curr_dict_vals = [deepcopy(cv._props) for cv in curr_val] else: curr_dict_vals = None if val is not None: new_dict_vals = [deepcopy(nv._props) for nv in val] else: new_dict_vals = None # Update _props dict # ------------------ if not self._in_batch_mode: if not new_dict_vals: if self._props and prop in self._props: self._props.pop(prop) else: self._init_props() self._props[prop] = new_dict_vals # Send update if there was a change in value # ------------------------------------------ if not BasePlotlyType._vals_equal(curr_dict_vals, new_dict_vals): self._send_prop_set(prop, new_dict_vals) # Reparent # -------- # ### Reparent new values and clear orphan data ### if val is not None: for v in val: v._orphan_props.clear() v._parent = self # ### Unparent old value and update orphan data ### if curr_val is not None: for cv, cv_dict in zip(curr_val, curr_dict_vals): if cv_dict is not None: cv._orphan_props.update(cv_dict) cv._parent = None # Update _compound_array_props # ---------------------------- self._compound_array_props[prop] = val return val def _send_prop_set(self, prop_path_str, val): """ Notify parent that a property has been set to a new value Parameters ---------- prop_path_str : str Property path string (e.g. 'foo[0].bar') of property that was set, relative to this object val New value for property. Either a simple value, a dict, or a tuple of dicts. This should *not* be a BasePlotlyType object. Returns ------- None """ raise NotImplementedError() def _prop_set_child(self, child, prop_path_str, val): """ Propagate property setting notification from child to parent Parameters ---------- child : BasePlotlyType Child object prop_path_str : str Property path string (e.g. 'foo[0].bar') of property that was set, relative to `child` val New value for property. Either a simple value, a dict, or a tuple of dicts. This should *not* be a BasePlotlyType object. Returns ------- None """ # Child is compound array property # -------------------------------- child_prop_val = getattr(self, child.plotly_name) if isinstance(child_prop_val, (list, tuple)): child_ind = BaseFigure._index_is(child_prop_val, child) obj_path = "{child_name}.{child_ind}.{prop}".format( child_name=child.plotly_name, child_ind=child_ind, prop=prop_path_str ) # Child is compound property # -------------------------- else: obj_path = "{child_name}.{prop}".format( child_name=child.plotly_name, prop=prop_path_str ) # Propagate to parent # ------------------- self._send_prop_set(obj_path, val) def _restyle_child(self, child, prop, val): """ Propagate _restyle_child to parent Note: This method must match the name and signature of the corresponding method on BaseFigure """ self._prop_set_child(child, prop, val) def _relayout_child(self, child, prop, val): """ Propagate _relayout_child to parent Note: This method must match the name and signature of the corresponding method on BaseFigure """ self._prop_set_child(child, prop, val) # Callbacks # --------- def _dispatch_change_callbacks(self, changed_paths): """ Execute the appropriate change callback functions given a set of changed property path tuples Parameters ---------- changed_paths : set[tuple[int|str]] Returns ------- None """ # Loop over registered callbacks # ------------------------------ for prop_path_tuples, callbacks in self._change_callbacks.items(): # ### Compute callback paths that changed ### common_paths = changed_paths.intersection(set(prop_path_tuples)) if common_paths: # #### Invoke callback #### callback_args = [self[cb_path] for cb_path in prop_path_tuples] for callback in callbacks: callback(self, *callback_args) def on_change(self, callback, *args, **kwargs): """ Register callback function to be called when certain properties or subproperties of this object are modified. Callback will be invoked whenever ANY of these properties is modified. Furthermore, the callback will only be invoked once even if multiple properties are modified during the same restyle / relayout / update operation. Parameters ---------- callback : function Function that accepts 1 + len(`args`) parameters. First parameter is this object. Second through last parameters are the property / subpropery values referenced by args. args : list[str|tuple[int|str]] List of property references where each reference may be one of: 1) A property name string (e.g. 'foo') for direct properties 2) A property path string (e.g. 'foo[0].bar') for subproperties 3) A property path tuple (e.g. ('foo', 0, 'bar')) for subproperties append : bool True if callback should be appended to previously registered callback on the same properties, False if callback should replace previously registered callbacks on the same properties. Defaults to False. Examples -------- Register callback that prints out the range extents of the xaxis and yaxis whenever either either of them changes. >>> import plotly.graph_objects as go >>> fig = go.Figure(go.Scatter(x=[1, 2], y=[1, 0])) >>> fig.layout.on_change( ... lambda obj, xrange, yrange: print("%s-%s" % (xrange, yrange)), ... ('xaxis', 'range'), ('yaxis', 'range')) Returns ------- None """ # Warn if object not descendent of a figure # ----------------------------------------- if not self.figure: class_name = self.__class__.__name__ msg = """ {class_name} object is not a descendant of a Figure. on_change callbacks are not supported in this case. """.format( class_name=class_name ) raise ValueError(msg) # Validate args not empty # ----------------------- if len(args) == 0: raise ValueError("At least one change property must be specified") # Validate args # ------------- invalid_args = [arg for arg in args if arg not in self] if invalid_args: raise ValueError("Invalid property specification(s): %s" % invalid_args) # Process append option # --------------------- append = kwargs.get("append", False) # Normalize args to path tuples # ----------------------------- arg_tuples = tuple([BaseFigure._str_to_dict_path(a) for a in args]) # Initialize callbacks list # ------------------------- # Initialize an empty callbacks list if there are no previously # defined callbacks for this collection of args, or if append is False if arg_tuples not in self._change_callbacks or not append: self._change_callbacks[arg_tuples] = [] # Register callback # ----------------- self._change_callbacks[arg_tuples].append(callback) def to_plotly_json(self): """ Return plotly JSON representation of object as a Python dict Returns ------- dict """ return deepcopy(self._props if self._props is not None else {}) @staticmethod def _vals_equal(v1, v2): """ Recursive equality function that handles nested dicts / tuples / lists that contain numpy arrays. v1 First value to compare v2 Second value to compare Returns ------- bool True if v1 and v2 are equal, False otherwise """ np = get_module("numpy", should_load=False) if np is not None and ( isinstance(v1, np.ndarray) or isinstance(v2, np.ndarray) ): return np.array_equal(v1, v2) elif isinstance(v1, (list, tuple)): # Handle recursive equality on lists and tuples return ( isinstance(v2, (list, tuple)) and len(v1) == len(v2) and all(BasePlotlyType._vals_equal(e1, e2) for e1, e2 in zip(v1, v2)) ) elif isinstance(v1, dict): # Handle recursive equality on dicts return ( isinstance(v2, dict) and set(v1.keys()) == set(v2.keys()) and all(BasePlotlyType._vals_equal(v1[k], v2[k]) for k in v1) ) else: return v1 == v2 class BaseLayoutHierarchyType(BasePlotlyType): """ Base class for all types in the layout hierarchy """ @property def _parent_path_str(self): pass def __init__(self, plotly_name, **kwargs): super(BaseLayoutHierarchyType, self).__init__(plotly_name, **kwargs) def _send_prop_set(self, prop_path_str, val): if self.parent: # ### Inform parent of relayout operation ### self.parent._relayout_child(self, prop_path_str, val) class BaseLayoutType(BaseLayoutHierarchyType): """ Base class for the layout type. The Layout class itself is a code-generated subclass. """ # Dynamic properties # ------------------ # Unlike all other plotly types, BaseLayoutType has dynamic properties. # These are used when a layout has multiple instances of subplot types # (xaxis2, yaxis3, geo4, etc.) # # The base version of each suplot type is defined in the schema and code # generated. So the Layout subclass has statically defined properties # for xaxis, yaxis, geo, ternary, and scene. But, we need to dynamically # generated properties/validators as needed for xaxis2, yaxis3, etc. @property def _subplotid_validators(self): """ dict of validator classes for each subplot type Returns ------- dict """ raise NotImplementedError() def _subplot_re_match(self, prop): raise NotImplementedError() def __init__(self, plotly_name, **kwargs): """ Construct a new BaseLayoutType object Parameters ---------- plotly_name : str Name of the object (should always be 'layout') kwargs : dict[str, any] Properties that were not recognized by the Layout subclass. These are subplot identifiers (xaxis2, geo4, etc.) or they are invalid properties. """ # Validate inputs # --------------- assert plotly_name == "layout" # Call superclass constructor # --------------------------- super(BaseLayoutHierarchyType, self).__init__(plotly_name) # Initialize _subplotid_props # --------------------------- # This is a set storing the names of the layout's dynamic subplot # properties self._subplotid_props = set() # Process kwargs # -------------- self._process_kwargs(**kwargs) def _process_kwargs(self, **kwargs): """ Process any extra kwargs that are not predefined as constructor params """ unknown_kwargs = { k: v for k, v in kwargs.items() if not self._subplot_re_match(k) } super(BaseLayoutHierarchyType, self)._process_kwargs(**unknown_kwargs) subplot_kwargs = {k: v for k, v in kwargs.items() if self._subplot_re_match(k)} for prop, value in subplot_kwargs.items(): self._set_subplotid_prop(prop, value) def _set_subplotid_prop(self, prop, value): """ Set a subplot property on the layout Parameters ---------- prop : str A valid subplot property value Subplot value """ # Get regular expression match # ---------------------------- # Note: we already tested that match exists in the constructor match = self._subplot_re_match(prop) subplot_prop = match.group(1) suffix_digit = int(match.group(2)) # Validate suffix digit # --------------------- if suffix_digit == 0: raise TypeError( "Subplot properties may only be suffixed by an " "integer >= 1\n" "Received {k}".format(k=prop) ) # Handle suffix_digit == 1 # ------------------------ # In this case we remove suffix digit (e.g. xaxis1 -> xaxis) if suffix_digit == 1: prop = subplot_prop # Construct and add validator # --------------------------- if prop not in self._valid_props: self._valid_props.add(prop) # Import value # ------------ # Use the standard _set_compound_prop method to # validate/coerce/import subplot value. This must be called AFTER # the validator instance is added to self._validators above. self._set_compound_prop(prop, value) self._subplotid_props.add(prop) def _strip_subplot_suffix_of_1(self, prop): """ Strip the suffix for subplot property names that have a suffix of 1. All other properties are returned unchanged e.g. 'xaxis1' -> 'xaxis' Parameters ---------- prop : str|tuple Returns ------- str|tuple """ # Let parent handle non-scalar cases # ---------------------------------- # e.g. ('xaxis', 'range') or 'xaxis.range' prop_tuple = BaseFigure._str_to_dict_path(prop) if len(prop_tuple) != 1 or not isinstance(prop_tuple[0], string_types): return prop else: # Unwrap to scalar string prop = prop_tuple[0] # Handle subplot suffix digit of 1 # -------------------------------- # Remove digit of 1 from subplot id (e.g.. xaxis1 -> xaxis) match = self._subplot_re_match(prop) if match: subplot_prop = match.group(1) suffix_digit = int(match.group(2)) if subplot_prop and suffix_digit == 1: prop = subplot_prop return prop def _get_prop_validator(self, prop): """ Custom _get_prop_validator that handles subplot properties """ prop = self._strip_subplot_suffix_of_1(prop) return super(BaseLayoutHierarchyType, self)._get_prop_validator(prop) def __getattr__(self, prop): """ Custom __getattr__ that handles dynamic subplot properties """ prop = self._strip_subplot_suffix_of_1(prop) if prop != "_subplotid_props" and prop in self._subplotid_props: validator = self._get_validator(prop) return validator.present(self._compound_props[prop]) else: return super(BaseLayoutHierarchyType, self).__getattribute__(prop) def __getitem__(self, prop): """ Custom __getitem__ that handles dynamic subplot properties """ prop = self._strip_subplot_suffix_of_1(prop) return super(BaseLayoutHierarchyType, self).__getitem__(prop) def __contains__(self, prop): """ Custom __contains__ that handles dynamic subplot properties """ prop = self._strip_subplot_suffix_of_1(prop) return super(BaseLayoutHierarchyType, self).__contains__(prop) def __setitem__(self, prop, value): """ Custom __setitem__ that handles dynamic subplot properties """ # Convert prop to prop tuple # -------------------------- prop_tuple = BaseFigure._str_to_dict_path(prop) if len(prop_tuple) != 1 or not isinstance(prop_tuple[0], string_types): # Let parent handle non-scalar non-string cases super(BaseLayoutHierarchyType, self).__setitem__(prop, value) return else: # Unwrap prop tuple prop = prop_tuple[0] # Check for subplot assignment # ---------------------------- match = self._subplot_re_match(prop) if match is None: # Set as ordinary property super(BaseLayoutHierarchyType, self).__setitem__(prop, value) else: # Set as subplotid property self._set_subplotid_prop(prop, value) def __setattr__(self, prop, value): """ Custom __setattr__ that handles dynamic subplot properties """ # Check for subplot assignment # ---------------------------- match = self._subplot_re_match(prop) if match is None: # Set as ordinary property super(BaseLayoutHierarchyType, self).__setattr__(prop, value) else: # Set as subplotid property self._set_subplotid_prop(prop, value) def __dir__(self): """ Custom __dir__ that handles dynamic subplot properties """ # Include any active subplot values if six.PY2: def get_attrs(obj): import types if not hasattr(obj, "__dict__"): return [] if not isinstance(obj.__dict__, (dict, types.DictProxyType)): raise TypeError("%s.__dict__ is not a dictionary" "" % obj.__name__) return obj.__dict__.keys() def dir2(obj): attrs = set() if not hasattr(obj, "__bases__"): # obj is an instance if not hasattr(obj, "__class__"): # slots return sorted(get_attrs(obj)) klass = obj.__class__ attrs.update(get_attrs(klass)) else: # obj is a class klass = obj for cls in klass.__bases__: attrs.update(get_attrs(cls)) attrs.update(dir2(cls)) attrs.update(get_attrs(obj)) return list(attrs) return dir2(self) + sorted(self._subplotid_props) else: return list(super(BaseLayoutHierarchyType, self).__dir__()) + sorted( self._subplotid_props ) class BaseTraceHierarchyType(BasePlotlyType): """ Base class for all types in the trace hierarchy """ def __init__(self, plotly_name, **kwargs): super(BaseTraceHierarchyType, self).__init__(plotly_name, **kwargs) def _send_prop_set(self, prop_path_str, val): if self.parent: # ### Inform parent of restyle operation ### self.parent._restyle_child(self, prop_path_str, val) class BaseTraceType(BaseTraceHierarchyType): """ Base class for the all trace types. Specific trace type classes (Scatter, Bar, etc.) are code generated as subclasses of this class. """ def __init__(self, plotly_name, **kwargs): super(BaseTraceHierarchyType, self).__init__(plotly_name, **kwargs) # Initialize callback function lists # ---------------------------------- # ### Callbacks to be called on hover ### self._hover_callbacks = [] # ### Callbacks to be called on unhover ### self._unhover_callbacks = [] # ### Callbacks to be called on click ### self._click_callbacks = [] # ### Callbacks to be called on selection ### self._select_callbacks = [] # ### Callbacks to be called on deselect ### self._deselect_callbacks = [] # ### Trace index in figure ### self._trace_ind = None # uid # --- # All trace types must have a top-level UID @property def uid(self): raise NotImplementedError @uid.setter def uid(self, val): raise NotImplementedError # Hover # ----- def on_hover(self, callback, append=False): """ Register function to be called when the user hovers over one or more points in this trace Note: Callbacks will only be triggered when the trace belongs to a instance of plotly.graph_objs.FigureWidget and it is displayed in an ipywidget context. Callbacks will not be triggered on figures that are displayed using plot/iplot. Parameters ---------- callback Callable function that accepts 3 arguments - this trace - plotly.callbacks.Points object - plotly.callbacks.InputDeviceState object append : bool If False (the default), this callback replaces any previously defined on_hover callbacks for this trace. If True, this callback is appended to the list of any previously defined callbacks. Returns ------- None Examples -------- >>> import plotly.graph_objects as go >>> from plotly.callbacks import Points, InputDeviceState >>> points, state = Points(), InputDeviceState() >>> def hover_fn(trace, points, state): ... inds = points.point_inds ... # Do something >>> trace = go.Scatter(x=[1, 2], y=[3, 0]) >>> trace.on_hover(hover_fn) Note: The creation of the `points` and `state` objects is optional, it's simply a convenience to help the text editor perform completion on the arguments inside `hover_fn` """ if not append: del self._hover_callbacks[:] if callback: self._hover_callbacks.append(callback) def _dispatch_on_hover(self, points, state): """ Dispatch points and device state all all hover callbacks """ for callback in self._hover_callbacks: callback(self, points, state) # Unhover # ------- def on_unhover(self, callback, append=False): """ Register function to be called when the user unhovers away from one or more points in this trace. Note: Callbacks will only be triggered when the trace belongs to a instance of plotly.graph_objs.FigureWidget and it is displayed in an ipywidget context. Callbacks will not be triggered on figures that are displayed using plot/iplot. Parameters ---------- callback Callable function that accepts 3 arguments - this trace - plotly.callbacks.Points object - plotly.callbacks.InputDeviceState object append : bool If False (the default), this callback replaces any previously defined on_unhover callbacks for this trace. If True, this callback is appended to the list of any previously defined callbacks. Returns ------- None Examples -------- >>> import plotly.graph_objects as go >>> from plotly.callbacks import Points, InputDeviceState >>> points, state = Points(), InputDeviceState() >>> def unhover_fn(trace, points, state): ... inds = points.point_inds ... # Do something >>> trace = go.Scatter(x=[1, 2], y=[3, 0]) >>> trace.on_unhover(unhover_fn) Note: The creation of the `points` and `state` objects is optional, it's simply a convenience to help the text editor perform completion on the arguments inside `unhover_fn` """ if not append: del self._unhover_callbacks[:] if callback: self._unhover_callbacks.append(callback) def _dispatch_on_unhover(self, points, state): """ Dispatch points and device state all all hover callbacks """ for callback in self._unhover_callbacks: callback(self, points, state) # Click # ----- def on_click(self, callback, append=False): """ Register function to be called when the user clicks on one or more points in this trace. Note: Callbacks will only be triggered when the trace belongs to a instance of plotly.graph_objs.FigureWidget and it is displayed in an ipywidget context. Callbacks will not be triggered on figures that are displayed using plot/iplot. Parameters ---------- callback Callable function that accepts 3 arguments - this trace - plotly.callbacks.Points object - plotly.callbacks.InputDeviceState object append : bool If False (the default), this callback replaces any previously defined on_click callbacks for this trace. If True, this callback is appended to the list of any previously defined callbacks. Returns ------- None Examples -------- >>> import plotly.graph_objects as go >>> from plotly.callbacks import Points, InputDeviceState >>> points, state = Points(), InputDeviceState() >>> def click_fn(trace, points, state): ... inds = points.point_inds ... # Do something >>> trace = go.Scatter(x=[1, 2], y=[3, 0]) >>> trace.on_click(click_fn) Note: The creation of the `points` and `state` objects is optional, it's simply a convenience to help the text editor perform completion on the arguments inside `click_fn` """ if not append: del self._click_callbacks[:] if callback: self._click_callbacks.append(callback) def _dispatch_on_click(self, points, state): """ Dispatch points and device state all all hover callbacks """ for callback in self._click_callbacks: callback(self, points, state) # Select # ------ def on_selection(self, callback, append=False): """ Register function to be called when the user selects one or more points in this trace. Note: Callbacks will only be triggered when the trace belongs to a instance of plotly.graph_objs.FigureWidget and it is displayed in an ipywidget context. Callbacks will not be triggered on figures that are displayed using plot/iplot. Parameters ---------- callback Callable function that accepts 4 arguments - this trace - plotly.callbacks.Points object - plotly.callbacks.BoxSelector or plotly.callbacks.LassoSelector append : bool If False (the default), this callback replaces any previously defined on_selection callbacks for this trace. If True, this callback is appended to the list of any previously defined callbacks. Returns ------- None Examples -------- >>> import plotly.graph_objects as go >>> from plotly.callbacks import Points >>> points = Points() >>> def selection_fn(trace, points, selector): ... inds = points.point_inds ... # Do something >>> trace = go.Scatter(x=[1, 2], y=[3, 0]) >>> trace.on_selection(selection_fn) Note: The creation of the `points` object is optional, it's simply a convenience to help the text editor perform completion on the `points` arguments inside `selection_fn` """ if not append: del self._select_callbacks[:] if callback: self._select_callbacks.append(callback) def _dispatch_on_selection(self, points, selector): """ Dispatch points and selector info to selection callbacks """ if "selectedpoints" in self: # Update the selectedpoints property, which will notify all views # of the selection change. This is a special case because no # restyle event is emitted by plotly.js on selection events # even though these events update the selectedpoints property. self.selectedpoints = points.point_inds for callback in self._select_callbacks: callback(self, points, selector) # deselect # -------- def on_deselect(self, callback, append=False): """ Register function to be called when the user deselects points in this trace using doubleclick. Note: Callbacks will only be triggered when the trace belongs to a instance of plotly.graph_objs.FigureWidget and it is displayed in an ipywidget context. Callbacks will not be triggered on figures that are displayed using plot/iplot. Parameters ---------- callback Callable function that accepts 3 arguments - this trace - plotly.callbacks.Points object append : bool If False (the default), this callback replaces any previously defined on_deselect callbacks for this trace. If True, this callback is appended to the list of any previously defined callbacks. Returns ------- None Examples -------- >>> import plotly.graph_objects as go >>> from plotly.callbacks import Points >>> points = Points() >>> def deselect_fn(trace, points): ... inds = points.point_inds ... # Do something >>> trace = go.Scatter(x=[1, 2], y=[3, 0]) >>> trace.on_deselect(deselect_fn) Note: The creation of the `points` object is optional, it's simply a convenience to help the text editor perform completion on the `points` arguments inside `selection_fn` """ if not append: del self._deselect_callbacks[:] if callback: self._deselect_callbacks.append(callback) def _dispatch_on_deselect(self, points): """ Dispatch points info to deselection callbacks """ if "selectedpoints" in self: # Update the selectedpoints property, which will notify all views # of the selection change. This is a special case because no # restyle event is emitted by plotly.js on selection events # even though these events update the selectedpoints property. self.selectedpoints = None for callback in self._deselect_callbacks: callback(self, points) class BaseFrameHierarchyType(BasePlotlyType): """ Base class for all types in the trace hierarchy """ def __init__(self, plotly_name, **kwargs): super(BaseFrameHierarchyType, self).__init__(plotly_name, **kwargs) def _send_prop_set(self, prop_path_str, val): # Note: Frames are not supported by FigureWidget, and updates are not # propagated to parents pass def _restyle_child(self, child, key_path_str, val): # Note: Frames are not supported by FigureWidget, and updates are not # propagated to parents pass def on_change(self, callback, *args): raise NotImplementedError("Change callbacks are not supported on Frames") def _get_child_props(self, child): """ Return the properties dict for a child trace or child layout Note: this method must match the name/signature of one on BasePlotlyType Parameters ---------- child : BaseTraceType | BaseLayoutType Returns ------- dict """ # Try to find index of child as a trace # ------------------------------------- try: trace_index = BaseFigure._index_is(self.data, child) except ValueError: trace_index = None # Child is a trace # ---------------- if trace_index is not None: if "data" in self._props: return self._props["data"][trace_index] else: return None # Child is the layout # ------------------- elif child is self.layout: return self._props.get("layout", None) # Unknown child # ------------- else: raise ValueError("Unrecognized child: %s" % child)