| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419 |
- 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'],
- }
- # Global variable
- # Set to False when running tests / doctests so that the plots don't show.
- _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):
- # Options for the graph as a whole.
- # The possible values for each option are described in the docstring of
- # Plot. They are based purely on convention, no checking is done.
- 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
- # Contains the data objects to be plotted. The backend should be smart
- # enough to iterate over this list.
- 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)
- # reduce code repetition
- 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
- # deprecations
- @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
|