Source code for spb.graphics.graphics

import param
from sympy import latex, Symbol
from spb.backends.base_backend import Plot, PlotAttributes
from spb.defaults import TWO_D_B, THREE_D_B
from spb.doc_utils.ipython import modify_graphics_doc
from spb.interactive import create_interactive_plot, IPlotAttributes
from spb.series import (
    LineOver1DRangeSeries, Parametric3DLineSeries,
    SurfaceOver2DRangeSeries, ContourSeries,
    ImplicitSeries, Implicit3DSeries, BaseSeries
)
from spb.utils import _instantiate_backend, _check_misspelled_kwargs


# NOTE: why is `graphics` a subclass of param.ParameterizedFunction?
# because it automatically gets the init signature according to
# `PlotAttributes`, which makes it more reliable and easier to update:
# no worries of forgetting to document some parameter.
[docs] @modify_graphics_doc(priority=["args"]) class graphics(PlotAttributes, IPlotAttributes, param.ParameterizedFunction): """ Plots a collection of data series. Parameters ========== args : Instances of ``BaseSeries`` or lists of instances of ``BaseSeries``. Returns ======= p : Plot or InteractivePlot This function returns: * an instance of ``InteractivePlot`` if any of the data series is interactive (``params`` has been set), or if ``app=True``. * an instance of ``Plot`` otherwise. Examples ======== Combining together multiple data series of the same type, enabling auto-update on pan: .. plot:: :context: close-figs :format: doctest :include-source: True >>> from sympy import * >>> from spb import * >>> x = symbols("x") >>> graphics( ... line(cos(x), label="a"), ... line(sin(x), (x, -pi, pi), label="b"), ... line(log(x), rendering_kw={"linestyle": "--"}), ... title="My title", ylabel="y", update_event=True ... ) Plot object containing: [0]: cartesian line: cos(x) for x over (-10.0, 10.0) [1]: cartesian line: sin(x) for x over (-3.141592653589793, 3.141592653589793) [2]: cartesian line: log(x) for x over (-10.0, 10.0) Combining together multiple data series of the different types: .. plot:: :context: close-figs :format: doctest :include-source: True >>> from sympy import * >>> from spb import * >>> x = symbols("x") >>> graphics( ... line((cos(x)+1)/2, (x, -pi, pi), label="a"), ... line(-(cos(x)+1)/2, (x, -pi, pi), label="b"), ... line_parametric_2d(cos(x), sin(x), (x, 0, 2*pi), label="c", use_cm=False), ... title="My title", ylabel="y", aspect="equal" ... ) Plot object containing: [0]: cartesian line: cos(x)/2 + 1/2 for x over (-3.141592653589793, 3.141592653589793) [1]: cartesian line: -cos(x)/2 - 1/2 for x over (-3.141592653589793, 3.141592653589793) [2]: parametric cartesian line: (cos(x), sin(x)) for x over (0.0, 6.283185307179586) Set tick labels to be some multiple of `pi`: .. plot:: :context: close-figs :format: doctest :include-source: True >>> x, y = symbols("x, y") >>> expr = 5 * (cos(x) - 0.2 * sin(y))**2 + 5 * (-0.2 * cos(x) + sin(y))**2 >>> graphics( ... contour(expr, (x, 0, 2 * pi), (y, 0, 2 * pi), fill=False), ... x_ticks_formatter=multiples_of_pi_over_4(), ... y_ticks_formatter=multiples_of_pi_over_3() ... ) Plot object containing: [0]: contour: 5*(-0.2*sin(y) + cos(x))**2 + 5*(sin(y) - 0.2*cos(x))**2 for x over (0, 2*pi) and y over (0, 2*pi) Use ``hooks`` to further customize the figure before it is shown on the screen, for example applying custom tick labels to a colorbar: .. plot:: :context: close-figs :format: doctest :include-source: True >>> def colorbar_ticks_formatter(plot_object): ... fig, ax = plot_object.fig, plot_object.ax ... cax = fig.axes[1] ... formatter = multiples_of_pi() ... cax.yaxis.set_major_locator(formatter.MB_major_locator()) ... cax.yaxis.set_major_formatter(formatter.MB_func_formatter()) >>> u = symbols("u") >>> graphics( ... line_parametric_2d( ... 2 * cos(u) + 5 * cos(2 * u / 3), ... 2 * sin(u) - 5 * sin(2 * u / 3), ... (u, 0, 6 * pi) ... ), ... hooks=[colorbar_ticks_formatter] ... ) Plot object containing: [0]: parametric cartesian line: (5*cos(2*u/3) + 2*cos(u), -5*sin(2*u/3) + 2*sin(u)) for u over (0, 6*pi) Plot over an existing figure. Note that: * If an existing Matplotlib's figure is available, users can specify one of the following keyword arguments: * ``fig=`` to provide the existing figure. The module will then plot the symbolic expressions over the first Matplotlib's axes. * ``ax=`` to provide the Matplotlib's axes over which symbolic expressions will be plotted. This is useful if users have a figure with multiple subplots. * If an existing Bokeh/Plotly/K3D's figure is available, user should pass the following keyword arguments: ``fig=`` for the existing figure and ``backend=`` to specify which backend should be used. * This module will override axis labels, title, and grid. .. plot:: :context: close-figs :format: doctest :include-source: True >>> from sympy import symbols, cos, pi >>> from spb import * >>> import numpy as np >>> import matplotlib.pyplot as plt >>> # plot some numerical data >>> fig, ax = plt.subplots() >>> xx = np.linspace(-np.pi, np.pi, 20) >>> yy = np.cos(xx) >>> noise = (np.random.random_sample(len(xx)) - 0.5) / 5 >>> yy = yy * (1+noise) >>> ax.scatter(xx, yy, marker="*", color="m") # doctest: +SKIP >>> # plot a symbolic expression >>> x = symbols("x") >>> graphics( ... line(cos(x), (x, -pi, pi), rendering_kw={"ls": "--", "lw": 0.8}), ... ax=ax, update_event=True) Plot object containing: [0]: cartesian line: cos(x) for x over (-3.141592653589793, 3.141592653589793) Interactive-widget plot combining together data series of different types: .. panel-screenshot:: from sympy import * from spb import * import k3d a, b, s, e, t = symbols("a, b, s, e, t") c = 2 * sqrt(a * b) r = a + b params = { a: (1.5, 0, 2), b: (1, 0, 2), s: (0, 0, 2), e: (2, 0, 2) } graphics( surface_revolution( (r * cos(t), r * sin(t)), (t, 0, pi), params=params, n=50, parallel_axis="x", show_curve=False, rendering_kw={"color":0x353535}, force_real_eval=True ), line_parametric_3d( a * cos(t) + b * cos(3 * t), a * sin(t) - b * sin(3 * t), c * sin(2 * t), prange(t, s*pi, e*pi), rendering_kw={"color_map": k3d.matplotlib_color_maps.Summer}, params=params ), backend=KB ) Interactive widget plot, showing widgets related to data series that allows to easily customize the data generation process: .. panel-screenshot:: :small-size: 1000, 550 from sympy import * from spb import * z = symbols("z") graphics( domain_coloring(sin(z), (z, -2-2j, 2+2j), coloring="b"), backend=MB, grid=False, layout="sbl", ncols=1, template={"sidebar_width": "30%"}, app=True ) See Also ======== plotgrid """ def __call__(self, *args, **params): p = param.ParamOverrides(self, {}) series = [] for a in args: if ( isinstance(a, (list, tuple)) and all(isinstance(s, BaseSeries) for s in a) ): series.extend(a) elif isinstance(a, BaseSeries): series.append(a) else: raise TypeError( "Only instances of ``BaseSeries`` or lists of " "instances of ``BaseSeries`` are supported. Received: " f"{type(a)}") is_3D = any(s.is_3D for s in series) params.setdefault("backend", TWO_D_B if is_3D else THREE_D_B) # allow data series to show their UI controls app = params.pop("app", False) # don't show an interactive application if the data series don't expose # this attribute app = app and any( hasattr(s, "_interactive_app_controls") for s in series) # TODO: this can be done without the params of this class, using instead # the params of Plot keys_to_be_aware_of = [ "process_piecewise", "backend", "show", "fig", "ax", # this enables animations "animation", # these enable interactive widgets plotting "pane_kw", "template", "servable", "plot_function" ] # remove keyword arguments that are not parameters of this backend keys_to_maintain = ( list(Plot.param) + list(IPlotAttributes.param) + keys_to_be_aware_of ) if not params.get("plot_function", False): _check_misspelled_kwargs( self, additional_keys=keys_to_be_aware_of, **params) params = {k: v for k, v in params.items() if k in keys_to_maintain} # set the appropriate transformation on 2D line series if polar axis # are requested if params.get("polar_axis", False): for s in series: if s.is_2Dline: s.is_polar = True # set axis labels if all(isinstance(s, LineOver1DRangeSeries) for s in series): fs = set([s.ranges[0][0] for s in series]) if len(fs) == 1: x = fs.pop() fx = lambda use_latex: x.name if not use_latex else latex(x) wrap = lambda use_latex: "f(%s)" if not use_latex else r"f\left(%s\right)" fy = lambda use_latex: wrap(use_latex) % fx(use_latex) params.setdefault("xlabel", fx) params.setdefault("ylabel", fy) elif ( all(isinstance(s, (ContourSeries, SurfaceOver2DRangeSeries)) for s in series) or (all(isinstance(s, (SurfaceOver2DRangeSeries, Parametric3DLineSeries)) for s in series) and any(s for s in series if isinstance(s, Parametric3DLineSeries) and s._is_wireframe_line)) ): free_x = set([ s.ranges[0][0] for s in series if isinstance(s, (ContourSeries, SurfaceOver2DRangeSeries))]) free_y = set([ s.ranges[1][0] for s in series if isinstance(s, (ContourSeries, SurfaceOver2DRangeSeries))]) if all(len(t) == 1 for t in [free_x, free_y]): x = free_x.pop() if free_x else Symbol("x") y = free_y.pop() if free_y else Symbol("y") fx = lambda use_latex: x.name if not use_latex else latex(x) fy = lambda use_latex: y.name if not use_latex else latex(y) wrap = lambda use_latex: "f(%s, %s)" if not use_latex else r"f\left(%s, %s\right)" fz = lambda use_latex: wrap(use_latex) % (fx(use_latex), fy(use_latex)) params.setdefault("xlabel", fx) params.setdefault("ylabel", fy) params.setdefault("zlabel", fz) elif all(isinstance(s, Implicit3DSeries) for s in series): free_x = set([s.ranges[0][0] for s in series]) free_y = set([s.ranges[1][0] for s in series]) free_z = set([s.ranges[2][0] for s in series]) if all(len(t) == 1 for t in [free_x, free_y, free_z]): fx = lambda use_latex: free_x.pop().name if not use_latex else latex(free_x.pop()) fy = lambda use_latex: free_y.pop().name if not use_latex else latex(free_y.pop()) fz = lambda use_latex: free_z.pop().name if not use_latex else latex(free_z.pop()) params.setdefault("xlabel", fx) params.setdefault("ylabel", fy) params.setdefault("zlabel", fz) elif all(isinstance(s, ImplicitSeries) for s in series): free_x = set([s.ranges[0][0] for s in series]) free_y = set([s.ranges[1][0] for s in series]) if all(len(t) == 1 for t in [free_x, free_y]): fx = lambda use_latex: free_x.pop().name if not use_latex else latex(free_x.pop()) fy = lambda use_latex: free_y.pop().name if not use_latex else latex(free_y.pop()) params.setdefault("xlabel", fx) params.setdefault("ylabel", fy) from spb.backends.matplotlib.matplotlib import MB if not issubclass(params.get("backend"), MB): params.pop("ax", None) if any(s.is_interactive for s in series) or app: return create_interactive_plot(*series, app=app, **params) Backend = params.pop("backend", TWO_D_B if is_3D else THREE_D_B) return _instantiate_backend(Backend, *series, **params)