Coverage for /usr/lib/python3/dist-packages/sympy/plotting/plot.py: 15%
866 statements
« prev ^ index » next coverage.py v7.9.1, created at 2025-06-14 15:55 +0200
« prev ^ index » next coverage.py v7.9.1, created at 2025-06-14 15:55 +0200
1"""Plotting module for SymPy.
3A plot is represented by the ``Plot`` class that contains a reference to the
4backend and a list of the data series to be plotted. The data series are
5instances of classes meant to simplify getting points and meshes from SymPy
6expressions. ``plot_backends`` is a dictionary with all the backends.
8This module gives only the essential. For all the fancy stuff use directly
9the backend. You can get the backend wrapper for every plot from the
10``_backend`` attribute. Moreover the data series classes have various useful
11methods like ``get_points``, ``get_meshes``, etc, that may
12be useful if you wish to use another plotting library.
14Especially if you need publication ready graphs and this module is not enough
15for you - just get the ``_backend`` attribute and add whatever you want
16directly to it. In the case of matplotlib (the common way to graph data in
17python) just copy ``_backend.fig`` which is the figure and ``_backend.ax``
18which is the axis and work on them as you would on any other matplotlib object.
20Simplicity of code takes much greater importance than performance. Do not use it
21if you care at all about performance. A new backend instance is initialized
22every time you call ``show()`` and the old one is left to the garbage collector.
23"""
26from collections.abc import Callable
29from sympy.core.basic import Basic
30from sympy.core.containers import Tuple
31from sympy.core.expr import Expr
32from sympy.core.function import arity, Function
33from sympy.core.symbol import (Dummy, Symbol)
34from sympy.core.sympify import sympify
35from sympy.external import import_module
36from sympy.printing.latex import latex
37from sympy.utilities.exceptions import sympy_deprecation_warning
38from sympy.utilities.iterables import is_sequence
39from .experimental_lambdify import (vectorized_lambdify, lambdify)
41# N.B.
42# When changing the minimum module version for matplotlib, please change
43# the same in the `SymPyDocTestFinder`` in `sympy/testing/runtests.py`
45# Backend specific imports - textplot
46from sympy.plotting.textplot import textplot
48# Global variable
49# Set to False when running tests / doctests so that the plots don't show.
50_show = True
53def unset_show():
54 """
55 Disable show(). For use in the tests.
56 """
57 global _show
58 _show = False
60def _str_or_latex(label):
61 if isinstance(label, Basic):
62 return latex(label, mode='inline')
63 return str(label)
65##############################################################################
66# The public interface
67##############################################################################
70class Plot:
71 """The central class of the plotting module.
73 Explanation
74 ===========
76 For interactive work the function :func:`plot()` is better suited.
78 This class permits the plotting of SymPy expressions using numerous
79 backends (:external:mod:`matplotlib`, textplot, the old pyglet module for SymPy, Google
80 charts api, etc).
82 The figure can contain an arbitrary number of plots of SymPy expressions,
83 lists of coordinates of points, etc. Plot has a private attribute _series that
84 contains all data series to be plotted (expressions for lines or surfaces,
85 lists of points, etc (all subclasses of BaseSeries)). Those data series are
86 instances of classes not imported by ``from sympy import *``.
88 The customization of the figure is on two levels. Global options that
89 concern the figure as a whole (e.g. title, xlabel, scale, etc) and
90 per-data series options (e.g. name) and aesthetics (e.g. color, point shape,
91 line type, etc.).
93 The difference between options and aesthetics is that an aesthetic can be
94 a function of the coordinates (or parameters in a parametric plot). The
95 supported values for an aesthetic are:
97 - None (the backend uses default values)
98 - a constant
99 - a function of one variable (the first coordinate or parameter)
100 - a function of two variables (the first and second coordinate or parameters)
101 - a function of three variables (only in nonparametric 3D plots)
103 Their implementation depends on the backend so they may not work in some
104 backends.
106 If the plot is parametric and the arity of the aesthetic function permits
107 it the aesthetic is calculated over parameters and not over coordinates.
108 If the arity does not permit calculation over parameters the calculation is
109 done over coordinates.
111 Only cartesian coordinates are supported for the moment, but you can use
112 the parametric plots to plot in polar, spherical and cylindrical
113 coordinates.
115 The arguments for the constructor Plot must be subclasses of BaseSeries.
117 Any global option can be specified as a keyword argument.
119 The global options for a figure are:
121 - title : str
122 - xlabel : str or Symbol
123 - ylabel : str or Symbol
124 - zlabel : str or Symbol
125 - legend : bool
126 - xscale : {'linear', 'log'}
127 - yscale : {'linear', 'log'}
128 - axis : bool
129 - axis_center : tuple of two floats or {'center', 'auto'}
130 - xlim : tuple of two floats
131 - ylim : tuple of two floats
132 - aspect_ratio : tuple of two floats or {'auto'}
133 - autoscale : bool
134 - margin : float in [0, 1]
135 - backend : {'default', 'matplotlib', 'text'} or a subclass of BaseBackend
136 - size : optional tuple of two floats, (width, height); default: None
138 The per data series options and aesthetics are:
139 There are none in the base series. See below for options for subclasses.
141 Some data series support additional aesthetics or options:
143 :class:`~.LineOver1DRangeSeries`, :class:`~.Parametric2DLineSeries`, and
144 :class:`~.Parametric3DLineSeries` support the following:
146 Aesthetics:
148 - line_color : string, or float, or function, optional
149 Specifies the color for the plot, which depends on the backend being
150 used.
152 For example, if ``MatplotlibBackend`` is being used, then
153 Matplotlib string colors are acceptable (``"red"``, ``"r"``,
154 ``"cyan"``, ``"c"``, ...).
155 Alternatively, we can use a float number, 0 < color < 1, wrapped in a
156 string (for example, ``line_color="0.5"``) to specify grayscale colors.
157 Alternatively, We can specify a function returning a single
158 float value: this will be used to apply a color-loop (for example,
159 ``line_color=lambda x: math.cos(x)``).
161 Note that by setting line_color, it would be applied simultaneously
162 to all the series.
164 Options:
166 - label : str
167 - steps : bool
168 - integers_only : bool
170 :class:`~.SurfaceOver2DRangeSeries` and :class:`~.ParametricSurfaceSeries`
171 support the following:
173 Aesthetics:
175 - surface_color : function which returns a float.
176 """
178 def __init__(self, *args,
179 title=None, xlabel=None, ylabel=None, zlabel=None, aspect_ratio='auto',
180 xlim=None, ylim=None, axis_center='auto', axis=True,
181 xscale='linear', yscale='linear', legend=False, autoscale=True,
182 margin=0, annotations=None, markers=None, rectangles=None,
183 fill=None, backend='default', size=None, **kwargs):
184 super().__init__()
186 # Options for the graph as a whole.
187 # The possible values for each option are described in the docstring of
188 # Plot. They are based purely on convention, no checking is done.
189 self.title = title
190 self.xlabel = xlabel
191 self.ylabel = ylabel
192 self.zlabel = zlabel
193 self.aspect_ratio = aspect_ratio
194 self.axis_center = axis_center
195 self.axis = axis
196 self.xscale = xscale
197 self.yscale = yscale
198 self.legend = legend
199 self.autoscale = autoscale
200 self.margin = margin
201 self.annotations = annotations
202 self.markers = markers
203 self.rectangles = rectangles
204 self.fill = fill
206 # Contains the data objects to be plotted. The backend should be smart
207 # enough to iterate over this list.
208 self._series = []
209 self._series.extend(args)
211 # The backend type. On every show() a new backend instance is created
212 # in self._backend which is tightly coupled to the Plot instance
213 # (thanks to the parent attribute of the backend).
214 if isinstance(backend, str):
215 self.backend = plot_backends[backend]
216 elif (type(backend) == type) and issubclass(backend, BaseBackend):
217 self.backend = backend
218 else:
219 raise TypeError(
220 "backend must be either a string or a subclass of BaseBackend")
222 is_real = \
223 lambda lim: all(getattr(i, 'is_real', True) for i in lim)
224 is_finite = \
225 lambda lim: all(getattr(i, 'is_finite', True) for i in lim)
227 # reduce code repetition
228 def check_and_set(t_name, t):
229 if t:
230 if not is_real(t):
231 raise ValueError(
232 "All numbers from {}={} must be real".format(t_name, t))
233 if not is_finite(t):
234 raise ValueError(
235 "All numbers from {}={} must be finite".format(t_name, t))
236 setattr(self, t_name, (float(t[0]), float(t[1])))
238 self.xlim = None
239 check_and_set("xlim", xlim)
240 self.ylim = None
241 check_and_set("ylim", ylim)
242 self.size = None
243 check_and_set("size", size)
246 def show(self):
247 # TODO move this to the backend (also for save)
248 if hasattr(self, '_backend'):
249 self._backend.close()
250 self._backend = self.backend(self)
251 self._backend.show()
253 def save(self, path):
254 if hasattr(self, '_backend'):
255 self._backend.close()
256 self._backend = self.backend(self)
257 self._backend.save(path)
259 def __str__(self):
260 series_strs = [('[%d]: ' % i) + str(s)
261 for i, s in enumerate(self._series)]
262 return 'Plot object containing:\n' + '\n'.join(series_strs)
264 def __getitem__(self, index):
265 return self._series[index]
267 def __setitem__(self, index, *args):
268 if len(args) == 1 and isinstance(args[0], BaseSeries):
269 self._series[index] = args
271 def __delitem__(self, index):
272 del self._series[index]
274 def append(self, arg):
275 """Adds an element from a plot's series to an existing plot.
277 Examples
278 ========
280 Consider two ``Plot`` objects, ``p1`` and ``p2``. To add the
281 second plot's first series object to the first, use the
282 ``append`` method, like so:
284 .. plot::
285 :format: doctest
286 :include-source: True
288 >>> from sympy import symbols
289 >>> from sympy.plotting import plot
290 >>> x = symbols('x')
291 >>> p1 = plot(x*x, show=False)
292 >>> p2 = plot(x, show=False)
293 >>> p1.append(p2[0])
294 >>> p1
295 Plot object containing:
296 [0]: cartesian line: x**2 for x over (-10.0, 10.0)
297 [1]: cartesian line: x for x over (-10.0, 10.0)
298 >>> p1.show()
300 See Also
301 ========
303 extend
305 """
306 if isinstance(arg, BaseSeries):
307 self._series.append(arg)
308 else:
309 raise TypeError('Must specify element of plot to append.')
311 def extend(self, arg):
312 """Adds all series from another plot.
314 Examples
315 ========
317 Consider two ``Plot`` objects, ``p1`` and ``p2``. To add the
318 second plot to the first, use the ``extend`` method, like so:
320 .. plot::
321 :format: doctest
322 :include-source: True
324 >>> from sympy import symbols
325 >>> from sympy.plotting import plot
326 >>> x = symbols('x')
327 >>> p1 = plot(x**2, show=False)
328 >>> p2 = plot(x, -x, show=False)
329 >>> p1.extend(p2)
330 >>> p1
331 Plot object containing:
332 [0]: cartesian line: x**2 for x over (-10.0, 10.0)
333 [1]: cartesian line: x for x over (-10.0, 10.0)
334 [2]: cartesian line: -x for x over (-10.0, 10.0)
335 >>> p1.show()
337 """
338 if isinstance(arg, Plot):
339 self._series.extend(arg._series)
340 elif is_sequence(arg):
341 self._series.extend(arg)
342 else:
343 raise TypeError('Expecting Plot or sequence of BaseSeries')
346class PlotGrid:
347 """This class helps to plot subplots from already created SymPy plots
348 in a single figure.
350 Examples
351 ========
353 .. plot::
354 :context: close-figs
355 :format: doctest
356 :include-source: True
358 >>> from sympy import symbols
359 >>> from sympy.plotting import plot, plot3d, PlotGrid
360 >>> x, y = symbols('x, y')
361 >>> p1 = plot(x, x**2, x**3, (x, -5, 5))
362 >>> p2 = plot((x**2, (x, -6, 6)), (x, (x, -5, 5)))
363 >>> p3 = plot(x**3, (x, -5, 5))
364 >>> p4 = plot3d(x*y, (x, -5, 5), (y, -5, 5))
366 Plotting vertically in a single line:
368 .. plot::
369 :context: close-figs
370 :format: doctest
371 :include-source: True
373 >>> PlotGrid(2, 1, p1, p2)
374 PlotGrid object containing:
375 Plot[0]:Plot object containing:
376 [0]: cartesian line: x for x over (-5.0, 5.0)
377 [1]: cartesian line: x**2 for x over (-5.0, 5.0)
378 [2]: cartesian line: x**3 for x over (-5.0, 5.0)
379 Plot[1]:Plot object containing:
380 [0]: cartesian line: x**2 for x over (-6.0, 6.0)
381 [1]: cartesian line: x for x over (-5.0, 5.0)
383 Plotting horizontally in a single line:
385 .. plot::
386 :context: close-figs
387 :format: doctest
388 :include-source: True
390 >>> PlotGrid(1, 3, p2, p3, p4)
391 PlotGrid object containing:
392 Plot[0]:Plot object containing:
393 [0]: cartesian line: x**2 for x over (-6.0, 6.0)
394 [1]: cartesian line: x for x over (-5.0, 5.0)
395 Plot[1]:Plot object containing:
396 [0]: cartesian line: x**3 for x over (-5.0, 5.0)
397 Plot[2]:Plot object containing:
398 [0]: cartesian surface: x*y for x over (-5.0, 5.0) and y over (-5.0, 5.0)
400 Plotting in a grid form:
402 .. plot::
403 :context: close-figs
404 :format: doctest
405 :include-source: True
407 >>> PlotGrid(2, 2, p1, p2, p3, p4)
408 PlotGrid object containing:
409 Plot[0]:Plot object containing:
410 [0]: cartesian line: x for x over (-5.0, 5.0)
411 [1]: cartesian line: x**2 for x over (-5.0, 5.0)
412 [2]: cartesian line: x**3 for x over (-5.0, 5.0)
413 Plot[1]:Plot object containing:
414 [0]: cartesian line: x**2 for x over (-6.0, 6.0)
415 [1]: cartesian line: x for x over (-5.0, 5.0)
416 Plot[2]:Plot object containing:
417 [0]: cartesian line: x**3 for x over (-5.0, 5.0)
418 Plot[3]:Plot object containing:
419 [0]: cartesian surface: x*y for x over (-5.0, 5.0) and y over (-5.0, 5.0)
421 """
422 def __init__(self, nrows, ncolumns, *args, show=True, size=None, **kwargs):
423 """
424 Parameters
425 ==========
427 nrows :
428 The number of rows that should be in the grid of the
429 required subplot.
430 ncolumns :
431 The number of columns that should be in the grid
432 of the required subplot.
434 nrows and ncolumns together define the required grid.
436 Arguments
437 =========
439 A list of predefined plot objects entered in a row-wise sequence
440 i.e. plot objects which are to be in the top row of the required
441 grid are written first, then the second row objects and so on
443 Keyword arguments
444 =================
446 show : Boolean
447 The default value is set to ``True``. Set show to ``False`` and
448 the function will not display the subplot. The returned instance
449 of the ``PlotGrid`` class can then be used to save or display the
450 plot by calling the ``save()`` and ``show()`` methods
451 respectively.
452 size : (float, float), optional
453 A tuple in the form (width, height) in inches to specify the size of
454 the overall figure. The default value is set to ``None``, meaning
455 the size will be set by the default backend.
456 """
457 self.nrows = nrows
458 self.ncolumns = ncolumns
459 self._series = []
460 self.args = args
461 for arg in args:
462 self._series.append(arg._series)
463 self.backend = DefaultBackend
464 self.size = size
465 if show:
466 self.show()
468 def show(self):
469 if hasattr(self, '_backend'):
470 self._backend.close()
471 self._backend = self.backend(self)
472 self._backend.show()
474 def save(self, path):
475 if hasattr(self, '_backend'):
476 self._backend.close()
477 self._backend = self.backend(self)
478 self._backend.save(path)
480 def __str__(self):
481 plot_strs = [('Plot[%d]:' % i) + str(plot)
482 for i, plot in enumerate(self.args)]
484 return 'PlotGrid object containing:\n' + '\n'.join(plot_strs)
487##############################################################################
488# Data Series
489##############################################################################
490#TODO more general way to calculate aesthetics (see get_color_array)
492### The base class for all series
493class BaseSeries:
494 """Base class for the data objects containing stuff to be plotted.
496 Explanation
497 ===========
499 The backend should check if it supports the data series that is given.
500 (e.g. TextBackend supports only LineOver1DRangeSeries).
501 It is the backend responsibility to know how to use the class of
502 data series that is given.
504 Some data series classes are grouped (using a class attribute like is_2Dline)
505 according to the api they present (based only on convention). The backend is
506 not obliged to use that api (e.g. LineOver1DRangeSeries belongs to the
507 is_2Dline group and presents the get_points method, but the
508 TextBackend does not use the get_points method).
509 """
511 # Some flags follow. The rationale for using flags instead of checking base
512 # classes is that setting multiple flags is simpler than multiple
513 # inheritance.
515 is_2Dline = False
516 # Some of the backends expect:
517 # - get_points returning 1D np.arrays list_x, list_y
518 # - get_color_array returning 1D np.array (done in Line2DBaseSeries)
519 # with the colors calculated at the points from get_points
521 is_3Dline = False
522 # Some of the backends expect:
523 # - get_points returning 1D np.arrays list_x, list_y, list_y
524 # - get_color_array returning 1D np.array (done in Line2DBaseSeries)
525 # with the colors calculated at the points from get_points
527 is_3Dsurface = False
528 # Some of the backends expect:
529 # - get_meshes returning mesh_x, mesh_y, mesh_z (2D np.arrays)
530 # - get_points an alias for get_meshes
532 is_contour = False
533 # Some of the backends expect:
534 # - get_meshes returning mesh_x, mesh_y, mesh_z (2D np.arrays)
535 # - get_points an alias for get_meshes
537 is_implicit = False
538 # Some of the backends expect:
539 # - get_meshes returning mesh_x (1D array), mesh_y(1D array,
540 # mesh_z (2D np.arrays)
541 # - get_points an alias for get_meshes
542 # Different from is_contour as the colormap in backend will be
543 # different
545 is_parametric = False
546 # The calculation of aesthetics expects:
547 # - get_parameter_points returning one or two np.arrays (1D or 2D)
548 # used for calculation aesthetics
550 def __init__(self):
551 super().__init__()
553 @property
554 def is_3D(self):
555 flags3D = [
556 self.is_3Dline,
557 self.is_3Dsurface
558 ]
559 return any(flags3D)
561 @property
562 def is_line(self):
563 flagslines = [
564 self.is_2Dline,
565 self.is_3Dline
566 ]
567 return any(flagslines)
570### 2D lines
571class Line2DBaseSeries(BaseSeries):
572 """A base class for 2D lines.
574 - adding the label, steps and only_integers options
575 - making is_2Dline true
576 - defining get_segments and get_color_array
577 """
579 is_2Dline = True
581 _dim = 2
583 def __init__(self):
584 super().__init__()
585 self.label = None
586 self.steps = False
587 self.only_integers = False
588 self.line_color = None
590 def get_data(self):
591 """ Return lists of coordinates for plotting the line.
593 Returns
594 =======
595 x : list
596 List of x-coordinates
598 y : list
599 List of y-coordinates
601 z : list
602 List of z-coordinates in case of Parametric3DLineSeries
603 """
604 np = import_module('numpy')
605 points = self.get_points()
606 if self.steps is True:
607 if len(points) == 2:
608 x = np.array((points[0], points[0])).T.flatten()[1:]
609 y = np.array((points[1], points[1])).T.flatten()[:-1]
610 points = (x, y)
611 else:
612 x = np.repeat(points[0], 3)[2:]
613 y = np.repeat(points[1], 3)[:-2]
614 z = np.repeat(points[2], 3)[1:-1]
615 points = (x, y, z)
616 return points
618 def get_segments(self):
619 sympy_deprecation_warning(
620 """
621 The Line2DBaseSeries.get_segments() method is deprecated.
623 Instead, use the MatplotlibBackend.get_segments() method, or use
624 The get_points() or get_data() methods.
625 """,
626 deprecated_since_version="1.9",
627 active_deprecations_target="deprecated-get-segments")
629 np = import_module('numpy')
630 points = type(self).get_data(self)
631 points = np.ma.array(points).T.reshape(-1, 1, self._dim)
632 return np.ma.concatenate([points[:-1], points[1:]], axis=1)
634 def get_color_array(self):
635 np = import_module('numpy')
636 c = self.line_color
637 if hasattr(c, '__call__'):
638 f = np.vectorize(c)
639 nargs = arity(c)
640 if nargs == 1 and self.is_parametric:
641 x = self.get_parameter_points()
642 return f(centers_of_segments(x))
643 else:
644 variables = list(map(centers_of_segments, self.get_points()))
645 if nargs == 1:
646 return f(variables[0])
647 elif nargs == 2:
648 return f(*variables[:2])
649 else: # only if the line is 3D (otherwise raises an error)
650 return f(*variables)
651 else:
652 return c*np.ones(self.nb_of_points)
655class List2DSeries(Line2DBaseSeries):
656 """Representation for a line consisting of list of points."""
658 def __init__(self, list_x, list_y):
659 np = import_module('numpy')
660 super().__init__()
661 self.list_x = np.array(list_x)
662 self.list_y = np.array(list_y)
663 self.label = 'list'
665 def __str__(self):
666 return 'list plot'
668 def get_points(self):
669 return (self.list_x, self.list_y)
672class LineOver1DRangeSeries(Line2DBaseSeries):
673 """Representation for a line consisting of a SymPy expression over a range."""
675 def __init__(self, expr, var_start_end, **kwargs):
676 super().__init__()
677 self.expr = sympify(expr)
678 self.label = kwargs.get('label', None) or self.expr
679 self.var = sympify(var_start_end[0])
680 self.start = float(var_start_end[1])
681 self.end = float(var_start_end[2])
682 self.nb_of_points = kwargs.get('nb_of_points', 300)
683 self.adaptive = kwargs.get('adaptive', True)
684 self.depth = kwargs.get('depth', 12)
685 self.line_color = kwargs.get('line_color', None)
686 self.xscale = kwargs.get('xscale', 'linear')
688 def __str__(self):
689 return 'cartesian line: %s for %s over %s' % (
690 str(self.expr), str(self.var), str((self.start, self.end)))
692 def get_points(self):
693 """ Return lists of coordinates for plotting. Depending on the
694 ``adaptive`` option, this function will either use an adaptive algorithm
695 or it will uniformly sample the expression over the provided range.
697 Returns
698 =======
699 x : list
700 List of x-coordinates
702 y : list
703 List of y-coordinates
706 Explanation
707 ===========
709 The adaptive sampling is done by recursively checking if three
710 points are almost collinear. If they are not collinear, then more
711 points are added between those points.
713 References
714 ==========
716 .. [1] Adaptive polygonal approximation of parametric curves,
717 Luiz Henrique de Figueiredo.
719 """
720 if self.only_integers or not self.adaptive:
721 return self._uniform_sampling()
722 else:
723 f = lambdify([self.var], self.expr)
724 x_coords = []
725 y_coords = []
726 np = import_module('numpy')
727 def sample(p, q, depth):
728 """ Samples recursively if three points are almost collinear.
729 For depth < 6, points are added irrespective of whether they
730 satisfy the collinearity condition or not. The maximum depth
731 allowed is 12.
732 """
733 # Randomly sample to avoid aliasing.
734 random = 0.45 + np.random.rand() * 0.1
735 if self.xscale == 'log':
736 xnew = 10**(np.log10(p[0]) + random * (np.log10(q[0]) -
737 np.log10(p[0])))
738 else:
739 xnew = p[0] + random * (q[0] - p[0])
740 ynew = f(xnew)
741 new_point = np.array([xnew, ynew])
743 # Maximum depth
744 if depth > self.depth:
745 x_coords.append(q[0])
746 y_coords.append(q[1])
748 # Sample irrespective of whether the line is flat till the
749 # depth of 6. We are not using linspace to avoid aliasing.
750 elif depth < 6:
751 sample(p, new_point, depth + 1)
752 sample(new_point, q, depth + 1)
754 # Sample ten points if complex values are encountered
755 # at both ends. If there is a real value in between, then
756 # sample those points further.
757 elif p[1] is None and q[1] is None:
758 if self.xscale == 'log':
759 xarray = np.logspace(p[0], q[0], 10)
760 else:
761 xarray = np.linspace(p[0], q[0], 10)
762 yarray = list(map(f, xarray))
763 if not all(y is None for y in yarray):
764 for i in range(len(yarray) - 1):
765 if not (yarray[i] is None and yarray[i + 1] is None):
766 sample([xarray[i], yarray[i]],
767 [xarray[i + 1], yarray[i + 1]], depth + 1)
769 # Sample further if one of the end points in None (i.e. a
770 # complex value) or the three points are not almost collinear.
771 elif (p[1] is None or q[1] is None or new_point[1] is None
772 or not flat(p, new_point, q)):
773 sample(p, new_point, depth + 1)
774 sample(new_point, q, depth + 1)
775 else:
776 x_coords.append(q[0])
777 y_coords.append(q[1])
779 f_start = f(self.start)
780 f_end = f(self.end)
781 x_coords.append(self.start)
782 y_coords.append(f_start)
783 sample(np.array([self.start, f_start]),
784 np.array([self.end, f_end]), 0)
786 return (x_coords, y_coords)
788 def _uniform_sampling(self):
789 np = import_module('numpy')
790 if self.only_integers is True:
791 if self.xscale == 'log':
792 list_x = np.logspace(int(self.start), int(self.end),
793 num=int(self.end) - int(self.start) + 1)
794 else:
795 list_x = np.linspace(int(self.start), int(self.end),
796 num=int(self.end) - int(self.start) + 1)
797 else:
798 if self.xscale == 'log':
799 list_x = np.logspace(self.start, self.end, num=self.nb_of_points)
800 else:
801 list_x = np.linspace(self.start, self.end, num=self.nb_of_points)
802 f = vectorized_lambdify([self.var], self.expr)
803 list_y = f(list_x)
804 return (list_x, list_y)
807class Parametric2DLineSeries(Line2DBaseSeries):
808 """Representation for a line consisting of two parametric SymPy expressions
809 over a range."""
811 is_parametric = True
813 def __init__(self, expr_x, expr_y, var_start_end, **kwargs):
814 super().__init__()
815 self.expr_x = sympify(expr_x)
816 self.expr_y = sympify(expr_y)
817 self.label = kwargs.get('label', None) or \
818 Tuple(self.expr_x, self.expr_y)
819 self.var = sympify(var_start_end[0])
820 self.start = float(var_start_end[1])
821 self.end = float(var_start_end[2])
822 self.nb_of_points = kwargs.get('nb_of_points', 300)
823 self.adaptive = kwargs.get('adaptive', True)
824 self.depth = kwargs.get('depth', 12)
825 self.line_color = kwargs.get('line_color', None)
827 def __str__(self):
828 return 'parametric cartesian line: (%s, %s) for %s over %s' % (
829 str(self.expr_x), str(self.expr_y), str(self.var),
830 str((self.start, self.end)))
832 def get_parameter_points(self):
833 np = import_module('numpy')
834 return np.linspace(self.start, self.end, num=self.nb_of_points)
836 def _uniform_sampling(self):
837 param = self.get_parameter_points()
838 fx = vectorized_lambdify([self.var], self.expr_x)
839 fy = vectorized_lambdify([self.var], self.expr_y)
840 list_x = fx(param)
841 list_y = fy(param)
842 return (list_x, list_y)
844 def get_points(self):
845 """ Return lists of coordinates for plotting. Depending on the
846 ``adaptive`` option, this function will either use an adaptive algorithm
847 or it will uniformly sample the expression over the provided range.
849 Returns
850 =======
851 x : list
852 List of x-coordinates
854 y : list
855 List of y-coordinates
858 Explanation
859 ===========
861 The adaptive sampling is done by recursively checking if three
862 points are almost collinear. If they are not collinear, then more
863 points are added between those points.
865 References
866 ==========
868 .. [1] Adaptive polygonal approximation of parametric curves,
869 Luiz Henrique de Figueiredo.
871 """
872 if not self.adaptive:
873 return self._uniform_sampling()
875 f_x = lambdify([self.var], self.expr_x)
876 f_y = lambdify([self.var], self.expr_y)
877 x_coords = []
878 y_coords = []
880 def sample(param_p, param_q, p, q, depth):
881 """ Samples recursively if three points are almost collinear.
882 For depth < 6, points are added irrespective of whether they
883 satisfy the collinearity condition or not. The maximum depth
884 allowed is 12.
885 """
886 # Randomly sample to avoid aliasing.
887 np = import_module('numpy')
888 random = 0.45 + np.random.rand() * 0.1
889 param_new = param_p + random * (param_q - param_p)
890 xnew = f_x(param_new)
891 ynew = f_y(param_new)
892 new_point = np.array([xnew, ynew])
894 # Maximum depth
895 if depth > self.depth:
896 x_coords.append(q[0])
897 y_coords.append(q[1])
899 # Sample irrespective of whether the line is flat till the
900 # depth of 6. We are not using linspace to avoid aliasing.
901 elif depth < 6:
902 sample(param_p, param_new, p, new_point, depth + 1)
903 sample(param_new, param_q, new_point, q, depth + 1)
905 # Sample ten points if complex values are encountered
906 # at both ends. If there is a real value in between, then
907 # sample those points further.
908 elif ((p[0] is None and q[1] is None) or
909 (p[1] is None and q[1] is None)):
910 param_array = np.linspace(param_p, param_q, 10)
911 x_array = list(map(f_x, param_array))
912 y_array = list(map(f_y, param_array))
913 if not all(x is None and y is None
914 for x, y in zip(x_array, y_array)):
915 for i in range(len(y_array) - 1):
916 if ((x_array[i] is not None and y_array[i] is not None) or
917 (x_array[i + 1] is not None and y_array[i + 1] is not None)):
918 point_a = [x_array[i], y_array[i]]
919 point_b = [x_array[i + 1], y_array[i + 1]]
920 sample(param_array[i], param_array[i], point_a,
921 point_b, depth + 1)
923 # Sample further if one of the end points in None (i.e. a complex
924 # value) or the three points are not almost collinear.
925 elif (p[0] is None or p[1] is None
926 or q[1] is None or q[0] is None
927 or not flat(p, new_point, q)):
928 sample(param_p, param_new, p, new_point, depth + 1)
929 sample(param_new, param_q, new_point, q, depth + 1)
930 else:
931 x_coords.append(q[0])
932 y_coords.append(q[1])
934 f_start_x = f_x(self.start)
935 f_start_y = f_y(self.start)
936 start = [f_start_x, f_start_y]
937 f_end_x = f_x(self.end)
938 f_end_y = f_y(self.end)
939 end = [f_end_x, f_end_y]
940 x_coords.append(f_start_x)
941 y_coords.append(f_start_y)
942 sample(self.start, self.end, start, end, 0)
944 return x_coords, y_coords
947### 3D lines
948class Line3DBaseSeries(Line2DBaseSeries):
949 """A base class for 3D lines.
951 Most of the stuff is derived from Line2DBaseSeries."""
953 is_2Dline = False
954 is_3Dline = True
955 _dim = 3
957 def __init__(self):
958 super().__init__()
961class Parametric3DLineSeries(Line3DBaseSeries):
962 """Representation for a 3D line consisting of three parametric SymPy
963 expressions and a range."""
965 is_parametric = True
967 def __init__(self, expr_x, expr_y, expr_z, var_start_end, **kwargs):
968 super().__init__()
969 self.expr_x = sympify(expr_x)
970 self.expr_y = sympify(expr_y)
971 self.expr_z = sympify(expr_z)
972 self.label = kwargs.get('label', None) or \
973 Tuple(self.expr_x, self.expr_y)
974 self.var = sympify(var_start_end[0])
975 self.start = float(var_start_end[1])
976 self.end = float(var_start_end[2])
977 self.nb_of_points = kwargs.get('nb_of_points', 300)
978 self.line_color = kwargs.get('line_color', None)
979 self._xlim = None
980 self._ylim = None
981 self._zlim = None
983 def __str__(self):
984 return '3D parametric cartesian line: (%s, %s, %s) for %s over %s' % (
985 str(self.expr_x), str(self.expr_y), str(self.expr_z),
986 str(self.var), str((self.start, self.end)))
988 def get_parameter_points(self):
989 np = import_module('numpy')
990 return np.linspace(self.start, self.end, num=self.nb_of_points)
992 def get_points(self):
993 np = import_module('numpy')
994 param = self.get_parameter_points()
995 fx = vectorized_lambdify([self.var], self.expr_x)
996 fy = vectorized_lambdify([self.var], self.expr_y)
997 fz = vectorized_lambdify([self.var], self.expr_z)
999 list_x = fx(param)
1000 list_y = fy(param)
1001 list_z = fz(param)
1003 list_x = np.array(list_x, dtype=np.float64)
1004 list_y = np.array(list_y, dtype=np.float64)
1005 list_z = np.array(list_z, dtype=np.float64)
1007 list_x = np.ma.masked_invalid(list_x)
1008 list_y = np.ma.masked_invalid(list_y)
1009 list_z = np.ma.masked_invalid(list_z)
1011 self._xlim = (np.amin(list_x), np.amax(list_x))
1012 self._ylim = (np.amin(list_y), np.amax(list_y))
1013 self._zlim = (np.amin(list_z), np.amax(list_z))
1014 return list_x, list_y, list_z
1017### Surfaces
1018class SurfaceBaseSeries(BaseSeries):
1019 """A base class for 3D surfaces."""
1021 is_3Dsurface = True
1023 def __init__(self):
1024 super().__init__()
1025 self.surface_color = None
1027 def get_color_array(self):
1028 np = import_module('numpy')
1029 c = self.surface_color
1030 if isinstance(c, Callable):
1031 f = np.vectorize(c)
1032 nargs = arity(c)
1033 if self.is_parametric:
1034 variables = list(map(centers_of_faces, self.get_parameter_meshes()))
1035 if nargs == 1:
1036 return f(variables[0])
1037 elif nargs == 2:
1038 return f(*variables)
1039 variables = list(map(centers_of_faces, self.get_meshes()))
1040 if nargs == 1:
1041 return f(variables[0])
1042 elif nargs == 2:
1043 return f(*variables[:2])
1044 else:
1045 return f(*variables)
1046 else:
1047 if isinstance(self, SurfaceOver2DRangeSeries):
1048 return c*np.ones(min(self.nb_of_points_x, self.nb_of_points_y))
1049 else:
1050 return c*np.ones(min(self.nb_of_points_u, self.nb_of_points_v))
1053class SurfaceOver2DRangeSeries(SurfaceBaseSeries):
1054 """Representation for a 3D surface consisting of a SymPy expression and 2D
1055 range."""
1056 def __init__(self, expr, var_start_end_x, var_start_end_y, **kwargs):
1057 super().__init__()
1058 self.expr = sympify(expr)
1059 self.var_x = sympify(var_start_end_x[0])
1060 self.start_x = float(var_start_end_x[1])
1061 self.end_x = float(var_start_end_x[2])
1062 self.var_y = sympify(var_start_end_y[0])
1063 self.start_y = float(var_start_end_y[1])
1064 self.end_y = float(var_start_end_y[2])
1065 self.nb_of_points_x = kwargs.get('nb_of_points_x', 50)
1066 self.nb_of_points_y = kwargs.get('nb_of_points_y', 50)
1067 self.surface_color = kwargs.get('surface_color', None)
1069 self._xlim = (self.start_x, self.end_x)
1070 self._ylim = (self.start_y, self.end_y)
1072 def __str__(self):
1073 return ('cartesian surface: %s for'
1074 ' %s over %s and %s over %s') % (
1075 str(self.expr),
1076 str(self.var_x),
1077 str((self.start_x, self.end_x)),
1078 str(self.var_y),
1079 str((self.start_y, self.end_y)))
1081 def get_meshes(self):
1082 np = import_module('numpy')
1083 mesh_x, mesh_y = np.meshgrid(np.linspace(self.start_x, self.end_x,
1084 num=self.nb_of_points_x),
1085 np.linspace(self.start_y, self.end_y,
1086 num=self.nb_of_points_y))
1087 f = vectorized_lambdify((self.var_x, self.var_y), self.expr)
1088 mesh_z = f(mesh_x, mesh_y)
1089 mesh_z = np.array(mesh_z, dtype=np.float64)
1090 mesh_z = np.ma.masked_invalid(mesh_z)
1091 self._zlim = (np.amin(mesh_z), np.amax(mesh_z))
1092 return mesh_x, mesh_y, mesh_z
1095class ParametricSurfaceSeries(SurfaceBaseSeries):
1096 """Representation for a 3D surface consisting of three parametric SymPy
1097 expressions and a range."""
1099 is_parametric = True
1101 def __init__(
1102 self, expr_x, expr_y, expr_z, var_start_end_u, var_start_end_v,
1103 **kwargs):
1104 super().__init__()
1105 self.expr_x = sympify(expr_x)
1106 self.expr_y = sympify(expr_y)
1107 self.expr_z = sympify(expr_z)
1108 self.var_u = sympify(var_start_end_u[0])
1109 self.start_u = float(var_start_end_u[1])
1110 self.end_u = float(var_start_end_u[2])
1111 self.var_v = sympify(var_start_end_v[0])
1112 self.start_v = float(var_start_end_v[1])
1113 self.end_v = float(var_start_end_v[2])
1114 self.nb_of_points_u = kwargs.get('nb_of_points_u', 50)
1115 self.nb_of_points_v = kwargs.get('nb_of_points_v', 50)
1116 self.surface_color = kwargs.get('surface_color', None)
1118 def __str__(self):
1119 return ('parametric cartesian surface: (%s, %s, %s) for'
1120 ' %s over %s and %s over %s') % (
1121 str(self.expr_x),
1122 str(self.expr_y),
1123 str(self.expr_z),
1124 str(self.var_u),
1125 str((self.start_u, self.end_u)),
1126 str(self.var_v),
1127 str((self.start_v, self.end_v)))
1129 def get_parameter_meshes(self):
1130 np = import_module('numpy')
1131 return np.meshgrid(np.linspace(self.start_u, self.end_u,
1132 num=self.nb_of_points_u),
1133 np.linspace(self.start_v, self.end_v,
1134 num=self.nb_of_points_v))
1136 def get_meshes(self):
1137 np = import_module('numpy')
1139 mesh_u, mesh_v = self.get_parameter_meshes()
1140 fx = vectorized_lambdify((self.var_u, self.var_v), self.expr_x)
1141 fy = vectorized_lambdify((self.var_u, self.var_v), self.expr_y)
1142 fz = vectorized_lambdify((self.var_u, self.var_v), self.expr_z)
1144 mesh_x = fx(mesh_u, mesh_v)
1145 mesh_y = fy(mesh_u, mesh_v)
1146 mesh_z = fz(mesh_u, mesh_v)
1148 mesh_x = np.array(mesh_x, dtype=np.float64)
1149 mesh_y = np.array(mesh_y, dtype=np.float64)
1150 mesh_z = np.array(mesh_z, dtype=np.float64)
1152 mesh_x = np.ma.masked_invalid(mesh_x)
1153 mesh_y = np.ma.masked_invalid(mesh_y)
1154 mesh_z = np.ma.masked_invalid(mesh_z)
1156 self._xlim = (np.amin(mesh_x), np.amax(mesh_x))
1157 self._ylim = (np.amin(mesh_y), np.amax(mesh_y))
1158 self._zlim = (np.amin(mesh_z), np.amax(mesh_z))
1160 return mesh_x, mesh_y, mesh_z
1163### Contours
1164class ContourSeries(BaseSeries):
1165 """Representation for a contour plot."""
1166 # The code is mostly repetition of SurfaceOver2DRange.
1167 # Presently used in contour_plot function
1169 is_contour = True
1171 def __init__(self, expr, var_start_end_x, var_start_end_y):
1172 super().__init__()
1173 self.nb_of_points_x = 50
1174 self.nb_of_points_y = 50
1175 self.expr = sympify(expr)
1176 self.var_x = sympify(var_start_end_x[0])
1177 self.start_x = float(var_start_end_x[1])
1178 self.end_x = float(var_start_end_x[2])
1179 self.var_y = sympify(var_start_end_y[0])
1180 self.start_y = float(var_start_end_y[1])
1181 self.end_y = float(var_start_end_y[2])
1183 self.get_points = self.get_meshes
1185 self._xlim = (self.start_x, self.end_x)
1186 self._ylim = (self.start_y, self.end_y)
1188 def __str__(self):
1189 return ('contour: %s for '
1190 '%s over %s and %s over %s') % (
1191 str(self.expr),
1192 str(self.var_x),
1193 str((self.start_x, self.end_x)),
1194 str(self.var_y),
1195 str((self.start_y, self.end_y)))
1197 def get_meshes(self):
1198 np = import_module('numpy')
1199 mesh_x, mesh_y = np.meshgrid(np.linspace(self.start_x, self.end_x,
1200 num=self.nb_of_points_x),
1201 np.linspace(self.start_y, self.end_y,
1202 num=self.nb_of_points_y))
1203 f = vectorized_lambdify((self.var_x, self.var_y), self.expr)
1204 return (mesh_x, mesh_y, f(mesh_x, mesh_y))
1207##############################################################################
1208# Backends
1209##############################################################################
1211class BaseBackend:
1212 """Base class for all backends. A backend represents the plotting library,
1213 which implements the necessary functionalities in order to use SymPy
1214 plotting functions.
1216 How the plotting module works:
1218 1. Whenever a plotting function is called, the provided expressions are
1219 processed and a list of instances of the :class:`BaseSeries` class is
1220 created, containing the necessary information to plot the expressions
1221 (e.g. the expression, ranges, series name, ...). Eventually, these
1222 objects will generate the numerical data to be plotted.
1223 2. A :class:`~.Plot` object is instantiated, which stores the list of
1224 series and the main attributes of the plot (e.g. axis labels, title, ...).
1225 3. When the ``show`` command is executed, a new backend is instantiated,
1226 which loops through each series object to generate and plot the
1227 numerical data. The backend is also going to set the axis labels, title,
1228 ..., according to the values stored in the Plot instance.
1230 The backend should check if it supports the data series that it is given
1231 (e.g. :class:`TextBackend` supports only :class:`LineOver1DRangeSeries`).
1233 It is the backend responsibility to know how to use the class of data series
1234 that it's given. Note that the current implementation of the ``*Series``
1235 classes is "matplotlib-centric": the numerical data returned by the
1236 ``get_points`` and ``get_meshes`` methods is meant to be used directly by
1237 Matplotlib. Therefore, the new backend will have to pre-process the
1238 numerical data to make it compatible with the chosen plotting library.
1239 Keep in mind that future SymPy versions may improve the ``*Series`` classes
1240 in order to return numerical data "non-matplotlib-centric", hence if you code
1241 a new backend you have the responsibility to check if its working on each
1242 SymPy release.
1244 Please explore the :class:`MatplotlibBackend` source code to understand how a
1245 backend should be coded.
1247 Methods
1248 =======
1250 In order to be used by SymPy plotting functions, a backend must implement
1251 the following methods:
1253 * show(self): used to loop over the data series, generate the numerical
1254 data, plot it and set the axis labels, title, ...
1255 * save(self, path): used to save the current plot to the specified file
1256 path.
1257 * close(self): used to close the current plot backend (note: some plotting
1258 library does not support this functionality. In that case, just raise a
1259 warning).
1261 See also
1262 ========
1264 MatplotlibBackend
1265 """
1266 def __init__(self, parent):
1267 super().__init__()
1268 self.parent = parent
1270 def show(self):
1271 raise NotImplementedError
1273 def save(self, path):
1274 raise NotImplementedError
1276 def close(self):
1277 raise NotImplementedError
1280# Don't have to check for the success of importing matplotlib in each case;
1281# we will only be using this backend if we can successfully import matploblib
1282class MatplotlibBackend(BaseBackend):
1283 """ This class implements the functionalities to use Matplotlib with SymPy
1284 plotting functions.
1285 """
1286 def __init__(self, parent):
1287 super().__init__(parent)
1288 self.matplotlib = import_module('matplotlib',
1289 import_kwargs={'fromlist': ['pyplot', 'cm', 'collections']},
1290 min_module_version='1.1.0', catch=(RuntimeError,))
1291 self.plt = self.matplotlib.pyplot
1292 self.cm = self.matplotlib.cm
1293 self.LineCollection = self.matplotlib.collections.LineCollection
1294 aspect = getattr(self.parent, 'aspect_ratio', 'auto')
1295 if aspect != 'auto':
1296 aspect = float(aspect[1]) / aspect[0]
1298 if isinstance(self.parent, Plot):
1299 nrows, ncolumns = 1, 1
1300 series_list = [self.parent._series]
1301 elif isinstance(self.parent, PlotGrid):
1302 nrows, ncolumns = self.parent.nrows, self.parent.ncolumns
1303 series_list = self.parent._series
1305 self.ax = []
1306 self.fig = self.plt.figure(figsize=parent.size)
1308 for i, series in enumerate(series_list):
1309 are_3D = [s.is_3D for s in series]
1311 if any(are_3D) and not all(are_3D):
1312 raise ValueError('The matplotlib backend cannot mix 2D and 3D.')
1313 elif all(are_3D):
1314 # mpl_toolkits.mplot3d is necessary for
1315 # projection='3d'
1316 mpl_toolkits = import_module('mpl_toolkits', # noqa
1317 import_kwargs={'fromlist': ['mplot3d']})
1318 self.ax.append(self.fig.add_subplot(nrows, ncolumns, i + 1, projection='3d', aspect=aspect))
1320 elif not any(are_3D):
1321 self.ax.append(self.fig.add_subplot(nrows, ncolumns, i + 1, aspect=aspect))
1322 self.ax[i].spines['left'].set_position('zero')
1323 self.ax[i].spines['right'].set_color('none')
1324 self.ax[i].spines['bottom'].set_position('zero')
1325 self.ax[i].spines['top'].set_color('none')
1326 self.ax[i].xaxis.set_ticks_position('bottom')
1327 self.ax[i].yaxis.set_ticks_position('left')
1329 @staticmethod
1330 def get_segments(x, y, z=None):
1331 """ Convert two list of coordinates to a list of segments to be used
1332 with Matplotlib's :external:class:`~matplotlib.collections.LineCollection`.
1334 Parameters
1335 ==========
1336 x : list
1337 List of x-coordinates
1339 y : list
1340 List of y-coordinates
1342 z : list
1343 List of z-coordinates for a 3D line.
1344 """
1345 np = import_module('numpy')
1346 if z is not None:
1347 dim = 3
1348 points = (x, y, z)
1349 else:
1350 dim = 2
1351 points = (x, y)
1352 points = np.ma.array(points).T.reshape(-1, 1, dim)
1353 return np.ma.concatenate([points[:-1], points[1:]], axis=1)
1355 def _process_series(self, series, ax, parent):
1356 np = import_module('numpy')
1357 mpl_toolkits = import_module(
1358 'mpl_toolkits', import_kwargs={'fromlist': ['mplot3d']})
1360 # XXX Workaround for matplotlib issue
1361 # https://github.com/matplotlib/matplotlib/issues/17130
1362 xlims, ylims, zlims = [], [], []
1364 for s in series:
1365 # Create the collections
1366 if s.is_2Dline:
1367 x, y = s.get_data()
1368 if (isinstance(s.line_color, (int, float)) or
1369 callable(s.line_color)):
1370 segments = self.get_segments(x, y)
1371 collection = self.LineCollection(segments)
1372 collection.set_array(s.get_color_array())
1373 ax.add_collection(collection)
1374 else:
1375 lbl = _str_or_latex(s.label)
1376 line, = ax.plot(x, y, label=lbl, color=s.line_color)
1377 elif s.is_contour:
1378 ax.contour(*s.get_meshes())
1379 elif s.is_3Dline:
1380 x, y, z = s.get_data()
1381 if (isinstance(s.line_color, (int, float)) or
1382 callable(s.line_color)):
1383 art3d = mpl_toolkits.mplot3d.art3d
1384 segments = self.get_segments(x, y, z)
1385 collection = art3d.Line3DCollection(segments)
1386 collection.set_array(s.get_color_array())
1387 ax.add_collection(collection)
1388 else:
1389 lbl = _str_or_latex(s.label)
1390 ax.plot(x, y, z, label=lbl, color=s.line_color)
1392 xlims.append(s._xlim)
1393 ylims.append(s._ylim)
1394 zlims.append(s._zlim)
1395 elif s.is_3Dsurface:
1396 x, y, z = s.get_meshes()
1397 collection = ax.plot_surface(x, y, z,
1398 cmap=getattr(self.cm, 'viridis', self.cm.jet),
1399 rstride=1, cstride=1, linewidth=0.1)
1400 if isinstance(s.surface_color, (float, int, Callable)):
1401 color_array = s.get_color_array()
1402 color_array = color_array.reshape(color_array.size)
1403 collection.set_array(color_array)
1404 else:
1405 collection.set_color(s.surface_color)
1407 xlims.append(s._xlim)
1408 ylims.append(s._ylim)
1409 zlims.append(s._zlim)
1410 elif s.is_implicit:
1411 points = s.get_raster()
1412 if len(points) == 2:
1413 # interval math plotting
1414 x, y = _matplotlib_list(points[0])
1415 ax.fill(x, y, facecolor=s.line_color, edgecolor='None')
1416 else:
1417 # use contourf or contour depending on whether it is
1418 # an inequality or equality.
1419 # XXX: ``contour`` plots multiple lines. Should be fixed.
1420 ListedColormap = self.matplotlib.colors.ListedColormap
1421 colormap = ListedColormap(["white", s.line_color])
1422 xarray, yarray, zarray, plot_type = points
1423 if plot_type == 'contour':
1424 ax.contour(xarray, yarray, zarray, cmap=colormap)
1425 else:
1426 ax.contourf(xarray, yarray, zarray, cmap=colormap)
1427 else:
1428 raise NotImplementedError(
1429 '{} is not supported in the SymPy plotting module '
1430 'with matplotlib backend. Please report this issue.'
1431 .format(ax))
1433 Axes3D = mpl_toolkits.mplot3d.Axes3D
1434 if not isinstance(ax, Axes3D):
1435 ax.autoscale_view(
1436 scalex=ax.get_autoscalex_on(),
1437 scaley=ax.get_autoscaley_on())
1438 else:
1439 # XXX Workaround for matplotlib issue
1440 # https://github.com/matplotlib/matplotlib/issues/17130
1441 if xlims:
1442 xlims = np.array(xlims)
1443 xlim = (np.amin(xlims[:, 0]), np.amax(xlims[:, 1]))
1444 ax.set_xlim(xlim)
1445 else:
1446 ax.set_xlim([0, 1])
1448 if ylims:
1449 ylims = np.array(ylims)
1450 ylim = (np.amin(ylims[:, 0]), np.amax(ylims[:, 1]))
1451 ax.set_ylim(ylim)
1452 else:
1453 ax.set_ylim([0, 1])
1455 if zlims:
1456 zlims = np.array(zlims)
1457 zlim = (np.amin(zlims[:, 0]), np.amax(zlims[:, 1]))
1458 ax.set_zlim(zlim)
1459 else:
1460 ax.set_zlim([0, 1])
1462 # Set global options.
1463 # TODO The 3D stuff
1464 # XXX The order of those is important.
1465 if parent.xscale and not isinstance(ax, Axes3D):
1466 ax.set_xscale(parent.xscale)
1467 if parent.yscale and not isinstance(ax, Axes3D):
1468 ax.set_yscale(parent.yscale)
1469 if not isinstance(ax, Axes3D) or self.matplotlib.__version__ >= '1.2.0': # XXX in the distant future remove this check
1470 ax.set_autoscale_on(parent.autoscale)
1471 if parent.axis_center:
1472 val = parent.axis_center
1473 if isinstance(ax, Axes3D):
1474 pass
1475 elif val == 'center':
1476 ax.spines['left'].set_position('center')
1477 ax.spines['bottom'].set_position('center')
1478 elif val == 'auto':
1479 xl, xh = ax.get_xlim()
1480 yl, yh = ax.get_ylim()
1481 pos_left = ('data', 0) if xl*xh <= 0 else 'center'
1482 pos_bottom = ('data', 0) if yl*yh <= 0 else 'center'
1483 ax.spines['left'].set_position(pos_left)
1484 ax.spines['bottom'].set_position(pos_bottom)
1485 else:
1486 ax.spines['left'].set_position(('data', val[0]))
1487 ax.spines['bottom'].set_position(('data', val[1]))
1488 if not parent.axis:
1489 ax.set_axis_off()
1490 if parent.legend:
1491 if ax.legend():
1492 ax.legend_.set_visible(parent.legend)
1493 if parent.margin:
1494 ax.set_xmargin(parent.margin)
1495 ax.set_ymargin(parent.margin)
1496 if parent.title:
1497 ax.set_title(parent.title)
1498 if parent.xlabel:
1499 xlbl = _str_or_latex(parent.xlabel)
1500 ax.set_xlabel(xlbl, position=(1, 0))
1501 if parent.ylabel:
1502 ylbl = _str_or_latex(parent.ylabel)
1503 ax.set_ylabel(ylbl, position=(0, 1))
1504 if isinstance(ax, Axes3D) and parent.zlabel:
1505 zlbl = _str_or_latex(parent.zlabel)
1506 ax.set_zlabel(zlbl, position=(0, 1))
1507 if parent.annotations:
1508 for a in parent.annotations:
1509 ax.annotate(**a)
1510 if parent.markers:
1511 for marker in parent.markers:
1512 # make a copy of the marker dictionary
1513 # so that it doesn't get altered
1514 m = marker.copy()
1515 args = m.pop('args')
1516 ax.plot(*args, **m)
1517 if parent.rectangles:
1518 for r in parent.rectangles:
1519 rect = self.matplotlib.patches.Rectangle(**r)
1520 ax.add_patch(rect)
1521 if parent.fill:
1522 ax.fill_between(**parent.fill)
1524 # xlim and ylim should always be set at last so that plot limits
1525 # doesn't get altered during the process.
1526 if parent.xlim:
1527 ax.set_xlim(parent.xlim)
1528 if parent.ylim:
1529 ax.set_ylim(parent.ylim)
1532 def process_series(self):
1533 """
1534 Iterates over every ``Plot`` object and further calls
1535 _process_series()
1536 """
1537 parent = self.parent
1538 if isinstance(parent, Plot):
1539 series_list = [parent._series]
1540 else:
1541 series_list = parent._series
1543 for i, (series, ax) in enumerate(zip(series_list, self.ax)):
1544 if isinstance(self.parent, PlotGrid):
1545 parent = self.parent.args[i]
1546 self._process_series(series, ax, parent)
1548 def show(self):
1549 self.process_series()
1550 #TODO after fixing https://github.com/ipython/ipython/issues/1255
1551 # you can uncomment the next line and remove the pyplot.show() call
1552 #self.fig.show()
1553 if _show:
1554 self.fig.tight_layout()
1555 self.plt.show()
1556 else:
1557 self.close()
1559 def save(self, path):
1560 self.process_series()
1561 self.fig.savefig(path)
1563 def close(self):
1564 self.plt.close(self.fig)
1567class TextBackend(BaseBackend):
1568 def __init__(self, parent):
1569 super().__init__(parent)
1571 def show(self):
1572 if not _show:
1573 return
1574 if len(self.parent._series) != 1:
1575 raise ValueError(
1576 'The TextBackend supports only one graph per Plot.')
1577 elif not isinstance(self.parent._series[0], LineOver1DRangeSeries):
1578 raise ValueError(
1579 'The TextBackend supports only expressions over a 1D range')
1580 else:
1581 ser = self.parent._series[0]
1582 textplot(ser.expr, ser.start, ser.end)
1584 def close(self):
1585 pass
1588class DefaultBackend(BaseBackend):
1589 def __new__(cls, parent):
1590 matplotlib = import_module('matplotlib', min_module_version='1.1.0', catch=(RuntimeError,))
1591 if matplotlib:
1592 return MatplotlibBackend(parent)
1593 else:
1594 return TextBackend(parent)
1597plot_backends = {
1598 'matplotlib': MatplotlibBackend,
1599 'text': TextBackend,
1600 'default': DefaultBackend
1601}
1604##############################################################################
1605# Finding the centers of line segments or mesh faces
1606##############################################################################
1608def centers_of_segments(array):
1609 np = import_module('numpy')
1610 return np.mean(np.vstack((array[:-1], array[1:])), 0)
1613def centers_of_faces(array):
1614 np = import_module('numpy')
1615 return np.mean(np.dstack((array[:-1, :-1],
1616 array[1:, :-1],
1617 array[:-1, 1:],
1618 array[:-1, :-1],
1619 )), 2)
1622def flat(x, y, z, eps=1e-3):
1623 """Checks whether three points are almost collinear"""
1624 np = import_module('numpy')
1625 # Workaround plotting piecewise (#8577):
1626 # workaround for `lambdify` in `.experimental_lambdify` fails
1627 # to return numerical values in some cases. Lower-level fix
1628 # in `lambdify` is possible.
1629 vector_a = (x - y).astype(np.float64)
1630 vector_b = (z - y).astype(np.float64)
1631 dot_product = np.dot(vector_a, vector_b)
1632 vector_a_norm = np.linalg.norm(vector_a)
1633 vector_b_norm = np.linalg.norm(vector_b)
1634 cos_theta = dot_product / (vector_a_norm * vector_b_norm)
1635 return abs(cos_theta + 1) < eps
1638def _matplotlib_list(interval_list):
1639 """
1640 Returns lists for matplotlib ``fill`` command from a list of bounding
1641 rectangular intervals
1642 """
1643 xlist = []
1644 ylist = []
1645 if len(interval_list):
1646 for intervals in interval_list:
1647 intervalx = intervals[0]
1648 intervaly = intervals[1]
1649 xlist.extend([intervalx.start, intervalx.start,
1650 intervalx.end, intervalx.end, None])
1651 ylist.extend([intervaly.start, intervaly.end,
1652 intervaly.end, intervaly.start, None])
1653 else:
1654 #XXX Ugly hack. Matplotlib does not accept empty lists for ``fill``
1655 xlist.extend((None, None, None, None))
1656 ylist.extend((None, None, None, None))
1657 return xlist, ylist
1660####New API for plotting module ####
1662# TODO: Add color arrays for plots.
1663# TODO: Add more plotting options for 3d plots.
1664# TODO: Adaptive sampling for 3D plots.
1666def plot(*args, show=True, **kwargs):
1667 """Plots a function of a single variable as a curve.
1669 Parameters
1670 ==========
1672 args :
1673 The first argument is the expression representing the function
1674 of single variable to be plotted.
1676 The last argument is a 3-tuple denoting the range of the free
1677 variable. e.g. ``(x, 0, 5)``
1679 Typical usage examples are in the following:
1681 - Plotting a single expression with a single range.
1682 ``plot(expr, range, **kwargs)``
1683 - Plotting a single expression with the default range (-10, 10).
1684 ``plot(expr, **kwargs)``
1685 - Plotting multiple expressions with a single range.
1686 ``plot(expr1, expr2, ..., range, **kwargs)``
1687 - Plotting multiple expressions with multiple ranges.
1688 ``plot((expr1, range1), (expr2, range2), ..., **kwargs)``
1690 It is best practice to specify range explicitly because default
1691 range may change in the future if a more advanced default range
1692 detection algorithm is implemented.
1694 show : bool, optional
1695 The default value is set to ``True``. Set show to ``False`` and
1696 the function will not display the plot. The returned instance of
1697 the ``Plot`` class can then be used to save or display the plot
1698 by calling the ``save()`` and ``show()`` methods respectively.
1700 line_color : string, or float, or function, optional
1701 Specifies the color for the plot.
1702 See ``Plot`` to see how to set color for the plots.
1703 Note that by setting ``line_color``, it would be applied simultaneously
1704 to all the series.
1706 title : str, optional
1707 Title of the plot. It is set to the latex representation of
1708 the expression, if the plot has only one expression.
1710 label : str, optional
1711 The label of the expression in the plot. It will be used when
1712 called with ``legend``. Default is the name of the expression.
1713 e.g. ``sin(x)``
1715 xlabel : str or expression, optional
1716 Label for the x-axis.
1718 ylabel : str or expression, optional
1719 Label for the y-axis.
1721 xscale : 'linear' or 'log', optional
1722 Sets the scaling of the x-axis.
1724 yscale : 'linear' or 'log', optional
1725 Sets the scaling of the y-axis.
1727 axis_center : (float, float), optional
1728 Tuple of two floats denoting the coordinates of the center or
1729 {'center', 'auto'}
1731 xlim : (float, float), optional
1732 Denotes the x-axis limits, ``(min, max)```.
1734 ylim : (float, float), optional
1735 Denotes the y-axis limits, ``(min, max)```.
1737 annotations : list, optional
1738 A list of dictionaries specifying the type of annotation
1739 required. The keys in the dictionary should be equivalent
1740 to the arguments of the :external:mod:`matplotlib`'s
1741 :external:meth:`~matplotlib.axes.Axes.annotate` method.
1743 markers : list, optional
1744 A list of dictionaries specifying the type the markers required.
1745 The keys in the dictionary should be equivalent to the arguments
1746 of the :external:mod:`matplotlib`'s :external:func:`~matplotlib.pyplot.plot()` function
1747 along with the marker related keyworded arguments.
1749 rectangles : list, optional
1750 A list of dictionaries specifying the dimensions of the
1751 rectangles to be plotted. The keys in the dictionary should be
1752 equivalent to the arguments of the :external:mod:`matplotlib`'s
1753 :external:class:`~matplotlib.patches.Rectangle` class.
1755 fill : dict, optional
1756 A dictionary specifying the type of color filling required in
1757 the plot. The keys in the dictionary should be equivalent to the
1758 arguments of the :external:mod:`matplotlib`'s
1759 :external:meth:`~matplotlib.axes.Axes.fill_between` method.
1761 adaptive : bool, optional
1762 The default value is set to ``True``. Set adaptive to ``False``
1763 and specify ``nb_of_points`` if uniform sampling is required.
1765 The plotting uses an adaptive algorithm which samples
1766 recursively to accurately plot. The adaptive algorithm uses a
1767 random point near the midpoint of two points that has to be
1768 further sampled. Hence the same plots can appear slightly
1769 different.
1771 depth : int, optional
1772 Recursion depth of the adaptive algorithm. A depth of value
1773 `n` samples a maximum of `2^{n}` points.
1775 If the ``adaptive`` flag is set to ``False``, this will be
1776 ignored.
1778 nb_of_points : int, optional
1779 Used when the ``adaptive`` is set to ``False``. The function
1780 is uniformly sampled at ``nb_of_points`` number of points.
1782 If the ``adaptive`` flag is set to ``True``, this will be
1783 ignored.
1785 size : (float, float), optional
1786 A tuple in the form (width, height) in inches to specify the size of
1787 the overall figure. The default value is set to ``None``, meaning
1788 the size will be set by the default backend.
1790 Examples
1791 ========
1793 .. plot::
1794 :context: close-figs
1795 :format: doctest
1796 :include-source: True
1798 >>> from sympy import symbols
1799 >>> from sympy.plotting import plot
1800 >>> x = symbols('x')
1802 Single Plot
1804 .. plot::
1805 :context: close-figs
1806 :format: doctest
1807 :include-source: True
1809 >>> plot(x**2, (x, -5, 5))
1810 Plot object containing:
1811 [0]: cartesian line: x**2 for x over (-5.0, 5.0)
1813 Multiple plots with single range.
1815 .. plot::
1816 :context: close-figs
1817 :format: doctest
1818 :include-source: True
1820 >>> plot(x, x**2, x**3, (x, -5, 5))
1821 Plot object containing:
1822 [0]: cartesian line: x for x over (-5.0, 5.0)
1823 [1]: cartesian line: x**2 for x over (-5.0, 5.0)
1824 [2]: cartesian line: x**3 for x over (-5.0, 5.0)
1826 Multiple plots with different ranges.
1828 .. plot::
1829 :context: close-figs
1830 :format: doctest
1831 :include-source: True
1833 >>> plot((x**2, (x, -6, 6)), (x, (x, -5, 5)))
1834 Plot object containing:
1835 [0]: cartesian line: x**2 for x over (-6.0, 6.0)
1836 [1]: cartesian line: x for x over (-5.0, 5.0)
1838 No adaptive sampling.
1840 .. plot::
1841 :context: close-figs
1842 :format: doctest
1843 :include-source: True
1845 >>> plot(x**2, adaptive=False, nb_of_points=400)
1846 Plot object containing:
1847 [0]: cartesian line: x**2 for x over (-10.0, 10.0)
1849 See Also
1850 ========
1852 Plot, LineOver1DRangeSeries
1854 """
1855 args = list(map(sympify, args))
1856 free = set()
1857 for a in args:
1858 if isinstance(a, Expr):
1859 free |= a.free_symbols
1860 if len(free) > 1:
1861 raise ValueError(
1862 'The same variable should be used in all '
1863 'univariate expressions being plotted.')
1864 x = free.pop() if free else Symbol('x')
1865 kwargs.setdefault('xlabel', x)
1866 kwargs.setdefault('ylabel', Function('f')(x))
1867 series = []
1868 plot_expr = check_arguments(args, 1, 1)
1869 series = [LineOver1DRangeSeries(*arg, **kwargs) for arg in plot_expr]
1871 plots = Plot(*series, **kwargs)
1872 if show:
1873 plots.show()
1874 return plots
1877def plot_parametric(*args, show=True, **kwargs):
1878 """
1879 Plots a 2D parametric curve.
1881 Parameters
1882 ==========
1884 args
1885 Common specifications are:
1887 - Plotting a single parametric curve with a range
1888 ``plot_parametric((expr_x, expr_y), range)``
1889 - Plotting multiple parametric curves with the same range
1890 ``plot_parametric((expr_x, expr_y), ..., range)``
1891 - Plotting multiple parametric curves with different ranges
1892 ``plot_parametric((expr_x, expr_y, range), ...)``
1894 ``expr_x`` is the expression representing $x$ component of the
1895 parametric function.
1897 ``expr_y`` is the expression representing $y$ component of the
1898 parametric function.
1900 ``range`` is a 3-tuple denoting the parameter symbol, start and
1901 stop. For example, ``(u, 0, 5)``.
1903 If the range is not specified, then a default range of (-10, 10)
1904 is used.
1906 However, if the arguments are specified as
1907 ``(expr_x, expr_y, range), ...``, you must specify the ranges
1908 for each expressions manually.
1910 Default range may change in the future if a more advanced
1911 algorithm is implemented.
1913 adaptive : bool, optional
1914 Specifies whether to use the adaptive sampling or not.
1916 The default value is set to ``True``. Set adaptive to ``False``
1917 and specify ``nb_of_points`` if uniform sampling is required.
1919 depth : int, optional
1920 The recursion depth of the adaptive algorithm. A depth of
1921 value $n$ samples a maximum of $2^n$ points.
1923 nb_of_points : int, optional
1924 Used when the ``adaptive`` flag is set to ``False``.
1926 Specifies the number of the points used for the uniform
1927 sampling.
1929 line_color : string, or float, or function, optional
1930 Specifies the color for the plot.
1931 See ``Plot`` to see how to set color for the plots.
1932 Note that by setting ``line_color``, it would be applied simultaneously
1933 to all the series.
1935 label : str, optional
1936 The label of the expression in the plot. It will be used when
1937 called with ``legend``. Default is the name of the expression.
1938 e.g. ``sin(x)``
1940 xlabel : str, optional
1941 Label for the x-axis.
1943 ylabel : str, optional
1944 Label for the y-axis.
1946 xscale : 'linear' or 'log', optional
1947 Sets the scaling of the x-axis.
1949 yscale : 'linear' or 'log', optional
1950 Sets the scaling of the y-axis.
1952 axis_center : (float, float), optional
1953 Tuple of two floats denoting the coordinates of the center or
1954 {'center', 'auto'}
1956 xlim : (float, float), optional
1957 Denotes the x-axis limits, ``(min, max)```.
1959 ylim : (float, float), optional
1960 Denotes the y-axis limits, ``(min, max)```.
1962 size : (float, float), optional
1963 A tuple in the form (width, height) in inches to specify the size of
1964 the overall figure. The default value is set to ``None``, meaning
1965 the size will be set by the default backend.
1967 Examples
1968 ========
1970 .. plot::
1971 :context: reset
1972 :format: doctest
1973 :include-source: True
1975 >>> from sympy import plot_parametric, symbols, cos, sin
1976 >>> u = symbols('u')
1978 A parametric plot with a single expression:
1980 .. plot::
1981 :context: close-figs
1982 :format: doctest
1983 :include-source: True
1985 >>> plot_parametric((cos(u), sin(u)), (u, -5, 5))
1986 Plot object containing:
1987 [0]: parametric cartesian line: (cos(u), sin(u)) for u over (-5.0, 5.0)
1989 A parametric plot with multiple expressions with the same range:
1991 .. plot::
1992 :context: close-figs
1993 :format: doctest
1994 :include-source: True
1996 >>> plot_parametric((cos(u), sin(u)), (u, cos(u)), (u, -10, 10))
1997 Plot object containing:
1998 [0]: parametric cartesian line: (cos(u), sin(u)) for u over (-10.0, 10.0)
1999 [1]: parametric cartesian line: (u, cos(u)) for u over (-10.0, 10.0)
2001 A parametric plot with multiple expressions with different ranges
2002 for each curve:
2004 .. plot::
2005 :context: close-figs
2006 :format: doctest
2007 :include-source: True
2009 >>> plot_parametric((cos(u), sin(u), (u, -5, 5)),
2010 ... (cos(u), u, (u, -5, 5)))
2011 Plot object containing:
2012 [0]: parametric cartesian line: (cos(u), sin(u)) for u over (-5.0, 5.0)
2013 [1]: parametric cartesian line: (cos(u), u) for u over (-5.0, 5.0)
2015 Notes
2016 =====
2018 The plotting uses an adaptive algorithm which samples recursively to
2019 accurately plot the curve. The adaptive algorithm uses a random point
2020 near the midpoint of two points that has to be further sampled.
2021 Hence, repeating the same plot command can give slightly different
2022 results because of the random sampling.
2024 If there are multiple plots, then the same optional arguments are
2025 applied to all the plots drawn in the same canvas. If you want to
2026 set these options separately, you can index the returned ``Plot``
2027 object and set it.
2029 For example, when you specify ``line_color`` once, it would be
2030 applied simultaneously to both series.
2032 .. plot::
2033 :context: close-figs
2034 :format: doctest
2035 :include-source: True
2037 >>> from sympy import pi
2038 >>> expr1 = (u, cos(2*pi*u)/2 + 1/2)
2039 >>> expr2 = (u, sin(2*pi*u)/2 + 1/2)
2040 >>> p = plot_parametric(expr1, expr2, (u, 0, 1), line_color='blue')
2042 If you want to specify the line color for the specific series, you
2043 should index each item and apply the property manually.
2045 .. plot::
2046 :context: close-figs
2047 :format: doctest
2048 :include-source: True
2050 >>> p[0].line_color = 'red'
2051 >>> p.show()
2053 See Also
2054 ========
2056 Plot, Parametric2DLineSeries
2057 """
2058 args = list(map(sympify, args))
2059 series = []
2060 plot_expr = check_arguments(args, 2, 1)
2061 series = [Parametric2DLineSeries(*arg, **kwargs) for arg in plot_expr]
2062 plots = Plot(*series, **kwargs)
2063 if show:
2064 plots.show()
2065 return plots
2068def plot3d_parametric_line(*args, show=True, **kwargs):
2069 """
2070 Plots a 3D parametric line plot.
2072 Usage
2073 =====
2075 Single plot:
2077 ``plot3d_parametric_line(expr_x, expr_y, expr_z, range, **kwargs)``
2079 If the range is not specified, then a default range of (-10, 10) is used.
2081 Multiple plots.
2083 ``plot3d_parametric_line((expr_x, expr_y, expr_z, range), ..., **kwargs)``
2085 Ranges have to be specified for every expression.
2087 Default range may change in the future if a more advanced default range
2088 detection algorithm is implemented.
2090 Arguments
2091 =========
2093 expr_x : Expression representing the function along x.
2095 expr_y : Expression representing the function along y.
2097 expr_z : Expression representing the function along z.
2099 range : (:class:`~.Symbol`, float, float)
2100 A 3-tuple denoting the range of the parameter variable, e.g., (u, 0, 5).
2102 Keyword Arguments
2103 =================
2105 Arguments for ``Parametric3DLineSeries`` class.
2107 nb_of_points : The range is uniformly sampled at ``nb_of_points``
2108 number of points.
2110 Aesthetics:
2112 line_color : string, or float, or function, optional
2113 Specifies the color for the plot.
2114 See ``Plot`` to see how to set color for the plots.
2115 Note that by setting ``line_color``, it would be applied simultaneously
2116 to all the series.
2118 label : str
2119 The label to the plot. It will be used when called with ``legend=True``
2120 to denote the function with the given label in the plot.
2122 If there are multiple plots, then the same series arguments are applied to
2123 all the plots. If you want to set these options separately, you can index
2124 the returned ``Plot`` object and set it.
2126 Arguments for ``Plot`` class.
2128 title : str
2129 Title of the plot.
2131 size : (float, float), optional
2132 A tuple in the form (width, height) in inches to specify the size of
2133 the overall figure. The default value is set to ``None``, meaning
2134 the size will be set by the default backend.
2136 Examples
2137 ========
2139 .. plot::
2140 :context: reset
2141 :format: doctest
2142 :include-source: True
2144 >>> from sympy import symbols, cos, sin
2145 >>> from sympy.plotting import plot3d_parametric_line
2146 >>> u = symbols('u')
2148 Single plot.
2150 .. plot::
2151 :context: close-figs
2152 :format: doctest
2153 :include-source: True
2155 >>> plot3d_parametric_line(cos(u), sin(u), u, (u, -5, 5))
2156 Plot object containing:
2157 [0]: 3D parametric cartesian line: (cos(u), sin(u), u) for u over (-5.0, 5.0)
2160 Multiple plots.
2162 .. plot::
2163 :context: close-figs
2164 :format: doctest
2165 :include-source: True
2167 >>> plot3d_parametric_line((cos(u), sin(u), u, (u, -5, 5)),
2168 ... (sin(u), u**2, u, (u, -5, 5)))
2169 Plot object containing:
2170 [0]: 3D parametric cartesian line: (cos(u), sin(u), u) for u over (-5.0, 5.0)
2171 [1]: 3D parametric cartesian line: (sin(u), u**2, u) for u over (-5.0, 5.0)
2174 See Also
2175 ========
2177 Plot, Parametric3DLineSeries
2179 """
2180 args = list(map(sympify, args))
2181 series = []
2182 plot_expr = check_arguments(args, 3, 1)
2183 series = [Parametric3DLineSeries(*arg, **kwargs) for arg in plot_expr]
2184 kwargs.setdefault("xlabel", "x")
2185 kwargs.setdefault("ylabel", "y")
2186 kwargs.setdefault("zlabel", "z")
2187 plots = Plot(*series, **kwargs)
2188 if show:
2189 plots.show()
2190 return plots
2193def plot3d(*args, show=True, **kwargs):
2194 """
2195 Plots a 3D surface plot.
2197 Usage
2198 =====
2200 Single plot
2202 ``plot3d(expr, range_x, range_y, **kwargs)``
2204 If the ranges are not specified, then a default range of (-10, 10) is used.
2206 Multiple plot with the same range.
2208 ``plot3d(expr1, expr2, range_x, range_y, **kwargs)``
2210 If the ranges are not specified, then a default range of (-10, 10) is used.
2212 Multiple plots with different ranges.
2214 ``plot3d((expr1, range_x, range_y), (expr2, range_x, range_y), ..., **kwargs)``
2216 Ranges have to be specified for every expression.
2218 Default range may change in the future if a more advanced default range
2219 detection algorithm is implemented.
2221 Arguments
2222 =========
2224 expr : Expression representing the function along x.
2226 range_x : (:class:`~.Symbol`, float, float)
2227 A 3-tuple denoting the range of the x variable, e.g. (x, 0, 5).
2229 range_y : (:class:`~.Symbol`, float, float)
2230 A 3-tuple denoting the range of the y variable, e.g. (y, 0, 5).
2232 Keyword Arguments
2233 =================
2235 Arguments for ``SurfaceOver2DRangeSeries`` class:
2237 nb_of_points_x : int
2238 The x range is sampled uniformly at ``nb_of_points_x`` of points.
2240 nb_of_points_y : int
2241 The y range is sampled uniformly at ``nb_of_points_y`` of points.
2243 Aesthetics:
2245 surface_color : Function which returns a float
2246 Specifies the color for the surface of the plot.
2247 See :class:`~.Plot` for more details.
2249 If there are multiple plots, then the same series arguments are applied to
2250 all the plots. If you want to set these options separately, you can index
2251 the returned ``Plot`` object and set it.
2253 Arguments for ``Plot`` class:
2255 title : str
2256 Title of the plot.
2258 size : (float, float), optional
2259 A tuple in the form (width, height) in inches to specify the size of the
2260 overall figure. The default value is set to ``None``, meaning the size will
2261 be set by the default backend.
2263 Examples
2264 ========
2266 .. plot::
2267 :context: reset
2268 :format: doctest
2269 :include-source: True
2271 >>> from sympy import symbols
2272 >>> from sympy.plotting import plot3d
2273 >>> x, y = symbols('x y')
2275 Single plot
2277 .. plot::
2278 :context: close-figs
2279 :format: doctest
2280 :include-source: True
2282 >>> plot3d(x*y, (x, -5, 5), (y, -5, 5))
2283 Plot object containing:
2284 [0]: cartesian surface: x*y for x over (-5.0, 5.0) and y over (-5.0, 5.0)
2287 Multiple plots with same range
2289 .. plot::
2290 :context: close-figs
2291 :format: doctest
2292 :include-source: True
2294 >>> plot3d(x*y, -x*y, (x, -5, 5), (y, -5, 5))
2295 Plot object containing:
2296 [0]: cartesian surface: x*y for x over (-5.0, 5.0) and y over (-5.0, 5.0)
2297 [1]: cartesian surface: -x*y for x over (-5.0, 5.0) and y over (-5.0, 5.0)
2300 Multiple plots with different ranges.
2302 .. plot::
2303 :context: close-figs
2304 :format: doctest
2305 :include-source: True
2307 >>> plot3d((x**2 + y**2, (x, -5, 5), (y, -5, 5)),
2308 ... (x*y, (x, -3, 3), (y, -3, 3)))
2309 Plot object containing:
2310 [0]: cartesian surface: x**2 + y**2 for x over (-5.0, 5.0) and y over (-5.0, 5.0)
2311 [1]: cartesian surface: x*y for x over (-3.0, 3.0) and y over (-3.0, 3.0)
2314 See Also
2315 ========
2317 Plot, SurfaceOver2DRangeSeries
2319 """
2321 args = list(map(sympify, args))
2322 series = []
2323 plot_expr = check_arguments(args, 1, 2)
2324 series = [SurfaceOver2DRangeSeries(*arg, **kwargs) for arg in plot_expr]
2325 kwargs.setdefault("xlabel", series[0].var_x)
2326 kwargs.setdefault("ylabel", series[0].var_y)
2327 kwargs.setdefault("zlabel", Function('f')(series[0].var_x, series[0].var_y))
2328 plots = Plot(*series, **kwargs)
2329 if show:
2330 plots.show()
2331 return plots
2334def plot3d_parametric_surface(*args, show=True, **kwargs):
2335 """
2336 Plots a 3D parametric surface plot.
2338 Explanation
2339 ===========
2341 Single plot.
2343 ``plot3d_parametric_surface(expr_x, expr_y, expr_z, range_u, range_v, **kwargs)``
2345 If the ranges is not specified, then a default range of (-10, 10) is used.
2347 Multiple plots.
2349 ``plot3d_parametric_surface((expr_x, expr_y, expr_z, range_u, range_v), ..., **kwargs)``
2351 Ranges have to be specified for every expression.
2353 Default range may change in the future if a more advanced default range
2354 detection algorithm is implemented.
2356 Arguments
2357 =========
2359 expr_x : Expression representing the function along ``x``.
2361 expr_y : Expression representing the function along ``y``.
2363 expr_z : Expression representing the function along ``z``.
2365 range_u : (:class:`~.Symbol`, float, float)
2366 A 3-tuple denoting the range of the u variable, e.g. (u, 0, 5).
2368 range_v : (:class:`~.Symbol`, float, float)
2369 A 3-tuple denoting the range of the v variable, e.g. (v, 0, 5).
2371 Keyword Arguments
2372 =================
2374 Arguments for ``ParametricSurfaceSeries`` class:
2376 nb_of_points_u : int
2377 The ``u`` range is sampled uniformly at ``nb_of_points_v`` of points
2379 nb_of_points_y : int
2380 The ``v`` range is sampled uniformly at ``nb_of_points_y`` of points
2382 Aesthetics:
2384 surface_color : Function which returns a float
2385 Specifies the color for the surface of the plot. See
2386 :class:`~Plot` for more details.
2388 If there are multiple plots, then the same series arguments are applied for
2389 all the plots. If you want to set these options separately, you can index
2390 the returned ``Plot`` object and set it.
2393 Arguments for ``Plot`` class:
2395 title : str
2396 Title of the plot.
2398 size : (float, float), optional
2399 A tuple in the form (width, height) in inches to specify the size of the
2400 overall figure. The default value is set to ``None``, meaning the size will
2401 be set by the default backend.
2403 Examples
2404 ========
2406 .. plot::
2407 :context: reset
2408 :format: doctest
2409 :include-source: True
2411 >>> from sympy import symbols, cos, sin
2412 >>> from sympy.plotting import plot3d_parametric_surface
2413 >>> u, v = symbols('u v')
2415 Single plot.
2417 .. plot::
2418 :context: close-figs
2419 :format: doctest
2420 :include-source: True
2422 >>> plot3d_parametric_surface(cos(u + v), sin(u - v), u - v,
2423 ... (u, -5, 5), (v, -5, 5))
2424 Plot object containing:
2425 [0]: parametric cartesian surface: (cos(u + v), sin(u - v), u - v) for u over (-5.0, 5.0) and v over (-5.0, 5.0)
2428 See Also
2429 ========
2431 Plot, ParametricSurfaceSeries
2433 """
2435 args = list(map(sympify, args))
2436 series = []
2437 plot_expr = check_arguments(args, 3, 2)
2438 series = [ParametricSurfaceSeries(*arg, **kwargs) for arg in plot_expr]
2439 kwargs.setdefault("xlabel", "x")
2440 kwargs.setdefault("ylabel", "y")
2441 kwargs.setdefault("zlabel", "z")
2442 plots = Plot(*series, **kwargs)
2443 if show:
2444 plots.show()
2445 return plots
2447def plot_contour(*args, show=True, **kwargs):
2448 """
2449 Draws contour plot of a function
2451 Usage
2452 =====
2454 Single plot
2456 ``plot_contour(expr, range_x, range_y, **kwargs)``
2458 If the ranges are not specified, then a default range of (-10, 10) is used.
2460 Multiple plot with the same range.
2462 ``plot_contour(expr1, expr2, range_x, range_y, **kwargs)``
2464 If the ranges are not specified, then a default range of (-10, 10) is used.
2466 Multiple plots with different ranges.
2468 ``plot_contour((expr1, range_x, range_y), (expr2, range_x, range_y), ..., **kwargs)``
2470 Ranges have to be specified for every expression.
2472 Default range may change in the future if a more advanced default range
2473 detection algorithm is implemented.
2475 Arguments
2476 =========
2478 expr : Expression representing the function along x.
2480 range_x : (:class:`Symbol`, float, float)
2481 A 3-tuple denoting the range of the x variable, e.g. (x, 0, 5).
2483 range_y : (:class:`Symbol`, float, float)
2484 A 3-tuple denoting the range of the y variable, e.g. (y, 0, 5).
2486 Keyword Arguments
2487 =================
2489 Arguments for ``ContourSeries`` class:
2491 nb_of_points_x : int
2492 The x range is sampled uniformly at ``nb_of_points_x`` of points.
2494 nb_of_points_y : int
2495 The y range is sampled uniformly at ``nb_of_points_y`` of points.
2497 Aesthetics:
2499 surface_color : Function which returns a float
2500 Specifies the color for the surface of the plot. See
2501 :class:`sympy.plotting.Plot` for more details.
2503 If there are multiple plots, then the same series arguments are applied to
2504 all the plots. If you want to set these options separately, you can index
2505 the returned ``Plot`` object and set it.
2507 Arguments for ``Plot`` class:
2509 title : str
2510 Title of the plot.
2512 size : (float, float), optional
2513 A tuple in the form (width, height) in inches to specify the size of
2514 the overall figure. The default value is set to ``None``, meaning
2515 the size will be set by the default backend.
2517 See Also
2518 ========
2520 Plot, ContourSeries
2522 """
2524 args = list(map(sympify, args))
2525 plot_expr = check_arguments(args, 1, 2)
2526 series = [ContourSeries(*arg) for arg in plot_expr]
2527 plot_contours = Plot(*series, **kwargs)
2528 if len(plot_expr[0].free_symbols) > 2:
2529 raise ValueError('Contour Plot cannot Plot for more than two variables.')
2530 if show:
2531 plot_contours.show()
2532 return plot_contours
2534def check_arguments(args, expr_len, nb_of_free_symbols):
2535 """
2536 Checks the arguments and converts into tuples of the
2537 form (exprs, ranges).
2539 Examples
2540 ========
2542 .. plot::
2543 :context: reset
2544 :format: doctest
2545 :include-source: True
2547 >>> from sympy import cos, sin, symbols
2548 >>> from sympy.plotting.plot import check_arguments
2549 >>> x = symbols('x')
2550 >>> check_arguments([cos(x), sin(x)], 2, 1)
2551 [(cos(x), sin(x), (x, -10, 10))]
2553 >>> check_arguments([x, x**2], 1, 1)
2554 [(x, (x, -10, 10)), (x**2, (x, -10, 10))]
2555 """
2556 if not args:
2557 return []
2558 if expr_len > 1 and isinstance(args[0], Expr):
2559 # Multiple expressions same range.
2560 # The arguments are tuples when the expression length is
2561 # greater than 1.
2562 if len(args) < expr_len:
2563 raise ValueError("len(args) should not be less than expr_len")
2564 for i in range(len(args)):
2565 if isinstance(args[i], Tuple):
2566 break
2567 else:
2568 i = len(args) + 1
2570 exprs = Tuple(*args[:i])
2571 free_symbols = list(set().union(*[e.free_symbols for e in exprs]))
2572 if len(args) == expr_len + nb_of_free_symbols:
2573 #Ranges given
2574 plots = [exprs + Tuple(*args[expr_len:])]
2575 else:
2576 default_range = Tuple(-10, 10)
2577 ranges = []
2578 for symbol in free_symbols:
2579 ranges.append(Tuple(symbol) + default_range)
2581 for i in range(len(free_symbols) - nb_of_free_symbols):
2582 ranges.append(Tuple(Dummy()) + default_range)
2583 plots = [exprs + Tuple(*ranges)]
2584 return plots
2586 if isinstance(args[0], Expr) or (isinstance(args[0], Tuple) and
2587 len(args[0]) == expr_len and
2588 expr_len != 3):
2589 # Cannot handle expressions with number of expression = 3. It is
2590 # not possible to differentiate between expressions and ranges.
2591 #Series of plots with same range
2592 for i in range(len(args)):
2593 if isinstance(args[i], Tuple) and len(args[i]) != expr_len:
2594 break
2595 if not isinstance(args[i], Tuple):
2596 args[i] = Tuple(args[i])
2597 else:
2598 i = len(args) + 1
2600 exprs = args[:i]
2601 assert all(isinstance(e, Expr) for expr in exprs for e in expr)
2602 free_symbols = list(set().union(*[e.free_symbols for expr in exprs
2603 for e in expr]))
2605 if len(free_symbols) > nb_of_free_symbols:
2606 raise ValueError("The number of free_symbols in the expression "
2607 "is greater than %d" % nb_of_free_symbols)
2608 if len(args) == i + nb_of_free_symbols and isinstance(args[i], Tuple):
2609 ranges = Tuple(*list(args[
2610 i:i + nb_of_free_symbols]))
2611 plots = [expr + ranges for expr in exprs]
2612 return plots
2613 else:
2614 # Use default ranges.
2615 default_range = Tuple(-10, 10)
2616 ranges = []
2617 for symbol in free_symbols:
2618 ranges.append(Tuple(symbol) + default_range)
2620 for i in range(nb_of_free_symbols - len(free_symbols)):
2621 ranges.append(Tuple(Dummy()) + default_range)
2622 ranges = Tuple(*ranges)
2623 plots = [expr + ranges for expr in exprs]
2624 return plots
2626 elif isinstance(args[0], Tuple) and len(args[0]) == expr_len + nb_of_free_symbols:
2627 # Multiple plots with different ranges.
2628 for arg in args:
2629 for i in range(expr_len):
2630 if not isinstance(arg[i], Expr):
2631 raise ValueError("Expected an expression, given %s" %
2632 str(arg[i]))
2633 for i in range(nb_of_free_symbols):
2634 if not len(arg[i + expr_len]) == 3:
2635 raise ValueError("The ranges should be a tuple of "
2636 "length 3, got %s" % str(arg[i + expr_len]))
2637 return args