Source code for plot_serializer.matplotlib.axesproxy

import itertools
import logging
from typing import (
    Any,
    Callable,
    List,
    Optional,
    Tuple,
    TypeVar,
)

import matplotlib.cbook as cbook
import matplotlib.cm as cm
import matplotlib.colors as mcolors
import numpy as np
from matplotlib.axes import Axes as MplAxes
from matplotlib.collections import PathCollection
from matplotlib.container import BarContainer, ErrorbarContainer
from matplotlib.lines import Line2D
from mpl_toolkits.mplot3d.art3d import Path3DCollection, Poly3DCollection
from mpl_toolkits.mplot3d.axes3d import Axes3D as MplAxes3D

from plot_serializer.model import (
    Axis,
    Bar2D,
    BarTrace2D,
    Box,
    BoxTrace2D,
    ErrorBar2DTrace,
    ErrorPoint2D,
    Figure,
    HistDataset,
    HistogramTrace,
    LineTrace2D,
    LineTrace3D,
    PiePlot,
    Plot,
    Plot2D,
    Plot3D,
    Point2D,
    Point3D,
    ScatterTrace2D,
    ScatterTrace3D,
    Slice,
    SurfaceTrace3D,
)
from plot_serializer.proxy import Proxy
from plot_serializer.serializer import Serializer

PLOTTING_METHODS = [
    "plot",
    "errorbar",
    "hist",
    "scatter",
    "step",
    "loglog",
    "semilogx",
    "semilogy",
    "bar",
    "barh",
    "stem",
    "eventplot",
    "pie",
    "stackplot",
    "broken_barh",
    "fill",
    "acorr",
    "angle_spectrum",
    "cohere",
    "csd",
    "magnitude_spectrum",
    "phase_spectrum",
    "psd",
    "specgram",
    "xcorr",
    "ecdf",
    "boxplot",
    "violinplot",
    "bxp",
    "violin",
    "hexbin",
    "hist",
    "hist2d",
    "contour",
    "contourf",
    "imshow",
    "matshow",
    "pcolor",
    "pcolorfast",
    "pcolormesh",
    "spy",
    "tripcolor",
    "triplot",
    "tricontourtricontourf",
]


F = TypeVar("F", bound=Callable[..., Any])


[docs] def inherit_and_extend_doc(base_class: Any, method_name: str, additional_doc: str) -> Callable[[F], F]: def decorator(func: F) -> F: base_doc = getattr(base_class, method_name).__doc__ if base_doc is None: base_doc = "" func.__doc__ = base_doc + additional_doc return func return decorator
def _convert_matplotlib_color( color_list: Any, length: int, cmap: Any, norm: Any ) -> Tuple[List[str] | List[None], bool]: cmap_used = False if not color_list: return ([None], cmap_used) colors: List[str] = [] color_type = type(color_list) if isinstance(color_list, np.generic): color_list = color_list.item() elif isinstance(color_list, np.ndarray): color_list = color_list.tolist() if color_type is str: colors.append(mcolors.to_hex(color_list, keep_alpha=True)) elif color_type is int or color_type is float: scalar_mappable = cm.ScalarMappable(norm=norm, cmap=cmap) rgba_tuple = scalar_mappable.to_rgba(color_list) hex_value = mcolors.to_hex(rgba_tuple, keep_alpha=True) # type: ignore colors.append(hex_value) cmap_used = True elif color_type is tuple and (len(color_list) == 3 or len(color_list) == 4): hex_value = mcolors.to_hex(color_list, keep_alpha=True) colors.append(hex_value) elif (color_type is list or isinstance(color_list, np.ndarray)) and all( isinstance(item, (int, float)) for item in color_list ): scalar_mappable = cm.ScalarMappable(norm=norm, cmap=cmap) rgba_tuples = scalar_mappable.to_rgba(color_list) hex_values = [mcolors.to_hex(rgba_value, keep_alpha=True) for rgba_value in rgba_tuples] colors.extend(hex_values) cmap_used = True elif color_type is list or isinstance(color_list, np.ndarray): for item in color_list: if (isinstance(item, str)) or (isinstance(item, tuple) and (len(item) == 3 or len(item) == 4)): colors.append(mcolors.to_hex(item, keep_alpha=True)) elif item is None: colors.append(None) # type: ignore else: raise NotImplementedError("Your color is not supported by PlotSerializer, see Documentation for more detail") if not (len(colors) == length): if not (len(colors) - 1): colors = [colors[0] for i in range(length)] else: raise ValueError("the lenth of your color array does not match the length of given data") return (colors, cmap_used)
[docs] class AxesProxy(Proxy[MplAxes]): def __init__(self, delegate: MplAxes, figure: Figure, serializer: Serializer) -> None: super().__init__(delegate) self._figure = figure self._serializer = serializer self._plot: Optional[Plot] = None
[docs] @inherit_and_extend_doc(MplAxes, "plot", "\n\n Serialized parameters: x, y, color, marker, label. \n\n") def pie(self, x: Any, **kwargs: Any) -> Any: """ Serialized parameters: x, labels, explode, radius, colors, title. ---------------- Original matplotlib documentation Plot a pie chart. Make a pie chart of array *x*. The fractional area of each wedge is given by ``x/sum(x)``. The wedges are plotted counterclockwise, by default starting from the x-axis. Parameters ---------- x : 1D array-like The wedge sizes. explode : array-like, default: None If not *None*, is a ``len(x)`` array which specifies the fraction of the radius with which to offset each wedge. labels : list, default: None A sequence of strings providing the labels for each wedge colors : :class:`color` or list of :class:`color`, default: None A sequence of colors through which the pie chart will cycle. If *None*, will use the colors in the currently active cycle. hatch : str or list, default: None Hatching pattern applied to all pie wedges or sequence of patterns through which the chart will cycle. For a list of valid patterns. .. versionadded:: 3.7 autopct : None or str or callable, default: None If not *None*, *autopct* is a string or function used to label the wedges with their numeric value. The label will be placed inside the wedge. If *autopct* is a format string, the label will be ``fmt % pct``. If *autopct* is a function, then it will be called. pctdistance : float, default: 0.6 The relative distance along the radius at which the text generated by *autopct* is drawn. To draw the text outside the pie, set *pctdistance* > 1. This parameter is ignored if *autopct* is ``None``. labeldistance : float or None, default: 1.1 The relative distance along the radius at which the labels are drawn. To draw the labels inside the pie, set *labeldistance* < 1. If set to ``None``, labels are not drawn but are still stored for use in `.legend`. shadow : bool or dict, default: False If bool, whether to draw a shadow beneath the pie. If dict, draw a shadow passing the properties in the dict to `.Shadow`. .. versionadded:: 3.8 *shadow* can be a dict. startangle : float, default: 0 degrees The angle by which the start of the pie is rotated, counterclockwise from the x-axis. radius : float, default: 1 The radius of the pie. counterclock : bool, default: True Specify fractions direction, clockwise or counterclockwise. wedgeprops : dict, default: None Dict of arguments passed to each `.patches.Wedge` of the pie. For example, ``wedgeprops = {'linewidth': 3}`` sets the width of the wedge border lines equal to 3. By default, ``clip_on=False``. When there is a conflict between these properties and other keywords, properties passed to *wedgeprops* take precedence. textprops : dict, default: None Dict of arguments to pass to the text objects. center : (float, float), default: (0, 0) The coordinates of the center of the chart. frame : bool, default: False Plot Axes frame with the chart if true. rotatelabels : bool, default: False Rotate each label to the angle of the corresponding slice if true. normalize : bool, default: True When *True*, always make a full pie by normalizing x so that ``sum(x) == 1``. *False* makes a partial pie if ``sum(x) <= 1`` and raises a `ValueError` for ``sum(x) > 1``. data : indexable object, optional DATA_PARAMETER_PLACEHOLDER Returns ------- patches : list A sequence of `matplotlib.patches.Wedge` instances texts : list A list of the label `.Text` instances. autotexts : list A list of `.Text` instances for the numeric labels. This will only be returned if the parameter *autopct* is not *None*. Notes ----- The pie chart will probably look best if the figure and Axes are square, or the Axes aspect is equal. This method sets the aspect ratio of the axis to "equal". The Axes aspect ratio can be controlled with `.Axes.set_aspect`. """ try: result = self.delegate.pie(x, **kwargs) except Exception as e: add_msg = " - This error was thrown by Matplotlib and is independent of PlotSerializer!" e.args = (e.args[0] + add_msg,) + e.args[1:] if e.args else (add_msg,) raise try: if self._plot is not None: raise NotImplementedError("PlotSerializer does not yet support adding multiple plots per axes!") explode_list = kwargs.get("explode") label_list = kwargs.get("labels") radius = kwargs.get("radius") or 1 color_list = kwargs.get("colors") c = kwargs.get("c") x = np.asarray(x) if not explode_list: explode_list = itertools.repeat(None) if not label_list: label_list = itertools.repeat(None) if c is not None and color_list is None: color_list = c color_list = _convert_matplotlib_color(color_list, len(x), cmap="viridis", norm="linear")[0] slices: List[Slice] = [] for index, (xi, label, explode) in enumerate(zip(x, label_list, explode_list)): color = color_list[index] if len(color_list) > index else None slices.append( Slice( x=xi, explode=explode, label=label, color=color, ) ) pie_plot = PiePlot(type="pie", radius=radius, slices=slices) self._plot = pie_plot except Exception as e: logging.warning( "An unexpected error occurred in PlotSerializer when trying to read plot data! " + "Parts of the plot will not be serialized!", exc_info=e, ) return result
[docs] @inherit_and_extend_doc(MplAxes, "bar", "\n\n Serialized parameters: x, height, color. \n\n") def bar( self, x: Any, height: Any, **kwargs: Any, ) -> BarContainer: r""" Serialized parameters: x, height, color. ---------------- Original matplotlib documentation: Make a bar plot. The bars are positioned at *x* with the given *align*\ment. Their dimensions are given by *height* and *width*. The vertical baseline is *bottom* (default 0). Many parameters can take either a single value applying to all bars or a sequence of values, one for each bar. Parameters ---------- x : float or array-like The x coordinates of the bars. See also *align* for the alignment of the bars to the coordinates. height : float or array-like The height(s) of the bars. Note that if *bottom* has units (e.g. datetime), *height* should be in units that are a difference from the value of *bottom* (e.g. timedelta). width : float or array-like, default: 0.8 The width(s) of the bars. Note that if *x* has units (e.g. datetime), then *width* should be in units that are a difference (e.g. timedelta) around the *x* values. bottom : float or array-like, default: 0 The y coordinate(s) of the bottom side(s) of the bars. Note that if *bottom* has units, then the y-axis will get a Locator and Formatter appropriate for the units (e.g. dates, or categorical). align : {'center', 'edge'}, default: 'center' Alignment of the bars to the *x* coordinates: - 'center': Center the base on the *x* positions. - 'edge': Align the left edges of the bars with the *x* positions. To align the bars on the right edge pass a negative *width* and ``align='edge'``. Returns ------- `.BarContainer` Container with all the bars and optionally errorbars. Other Parameters ---------------- color : :class:`color` or list of :class:`color`, optional The colors of the bar faces. edgecolor : :class:`color` or list of :class:`color`, optional The colors of the bar edges. linewidth : float or array-like, optional Width of the bar edge(s). If 0, don't draw edges. tick_label : str or list of str, optional The tick labels of the bars. Default: None (Use default numeric labels.) label : str or list of str, optional A single label is attached to the resulting `.BarContainer` as a label for the whole dataset. If a list is provided, it must be the same length as *x* and labels the individual bars. Repeated labels are not de-duplicated and will cause repeated label entries, so this is best used when bars also differ in style (e.g., by passing a list to *color*.) xerr, yerr : float or array-like of shape(N,) or shape(2, N), optional If not *None*, add horizontal / vertical errorbars to the bar tips. The values are +/- sizes relative to the data: - scalar: symmetric +/- values for all bars - shape(N,): symmetric +/- values for each bar - shape(2, N): Separate - and + values for each bar. First row contains the lower errors, the second row contains the upper errors. - *None*: No errorbar. (Default) ecolor : :class:`color` or list of :class:`color`, default: 'black' The line color of the errorbars. capsize : float, default: :rc:`errorbar.capsize` The length of the error bar caps in points. error_kw : dict, optional Dictionary of keyword arguments to be passed to the `~.Axes.errorbar` method. Values of *ecolor* or *capsize* defined here take precedence over the independent keyword arguments. log : bool, default: False If *True*, set the y-axis to be log scale. data : indexable object, optional DATA_PARAMETER_PLACEHOLDER **kwargs : `.Rectangle` properties %(Rectangle:kwdoc)s See Also -------- barh : Plot a horizontal bar plot. Notes ----- Stacked bars can be achieved by passing individual *bottom* values per bar. """ try: result = self.delegate.bar(x, height, **kwargs) except Exception as e: add_msg = " - This error was thrown by Matplotlib and is independent of PlotSerializer!" e.args = (e.args[0] + add_msg,) + e.args[1:] if e.args else (add_msg,) raise try: color_list = kwargs.get("color") c = kwargs.get("c") if c is not None and color_list is None: color_list = c if isinstance(x, np.generic): x = x.item() if isinstance(x, (float, int, str)): x = [x] else: x = np.asarray(x) if isinstance(height, np.generic): height = height.item() if isinstance(height, (float, int, str)): height = [height] else: height = np.asarray(height) color_list = _convert_matplotlib_color(color_list, len(x), cmap="viridis", norm="linear")[0] bars: List[Bar2D] = [] for index, (xi, h) in enumerate(zip(x, height)): color = color_list[index] if len(color_list) > index else None bars.append(Bar2D(x_i=xi, height=h, color=color)) trace = BarTrace2D(type="bar", datapoints=bars) if self._plot is not None: if not isinstance(self._plot, Plot2D): raise NotImplementedError("PlotSerializer does not yet support mixing 2d plots with other plots!") self._plot.traces.append(trace) else: self._plot = Plot2D(type="2d", x_axis=Axis(), y_axis=Axis(), traces=[trace]) except Exception as e: logging.warning( "An unexpected error occurred in PlotSerializer when trying to read plot data! " + "Parts of the plot will not be serialized!", exc_info=e, ) return result
[docs] def plot(self, *args: Any, **kwargs: Any) -> list[Line2D]: """ Serialized parameters: x, y, linewidth, linestyle, marker, color, label. ---------------- Original matplotlib documentation: Plot y versus x as lines and/or markers. Call signatures:: plot([x], y, [fmt], *, data=None, **kwargs) plot([x], y, [fmt], [x2], y2, [fmt2], ..., **kwargs) The coordinates of the points or line nodes are given by *x*, *y*. The optional parameter *fmt* is a convenient way for defining basic formatting like color, marker and linestyle. It's a shortcut string notation described in the *Notes* section below. >>> plot(x, y) # plot x and y using default line style and color >>> plot(x, y, "bo") # plot x and y using blue circle markers >>> plot(y) # plot y using x as index array 0..N-1 >>> plot(y, "r+") # ditto, but with red plusses You can use `.Line2D` properties as keyword arguments for more control on the appearance. Line properties and *fmt* can be mixed. The following two calls yield identical results: >>> plot(x, y, "go--", linewidth=2, markersize=12) >>> plot(x, y, color="green", marker="o", linestyle="dashed", linewidth=2, markersize=12) When conflicting with *fmt*, keyword arguments take precedence. **Plotting labelled data** There's a convenient way for plotting objects with labelled data (i.e. data that can be accessed by index ``obj['y']``). Instead of giving the data in *x* and *y*, you can provide the object in the *data* parameter and just give the labels for *x* and *y*:: >>> plot("xlabel", "ylabel", data=obj) All indexable objects are supported. This could e.g. be a `dict`, a `pandas.DataFrame` or a structured numpy array. **Plotting multiple sets of data** There are various ways to plot multiple sets of data. - The most straight forward way is just to call `plot` multiple times. Example: >>> plot(x1, y1, "bo") >>> plot(x2, y2, "go") - If *x* and/or *y* are 2D arrays, a separate data set will be drawn for every column. If both *x* and *y* are 2D, they must have the same shape. If only one of them is 2D with shape (N, m) the other must have length N and will be used for every data set m. Example: >>> x = [1, 2, 3] >>> y = np.array([[1, 2], [3, 4], [5, 6]]) >>> plot(x, y) is equivalent to: >>> for col in range(y.shape[1]): ... plot(x, y[:, col]) - The third way is to specify multiple sets of *[x]*, *y*, *[fmt]* groups:: >>> plot(x1, y1, "g^", x2, y2, "g-") In this case, any additional keyword argument applies to all datasets. Also, this syntax cannot be combined with the *data* parameter. By default, each line is assigned a different style specified by a 'style cycle'. The *fmt* and line property parameters are only necessary if you want explicit deviations from these defaults. Alternatively, you can also change the style cycle using :rc:`axes.prop_cycle`. Parameters ---------- x, y : array-like or scalar The horizontal / vertical coordinates of the data points. *x* values are optional and default to ``range(len(y))``. Commonly, these parameters are 1D arrays. They can also be scalars, or two-dimensional (in that case, the columns represent separate data sets). These arguments cannot be passed as keywords. fmt : str, optional A format string, e.g. 'ro' for red circles. See the *Notes* section for a full description of the format strings. Format strings are just an abbreviation for quickly setting basic line properties. All of these and more can also be controlled by keyword arguments. This argument cannot be passed as keyword. data : indexable object, optional An object with labelled data. If given, provide the label names to plot in *x* and *y*. .. note:: Technically there's a slight ambiguity in calls where the second label is a valid *fmt*. ``plot('n', 'o', data=obj)`` could be ``plt(x, y)`` or ``plt(y, fmt)``. In such cases, the former interpretation is chosen, but a warning is issued. You may suppress the warning by adding an empty format string ``plot('n', 'o', '', data=obj)``. Returns ------- list of `.Line2D` A list of lines representing the plotted data. Other Parameters ---------------- scalex, scaley : bool, default: True These parameters determine if the view limits are adapted to the data limits. The values are passed on to `~.axes.Axes.autoscale_view`. **kwargs : `~matplotlib.lines.Line2D` properties, optional *kwargs* are used to specify properties like a line label (for auto legends), linewidth, antialiasing, marker face color. Example:: >>> plot([1, 2, 3], [1, 2, 3], "go-", label="line 1", linewidth=2) >>> plot([1, 2, 3], [1, 4, 9], "rs", label="line 2") If you specify multiple lines with one plot call, the kwargs apply to all those lines. In case the label object is iterable, each element is used as labels for each set of data. Here is a list of available `.Line2D` properties: %(Line2D:kwdoc)s See Also -------- scatter : XY scatter plot with markers of varying size and/or color ( sometimes also called bubble chart). Notes ----- **Format Strings** A format string consists of a part for color, marker and line:: fmt = "[marker][line][color]" Each of them is optional. If not provided, the value from the style cycle is used. Exception: If ``line`` is given, but no ``marker``, the data will be a line without markers. Other combinations such as ``[color][marker][line]`` are also supported, but note that their parsing may be ambiguous. **Markers** ============= =============================== character description ============= =============================== ``'.'`` point marker ``','`` pixel marker ``'o'`` circle marker ``'v'`` triangle_down marker ``'^'`` triangle_up marker ``'<'`` triangle_left marker ``'>'`` triangle_right marker ``'1'`` tri_down marker ``'2'`` tri_up marker ``'3'`` tri_left marker ``'4'`` tri_right marker ``'8'`` octagon marker ``'s'`` square marker ``'p'`` pentagon marker ``'P'`` plus (filled) marker ``'*'`` star marker ``'h'`` hexagon1 marker ``'H'`` hexagon2 marker ``'+'`` plus marker ``'x'`` x marker ``'X'`` x (filled) marker ``'D'`` diamond marker ``'d'`` thin_diamond marker ``'|'`` vline marker ``'_'`` hline marker ============= =============================== **Line Styles** ============= =============================== character description ============= =============================== ``'-'`` solid line style ``'--'`` dashed line style ``'-.'`` dash-dot line style ``':'`` dotted line style ============= =============================== Example format strings:: "b" # blue markers with default shape "or" # red circles "-g" # green solid line "--" # dashed line with default color "^k:" # black triangle_up markers connected by a dotted line **Colors** The supported color abbreviations are the single letter codes ============= =============================== character color ============= =============================== ``'b'`` blue ``'g'`` green ``'r'`` red ``'c'`` cyan ``'m'`` magenta ``'y'`` yellow ``'k'`` black ``'w'`` white ============= =============================== and the ``'CN'`` colors that index into the default property cycle. If the color is the only part of the format string, you can additionally use any `matplotlib.colors` spec, e.g. full names (``'green'``) or hex strings (``'#008000'``). """ try: mpl_lines = self.delegate.plot(*args, **kwargs) except Exception as e: add_msg = " - This error was thrown by Matplotlib and is independent of PlotSerializer!" e.args = (e.args[0] + add_msg,) + e.args[1:] if e.args else (add_msg,) raise try: traces: List[ScatterTrace2D | LineTrace2D | BarTrace2D | BoxTrace2D | HistogramTrace | ErrorBar2DTrace] = [] for mpl_line in mpl_lines: xdata = mpl_line.get_xdata() ydata = mpl_line.get_ydata() thickness = mpl_line.get_linewidth() linestyle = mpl_line.get_linestyle() marker = mpl_line.get_marker() label = mpl_line.get_label() color_list = kwargs.get("color") c = kwargs.get("c") if isinstance(xdata, np.generic): xdata = xdata.item() if isinstance(xdata, (float, int, str)): xdata = [xdata] else: xdata = np.asarray(xdata) if isinstance(ydata, np.generic): ydata = ydata.item() if isinstance(ydata, (float, int, str)): ydata = [ydata] else: ydata = np.asarray(ydata) if c is not None and color_list is None: color_list = c color_list = _convert_matplotlib_color(color_list, len(xdata), cmap="viridis", norm="linear")[0] points: List[Point2D] = [] for x, y in zip(xdata, ydata): points.append(Point2D(x=x, y=y)) traces.append( LineTrace2D( type="line", color=color_list[0], linewidth=thickness, linestyle=linestyle, # type: ignore label=label, # type: ignore datapoints=points, marker=marker, # type: ignore ) ) if self._plot is not None: if not isinstance(self._plot, Plot2D): raise NotImplementedError("PlotSerializer does not yet support mixing 2d plots with other plots!") self._plot.traces += traces else: self._plot = Plot2D(type="2d", x_axis=Axis(), y_axis=Axis(), traces=traces) except Exception as e: logging.warning( "An unexpected error occurred in PlotSerializer when trying to read plot data! " + "Parts of the plot will not be serialized!", exc_info=e, ) return mpl_lines
[docs] def scatter( self, x: Any, y: Any, *args: Any, **kwargs: Any, ) -> PathCollection: """ Serialized parameters: x, y, s, c, marker, cmap, norm. ---------------- Original matplotlib documentation: A scatter plot of *y* vs. *x* with varying marker size and/or color. Parameters ---------- x, y : float or array-like, shape (n, ) The data positions. s : float or array-like, shape (n, ), optional The marker size in points**2 (typographic points are 1/72 in.). Default is ``rcParams['lines.markersize'] ** 2``. The linewidth and edgecolor can visually interact with the marker size, and can lead to artifacts if the marker size is smaller than the linewidth. If the linewidth is greater than 0 and the edgecolor is anything but *'none'*, then the effective size of the marker will be increased by half the linewidth because the stroke will be centered on the edge of the shape. To eliminate the marker edge either set *linewidth=0* or *edgecolor='none'*. c : array-like or list of :class:`color` or :class:`color`, optional The marker colors. Possible values: - A scalar or sequence of n numbers to be mapped to colors using *cmap* and *norm*. - A 2D array in which the rows are RGB or RGBA. - A sequence of colors of length n. - A single color format string. Note that *c* should not be a single numeric RGB or RGBA sequence because that is indistinguishable from an array of values to be colormapped. If you want to specify the same RGB or RGBA value for all points, use a 2D array with a single row. Otherwise, value-matching will have precedence in case of a size matching with *x* and *y*. If you wish to specify a single color for all points prefer the *color* keyword argument. Defaults to `None`. In that case the marker color is determined by the value of *color*, *facecolor* or *facecolors*. In case those are not specified or `None`, the marker color is determined by the next color of the ``Axes``' current "shape and fill" color cycle. This cycle defaults to :rc:`axes.prop_cycle`. marker : `~.markers.MarkerStyle`, default: :rc:`scatter.marker` The marker style. *marker* can be either an instance of the class or the text shorthand for a particular marker. See :mod:`matplotlib.markers` for more information about marker styles. %(cmap_doc)s This parameter is ignored if *c* is RGB(A). %(norm_doc)s This parameter is ignored if *c* is RGB(A). %(vmin_vmax_doc)s This parameter is ignored if *c* is RGB(A). alpha : float, default: None The alpha blending value, between 0 (transparent) and 1 (opaque). linewidths : float or array-like, default: :rc:`lines.linewidth` The linewidth of the marker edges. Note: The default *edgecolors* is 'face'. You may want to change this as well. edgecolors : {'face', 'none', *None*} or :class:`color` or list of \ :class:`color`, default: :rc:`scatter.edgecolors` The edge color of the marker. Possible values: - 'face': The edge color will always be the same as the face color. - 'none': No patch boundary will be drawn. - A color or sequence of colors. For non-filled markers, *edgecolors* is ignored. Instead, the color is determined like with 'face', i.e. from *c*, *colors*, or *facecolors*. plotnonfinite : bool, default: False Whether to plot points with nonfinite *c* (i.e. ``inf``, ``-inf`` or ``nan``). If ``True`` the points are drawn with the *bad* colormap color (see `.Colormap.set_bad`). Returns ------- `~matplotlib.collections.PathCollection` Other Parameters ---------------- data : indexable object, optional DATA_PARAMETER_PLACEHOLDER **kwargs : `~matplotlib.collections.Collection` properties See Also -------- plot : To plot scatter plots when markers are identical in size and color. Notes ----- * The `.plot` function will be faster for scatterplots where markers don't vary in size or color. * Any or all of *x*, *y*, *s*, and *c* may be masked arrays, in which case all masks will be combined and only unmasked points will be plotted. * Fundamentally, scatter works with 1D arrays; *x*, *y*, *s*, and *c* may be input as N-D arrays, but within scatter they will be flattened. The exception is *c*, which will be flattened only if its size matches the size of *x* and *y*. """ try: path = self.delegate.scatter(x, y, *args, **kwargs) except Exception as e: add_msg = " - This error was thrown by Matplotlib and is independent of PlotSerializer!" e.args = (e.args[0] + add_msg,) + e.args[1:] if e.args else (add_msg,) raise try: verteces = path.get_offsets().tolist() # type: ignore marker = kwargs.get("marker") or "o" color_list = kwargs.get("c") color = kwargs.get("color") if color is not None and color_list is None: color_list = color sizes_list = kwargs.get("s") cmap = kwargs.get("cmap") or "viridis" norm = kwargs.get("norm") or "linear" label = str(path.get_label()) if isinstance(x, np.generic): x = x.item() if isinstance(x, (float, int, str)): x = [x] (color_list, cmap_used) = _convert_matplotlib_color(color_list, len(x), cmap, norm) if not cmap_used: cmap = None norm = None if sizes_list is not None: sizes_list = path.get_sizes() else: sizes_list = itertools.repeat(None) if isinstance(sizes_list, np.generic): sizes_list = [sizes_list] * len(x) datapoints: List[Point2D] = [] for index, (vertex, size) in enumerate(zip(verteces, sizes_list)): color = color_list[index] if len(color_list) > index else None datapoints.append( Point2D( x=vertex[0], y=vertex[1], color=color, size=size, ) ) trace: List[ScatterTrace2D | LineTrace2D | BarTrace2D | BoxTrace2D | HistogramTrace | ErrorBar2DTrace] = [] trace.append( ScatterTrace2D(type="scatter", cmap=cmap, norm=norm, label=label, datapoints=datapoints, marker=marker) ) if self._plot is not None: if not isinstance(self._plot, Plot2D): raise NotImplementedError("PlotSerializer does not yet support mixing 2d plots with other plots!") self._plot.traces += trace else: self._plot = Plot2D(type="2d", x_axis=Axis(), y_axis=Axis(), traces=trace) except Exception as e: logging.warning( "An unexpected error occurred in PlotSerializer when trying to read plot data! " + "Parts of the plot will not be serialized!", exc_info=e, ) return path
[docs] def boxplot(self, x: Any, *args: Any, **kwargs: Any) -> dict[Any, Any]: """ Serialized parameters: x, notch, whis, bootstrap, usermedians, conf_intervals, tick_labels. ---------------- Original matplotlib documentation: Draw a box and whisker plot. The box extends from the first quartile (Q1) to the third quartile (Q3) of the data, with a line at the median. The whiskers extend from the box to the farthest data point lying within 1.5x the inter-quartile range (IQR) from the box. Flier points are those past the end of the whiskers. See https://en.wikipedia.org/wiki/Box_plot for reference. .. code-block:: none Q1-1.5IQR Q1 median Q3 Q3+1.5IQR |-----:-----| o |--------| : |--------| o o |-----:-----| flier <-----------> fliers IQR Parameters ---------- x : Array or a sequence of vectors. The input data. If a 2D array, a boxplot is drawn for each column in *x*. If a sequence of 1D arrays, a boxplot is drawn for each array in *x*. notch : bool, default: :rc:`boxplot.notch` Whether to draw a notched boxplot (`True`), or a rectangular boxplot (`False`). The notches represent the confidence interval (CI) around the median. The documentation for *bootstrap* describes how the locations of the notches are computed by default, but their locations may also be overridden by setting the *conf_intervals* parameter. .. note:: In cases where the values of the CI are less than the lower quartile or greater than the upper quartile, the notches will extend beyond the box, giving it a distinctive "flipped" appearance. This is expected behavior and consistent with other statistical visualization packages. sym : str, optional The default symbol for flier points. An empty string ('') hides the fliers. If `None`, then the fliers default to 'b+'. More control is provided by the *flierprops* parameter. vert : bool, default: :rc:`boxplot.vertical` If `True`, draws vertical boxes. If `False`, draw horizontal boxes. whis : float or (float, float), default: 1.5 The position of the whiskers. If a float, the lower whisker is at the lowest datum above ``Q1 - whis*(Q3-Q1)``, and the upper whisker at the highest datum below ``Q3 + whis*(Q3-Q1)``, where Q1 and Q3 are the first and third quartiles. The default value of ``whis = 1.5`` corresponds to Tukey's original definition of boxplots. If a pair of floats, they indicate the percentiles at which to draw the whiskers (e.g., (5, 95)). In particular, setting this to (0, 100) results in whiskers covering the whole range of the data. In the edge case where ``Q1 == Q3``, *whis* is automatically set to (0, 100) (cover the whole range of the data) if *autorange* is True. Beyond the whiskers, data are considered outliers and are plotted as individual points. bootstrap : int, optional Specifies whether to bootstrap the confidence intervals around the median for notched boxplots. If *bootstrap* is None, no bootstrapping is performed, and notches are calculated using a Gaussian-based asymptotic approximation (see McGill, R., Tukey, J.W., and Larsen, W.A., 1978, and Kendall and Stuart, 1967). Otherwise, bootstrap specifies the number of times to bootstrap the median to determine its 95% confidence intervals. Values between 1000 and 10000 are recommended. usermedians : 1D array-like, optional A 1D array-like of length ``len(x)``. Each entry that is not `None` forces the value of the median for the corresponding dataset. For entries that are `None`, the medians are computed by Matplotlib as normal. conf_intervals : array-like, optional A 2D array-like of shape ``(len(x), 2)``. Each entry that is not None forces the location of the corresponding notch (which is only drawn if *notch* is `True`). For entries that are `None`, the notches are computed by the method specified by the other parameters (e.g., *bootstrap*). positions : array-like, optional The positions of the boxes. The ticks and limits are automatically set to match the positions. Defaults to ``range(1, N+1)`` where N is the number of boxes to be drawn. widths : float or array-like The widths of the boxes. The default is 0.5, or ``0.15*(distance between extreme positions)``, if that is smaller. patch_artist : bool, default: :rc:`boxplot.patchartist` If `False` produces boxes with the Line2D artist. Otherwise, boxes are drawn with Patch artists. tick_labels : list of str, optional The tick labels of each boxplot. Ticks are always placed at the box *positions*. If *tick_labels* is given, the ticks are labelled accordingly. Otherwise, they keep their numeric values. .. versionchanged:: 3.9 Renamed from *labels*, which is deprecated since 3.9 and will be removed in 3.11. manage_ticks : bool, default: True If True, the tick locations and labels will be adjusted to match the boxplot positions. autorange : bool, default: False When `True` and the data are distributed such that the 25th and 75th percentiles are equal, *whis* is set to (0, 100) such that the whisker ends are at the minimum and maximum of the data. meanline : bool, default: :rc:`boxplot.meanline` If `True` (and *showmeans* is `True`), will try to render the mean as a line spanning the full width of the box according to *meanprops* (see below). Not recommended if *shownotches* is also True. Otherwise, means will be shown as points. zorder : float, default: ``Line2D.zorder = 2`` The zorder of the boxplot. Returns ------- dict A dictionary mapping each component of the boxplot to a list of the `.Line2D` instances created. That dictionary has the following keys (assuming vertical boxplots): - ``boxes``: the main body of the boxplot showing the quartiles and the median's confidence intervals if enabled. - ``medians``: horizontal lines at the median of each box. - ``whiskers``: the vertical lines extending to the most extreme, non-outlier data points. - ``caps``: the horizontal lines at the ends of the whiskers. - ``fliers``: points representing data that extend beyond the whiskers (fliers). - ``means``: points or lines representing the means. Other Parameters ---------------- showcaps : bool, default: :rc:`boxplot.showcaps` Show the caps on the ends of whiskers. showbox : bool, default: :rc:`boxplot.showbox` Show the central box. showfliers : bool, default: :rc:`boxplot.showfliers` Show the outliers beyond the caps. showmeans : bool, default: :rc:`boxplot.showmeans` Show the arithmetic means. capprops : dict, default: None The style of the caps. capwidths : float or array, default: None The widths of the caps. boxprops : dict, default: None The style of the box. whiskerprops : dict, default: None The style of the whiskers. flierprops : dict, default: None The style of the fliers. medianprops : dict, default: None The style of the median. meanprops : dict, default: None The style of the mean. label : str or list of str, optional Legend labels. Use a single string when all boxes have the same style and you only want a single legend entry for them. Use a list of strings to label all boxes individually. To be distinguishable, the boxes should be styled individually, which is currently only possible by modifying the returned artists. In the case of a single string, the legend entry will technically be associated with the first box only. By default, the legend will show the median line (``result["medians"]``); if *patch_artist* is True, the legend will show the box `.Patch` artists (``result["boxes"]``) instead. .. versionadded:: 3.9 data : indexable object, optional DATA_PARAMETER_PLACEHOLDER See Also -------- .Axes.bxp : Draw a boxplot from pre-computed statistics. violinplot : Draw an estimate of the probability density function. """ try: dic = self.delegate.boxplot(x, *args, **kwargs) except Exception as e: add_msg = " - This error was thrown by Matplotlib and is independent of PlotSerializer!" e.args = (e.args[0] + add_msg,) + e.args[1:] if e.args else (add_msg,) raise try: notch = kwargs.get("notch") whis = kwargs.get("whis") bootstrap = kwargs.get("bootstrap") usermedians = kwargs.get("usermedians") conf_intervals = kwargs.get("conf_intervals") labels = kwargs.get("tick_labels") x = cbook._reshape_2D(x, "x") # type: ignore if not labels: labels = itertools.repeat(None) if not usermedians: usermedians = itertools.repeat(None) if not conf_intervals: conf_intervals = itertools.repeat(None) trace: List[ScatterTrace2D | LineTrace2D | BarTrace2D | BoxTrace2D | HistogramTrace | ErrorBar2DTrace] = [] boxes: List[Box] = [] for dataset, label, umedian, cintervals in zip(x, labels, usermedians, conf_intervals): x = np.ma.asarray(x, dtype="object") # type: ignore x = x.data[~x.mask].ravel() boxes.append( Box( x_i=dataset, tick_label=label, usermedian=umedian, conf_interval=cintervals, ) ) trace.append(BoxTrace2D(type="box", x=boxes, notch=notch, whis=whis, bootstrap=bootstrap)) if self._plot is not None: if not isinstance(self._plot, Plot2D): raise NotImplementedError("PlotSerializer does not yet support mixing 2d plots with other plots!") self._plot.traces += trace else: self._plot = Plot2D(type="2d", x_axis=Axis(), y_axis=Axis(), traces=trace) except Exception as e: logging.warning( "An unexpected error occurred in PlotSerializer when trying to read plot data! " + "Parts of the plot will not be serialized!", exc_info=e, ) return dic
[docs] def errorbar(self, x: Any, y: Any, *args: Any, **kwargs: Any) -> ErrorbarContainer: """ Serialized parameters: x, y, xerr, yerr, color, ecolor, marker, label. ---------------- Original matplotlib documentation: Plot y versus x as lines and/or markers with attached errorbars. *x*, *y* define the data locations, *xerr*, *yerr* define the errorbar sizes. By default, this draws the data markers/lines as well as the errorbars. Use fmt='none' to draw errorbars without any data markers. .. versionadded:: 3.7 Caps and error lines are drawn in polar coordinates on polar plots. Parameters ---------- x, y : float or array-like The data positions. xerr, yerr : float or array-like, shape(N,) or shape(2, N), optional The errorbar sizes: - scalar: Symmetric +/- values for all data points. - shape(N,): Symmetric +/-values for each data point. - shape(2, N): Separate - and + values for each bar. First row contains the lower errors, the second row contains the upper errors. - *None*: No errorbar. All values must be >= 0. fmt : str, default: '' The format for the data points / data lines. See `.plot` for details. Use 'none' (case-insensitive) to plot errorbars without any data markers. ecolor : :class:`color`, default: None The color of the errorbar lines. If None, use the color of the line connecting the markers. elinewidth : float, default: None The linewidth of the errorbar lines. If None, the linewidth of the current style is used. capsize : float, default: :rc:`errorbar.capsize` The length of the error bar caps in points. capthick : float, default: None An alias to the keyword argument *markeredgewidth* (a.k.a. *mew*). This setting is a more sensible name for the property that controls the thickness of the error bar cap in points. For backwards compatibility, if *mew* or *markeredgewidth* are given, then they will over-ride *capthick*. This may change in future releases. barsabove : bool, default: False If True, will plot the errorbars above the plot symbols. Default is below. lolims, uplims, xlolims, xuplims : bool or array-like, default: False These arguments can be used to indicate that a value gives only upper/lower limits. In that case a caret symbol is used to indicate this. *lims*-arguments may be scalars, or array-likes of the same length as *xerr* and *yerr*. To use limits with inverted axes, `~.Axes.set_xlim` or `~.Axes.set_ylim` must be called before :meth:`errorbar`. Note the tricky parameter names: setting e.g. *lolims* to True means that the y-value is a *lower* limit of the True value, so, only an *upward*-pointing arrow will be drawn! errorevery : int or (int, int), default: 1 draws error bars on a subset of the data. *errorevery* =N draws error bars on the points (x[::N], y[::N]). *errorevery* =(start, N) draws error bars on the points (x[start::N], y[start::N]). e.g. errorevery=(6, 3) adds error bars to the data at (x[6], x[9], x[12], x[15], ...). Used to avoid overlapping error bars when two series share x-axis values. Returns ------- `.ErrorbarContainer` The container contains: - data_line : A `~matplotlib.lines.Line2D` instance of x, y plot markers and/or line. - caplines : A tuple of `~matplotlib.lines.Line2D` instances of the error bar caps. - barlinecols : A tuple of `.LineCollection` with the horizontal and vertical error ranges. Other Parameters ---------------- data : indexable object, optional DATA_PARAMETER_PLACEHOLDER **kwargs All other keyword arguments are passed on to the `~.Axes.plot` call drawing the markers. For example, this code makes big red squares with thick green edges:: x, y, yerr = rand(3, 10) errorbar(x, y, yerr, marker="s", mfc="red", mec="green", ms=20, mew=4) where *mfc*, *mec*, *ms* and *mew* are aliases for the longer property names, *markerfacecolor*, *markeredgecolor*, *markersize* and *markeredgewidth*. Valid kwargs for the marker properties are: - *dashes* - *dash_capstyle* - *dash_joinstyle* - *drawstyle* - *fillstyle* - *linestyle* - *marker* - *markeredgecolor* - *markeredgewidth* - *markerfacecolor* - *markerfacecoloralt* - *markersize* - *markevery* - *solid_capstyle* - *solid_joinstyle* Refer to the corresponding `.Line2D` property for more details: %(Line2D:kwdoc)s """ def _upcast_err(err: Any) -> Any: """ Imported local function from Matplotlib errorbar function. """ if np.iterable(err) and len(err) > 0 and isinstance(cbook._safe_first_finite(err), np.ndarray): # type: ignore atype = type(cbook._safe_first_finite(err)) # type: ignore if atype is np.ndarray: return np.asarray(err, dtype=object) return atype(err) return np.asarray(err) try: container = self.delegate.errorbar(x, y, *args, **kwargs) except Exception as e: add_msg = " - This error was thrown by Matplotlib and is independent of PlotSerializer!" e.args = (e.args[0] + add_msg,) + e.args[1:] if e.args else (add_msg,) raise try: xerr = kwargs.get("xerr") yerr = kwargs.get("yerr") marker = kwargs.get("marker") color = kwargs.get("color") c = kwargs.get("c") if c is not None and color is None: color = c ecolor = kwargs.get("ecolor") label = kwargs.get("label") if not isinstance(x, np.ndarray): x = np.asarray(x, dtype=object) if not isinstance(y, np.ndarray): y = np.asarray(y, dtype=object) x, y = np.atleast_1d(x, y) if xerr is not None and not isinstance(xerr, np.ndarray): xerr = _upcast_err(xerr) np.broadcast_to(xerr, (2, len(x))) if yerr is not None and not isinstance(yerr, np.ndarray): yerr = _upcast_err(yerr) np.broadcast_to(xerr, (2, len(y))) if xerr is None: xerr = itertools.repeat(None) else: if xerr.ndim == 0 or xerr.ndim == 1: xerr = np.broadcast_to(xerr, (2, len(x))) xerr = xerr.T if yerr is None: yerr = itertools.repeat(None) else: if yerr.ndim == 0 or yerr.ndim == 1: yerr = np.broadcast_to(yerr, (2, len(y))) yerr = yerr.T color = mcolors.to_hex(color) if color else None ecolor = mcolors.to_hex(ecolor) if ecolor else None errorpoints: List[ErrorPoint2D] = [] for xi, yi, x_error, y_error in zip(x, y, xerr, yerr): errorpoints.append( ErrorPoint2D( x=xi, y=yi, xerr=x_error, yerr=y_error, ) ) trace = ErrorBar2DTrace( type="errorbar2d", label=label, marker=marker, datapoints=errorpoints, color=color, ecolor=ecolor, ) if self._plot is not None: if not isinstance(self._plot, Plot2D): raise NotImplementedError("PlotSerializer does not yet support mixing 2d plots with other plots!") self._plot.traces.append(trace) else: self._plot = Plot2D(type="2d", x_axis=Axis(), y_axis=Axis(), traces=[trace]) except Exception as e: logging.warning( "An unexpected error occurred in PlotSerializer when trying to read plot data! " + "Parts of the plot will not be serialized!", exc_info=e, ) return container
[docs] def hist(self, x: Any, *args: Any, **kwargs: Any) -> Any: """ Serialized parameters: x, bins, range, cumulative, color, label. ---------------- Original matplotlib documentation: Compute and plot a histogram. This method uses `numpy.histogram` to bin the data in *x* and count the number of values in each bin, then draws the distribution either as a `.BarContainer` or `.Polygon`. The *bins*, *range*, *density*, and *weights* parameters are forwarded to `numpy.histogram`. If the data has already been binned and counted, use `~.bar` or `~.stairs` to plot the distribution:: counts, bins = np.histogram(x) plt.stairs(counts, bins) Alternatively, plot pre-computed bins and counts using ``hist()`` by treating each bin as a single point with a weight equal to its count:: plt.hist(bins[:-1], bins, weights=counts) The data input *x* can be a singular array, a list of datasets of potentially different lengths ([*x0*, *x1*, ...]), or a 2D ndarray in which each column is a dataset. Note that the ndarray form is transposed relative to the list form. If the input is an array, then the return value is a tuple (*n*, *bins*, *patches*); if the input is a sequence of arrays, then the return value is a tuple ([*n0*, *n1*, ...], *bins*, [*patches0*, *patches1*, ...]). Masked arrays are not supported. Parameters ---------- x : (n,) array or sequence of (n,) arrays Input values, this takes either a single array or a sequence of arrays which are not required to be of the same length. bins : int or sequence or str, default: :rc:`hist.bins` If *bins* is an integer, it defines the number of equal-width bins in the range. If *bins* is a sequence, it defines the bin edges, including the left edge of the first bin and the right edge of the last bin; in this case, bins may be unequally spaced. All but the last (righthand-most) bin is half-open. In other words, if *bins* is:: [1, 2, 3, 4] then the first bin is ``[1, 2)`` (including 1, but excluding 2) and the second ``[2, 3)``. The last bin, however, is ``[3, 4]``, which *includes* 4. If *bins* is a string, it is one of the binning strategies supported by `numpy.histogram_bin_edges`: 'auto', 'fd', 'doane', 'scott', 'stone', 'rice', 'sturges', or 'sqrt'. range : tuple or None, default: None The lower and upper range of the bins. Lower and upper outliers are ignored. If not provided, *range* is ``(x.min(), x.max())``. Range has no effect if *bins* is a sequence. If *bins* is a sequence or *range* is specified, autoscaling is based on the specified bin range instead of the range of x. density : bool, default: False If ``True``, draw and return a probability density: each bin will display the bin's raw count divided by the total number of counts *and the bin width* (``density = counts / (sum(counts) * np.diff(bins))``), so that the area under the histogram integrates to 1 (``np.sum(density * np.diff(bins)) == 1``). If *stacked* is also ``True``, the sum of the histograms is normalized to 1. weights : (n,) array-like or None, default: None An array of weights, of the same shape as *x*. Each value in *x* only contributes its associated weight towards the bin count (instead of 1). If *density* is ``True``, the weights are normalized, so that the integral of the density over the range remains 1. cumulative : bool or -1, default: False If ``True``, then a histogram is computed where each bin gives the counts in that bin plus all bins for smaller values. The last bin gives the total number of datapoints. If *density* is also ``True`` then the histogram is normalized such that the last bin equals 1. If *cumulative* is a number less than 0 (e.g., -1), the direction of accumulation is reversed. In this case, if *density* is also ``True``, then the histogram is normalized such that the first bin equals 1. bottom : array-like, scalar, or None, default: None Location of the bottom of each bin, i.e. bins are drawn from ``bottom`` to ``bottom + hist(x, bins)`` If a scalar, the bottom of each bin is shifted by the same amount. If an array, each bin is shifted independently and the length of bottom must match the number of bins. If None, defaults to 0. histtype : {'bar', 'barstacked', 'step', 'stepfilled'}, default: 'bar' The type of histogram to draw. - 'bar' is a traditional bar-type histogram. If multiple data are given the bars are arranged side by side. - 'barstacked' is a bar-type histogram where multiple data are stacked on top of each other. - 'step' generates a lineplot that is by default unfilled. - 'stepfilled' generates a lineplot that is by default filled. align : {'left', 'mid', 'right'}, default: 'mid' The horizontal alignment of the histogram bars. - 'left': bars are centered on the left bin edges. - 'mid': bars are centered between the bin edges. - 'right': bars are centered on the right bin edges. orientation : {'vertical', 'horizontal'}, default: 'vertical' If 'horizontal', `~.Axes.barh` will be used for bar-type histograms and the *bottom* kwarg will be the left edges. rwidth : float or None, default: None The relative width of the bars as a fraction of the bin width. If ``None``, automatically compute the width. Ignored if *histtype* is 'step' or 'stepfilled'. log : bool, default: False If ``True``, the histogram axis will be set to a log scale. color : :class:`color` or list of :class:`color` or None, default: None Color or sequence of colors, one per dataset. Default (``None``) uses the standard line color sequence. label : str or list of str, optional String, or sequence of strings to match multiple datasets. Bar charts yield multiple patches per dataset, but only the first gets the label, so that `~.Axes.legend` will work as expected. stacked : bool, default: False If ``True``, multiple data are stacked on top of each other If ``False`` multiple data are arranged side by side if histtype is 'bar' or on top of each other if histtype is 'step' Returns ------- n : array or list of arrays The values of the histogram bins. See *density* and *weights* for a description of the possible semantics. If input *x* is an array, then this is an array of length *nbins*. If input is a sequence of arrays ``[data1, data2, ...]``, then this is a list of arrays with the values of the histograms for each of the arrays in the same order. The dtype of the array *n* (or of its element arrays) will always be float even if no weighting or normalization is used. bins : array The edges of the bins. Length nbins + 1 (nbins left edges and right edge of last bin). Always a single array even when multiple data sets are passed in. patches : `.BarContainer` or list of a single `.Polygon` or list of \ such objects Container of individual artists used to create the histogram or list of such containers if there are multiple input datasets. Other Parameters ---------------- data : indexable object, optional DATA_PARAMETER_PLACEHOLDER **kwargs `~matplotlib.patches.Patch` properties See Also -------- hist2d : 2D histogram with rectangular bins hexbin : 2D histogram with hexagonal bins stairs : Plot a pre-computed histogram bar : Plot a pre-computed histogram Notes ----- For large numbers of bins (>1000), plotting can be significantly accelerated by using `~.Axes.stairs` to plot a pre-computed histogram (``plt.stairs(*np.histogram(data))``), or by setting *histtype* to 'step' or 'stepfilled' rather than 'bar' or 'barstacked'. """ try: ret = self.delegate.hist(x, *args, **kwargs) except Exception as e: add_msg = " - This error was thrown by Matplotlib and is independent of PlotSerializer!" e.args = (e.args[0] + add_msg,) + e.args[1:] if e.args else (add_msg,) raise try: bins = kwargs.get("bins") or 10 density = kwargs.get("density") or False cumulative = kwargs.get("cumulative") or False label_list = kwargs.get("label") color_list = kwargs.get("color") c = kwargs.get("c") if c is not None and color_list is None: color_list = c if not label_list: label_list = itertools.repeat(None) else: label_list = np.atleast_1d(np.asarray(label_list, str)) if np.isscalar(x): x = [x] x = cbook._reshape_2D(x, "x") # type: ignore color_list = _convert_matplotlib_color(color_list, len(x), "viridis", "linear")[0] datasets: List[HistDataset] = [] for index, (element, label) in enumerate(zip(x, label_list)): color = color_list[index] if len(color_list) > index else None datasets.append(HistDataset(x_i=element, color=color, label=label)) trace = HistogramTrace( type="histogram", x=datasets, bins=bins, density=density, cumulative=cumulative, ) if self._plot is not None: if not isinstance(self._plot, Plot2D): raise NotImplementedError("PlotSerializer does not yet support mixing 2d plots with other plots!") self._plot.traces.append(trace) else: self._plot = Plot2D(type="2d", x_axis=Axis(), y_axis=Axis(), traces=[trace]) except Exception as e: logging.warning( "An unexpected error occurred in PlotSerializer when trying to read plot data! " + "Parts of the plot will not be serialized!", exc_info=e, ) return ret
def _on_collect(self) -> None: if self._plot is None: return self._plot.title = self.delegate.get_title() if isinstance(self._plot, Plot2D): for spine in self.delegate.spines: if not self.delegate.spines[spine].get_visible(): if not self._plot.spines_removed: self._plot.spines_removed = [spine] else: self._plot.spines_removed.append(spine) xlabel = self.delegate.get_xlabel() xscale = self.delegate.get_xscale() self._plot.x_axis.label = xlabel self._plot.x_axis.scale = xscale if not self.delegate.get_autoscalex_on(): self._plot.x_axis.limit = self.delegate.get_xlim() ylabel = self.delegate.get_ylabel() yscale = self.delegate.get_yscale() if not self.delegate.get_autoscaley_on(): self._plot.y_axis.limit = self.delegate.get_ylim() self._plot.y_axis.label = ylabel self._plot.y_axis.scale = yscale self._figure.plots.append(self._plot) def __getattr__(self, __name: str) -> Any: if __name in PLOTTING_METHODS: logging.warning(f"{__name} is not supported by PlotSerializer! Data will be lost!") return super().__getattr__(__name)
[docs] class AxesProxy3D(Proxy[MplAxes3D]): def __init__(self, delegate: MplAxes3D, figure: Figure, serializer: Serializer) -> None: super().__init__(delegate) self._figure = figure self._serializer = serializer self._plot: Optional[Plot] = None
[docs] def scatter( self, xs: Any, ys: Any, zs: Any, *args: Any, **kwargs: Any, ) -> Path3DCollection: """ Serialized parameters: xs, ys, zs, s, c, cmap, norm, marker, label. ---------------- Original matplotlib documentation: Create a scatter plot. Parameters ---------- xs, ys : array-like The data positions. zs : float or array-like, default: 0 The z-positions. Either an array of the same length as *xs* and *ys* or a single value to place all points in the same plane. zdir : {'x', 'y', 'z', '-x', '-y', '-z'}, default: 'z' The axis direction for the *zs*. This is useful when plotting 2D data on a 3D Axes. The data must be passed as *xs*, *ys*. Setting *zdir* to 'y' then plots the data to the x-z-plane. s : float or array-like, default: 20 The marker size in points**2. Either an array of the same length as *xs* and *ys* or a single value to make all markers the same size. c : :class:`color`, sequence, or sequence of colors, optional The marker color. Possible values: - A single color format string. - A sequence of colors of length n. - A sequence of n numbers to be mapped to colors using *cmap* and *norm*. - A 2D array in which the rows are RGB or RGBA. For more details see the *c* argument of `~.axes.Axes.scatter`. depthshade : bool, default: True Whether to shade the scatter markers to give the appearance of depth. Each call to ``scatter()`` will perform its depthshading independently. data : indexable object, optional DATA_PARAMETER_PLACEHOLDER **kwargs All other keyword arguments are passed on to `~.axes.Axes.scatter`. Returns ------- paths : `~matplotlib.collections.PathCollection` """ try: path = self.delegate.scatter(xs, ys, zs, *args, **kwargs) except Exception as e: add_msg = " - This error was thrown by Matplotlib and is independent of PlotSerializer!" e.args = (e.args[0] + add_msg,) + e.args[1:] if e.args else (add_msg,) raise try: sizes_list = kwargs.get("s") marker = kwargs.get("marker") or "o" color_list = kwargs.get("c") color = kwargs.get("color") if color is not None and color_list is None: color_list = color cmap = kwargs.get("cmap") or "viridis" norm = kwargs.get("norm") or "linear" label = str(path.get_label()) if isinstance(xs, np.generic): xs = xs.item() if isinstance(xs, (float, int, str)): xs = [xs] (color_list, cmap_used) = _convert_matplotlib_color(color_list, len(xs), cmap, norm) if not cmap_used: cmap = None norm = None xs, ys, zs = cbook._broadcast_with_masks(xs, ys, zs) xs, ys, zs, sizes_list, color_list, color = cbook.delete_masked_points( # type: ignore xs, ys, zs, sizes_list, color_list, kwargs.get("color", None) ) if sizes_list is None: sizes_list = itertools.repeat(None) if isinstance(sizes_list, (np.generic, float, int)): sizes_list = [sizes_list] * len(xs) trace: List[ScatterTrace3D | LineTrace3D | SurfaceTrace3D] = [] datapoints: List[Point3D] = [] for index, (xi, yi, zi, s) in enumerate(zip(xs, ys, zs, sizes_list)): c = color_list[index] if len(color_list) > index else None datapoints.append(Point3D(x=xi, y=yi, z=zi, color=c, size=s)) trace.append( ScatterTrace3D( type="scatter3D", cmap=cmap, norm=norm, label=label, datapoints=datapoints, marker=marker ) ) if self._plot is not None: if not isinstance(self._plot, Plot3D): raise NotImplementedError("PlotSerializer does not yet support mixing 3d plots with other plots!") self._plot.traces += trace else: self._plot = Plot3D(type="3d", x_axis=Axis(), y_axis=Axis(), z_axis=Axis(), traces=trace) except Exception as e: logging.warning( "An unexpected error occurred in PlotSerializer when trying to read plot data! " + "Parts of the plot will not be serialized!", exc_info=e, ) return path
[docs] def plot( self, x_values: Any, y_values: Any, *args: Any, **kwargs: Any, ) -> Path3DCollection: """ Serialized parameters: x, y, color, linestyle, linewidth, marker, label. ---------------- Original matplotlib documentation: Plot 2D or 3D data. Parameters ---------- xs : 1D array-like x coordinates of vertices. ys : 1D array-like y coordinates of vertices. zs : float or 1D array-like z coordinates of vertices; either one for all points or one for each point. zdir : {'x', 'y', 'z'}, default: 'z' When plotting 2D data, the direction to use as z. **kwargs Other arguments are forwarded to `matplotlib.axes.Axes.plot`. """ try: path = self.delegate.plot(x_values, y_values, *args, **kwargs) except Exception as e: add_msg = " - This error was thrown by Matplotlib and is independent of PlotSerializer!" e.args = (e.args[0] + add_msg,) + e.args[1:] if e.args else (add_msg,) raise try: mpl_line = path[0] xdata, ydata, zdata = mpl_line.get_data_3d() label = mpl_line.get_label() thickness = mpl_line.get_linewidth() linestyle = mpl_line.get_linestyle() marker = kwargs.get("marker") color_list = kwargs.get("color") c = kwargs.get("c") if c is not None and color_list is None: color_list = c color_list = _convert_matplotlib_color(color_list, len(x_values), "viridis", "linear")[0] datapoints: List[Point3D] = [] for i in range(len(xdata)): datapoints.append(Point3D(x=xdata[i], y=ydata[i], z=zdata[i])) trace: List[ScatterTrace3D | LineTrace3D | SurfaceTrace3D] = [] trace.append( LineTrace3D( type="line3D", color=color_list[0], linewidth=thickness, linestyle=linestyle, label=label, datapoints=datapoints, marker=marker, ) ) if self._plot is not None: if not isinstance(self._plot, Plot3D): raise NotImplementedError("PlotSerializer does not yet support mixing 3d plots with other plots!") self._plot.traces += trace else: self._plot = Plot3D(type="3d", x_axis=Axis(), y_axis=Axis(), z_axis=Axis(), traces=trace) except Exception as e: logging.warning( "An unexpected error occurred in PlotSerializer when trying to read plot data! " + "Parts of the plot will not be serialized!", exc_info=e, ) return path
[docs] def plot_surface( self, x: Any, y: Any, z: Any, *args: Any, **kwargs: Any, ) -> Poly3DCollection: """ Serialized parameters: x, y, z, label. ---------------- Original matplotlib documentation: Create a surface plot. By default, it will be colored in shades of a solid color, but it also supports colormapping by supplying the *cmap* argument. .. note:: The *rcount* and *ccount* kwargs, which both default to 50, determine the maximum number of samples used in each direction. If the input data is larger, it will be downsampled (by slicing) to these numbers of points. .. note:: To maximize rendering speed consider setting *rstride* and *cstride* to divisors of the number of rows minus 1 and columns minus 1 respectively. For example, given 51 rows rstride can be any of the divisors of 50. Similarly, a setting of *rstride* and *cstride* equal to 1 (or *rcount* and *ccount* equal the number of rows and columns) can use the optimized path. Parameters ---------- X, Y, Z : 2D arrays Data values. rcount, ccount : int Maximum number of samples used in each direction. If the input data is larger, it will be downsampled (by slicing) to these numbers of points. Defaults to 50. rstride, cstride : int Downsampling stride in each direction. These arguments are mutually exclusive with *rcount* and *ccount*. If only one of *rstride* or *cstride* is set, the other defaults to 10. 'classic' mode uses a default of ``rstride = cstride = 10`` instead of the new default of ``rcount = ccount = 50``. color : :class:`color` Color of the surface patches. cmap : Colormap, optional Colormap of the surface patches. facecolors : list of :class:`color` Colors of each individual patch. norm : `~matplotlib.colors.Normalize`, optional Normalization for the colormap. vmin, vmax : float, optional Bounds for the normalization. shade : bool, default: True Whether to shade the facecolors. Shading is always disabled when *cmap* is specified. lightsource : `~matplotlib.colors.LightSource`, optional The lightsource to use when *shade* is True. **kwargs Other keyword arguments are forwarded to `.Poly3DCollection`. """ try: surface = self.delegate.plot_surface(x, y, z, *args, **kwargs) except Exception as e: add_msg = " - This error was thrown by Matplotlib and is independent of PlotSerializer!" e.args = (e.args[0] + add_msg,) + e.args[1:] if e.args else (add_msg,) raise try: color = kwargs.get("color") c = kwargs.get("c") if c is not None and color is None: color = c label = surface.get_label() length = len(x) width = len(x[0]) z = cbook._to_unmasked_float_array(z) # type: ignore x, y, z = np.broadcast_arrays(x, y, z) traces: List[ScatterTrace3D | LineTrace3D | SurfaceTrace3D] = [] datapoints: List[Point3D] = [] for xi, yi, zi in zip(x, y, z): for xj, yj, zj in zip(xi, yi, zi): datapoints.append( Point3D( x=xj, y=yj, z=zj, color=color, ) ) traces.append( SurfaceTrace3D( type="surface3D", length=length, width=width, label=label, datapoints=datapoints, ) ) if self._plot is not None: if not isinstance(self._plot, Plot3D): raise NotImplementedError("PlotSerializer does not yet support mixing 3d plots with other plots!") self._plot.traces += traces else: self._plot = Plot3D( type="3d", x_axis=Axis(), y_axis=Axis(), z_axis=Axis(), traces=traces, ) except Exception as e: logging.warning( "An unexpected error occurred in PlotSerializer when trying to read plot data! " + "Parts of the plot will not be serialized!", exc_info=e, ) return surface
def _on_collect(self) -> None: if self._plot is None: return self._plot.title = self.delegate.get_title() if isinstance(self._plot, Plot3D): xlabel = self.delegate.get_xlabel() xscale = self.delegate.get_xscale() self._plot.x_axis.label = xlabel self._plot.x_axis.scale = xscale if not self.delegate.get_autoscalex_on(): self._plot.x_axis.limit = self.delegate.get_xlim() ylabel = self.delegate.get_ylabel() yscale = self.delegate.get_yscale() self._plot.y_axis.label = ylabel self._plot.y_axis.scale = yscale if not self.delegate.get_autoscaley_on(): self._plot.y_axis.limit = self.delegate.get_ylim() zlabel = self.delegate.get_zlabel() zscale = self.delegate.get_zscale() self._plot.z_axis.label = zlabel self._plot.z_axis.scale = zscale if not self.delegate.get_autoscalez_on(): self._plot.z_axis.limit = self.delegate.get_zlim() self._figure.plots.append(self._plot) def __getattr__(self, __name: str) -> Any: if __name in PLOTTING_METHODS: logging.warning(f"{__name} is not supported by PlotSerializer, the Data will not be saved!") return super().__getattr__(__name)