from spb.defaults import TWO_D_B, THREE_D_B, cfg
from spb.functions import _set_labels
from spb.series import (
BaseSeries, Vector2DSeries, Vector3DSeries, ContourSeries,
SliceVector3DSeries, InteractiveSeries, _set_discretization_points
)
from spb.utils import (
_plot_sympify, _unpack_args_extended, _split_vector, _is_range,
_instantiate_backend
)
from sympy import (
Tuple, sqrt, Expr, S, Plane
)
from sympy.external import import_module
def _build_series(*args, interactive=False, **kwargs):
"""Loop over args and create all the necessary series to display a vector
plot.
"""
np = import_module('numpy')
series = []
all_ranges = []
is_vec_lambda_function = False
for a in args:
split_expr, ranges, s = _series(
a[0], *a[1:-1], label=a[-1], interactive=interactive, **kwargs
)
all_ranges.append(ranges)
if isinstance(s, (list, tuple)):
series += s
else:
series.append(s)
if any(callable(e) for e in split_expr):
is_vec_lambda_function = True
# add a scalar series only on 2D plots
if all([s.is_2Dvector for s in series]):
# NOTE: don't pop this keyword: some backend needs it to decide the
# color for quivers (solid color if a scalar field is present, gradient
# color otherwise)
scalar = kwargs.get("scalar", True)
if (len(series) == 1) and (scalar is True):
if not is_vec_lambda_function:
scalar_field = sqrt(split_expr[0] ** 2 + split_expr[1] ** 2)
else:
scalar_field = lambda x, y: (np.sqrt(
split_expr[0](x, y) ** 2 + split_expr[1](x, y) ** 2))
scalar_label = "Magnitude"
elif scalar is True:
scalar_field = None # do nothing when
elif isinstance(scalar, Expr):
scalar_field = scalar
scalar_label = str(scalar)
elif isinstance(scalar, (list, tuple)):
scalar_field = scalar[0]
scalar_label = scalar[1]
elif callable(scalar):
scalar_field = scalar
scalar_label = "Magnitude"
elif not scalar:
scalar_field = None
else:
raise ValueError(
"`scalar` must be either:\n"
+ "1. True, in which case the magnitude of the vector field "
+ "will be plotted.\n"
+ "2. a symbolic expression representing a scalar field.\n"
+ "3. None/False: do not plot any scalar field.\n"
+ "4. list/tuple of two elements, [scalar_expr, label].\n"
+ "5. a numerical function of 2 variables supporting "
+ "vectorization."
)
if scalar_field:
# plot the scalar field over the entire region covered by all
# vector fields
_minx, _maxx = float("inf"), -float("inf")
_miny, _maxy = float("inf"), -float("inf")
for r in all_ranges:
_xr, _yr = r
if _xr[1] < _minx:
_minx = _xr[1]
if _xr[2] > _maxx:
_maxx = _xr[2]
if _yr[1] < _miny:
_miny = _yr[1]
if _yr[2] > _maxy:
_maxy = _yr[2]
cranges = [
Tuple(all_ranges[-1][0][0], _minx, _maxx),
Tuple(all_ranges[-1][1][0], _miny, _maxy),
]
nc = kwargs.pop("nc", 100)
cs_kwargs = kwargs.copy()
cs_kwargs["n1"] = nc
cs_kwargs["n2"] = nc
if not interactive:
cs = ContourSeries(scalar_field, *cranges, scalar_label, **cs_kwargs)
else:
cs = InteractiveSeries(
[scalar_field], cranges, scalar_label, **cs_kwargs
)
series = [cs] + series
return series
def _series(expr, *ranges, label="", interactive=False, **kwargs):
"""Create a vector series from the provided arguments."""
params = kwargs.get("params", dict())
fill_ranges = True if params == dict() else False
# convert expr to a list of 3 elements
split_expr, ranges = _split_vector(expr, ranges, fill_ranges)
# free symbols contained in the provided vector
fs = set()
if not any(callable(e) for e in split_expr):
fs = fs.union(*[e.free_symbols for e in split_expr])
# if we are building a parametric-interactive series, remove the
# parameters
fs = fs.difference(params.keys())
if split_expr[2] is S.Zero: # 2D case
kwargs = _set_discretization_points(kwargs.copy(), Vector2DSeries)
if len(fs) > 2:
raise ValueError(
"Too many free symbols. 2D vector plots require "
+ "at most 2 free symbols. Received {}".format(fs)
)
# check validity of ranges
fs_ranges = set().union([r[0] for r in ranges])
if len(fs_ranges) < 2:
missing = fs.difference(fs_ranges)
if not missing:
raise ValueError(
"In a 2D vector field, 2 unique ranges are expected. "
+ "Unfortunately, it is not possible to deduce them from "
+ "the provided vector.\n"
+ "Vector: {}, Free symbols: {}\n".format(expr, fs)
+ "Provided ranges: {}".format(ranges)
)
ranges = list(ranges)
for m in missing:
ranges.append(Tuple(m, cfg["plot_range"]["min"], cfg["plot_range"]["max"]))
if len(ranges) > 2:
raise ValueError("Too many ranges for 2D vector plot.")
if not interactive:
return (
split_expr,
ranges,
Vector2DSeries(*split_expr[:2], *ranges, label, **kwargs),
)
return (
split_expr,
ranges,
InteractiveSeries(split_expr[:2], ranges, label, **kwargs),
)
else: # 3D case
kwargs = _set_discretization_points(kwargs.copy(), Vector3DSeries)
if len(fs) > 3:
raise ValueError(
"Too many free symbols. 3D vector plots require "
+ "at most 3 free symbols. Received {}".format(fs)
)
# check validity of ranges
fs_ranges = set().union([r[0] for r in ranges])
if len(fs_ranges) < 3:
missing = fs.difference(fs_ranges)
if not missing:
raise ValueError(
"In a 3D vector field, 3 unique ranges are expected. "
+ "Unfortunately, it is not possible to deduce them from "
+ "the provided vector.\n"
+ "Vector: {}, Free symbols: {}\n".format(expr, fs)
+ "Provided ranges: {}".format(ranges)
)
ranges = list(ranges)
for m in missing:
ranges.append(Tuple(m, cfg["plot_range"]["min"], cfg["plot_range"]["max"]))
if len(ranges) > 3:
raise ValueError("Too many ranges for 3D vector plot.")
_slice = kwargs.pop("slice", None)
if _slice is None:
if not interactive:
return (
split_expr,
ranges,
Vector3DSeries(*split_expr, *ranges, label, **kwargs),
)
return (
split_expr,
ranges,
InteractiveSeries(split_expr, ranges, label, **kwargs),
)
# verify that the slices are of the correct type
# NOTE: currently, the slice cannot be a lambda function. To understand
# the reason, look at series.py -> _build_slice_series: we use
# symbolic manipulation!
def _check_slice(s):
if not isinstance(s, (Expr, Plane, BaseSeries)):
raise ValueError(
"A slice must be of type Plane or Expr or BaseSeries.\n"
+ "Received: {}, {}".format(type(s), s)
)
if isinstance(_slice, (list, tuple, Tuple)):
for s in _slice:
_check_slice(s)
else:
_check_slice(_slice)
_slice = [_slice]
series = []
for s in _slice:
if not interactive:
series.append(
SliceVector3DSeries(s, *split_expr, *ranges, label, **kwargs)
)
else:
# TODO: this needs to be redone
series.append(
InteractiveSeries(split_expr, ranges, label, slice=s, **kwargs)
)
return split_expr, ranges, series
def _preprocess(*args, matrices=False, fill_ranges=True):
"""Loops over the arguments and build a list of arguments having the
following form: [expr, *ranges, label].
`expr` can be a vector, a matrix or a list/tuple/Tuple.
`matrices` and `fill_ranges` are going to be passed to
`_unpack_args_extended`.
"""
if not all([isinstance(a, (list, tuple, Tuple)) for a in args]):
# In this case we received arguments in one of the following forms.
# Here we wrapped them into a list, so that they can be further
# processed:
# v -> [v]
# v, range -> [v, range]
# v1, v2, ..., range -> [v1, v2, range]
args = [args]
if any([_is_range(a) for a in args]):
args = [args]
new_args = []
for a in args:
exprs, ranges, label, rendering_kw = _unpack_args_extended(
*a, matrices=matrices, fill_ranges=fill_ranges
)
if len(exprs) == 1:
new_args.append([*exprs, *ranges, label])
else:
# this is the case where the user provided: v1, v2, ..., range
# we use the same ranges for each expression
for e in exprs:
new_args.append([e, *ranges, None])
return new_args
[docs]def plot_vector(*args, **kwargs):
"""
Plot a 2D or 3D vector field. By default, the aspect ratio of the plot
is set to `aspect="equal"`.
Typical usage examples are in the followings:
- Plotting a vector field with a single range.
`plot(expr, range1, range2, range3 [optional], **kwargs)`
- Plotting multiple vector fields with different ranges and custom labels.
`plot((expr1, range1, range2, range3 [optional], label1 [optional]), (expr2, range4, range5, range6 [optional], label2 [optional]), **kwargs)`
Parameters
==========
args :
expr : Vector, or Matrix/list/tuple with 2 or 3 elements
Represents the vector to be plotted. It can be a:
* Vector from the `sympy.vector` module or from the
`sympy.physics.mechanics` module.
* Matrix/list/tuple with 2 (or 3) symbolic elements.
* list/tuple with 2 (or 3) numerical functions of 2 (or 3)
variables.
Note: if a 3D symbolic vector is given with a list/tuple, it might
happens that the internal algorithm thinks of it as a range.
Therefore, 3D vectors should be given as a Matrix or as a Vector:
this reduces ambiguities.
ranges : 3-element tuples
Denotes the range of the variables. For example (x, -5, 5). For 2D
vector plots, 2 ranges must be provided. For 3D vector plots, 3
ranges are needed.
label : str, optional
The name of the vector field to be eventually shown on the legend
or colorbar. If none is provided, the string representation of
the vector will be used.
aspect : (float, float) or str, optional
Set the aspect ratio of the plot. The value depends on the backend
being used. Read that backend's documentation to find out the
possible values.
backend : Plot, optional
A subclass of `Plot`, which will perform the rendering.
Default to `MatplotlibBackend`.
contour_kw : dict
A dictionary of keywords/values which is passed to the backend
contour function to customize the appearance. Refer to the plotting
library (backend) manual for more informations.
label : list/tuple, optional
The label to be shown in the colorbar if ``scalar=None``.
If not provided, the string representation of `expr` will be used.
The number of labels must be equal to the number of series generated
by the plotting function. For example:
* if a scalar field and a quiver (or streamline) are shown
simultaneously, then two labels must be provided;
* if only a quiver (or streamline) is shown, then one label must be
provided.
* if two or more quivers (or streamlines) are shown, then two or more
labels must be provided.
n1, n2, n3 : int
Number of discretization points for the quivers or streamlines in the
x/y/z-direction, respectively. Default to 25.
n : int or three-elements tuple (n1, n2, n3), optional
If an integer is provided, the ranges are sampled uniformly
at `n` number of points. If a tuple is provided, it overrides
`n1`, `n2` and `n3`. Default to 25.
nc : int
Number of discretization points for the scalar contour plot.
Default to 100.
normalize : bool
Default to False. If True, the vector field will be normalized,
resulting in quivers having the same length. If ``use_cm=True``, the
backend will color the quivers by the (pre-normalized) vector field's
magnitude. Note: only quivers will be affected by this option.
params : dict
A dictionary mapping symbols to parameters. This keyword argument
enables the interactive-widgets plot, which doesn't support the
adaptive algorithm (meaning it will use ``adaptive=False``).
Learn more by reading the documentation of ``iplot``.
quiver_kw : dict
A dictionary of keywords/values which is passed to the backend quivers-
plotting function to customize the appearance. Refer to the plotting
library (backend) manual for more informations.
rendering_kw : list of dicts, optional
A list of dictionaries of keywords/values which is passed to the
backend's functions to customize the appearance.
The number of dictionaries must be equal to the number of series
generated by the plotting function. For example:
* if a scalar field and a quiver (or streamline) are shown
simultaneously, then two dictionaries must be provided;
* if only a quiver (or streamline) is shown, then one dictionary must
be provided.
* if two or more quivers (or streamlines) are shown, then two or more
dictionaries must be provided.
Note that this will override ``quiver_kw``, ``stream_kw``,
``contour_kw``.
scalar : boolean, Expr, None or list/tuple of 2 elements
Represents the scalar field to be plotted in the background of a 2D
vector field plot. Can be:
- `True`: plot the magnitude of the vector field. Only works when a
single vector field is plotted.
- `False`/`None`: do not plot any scalar field.
- `Expr`: a symbolic expression representing the scalar field.
- a numerical function of 2 variables supporting vectorization.
- `list`/`tuple`: [scalar_expr, label], where the label will be
shown on the colorbar. scalar_expr can be a symbolic expression
or a numerical function of 2 variables supporting vectorization.
Default to True.
show : boolean
The default value is set to `True`. Set show to `False` and
the function will not display the plot. The returned instance of
the `Plot` class can then be used to save or display the plot
by calling the `save()` and `show()` methods respectively.
size : (float, float), optional
A tuple in the form (width, height) to specify the size of
the overall figure. The default value is set to `None`, meaning
the size will be set by the backend.
slice : Plane, list, Expr
Plot the 3D vector field over the provided slice. It can be:
- a Plane object from sympy.geometry module.
- a list of planes.
- an instance of ``SurfaceOver2DRangeSeries`` or
``ParametricSurfaceSeries``.
- a symbolic expression representing a surface of two variables.
The number of discretization points will be `n1`, `n2`, `n3`.
Note that:
- only quivers plots are supported with slices. Streamlines plots
are unaffected.
- `n3` will only be used with planes parallel to xz or yz.
- `n1`, `n2`, `n3` doesn't affect the slice if it is an instance of
``SurfaceOver2DRangeSeries`` or ``ParametricSurfaceSeries``.
streamlines : boolean
Whether to plot the vector field using streamlines (True) or quivers
(False). Default to False.
stream_kw : dict
A dictionary of keywords/values which is passed to the backend
streamlines-plotting function to customize the appearance. Refer to
the Notes section to learn more.
For 3D vector fields, by default the streamlines will start at the
boundaries of the domain where the vectors are pointed inward.
Depending on the vector field, this may results in too tight
streamlines. Use the `starts` keyword argument to control the
generation of streamlines:
- `starts=None`: the default aforementioned behaviour.
- `starts=dict(x=x_list, y=y_list, z=z_list)`: specify the starting
points of the streamlines.
- `starts=True`: randomly create starting points inside the domain.
In this setup we can set the number of starting point with `npoints`
(default value to 200).
If 3D streamlines appears to be cut short inside the specified domain,
try to increase `max_prop` (default value to 5000).
title : str, optional
Title of the plot. It is set to the latex representation of
the expression, if the plot has only one expression.
use_latex : boolean, optional
Turn on/off the rendering of latex labels. If the backend doesn't
support latex, it will render the string representations instead.
xlabel : str, optional
Label for the x-axis.
ylabel : str, optional
Label for the y-axis.
zlabel : str, optional
Label for the z-axis. Only available for 3D plots.
xlim : (float, float), optional
Denotes the x-axis limits, `(min, max)`.
ylim : (float, float), optional
Denotes the y-axis limits, `(min, max)`.
zlim : (float, float), optional
Denotes the z-axis limits, `(min, max)`. Only available for 3D plots.
Examples
========
.. plot::
:context: reset
:format: doctest
:include-source: True
>>> from sympy import symbols, sin, cos, Plane, Matrix, sqrt, latex
>>> from spb.vectors import plot_vector
>>> x, y, z = symbols('x, y, z')
Quivers plot of a 2D vector field with a contour plot in background
representing the vector's magnitude (a scalar field).
.. plot::
:context: close-figs
:format: doctest
:include-source: True
>>> v = [-sin(y), cos(x)]
>>> plot_vector(v, (x, -3, 3), (y, -3, 3),
... quiver_kw=dict(color="black"),
... contour_kw={"cmap": "Blues_r", "levels": 20},
... grid=False, xlabel="x", ylabel="y")
Plot object containing:
[0]: contour: sqrt(sin(y)**2 + cos(x)**2) for x over (-3.0, 3.0) and y over (-3.0, 3.0)
[1]: 2D vector series: [-sin(y), cos(x)] over (x, -3.0, 3.0), (y, -3.0, 3.0)
Quivers plot of a 2D vector field with no background scalar field,
a custom label and normalized quiver lengths:
.. plot::
:context: close-figs
:format: doctest
:include-source: True
>>> plot_vector(
... v, (x, -3, 3), (y, -3, 3),
... label="Magnitude of $%s$" % latex([-sin(y), cos(x)]),
... scalar=False, normalize=True,
... grid=False, xlabel="x", ylabel="y")
Plot object containing:
[0]: contour: sqrt(sin(y)**2 + cos(x)**2) for x over (-3.0, 3.0) and y over (-3.0, 3.0)
[1]: 2D vector series: [-sin(y), cos(x)] over (x, -3.0, 3.0), (y, -3.0, 3.0)
Streamlines plot of a 2D vector field with no background scalar field, and
a custom label:
.. plot::
:context: close-figs
:format: doctest
:include-source: True
>>> expr = [-sin(y), cos(x)]
>>> plot_vector(expr, (x, -3, 3), (y, -3, 3),
... streamlines=True, scalar=None,
... label="Magnitude of %s" % str(expr), xlabel="x", ylabel="y")
Plot object containing:
[0]: 2D vector series: [-sin(y), cos(x)] over (x, -3.0, 3.0), (y, -3.0, 3.0)
Plot multiple 2D vectors fields, setting a background scalar field to be
the magnitude of the first vector. Also, apply custom rendering options
to all data series.
.. plot::
:context: close-figs
:format: doctest
:include-source: True
>>> scalar_expr = sqrt((-sin(y))**2 + cos(x)**2)
>>> plot_vector([-sin(y), cos(x)], [2 * y, x], (x, -5, 5), (y, -3, 3),
... n=20, legend=True, grid=False, xlabel="x", ylabel="y",
... scalar=[scalar_expr, "$%s$" % latex(scalar_expr)],
... rendering_kw=[
... {"cmap": "summer"}, # to the contour
... {"color": "k"}, # to the first quiver
... {"color": "w"} # to the second quiver
... ])
Plot object containing:
[0]: contour: sqrt(sin(y)**2 + cos(x)**2) for x over (-5.0, 5.0) and y over (-3.0, 3.0)
[1]: 2D vector series: [-sin(y), cos(x)] over (x, -5.0, 5.0), (y, -3.0, 3.0)
[2]: 2D vector series: [y, x] over (x, -5.0, 5.0), (y, -3.0, 3.0)
Plotting a the streamlines of a 2D vector field defined with numerical
functions instead of symbolic expressions:
.. plot::
:context: close-figs
:format: doctest
:include-source: True
>>> import numpy as np
>>> f = lambda x, y: np.sin(2 * x + 2 * y)
>>> fx = lambda x, y: np.cos(f(x, y))
>>> fy = lambda x, y: np.sin(f(x, y))
>>> plot_vector([fx, fy], ("x", -1, 1), ("y", -1, 1),
... streamlines=True, scalar=False, use_cm=False)
Interactive-widget 2D vector plot. Refer to ``iplot`` documentation to
learn more about the ``params`` dictionary.
.. code-block:: python
from sympy import *
x, y, u = symbols("x y u")
plot_vector(
[-sin(u * y), cos(x)], (x, -3, 3), (y, -3, 3),
params={u: (1, 0, 2)},
n=20,
quiver_kw=dict(color="black"),
contour_kw={"cmap": "Blues_r", "levels": 20},
grid=False, xlabel="x", ylabel="y")
3D vector field.
.. plot::
:context: close-figs
:format: doctest
:include-source: True
>>> plot_vector([z, y, x], (x, -10, 10), (y, -10, 10), (z, -10, 10),
... label="Magnitude", n=8, quiver_kw={"length": 0.1},
... xlabel="x", ylabel="y", zlabel="z")
Plot object containing:
[0]: 3D vector series: [z, y, x] over (x, -10.0, 10.0), (y, -10.0, 10.0), (z, -10.0, 10.0)
3D vector field with 3 orthogonal slice planes.
.. plot::
:context: close-figs
:format: doctest
:include-source: True
>>> plot_vector([z, y, x], (x, -10, 10), (y, -10, 10), (z, -10, 10),
... n=8, quiver_kw={"length": 0.1},
... slice=[
... Plane((-10, 0, 0), (1, 0, 0)),
... Plane((0, 10, 0), (0, 2, 0)),
... Plane((0, 0, -10), (0, 0, 1))],
... label=["Magnitude"] * 3, xlabel="x", ylabel="y", zlabel="z")
Plot object containing:
[0]: sliced 3D vector series: [z, y, x] over (x, -10.0, 10.0), (y, -10.0, 10.0), (z, -10.0, 10.0) at Plane(Point3D(-10, 0, 0), (1, 0, 0))
[1]: sliced 3D vector series: [z, y, x] over (x, -10.0, 10.0), (y, -10.0, 10.0), (z, -10.0, 10.0) at Plane(Point3D(0, 10, 0), (0, 2, 0))
[2]: sliced 3D vector series: [z, y, x] over (x, -10.0, 10.0), (y, -10.0, 10.0), (z, -10.0, 10.0) at Plane(Point3D(0, 0, -10), (0, 0, 1))
3D vector streamlines starting at a 300 random points:
.. plot::
:context: close-figs
:format: doctest
:include-source: True
>>> plot_vector(Matrix([z, -x, y]), (x, -3, 3), (y, -3, 3), (z, -3, 3),
... n=40, streamlines=True,
... stream_kw=dict(
... starts=True,
... npoints=300
... ),
... label="Magnitude", xlabel="x", ylabel="y", zlabel="z")
Plot object containing:
[0]: 3D vector series: [z, -x, y] over (x, -3.0, 3.0), (y, -3.0, 3.0), (z, -3.0, 3.0)
Visually verify the normal vector to a circular cone surface.
The following steps are executed:
1. compute the normal vector to a circular cone surface. This will be the
vector field to be plotted.
2. plot the cone surface for visualization purposes (use high number of
discretization points).
3. plot the cone surface that will be used to slice the vector field (use
a low number of discretization points). The data series associated to
this plot will be used in the ``slice`` keyword argument in the next
step.
4. plot the sliced vector field.
5. combine the plots of step 4 and 2 to get a nice visualization.
.. plot::
:context: close-figs
:format: doctest
:include-source: True
>>> from sympy import tan, cos, sin, pi, symbols
>>> from spb import plot3d_parametric_surface
>>> from sympy.vector import CoordSys3D, gradient
>>> u, v = symbols("u, v")
>>> N = CoordSys3D("N")
>>> i, j, k = N.base_vectors()
>>> xn, yn, zn = N.base_scalars()
>>> t = 0.35 # half-cone angle in radians
>>> expr = -xn**2 * tan(t)**2 + yn**2 + zn**2 # cone surface equation
>>> g = gradient(expr)
>>> n = g / g.magnitude() # unit normal vector
>>> n1, n2 = 10, 20 # number of discretization points for the vector field
>>> # cone surface for visualization (high number of discretization points)
>>> p1 = plot3d_parametric_surface(
... u / tan(t), u * cos(v), u * sin(v), (u, 0, 1), (v, 0 , 2*pi),
... {"alpha": 0.15}, show=False, wireframe=True,
... wf_n1=n1, wf_n2=n2, wf_rendering_kw={"lw": 0.75, "alpha": 0.5})
>>> # cone surface for data series generation (low numb of discret points)
>>> p2 = plot3d_parametric_surface(
... u / tan(t), u * cos(v), u * sin(v), (u, 0, 1), (v, 0 , 2*pi),
... n1=n1, n2=n2, show=False)
>>> p3 = plot_vector(
... n, (xn, -5, 5), (yn, -5, 5), (zn, -5, 5), slice=p2[0],
... use_cm=False, show=False, quiver_kw={"length": 0.2})
>>> (p1 + p3).show()
See Also
========
iplot
"""
args = _plot_sympify(args)
args = _preprocess(*args)
kwargs = _set_discretization_points(kwargs, Vector3DSeries)
kwargs.setdefault("aspect", "equal")
kwargs.setdefault("legend", True)
params = kwargs.get("params", None)
is_interactive = False if params is None else True
kwargs["is_interactive"] = is_interactive
if is_interactive:
from spb.interactive import iplot
kwargs["is_vector"] = True
return iplot(*args, **kwargs)
labels = kwargs.pop("label", [])
rendering_kw = kwargs.pop("rendering_kw", None)
series = _build_series(*args, **kwargs)
if all([isinstance(s, (Vector2DSeries, ContourSeries)) for s in series]):
Backend = kwargs.pop("backend", TWO_D_B)
elif all([isinstance(s, Vector3DSeries) for s in series]):
Backend = kwargs.pop("backend", THREE_D_B)
else:
raise ValueError("Mixing 2D vectors with 3D vectors is not allowed.")
_set_labels(series, labels, rendering_kw)
return _instantiate_backend(Backend, *series, **kwargs)