| | from sympy.plotting.series import BaseSeries, GenericDataSeries |
| | from sympy.utilities.exceptions import sympy_deprecation_warning |
| | from sympy.utilities.iterables import is_sequence |
| |
|
| |
|
| | __doctest_requires__ = { |
| | ('Plot.append', 'Plot.extend'): ['matplotlib'], |
| | } |
| |
|
| |
|
| | |
| | |
| | _show = True |
| |
|
| | def unset_show(): |
| | """ |
| | Disable show(). For use in the tests. |
| | """ |
| | global _show |
| | _show = False |
| |
|
| |
|
| | def _deprecation_msg_m_a_r_f(attr): |
| | sympy_deprecation_warning( |
| | f"The `{attr}` property is deprecated. The `{attr}` keyword " |
| | "argument should be passed to a plotting function, which generates " |
| | "the appropriate data series. If needed, index the plot object to " |
| | "retrieve a specific data series.", |
| | deprecated_since_version="1.13", |
| | active_deprecations_target="deprecated-markers-annotations-fill-rectangles", |
| | stacklevel=4) |
| |
|
| |
|
| | def _create_generic_data_series(**kwargs): |
| | keywords = ["annotations", "markers", "fill", "rectangles"] |
| | series = [] |
| | for kw in keywords: |
| | dictionaries = kwargs.pop(kw, []) |
| | if dictionaries is None: |
| | dictionaries = [] |
| | if isinstance(dictionaries, dict): |
| | dictionaries = [dictionaries] |
| | for d in dictionaries: |
| | args = d.pop("args", []) |
| | series.append(GenericDataSeries(kw, *args, **d)) |
| | return series |
| |
|
| |
|
| | class Plot: |
| | """Base class for all backends. A backend represents the plotting library, |
| | which implements the necessary functionalities in order to use SymPy |
| | plotting functions. |
| | |
| | For interactive work the function :func:`plot` is better suited. |
| | |
| | This class permits the plotting of SymPy expressions using numerous |
| | backends (:external:mod:`matplotlib`, textplot, the old pyglet module for SymPy, Google |
| | charts api, etc). |
| | |
| | The figure can contain an arbitrary number of plots of SymPy expressions, |
| | lists of coordinates of points, etc. Plot has a private attribute _series that |
| | contains all data series to be plotted (expressions for lines or surfaces, |
| | lists of points, etc (all subclasses of BaseSeries)). Those data series are |
| | instances of classes not imported by ``from sympy import *``. |
| | |
| | The customization of the figure is on two levels. Global options that |
| | concern the figure as a whole (e.g. title, xlabel, scale, etc) and |
| | per-data series options (e.g. name) and aesthetics (e.g. color, point shape, |
| | line type, etc.). |
| | |
| | The difference between options and aesthetics is that an aesthetic can be |
| | a function of the coordinates (or parameters in a parametric plot). The |
| | supported values for an aesthetic are: |
| | |
| | - None (the backend uses default values) |
| | - a constant |
| | - a function of one variable (the first coordinate or parameter) |
| | - a function of two variables (the first and second coordinate or parameters) |
| | - a function of three variables (only in nonparametric 3D plots) |
| | |
| | Their implementation depends on the backend so they may not work in some |
| | backends. |
| | |
| | If the plot is parametric and the arity of the aesthetic function permits |
| | it the aesthetic is calculated over parameters and not over coordinates. |
| | If the arity does not permit calculation over parameters the calculation is |
| | done over coordinates. |
| | |
| | Only cartesian coordinates are supported for the moment, but you can use |
| | the parametric plots to plot in polar, spherical and cylindrical |
| | coordinates. |
| | |
| | The arguments for the constructor Plot must be subclasses of BaseSeries. |
| | |
| | Any global option can be specified as a keyword argument. |
| | |
| | The global options for a figure are: |
| | |
| | - title : str |
| | - xlabel : str or Symbol |
| | - ylabel : str or Symbol |
| | - zlabel : str or Symbol |
| | - legend : bool |
| | - xscale : {'linear', 'log'} |
| | - yscale : {'linear', 'log'} |
| | - axis : bool |
| | - axis_center : tuple of two floats or {'center', 'auto'} |
| | - xlim : tuple of two floats |
| | - ylim : tuple of two floats |
| | - aspect_ratio : tuple of two floats or {'auto'} |
| | - autoscale : bool |
| | - margin : float in [0, 1] |
| | - backend : {'default', 'matplotlib', 'text'} or a subclass of BaseBackend |
| | - size : optional tuple of two floats, (width, height); default: None |
| | |
| | The per data series options and aesthetics are: |
| | There are none in the base series. See below for options for subclasses. |
| | |
| | Some data series support additional aesthetics or options: |
| | |
| | :class:`~.LineOver1DRangeSeries`, :class:`~.Parametric2DLineSeries`, and |
| | :class:`~.Parametric3DLineSeries` support the following: |
| | |
| | Aesthetics: |
| | |
| | - line_color : string, or float, or function, optional |
| | Specifies the color for the plot, which depends on the backend being |
| | used. |
| | |
| | For example, if ``MatplotlibBackend`` is being used, then |
| | Matplotlib string colors are acceptable (``"red"``, ``"r"``, |
| | ``"cyan"``, ``"c"``, ...). |
| | Alternatively, we can use a float number, 0 < color < 1, wrapped in a |
| | string (for example, ``line_color="0.5"``) to specify grayscale colors. |
| | Alternatively, We can specify a function returning a single |
| | float value: this will be used to apply a color-loop (for example, |
| | ``line_color=lambda x: math.cos(x)``). |
| | |
| | Note that by setting line_color, it would be applied simultaneously |
| | to all the series. |
| | |
| | Options: |
| | |
| | - label : str |
| | - steps : bool |
| | - integers_only : bool |
| | |
| | :class:`~.SurfaceOver2DRangeSeries` and :class:`~.ParametricSurfaceSeries` |
| | support the following: |
| | |
| | Aesthetics: |
| | |
| | - surface_color : function which returns a float. |
| | |
| | Notes |
| | ===== |
| | |
| | How the plotting module works: |
| | |
| | 1. Whenever a plotting function is called, the provided expressions are |
| | processed and a list of instances of the |
| | :class:`~sympy.plotting.series.BaseSeries` class is created, containing |
| | the necessary information to plot the expressions |
| | (e.g. the expression, ranges, series name, ...). Eventually, these |
| | objects will generate the numerical data to be plotted. |
| | 2. A subclass of :class:`~.Plot` class is instantiaed (referred to as |
| | backend, from now on), which stores the list of series and the main |
| | attributes of the plot (e.g. axis labels, title, ...). |
| | The backend implements the logic to generate the actual figure with |
| | some plotting library. |
| | 3. When the ``show`` command is executed, series are processed one by one |
| | to generate numerical data and add it to the figure. The backend is also |
| | going to set the axis labels, title, ..., according to the values stored |
| | in the Plot instance. |
| | |
| | The backend should check if it supports the data series that it is given |
| | (e.g. :class:`TextBackend` supports only |
| | :class:`~sympy.plotting.series.LineOver1DRangeSeries`). |
| | |
| | It is the backend responsibility to know how to use the class of data series |
| | that it's given. Note that the current implementation of the ``*Series`` |
| | classes is "matplotlib-centric": the numerical data returned by the |
| | ``get_points`` and ``get_meshes`` methods is meant to be used directly by |
| | Matplotlib. Therefore, the new backend will have to pre-process the |
| | numerical data to make it compatible with the chosen plotting library. |
| | Keep in mind that future SymPy versions may improve the ``*Series`` classes |
| | in order to return numerical data "non-matplotlib-centric", hence if you code |
| | a new backend you have the responsibility to check if its working on each |
| | SymPy release. |
| | |
| | Please explore the :class:`MatplotlibBackend` source code to understand |
| | how a backend should be coded. |
| | |
| | In order to be used by SymPy plotting functions, a backend must implement |
| | the following methods: |
| | |
| | * show(self): used to loop over the data series, generate the numerical |
| | data, plot it and set the axis labels, title, ... |
| | * save(self, path): used to save the current plot to the specified file |
| | path. |
| | * close(self): used to close the current plot backend (note: some plotting |
| | library does not support this functionality. In that case, just raise a |
| | warning). |
| | """ |
| |
|
| | def __init__(self, *args, |
| | title=None, xlabel=None, ylabel=None, zlabel=None, aspect_ratio='auto', |
| | xlim=None, ylim=None, axis_center='auto', axis=True, |
| | xscale='linear', yscale='linear', legend=False, autoscale=True, |
| | margin=0, annotations=None, markers=None, rectangles=None, |
| | fill=None, backend='default', size=None, **kwargs): |
| |
|
| | |
| | |
| | |
| | self.title = title |
| | self.xlabel = xlabel |
| | self.ylabel = ylabel |
| | self.zlabel = zlabel |
| | self.aspect_ratio = aspect_ratio |
| | self.axis_center = axis_center |
| | self.axis = axis |
| | self.xscale = xscale |
| | self.yscale = yscale |
| | self.legend = legend |
| | self.autoscale = autoscale |
| | self.margin = margin |
| | self._annotations = annotations |
| | self._markers = markers |
| | self._rectangles = rectangles |
| | self._fill = fill |
| |
|
| | |
| | |
| | self._series = [] |
| | self._series.extend(args) |
| | self._series.extend(_create_generic_data_series( |
| | annotations=annotations, markers=markers, rectangles=rectangles, |
| | fill=fill)) |
| |
|
| | is_real = \ |
| | lambda lim: all(getattr(i, 'is_real', True) for i in lim) |
| | is_finite = \ |
| | lambda lim: all(getattr(i, 'is_finite', True) for i in lim) |
| |
|
| | |
| | def check_and_set(t_name, t): |
| | if t: |
| | if not is_real(t): |
| | raise ValueError( |
| | "All numbers from {}={} must be real".format(t_name, t)) |
| | if not is_finite(t): |
| | raise ValueError( |
| | "All numbers from {}={} must be finite".format(t_name, t)) |
| | setattr(self, t_name, (float(t[0]), float(t[1]))) |
| |
|
| | self.xlim = None |
| | check_and_set("xlim", xlim) |
| | self.ylim = None |
| | check_and_set("ylim", ylim) |
| | self.size = None |
| | check_and_set("size", size) |
| |
|
| | @property |
| | def _backend(self): |
| | return self |
| |
|
| | @property |
| | def backend(self): |
| | return type(self) |
| |
|
| | def __str__(self): |
| | series_strs = [('[%d]: ' % i) + str(s) |
| | for i, s in enumerate(self._series)] |
| | return 'Plot object containing:\n' + '\n'.join(series_strs) |
| |
|
| | def __getitem__(self, index): |
| | return self._series[index] |
| |
|
| | def __setitem__(self, index, *args): |
| | if len(args) == 1 and isinstance(args[0], BaseSeries): |
| | self._series[index] = args |
| |
|
| | def __delitem__(self, index): |
| | del self._series[index] |
| |
|
| | def append(self, arg): |
| | """Adds an element from a plot's series to an existing plot. |
| | |
| | Examples |
| | ======== |
| | |
| | Consider two ``Plot`` objects, ``p1`` and ``p2``. To add the |
| | second plot's first series object to the first, use the |
| | ``append`` method, like so: |
| | |
| | .. plot:: |
| | :format: doctest |
| | :include-source: True |
| | |
| | >>> from sympy import symbols |
| | >>> from sympy.plotting import plot |
| | >>> x = symbols('x') |
| | >>> p1 = plot(x*x, show=False) |
| | >>> p2 = plot(x, show=False) |
| | >>> p1.append(p2[0]) |
| | >>> p1 |
| | Plot object containing: |
| | [0]: cartesian line: x**2 for x over (-10.0, 10.0) |
| | [1]: cartesian line: x for x over (-10.0, 10.0) |
| | >>> p1.show() |
| | |
| | See Also |
| | ======== |
| | |
| | extend |
| | |
| | """ |
| | if isinstance(arg, BaseSeries): |
| | self._series.append(arg) |
| | else: |
| | raise TypeError('Must specify element of plot to append.') |
| |
|
| | def extend(self, arg): |
| | """Adds all series from another plot. |
| | |
| | Examples |
| | ======== |
| | |
| | Consider two ``Plot`` objects, ``p1`` and ``p2``. To add the |
| | second plot to the first, use the ``extend`` method, like so: |
| | |
| | .. plot:: |
| | :format: doctest |
| | :include-source: True |
| | |
| | >>> from sympy import symbols |
| | >>> from sympy.plotting import plot |
| | >>> x = symbols('x') |
| | >>> p1 = plot(x**2, show=False) |
| | >>> p2 = plot(x, -x, show=False) |
| | >>> p1.extend(p2) |
| | >>> p1 |
| | Plot object containing: |
| | [0]: cartesian line: x**2 for x over (-10.0, 10.0) |
| | [1]: cartesian line: x for x over (-10.0, 10.0) |
| | [2]: cartesian line: -x for x over (-10.0, 10.0) |
| | >>> p1.show() |
| | |
| | """ |
| | if isinstance(arg, Plot): |
| | self._series.extend(arg._series) |
| | elif is_sequence(arg): |
| | self._series.extend(arg) |
| | else: |
| | raise TypeError('Expecting Plot or sequence of BaseSeries') |
| |
|
| | def show(self): |
| | raise NotImplementedError |
| |
|
| | def save(self, path): |
| | raise NotImplementedError |
| |
|
| | def close(self): |
| | raise NotImplementedError |
| |
|
| | |
| |
|
| | @property |
| | def markers(self): |
| | """.. deprecated:: 1.13""" |
| | _deprecation_msg_m_a_r_f("markers") |
| | return self._markers |
| |
|
| | @markers.setter |
| | def markers(self, v): |
| | """.. deprecated:: 1.13""" |
| | _deprecation_msg_m_a_r_f("markers") |
| | self._series.extend(_create_generic_data_series(markers=v)) |
| | self._markers = v |
| |
|
| | @property |
| | def annotations(self): |
| | """.. deprecated:: 1.13""" |
| | _deprecation_msg_m_a_r_f("annotations") |
| | return self._annotations |
| |
|
| | @annotations.setter |
| | def annotations(self, v): |
| | """.. deprecated:: 1.13""" |
| | _deprecation_msg_m_a_r_f("annotations") |
| | self._series.extend(_create_generic_data_series(annotations=v)) |
| | self._annotations = v |
| |
|
| | @property |
| | def rectangles(self): |
| | """.. deprecated:: 1.13""" |
| | _deprecation_msg_m_a_r_f("rectangles") |
| | return self._rectangles |
| |
|
| | @rectangles.setter |
| | def rectangles(self, v): |
| | """.. deprecated:: 1.13""" |
| | _deprecation_msg_m_a_r_f("rectangles") |
| | self._series.extend(_create_generic_data_series(rectangles=v)) |
| | self._rectangles = v |
| |
|
| | @property |
| | def fill(self): |
| | """.. deprecated:: 1.13""" |
| | _deprecation_msg_m_a_r_f("fill") |
| | return self._fill |
| |
|
| | @fill.setter |
| | def fill(self, v): |
| | """.. deprecated:: 1.13""" |
| | _deprecation_msg_m_a_r_f("fill") |
| | self._series.extend(_create_generic_data_series(fill=v)) |
| | self._fill = v |
| |
|