Source code for manim.mobject.coordinate_systems

"""Mobjects that represent coordinate systems."""


__all__ = [
    "CoordinateSystem",
    "Axes",
    "ThreeDAxes",
    "NumberPlane",
    "PolarPlane",
    "ComplexPlane",
]

import fractions as fr
import numbers
from typing import Callable, Dict, Iterable, List, Optional, Sequence, Tuple, Union

import numpy as np
from colour import Color

from manim.mobject.opengl_compatibility import ConvertToOpenGL

from .. import config
from ..constants import *
from ..mobject.functions import ImplicitFunction, ParametricFunction
from ..mobject.geometry import (
    Arrow,
    Circle,
    DashedLine,
    Dot,
    Line,
    Polygon,
    Rectangle,
    RegularPolygon,
)
from ..mobject.number_line import NumberLine
from ..mobject.svg.tex_mobject import MathTex
from ..mobject.types.vectorized_mobject import (
    Mobject,
    VDict,
    VectorizedPoint,
    VGroup,
    VMobject,
)
from ..utils.color import (
    BLACK,
    BLUE,
    BLUE_D,
    GREEN,
    LIGHT_GREY,
    WHITE,
    YELLOW,
    color_gradient,
    invert_color,
)
from ..utils.config_ops import merge_dicts_recursively, update_dict_recursively
from ..utils.deprecation import deprecated, deprecated_params
from ..utils.simple_functions import binary_search
from ..utils.space_ops import angle_of_vector


[docs]class CoordinateSystem: r""" Abstract class for Axes and NumberPlane Examples -------- .. manim:: CoordSysExample :save_last_frame: class CoordSysExample(Scene): def construct(self): # the location of the ticks depends on the x_range and y_range. grid = Axes( x_range=[0, 1, 0.05], # step size determines num_decimal_places. y_range=[0, 1, 0.05], x_length=9, y_length=5.5, axis_config={ "numbers_to_include": np.arange(0, 1 + 0.1, 0.1), "font_size": 24, }, tips=False, ) # Labels for the x-axis and y-axis. y_label = grid.get_y_axis_label("y", edge=LEFT, direction=LEFT, buff=0.4) x_label = grid.get_x_axis_label("x") grid_labels = VGroup(x_label, y_label) graphs = VGroup() for n in np.arange(1, 20 + 0.5, 0.5): graphs += grid.get_graph(lambda x: x ** n, color=WHITE) graphs += grid.get_graph( lambda x: x ** (1 / n), color=WHITE, use_smoothing=False ) # Extra lines and labels for point (1,1) graphs += grid.get_horizontal_line(grid.c2p(1, 1, 0), color=BLUE) graphs += grid.get_vertical_line(grid.c2p(1, 1, 0), color=BLUE) graphs += Dot(point=grid.c2p(1, 1, 0), color=YELLOW) graphs += Tex("(1,1)").scale(0.75).next_to(grid.c2p(1, 1, 0)) title = Title( # spaces between braces to prevent SyntaxError r"Graphs of $y=x^{ {1}\over{n} }$ and $y=x^n (n=1,2,3,...,20)$", include_underline=False, font_size=40, ) self.add(title, graphs, grid, grid_labels) """ def __init__( self, x_range=None, y_range=None, x_length=None, y_length=None, dimension=2, ): self.dimension = dimension default_step = 1 if x_range is None: x_range = [ round(-config["frame_x_radius"]), round(config["frame_x_radius"]), default_step, ] elif len(x_range) == 2: x_range = [*x_range, default_step] if y_range is None: y_range = [ round(-config["frame_y_radius"]), round(config["frame_y_radius"]), default_step, ] elif len(y_range) == 2: y_range = [*y_range, default_step] self.x_range = x_range self.y_range = y_range self.x_length = x_length self.y_length = y_length self.num_sampled_graph_points_per_tick = 10 def coords_to_point(self, *coords): raise NotImplementedError() def point_to_coords(self, point): raise NotImplementedError()
[docs] def c2p(self, *coords): """Abbreviation for coords_to_point""" return self.coords_to_point(*coords)
[docs] def p2c(self, point): """Abbreviation for point_to_coords""" return self.point_to_coords(point)
def get_axes(self): raise NotImplementedError() def get_axis(self, index): return self.get_axes()[index]
[docs] @deprecated(since="v0.10.0", until="v0.11.0", message="Use get_origin instead.") def get_center_point(self) -> np.ndarray: """Gets the origin of :class:`~.Axes`. Returns ------- np.ndarray The center point. """ return self.coords_to_point(0, 0)
[docs] def get_origin(self) -> np.ndarray: """Gets the origin of :class:`~.Axes`. Returns ------- np.ndarray The center point. """ return self.coords_to_point(0, 0)
def get_x_axis(self): return self.get_axis(0) def get_y_axis(self): return self.get_axis(1) def get_z_axis(self): return self.get_axis(2) def get_x_unit_size(self): return self.get_x_axis().get_unit_size() def get_y_unit_size(self): return self.get_y_axis().get_unit_size()
[docs] def get_x_axis_label( self, label: Union[float, str, "Mobject"], edge: Sequence[float] = UR, direction: Sequence[float] = UR, buff: float = SMALL_BUFF, **kwargs, ) -> "Mobject": """Generate an x-axis label. Examples -------- .. manim:: GetXAxisLabelExample :save_last_frame: class GetXAxisLabelExample(Scene): def construct(self): ax = Axes(x_range=(0, 8), y_range=(0, 5), x_length=8, y_length=5) x_label = ax.get_x_axis_label( Tex("$x$-values").scale(0.65), edge=DOWN, direction=DOWN, buff=0.5 ) self.add(ax, x_label) Parameters ---------- label The label. Defaults to :class:`~.MathTex` for ``str`` and ``float`` inputs. edge The edge of the x-axis to which the label will be added, by default ``UR``. direction Allows for further positioning of the label from an edge, by default ``UR``. buff The distance of the label from the line. Returns ------- :class:`~.Mobject` The positioned label. """ return self._get_axis_label( label, self.get_x_axis(), edge, direction, buff=buff, **kwargs )
[docs] def get_y_axis_label( self, label: Union[float, str, "Mobject"], edge: Sequence[float] = UR, direction: Sequence[float] = UP * 0.5 + RIGHT, buff: float = SMALL_BUFF, **kwargs, ): """Generate a y-axis label. Examples -------- .. manim:: GetYAxisLabelExample :save_last_frame: class GetYAxisLabelExample(Scene): def construct(self): ax = Axes(x_range=(0, 8), y_range=(0, 5), x_length=8, y_length=5) y_label = ax.get_y_axis_label( Tex("$y$-values").scale(0.65).rotate(90 * DEGREES), edge=LEFT, direction=LEFT, buff=0.3, ) self.add(ax, y_label) Parameters ---------- label The label. Defaults to :class:`~.MathTex` for ``str`` and ``float`` inputs. edge The edge of the x-axis to which the label will be added, by default ``UR``. direction Allows for further positioning of the label from an edge, by default ``UR`` buff The distance of the label from the line. Returns ------- :class:`~.Mobject` The positioned label. """ return self._get_axis_label( label, self.get_y_axis(), edge, direction, buff=buff, **kwargs )
# move to a util_file, or Mobject()?? @staticmethod def _create_label_tex(label_tex) -> "Mobject": """Checks if the label is a ``float``, ``int`` or a ``str`` and creates a :class:`~.MathTex` label accordingly. Parameters ---------- label_tex : The label to be compared against the above types. Returns ------- :class:`~.Mobject` The label. """ if isinstance(label_tex, (float, int, str)): label_tex = MathTex(label_tex) return label_tex def _get_axis_label( self, label: Union[float, str, "Mobject"], axis: "Mobject", edge: Sequence[float], direction: Sequence[float], buff: float = SMALL_BUFF, ) -> "Mobject": """Gets the label for an axis. Parameters ---------- label The label. Defaults to :class:`~.MathTex` for ``str`` and ``float`` inputs. axis The axis to which the label will be added. edge The edge of the axes to which the label will be added. ``RIGHT`` adds to the right side of the axis direction Allows for further positioning of the label. buff The distance of the label from the line. Returns ------- :class:`~.Mobject` The positioned label along the given axis. """ label = self._create_label_tex(label) label.next_to(axis.get_edge_center(edge), direction=direction, buff=buff) label.shift_onto_screen(buff=MED_SMALL_BUFF) return label
[docs] def get_axis_labels( self, x_label: Union[float, str, "Mobject"] = "x", y_label: Union[float, str, "Mobject"] = "y", ) -> VGroup: """Defines labels for the x_axis and y_axis of the graph. For increased control over the position of the labels, use :meth:`get_x_axis_label` and :meth:`get_y_axis_label`. Examples -------- .. manim:: GetAxisLabelsExample :save_last_frame: class GetAxisLabelsExample(Scene): def construct(self): ax = Axes() labels = ax.get_axis_labels( Tex("x-axis").scale(0.7), Text("y-axis").scale(0.45) ) self.add(ax, labels) Parameters ---------- x_label The label for the x_axis. Defaults to :class:`~.MathTex` for ``str`` and ``float`` inputs. y_label The label for the y_axis. Defaults to :class:`~.MathTex` for ``str`` and ``float`` inputs. Returns ------- :class:`~.VGroup` A :class:`~.Vgroup` of the labels for the x_axis and y_axis. See Also -------- :class:`get_x_axis_label` :class:`get_y_axis_label` """ self.axis_labels = VGroup( self.get_x_axis_label(x_label), self.get_y_axis_label(y_label), ) return self.axis_labels
[docs] def add_coordinates( self, *axes_numbers: Union[ Optional[Iterable[float]], Union[Dict[float, Union[str, float, "Mobject"]]], ], **kwargs, ): """Adds labels to the axes. Use ``Axes.coordinate_labels`` to access the coordinates after creation. Parameters ---------- axes_numbers The numbers to be added to the axes. Use ``None`` to represent an axis with default labels. Examples -------- .. code-block:: python ax = ThreeDAxes() x_labels = range(-4, 5) z_labels = range(-4, 4, 2) ax.add_coordinates(x_labels, None, z_labels) # default y labels, custom x & z labels ax.add_coordinates(x_labels) # only x labels You can also specifically control the position and value of the labels using a dict. .. code-block:: python ax = Axes(x_range=[0, 7]) x_pos = [x for x in range(1, 8)] # strings are automatically converted into a `Tex` mobject. x_vals = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"] x_dict = dict(zip(x_pos, x_vals)) ax.add_coordinates(x_dict) """ self.coordinate_labels = VGroup() # if nothing is passed to axes_numbers, produce axes with default labelling if not axes_numbers: axes_numbers = [None for _ in range(self.dimension)] for axis, values in zip(self.axes, axes_numbers): if isinstance(values, dict): axis.add_labels(values, **kwargs) labels = axis.labels else: axis.add_numbers(values, **kwargs) labels = axis.numbers self.coordinate_labels.add(labels) return self
[docs] def get_line_from_axis_to_point( self, index: int, point: Sequence[float], line_func: Line = DashedLine, line_config: Optional[Dict] = None, color: Color = LIGHT_GREY, stroke_width: float = 2, ) -> "Line": """Returns a straight line from a given axis to a point in the scene. Parameters ---------- index Specifies the axis from which to draw the line. `0 = x_axis`, `1 = y_axis` point The point to which the line will be drawn. line_func The function of the :class:`~.Line` mobject used to construct the line. line_config Optional arguments to passed to :attr:`line_func`. color The color of the line. stroke_width The stroke width of the line. Returns ------- :class:`~.Line` The line from an axis to a point. See Also -------- :meth:`~.CoordinateSystem.get_vertical_line` :meth:`~.CoordinateSystem.get_horizontal_line` """ line_config = line_config if line_config is not None else {} line_config["color"] = color line_config["stroke_width"] = stroke_width axis = self.get_axis(index) line = line_func(axis.get_projection(point), point, **line_config) return line
[docs] def get_vertical_line(self, point: Sequence[float], **kwargs) -> "Line": """A vertical line from the x-axis to a given point in the scene. Examples -------- .. manim:: GetVerticalLineExample :save_last_frame: class GetVerticalLineExample(Scene): def construct(self): ax = Axes().add_coordinates() point = ax.coords_to_point(-3.5, 2) dot = Dot(point) line = ax.get_vertical_line(point, line_config={"dashed_ratio": 0.85}) self.add(ax, line, dot) Parameters ---------- point The point to which the vertical line will be drawn. kwargs Additional parameters to be passed to :class:`get_line_from_axis_to_point` Returns ------- :class:`Line` A vertical line from the x-axis to the point. """ return self.get_line_from_axis_to_point(0, point, **kwargs)
[docs] def get_horizontal_line(self, point: Sequence[float], **kwargs) -> "Line": """A horizontal line from the y-axis to a given point in the scene. Examples -------- .. manim:: GetHorizontalLineExample :save_last_frame: class GetHorizontalLineExample(Scene): def construct(self): ax = Axes().add_coordinates() point = ax.c2p(-4, 1.5) dot = Dot(point) line = ax.get_horizontal_line(point, line_func=Line) self.add(ax, line, dot) Parameters ---------- point The point to which the horizontal line will be drawn. kwargs Additional parameters to be passed to :class:`get_line_from_axis_to_point` Returns ------- :class:`Line` A horizontal line from the y-axis to the point. """ return self.get_line_from_axis_to_point(1, point, **kwargs)
[docs] def get_lines_to_point(self, point: Sequence[float], **kwargs) -> VGroup: """Generate both horizontal and vertical lines from the axis to a point. Examples -------- .. manim:: GetLinesToPointExample :save_last_frame: class GetLinesToPointExample(Scene): def construct(self): ax = Axes() circ = Circle(radius=0.5).move_to([-4, -1.5, 0]) lines_1 = ax.get_lines_to_point(circ.get_right(), color=GREEN_B) lines_2 = ax.get_lines_to_point(circ.get_corner(DL), color=BLUE_B) self.add(ax, lines_1, lines_2, circ) Parameters ---------- point A point on the scene. kwargs Additional arguments to be provided :meth:`CoordinateSystem.get_line_from_axis_to_point`. Returns ------- :class:`~.VGroup` A :class:`~.VGroup` of the horizontal and vertical lines. See Also -------- :meth:`~.CoordinateSystem.get_vertical_line` :meth:`~.CoordinateSystem.get_horizontal_line` """ return VGroup( self.get_horizontal_line(point, **kwargs), self.get_vertical_line(point, **kwargs), )
# graphing
[docs] def get_graph( self, function: Callable[[float], float], x_range: Optional[Sequence[float]] = None, **kwargs, ): """Generates a curve based on a function. .. warning:: This method may not produce accurate graphs since Manim currently relies on interpolation between evenly-spaced samples of the curve, instead of intelligent plotting. See the example below for some solutions to this problem. Examples -------- .. manim:: GetGraphExample :save_last_frame: class GetGraphExample(Scene): def construct(self): # construct the axes ax_1 = Axes( x_range=[0.001, 6], y_range=[-8, 2], x_length=5, y_length=3, tips=False, ) ax_2 = ax_1.copy() ax_3 = ax_1.copy() # position the axes ax_1.to_corner(UL) ax_2.to_corner(UR) ax_3.to_edge(DOWN) axes = VGroup(ax_1, ax_2, ax_3) # create the logarithmic curves def log_func(x): return np.log(x) # a curve without adjustments; poor interpolation. curve_1 = ax_1.get_graph(log_func, color=PURE_RED) # disabling interpolation makes the graph look choppy as not enough # inputs are available curve_2 = ax_2.get_graph(log_func, use_smoothing=False, color=ORANGE) # taking more inputs of the curve by specifying a step for the # x_range yields expected results, but increases rendering time. curve_3 = ax_3.get_graph( log_func, x_range=(0.001, 6, 0.001), color=PURE_GREEN ) curves = VGroup(curve_1, curve_2, curve_3) self.add(axes, curves) Parameters ---------- function The function used to construct the :class:`~.ParametricFunction`. x_range The range of the curve along the axes. ``x_range = [x_min, x_max, x_step]``. kwargs Additional parameters to be passed to :class:`~.ParametricFunction`. Returns ------- :class:`~.ParametricFunction` The plotted curve. """ t_range = np.array(self.x_range, dtype=float) if x_range is not None: t_range[: len(x_range)] = x_range if x_range is None or len(x_range) < 3: # if t_range has a defined step size, increase the number of sample points per tick t_range[2] /= self.num_sampled_graph_points_per_tick # For axes, the third coordinate of x_range indicates # tick frequency. But for functions, it indicates a # sample frequency graph = ParametricFunction( lambda t: self.coords_to_point(t, function(t)), t_range=t_range, **kwargs ) graph.underlying_function = function return graph
[docs] def get_implicit_curve( self, func: Callable, min_depth: int = 5, max_quads: int = 1500, **kwargs, ) -> ImplicitFunction: """Creates the curves of an implicit function. Parameters ---------- func The function to graph, in the form of f(x, y) = 0. min_depth The minimum depth of the function to calculate. max_quads The maximum number of quads to use. kwargs Additional parameters to pass into :class:`ImplicitFunction` Examples -------- .. manim:: ImplicitExample :save_last_frame: class ImplicitExample(Scene): def construct(self): ax = Axes() a = ax.get_implicit_curve( lambda x, y: y * (x - y) ** 2 - 4 * x - 8, color=BLUE ) self.add(ax, a) """ graph = ImplicitFunction( func=func, x_range=self.x_range[:2], y_range=self.y_range[:2], min_depth=min_depth, max_quads=max_quads, **kwargs, ) ( graph.stretch(self.get_x_unit_size(), 0, about_point=ORIGIN) .stretch(self.get_y_unit_size(), 1, about_point=ORIGIN) .shift(self.get_origin()) ) return graph
def get_parametric_curve(self, function, **kwargs): dim = self.dimension graph = ParametricFunction( lambda t: self.coords_to_point(*function(t)[:dim]), **kwargs ) graph.underlying_function = function return graph
[docs] def input_to_graph_point( self, x: float, graph: Union["ParametricFunction", VMobject], ) -> np.ndarray: """Returns the coordinates of the point on a ``graph`` corresponding to an ``x`` value. Examples -------- .. manim:: InputToGraphPointExample :save_last_frame: class InputToGraphPointExample(Scene): def construct(self): ax = Axes() curve = ax.get_graph(lambda x : np.cos(x)) # move a square to PI on the cosine curve. position = ax.input_to_graph_point(x=PI, graph=curve) sq = Square(side_length=1, color=YELLOW).move_to(position) self.add(ax, curve, sq) Parameters ---------- x The x-value of a point on the ``graph``. graph The :class:`~.ParametricFunction` on which the point lies. Returns ------- :class:`np.ndarray` The coordinates of the point on the :attr:`graph` corresponding to the :attr:`x` value. Raises ------ :exc:`ValueError` When the target x is not in the range of the line graph. """ if hasattr(graph, "underlying_function"): return graph.function(x) else: alpha = binary_search( function=lambda a: self.point_to_coords(graph.point_from_proportion(a))[ 0 ], target=x, lower_bound=0, upper_bound=1, ) if alpha is not None: return graph.point_from_proportion(alpha) else: raise ValueError( f"x={x} not located in the range of the graph ([{self.p2c(graph.get_start())[0]}, {self.p2c(graph.get_end())[0]}])", )
[docs] def i2gp(self, x: float, graph: "ParametricFunction") -> np.ndarray: """ Alias for :meth:`input_to_graph_point`. """ return self.input_to_graph_point(x, graph)
[docs] def get_graph_label( self, graph: "ParametricFunction", label: Union[float, str, "Mobject"] = "f(x)", x_val: Optional[float] = None, direction: Sequence[float] = RIGHT, buff: float = MED_SMALL_BUFF, color: Optional[Color] = None, dot: bool = False, dot_config: Optional[dict] = None, ) -> Mobject: """Creates a properly positioned label for the passed graph, with an optional dot. Examples -------- .. manim:: GetGraphLabelExample :save_last_frame: class GetGraphLabelExample(Scene): def construct(self): ax = Axes() sin = ax.get_graph(lambda x: np.sin(x), color=PURPLE_B) label = ax.get_graph_label( graph=sin, label= MathTex(r"\\frac{\\pi}{2}"), x_val=PI / 2, dot=True, direction=UR, ) self.add(ax, sin, label) Parameters ---------- graph The curve. label The label for the function's curve. Defaults to :class:`~.MathTex` for ``str`` and ``float`` inputs. x_val The x_value along the curve that positions the label. direction The cartesian position, relative to the curve that the label will be at --> ``LEFT``, ``RIGHT`` buff The distance between the curve and the label. color The color of the label. Defaults to the color of the curve. dot Whether to add a dot at the point on the graph. dot_config Additional parameters to be passed to :class:`~.Dot`. Returns ------- :class:`Mobject` The positioned label and :class:`~.Dot`, if applicable. """ if dot_config is None: dot_config = {} label = self._create_label_tex(label) color = color or graph.get_color() label.set_color(color) if x_val is None: # Search from right to left for x in np.linspace(self.x_range[1], self.x_range[0], 100): point = self.input_to_graph_point(x, graph) if point[1] < config["frame_y_radius"]: break else: point = self.input_to_graph_point(x_val, graph) label.next_to(point, direction, buff=buff) label.shift_onto_screen() if dot: dot = Dot(point=point, **dot_config) label.add(dot) label.dot = dot return label
# calculus
[docs] def get_riemann_rectangles( self, graph: "ParametricFunction", x_range: Optional[Sequence[float]] = None, dx: Optional[float] = 0.1, input_sample_type: str = "left", stroke_width: float = 1, stroke_color: Color = BLACK, fill_opacity: float = 1, color: Union[Iterable[Color], Color] = np.array((BLUE, GREEN)), show_signed_area: bool = True, bounded_graph: "ParametricFunction" = None, blend: bool = False, width_scale_factor: float = 1.001, ) -> VGroup: """Generates a :class:`~.VGroup` of the Riemann Rectangles for a given curve. Examples -------- .. manim:: GetRiemannRectanglesExample :save_last_frame: class GetRiemannRectanglesExample(Scene): def construct(self): ax = Axes(y_range=[-2, 10]) quadratic = ax.get_graph(lambda x: 0.5 * x ** 2 - 0.5) # the rectangles are constructed from their top right corner. # passing an iterable to `color` produces a gradient rects_right = ax.get_riemann_rectangles( quadratic, x_range=[-4, -3], dx=0.25, color=(TEAL, BLUE_B, DARK_BLUE), input_sample_type="right", ) # the colour of rectangles below the x-axis is inverted # due to show_signed_area rects_left = ax.get_riemann_rectangles( quadratic, x_range=[-1.5, 1.5], dx=0.15, color=YELLOW ) bounding_line = ax.get_graph( lambda x: 1.5 * x, color=BLUE_B, x_range=[3.3, 6] ) bounded_rects = ax.get_riemann_rectangles( bounding_line, bounded_graph=quadratic, dx=0.15, x_range=[4, 5], show_signed_area=False, color=(MAROON_A, RED_B, PURPLE_D), ) self.add( ax, bounding_line, quadratic, rects_right, rects_left, bounded_rects ) Parameters ---------- graph The graph whose area will be approximated by Riemann rectangles. x_range The minimum and maximum x-values of the rectangles. ``x_range = [x_min, x_max]``. dx The change in x-value that separates each rectangle. input_sample_type Can be any of ``"left"``, ``"right"`` or ``"center"``. Refers to where the sample point for the height of each Riemann Rectangle will be inside the segments of the partition. stroke_width The stroke_width of the border of the rectangles. stroke_color The color of the border of the rectangle. fill_opacity The opacity of the rectangles. color The colors of the rectangles. Creates a balanced gradient if multiple colors are passed. show_signed_area Indicates negative area when the curve dips below the x-axis by inverting its color. blend Sets the :attr:`stroke_color` to :attr:`fill_color`, blending the rectangles without clear separation. bounded_graph If a secondary graph is specified, encloses the area between the two curves. width_scale_factor The factor by which the width of the rectangles is scaled. Returns ------- :class:`~.VGroup` A :class:`~.VGroup` containing the Riemann Rectangles. """ # setting up x_range, overwrite user's third input if x_range is None: if bounded_graph is None: x_range = [graph.t_min, graph.t_max] else: x_min = max(graph.t_min, bounded_graph.t_min) x_max = min(graph.t_max, bounded_graph.t_max) x_range = [x_min, x_max] x_range = [*x_range[:2], dx] rectangles = VGroup() x_range = np.arange(*x_range) # allows passing a string to color the graph if type(color) is str: colors = [color] * len(x_range) else: colors = color_gradient(color, len(x_range)) for x, color in zip(x_range, colors): if input_sample_type == "left": sample_input = x elif input_sample_type == "right": sample_input = x + dx elif input_sample_type == "center": sample_input = x + 0.5 * dx else: raise ValueError("Invalid input sample type") graph_point = self.input_to_graph_point(sample_input, graph) if bounded_graph is None: y_point = self._origin_shift(self.y_range) else: y_point = bounded_graph.underlying_function(x) points = VGroup( *list( map( VectorizedPoint, [ self.coords_to_point(x, y_point), self.coords_to_point(x + width_scale_factor * dx, y_point), graph_point, ], ), ) ) rect = Rectangle().replace(points, stretch=True) rectangles.add(rect) # checks if the rectangle is under the x-axis if self.p2c(graph_point)[1] < y_point and show_signed_area: color = invert_color(color) # blends rectangles smoothly if blend: stroke_color = color rect.set_style( fill_color=color, fill_opacity=fill_opacity, stroke_color=stroke_color, stroke_width=stroke_width, ) return rectangles
[docs] def get_area( self, graph: "ParametricFunction", x_range: Optional[Tuple[float, float]] = None, color: Union[Color, Iterable[Color]] = [BLUE, GREEN], opacity: float = 0.3, bounded_graph: "ParametricFunction" = None, **kwargs, ): """Returns a :class:`~.Polygon` representing the area under the graph passed. Examples -------- .. manim:: GetAreaExample :save_last_frame: class GetAreaExample(Scene): def construct(self): ax = Axes().add_coordinates() curve = ax.get_graph(lambda x: 2 * np.sin(x), color=DARK_BLUE) area = ax.get_area( curve, x_range=(PI / 2, 3 * PI / 2), color=(GREEN_B, GREEN_D), opacity=1, ) self.add(ax, curve, area) Parameters ---------- graph The graph/curve for which the area needs to be gotten. x_range The range of the minimum and maximum x-values of the area. ``x_range = [x_min, x_max]``. color The color of the area. Creates a gradient if a list of colors is provided. opacity The opacity of the area. bounded_graph If a secondary :attr:`graph` is specified, encloses the area between the two curves. kwargs Additional parameters passed to :class:`~.Polygon` Returns ------- :class:`~.Polygon` The :class:`~.Polygon` representing the area. Raises ------ :exc:`ValueError` When x_ranges do not match (either area x_range, graph's x_range or bounded_graph's x_range). """ if x_range is None: a = graph.t_min b = graph.t_max else: a, b = x_range if bounded_graph is not None: if bounded_graph.t_min > b: raise ValueError( f"Ranges not matching: {bounded_graph.t_min} < {b}", ) if bounded_graph.t_max < a: raise ValueError( f"Ranges not matching: {bounded_graph.t_max} > {a}", ) a = max(a, bounded_graph.t_min) b = min(b, bounded_graph.t_max) if bounded_graph is None: points = ( [self.c2p(a)] + [p for p in graph.points if a <= self.p2c(p)[0] <= b] + [self.c2p(b)] ) else: points = [p for p in graph.points if a <= self.p2c(p)[0] <= b] + [ p for p in bounded_graph.points if a <= self.p2c(p)[0] <= b ][::-1] return Polygon(*points, **kwargs).set_opacity(opacity).set_color(color)
[docs] def angle_of_tangent( self, x: float, graph: "ParametricFunction", dx: float = 1e-8, ) -> float: """Returns the angle to the x-axis of the tangent to the plotted curve at a particular x-value. Examples -------- .. code-block:: python ax = Axes() curve = ax.get_graph(lambda x: x ** 2) ax.angle_of_tangent(x=3, graph=curve) # 1.3825747960950903 Parameters ---------- x The x-value at which the tangent must touch the curve. graph The :class:`~.ParametricFunction` for which to calculate the tangent. dx The change in `x` used to determine the angle of the tangent to the curve. Returns ------- :class:`float` The angle of the tangent to the curve. """ p0 = self.input_to_graph_point(x, graph) p1 = self.input_to_graph_point(x + dx, graph) return angle_of_vector(p1 - p0)
[docs] def slope_of_tangent( self, x: float, graph: "ParametricFunction", **kwargs ) -> float: """Returns the slope of the tangent to the plotted curve at a particular x-value. Examples -------- .. code-block:: python ax = Axes() curve = ax.get_graph(lambda x: x ** 2) ax.slope_of_tangent(x=-2, graph=curve) # -3.5000000259052038 Parameters ---------- x The x-value at which the tangent must touch the curve. graph The :class:`~.ParametricFunction` for which to calculate the tangent. Returns ------- :class:`float` The slope of the tangent with the x axis. """ return np.tan(self.angle_of_tangent(x, graph, **kwargs))
[docs] def get_derivative_graph( self, graph: "ParametricFunction", color: Color = GREEN, **kwargs ) -> ParametricFunction: """Returns the curve of the derivative of the passed graph. Examples -------- .. manim:: GetDerivativeGraphExample :save_last_frame: class GetDerivativeGraphExample(Scene): def construct(self): ax = NumberPlane(y_range=[-1, 7], background_line_style={"stroke_opacity": 0.4}) curve_1 = ax.get_graph(lambda x: x ** 2, color=PURPLE_B) curve_2 = ax.get_derivative_graph(curve_1) curves = VGroup(curve_1, curve_2) label_1 = ax.get_graph_label(curve_1, "x^2", x_val=-2, direction=DL) label_2 = ax.get_graph_label(curve_2, "2x", x_val=3, direction=RIGHT) labels = VGroup(label_1, label_2) self.add(ax, curves, labels) Parameters ---------- graph The graph for which the derivative will be found. color The color of the derivative curve. **kwargs Any valid keyword argument of :class:`~.ParametricFunction` Returns ------- :class:`~.ParametricFunction` The curve of the derivative. """ def deriv(x): return self.slope_of_tangent(x, graph) return self.get_graph(deriv, color=color, **kwargs)
[docs] def get_secant_slope_group( self, x: float, graph: ParametricFunction, dx: Optional[float] = None, dx_line_color: Color = YELLOW, dy_line_color: Optional[Color] = None, dx_label: Optional[Union[float, str]] = None, dy_label: Optional[Union[float, str]] = None, include_secant_line: bool = True, secant_line_color: Color = GREEN, secant_line_length: float = 10, ) -> VGroup: """Creates two lines representing `dx` and `df`, the labels for `dx` and `df`, and the secant to the curve at a particular x-value. Examples -------- .. manim:: GetSecantSlopeGroupExample :save_last_frame: class GetSecantSlopeGroupExample(Scene): def construct(self): ax = Axes(y_range=[-1, 7]) graph = ax.get_graph(lambda x: 1 / 4 * x ** 2, color=BLUE) slopes = ax.get_secant_slope_group( x=2.0, graph=graph, dx=1.0, dx_label=Tex("dx = 1.0"), dy_label="dy", dx_line_color=GREEN_B, secant_line_length=4, secant_line_color=RED_D, ) self.add(ax, graph, slopes) Parameters ---------- x The x-value at which the secant intersects the graph for the first time. graph The curve for which the secant will be found. dx The change in `x` after which the secant exits. dx_line_color The color of the line that indicates the change in `x`. dy_line_color The color of the line that indicates the change in `y`. Defaults to the color of :attr:`graph`. dx_label The label for the `dx` line. Defaults to :class:`~.MathTex` for ``str`` and ``float`` inputs. dy_label The label for the `dy` line. Defaults to :class:`~.MathTex` for ``str`` and ``float`` inputs. include_secant_line Whether to include the secant line in the graph, or just the df/dx lines and labels. secant_line_color The color of the secant line. secant_line_length The length of the secant line. Returns ------- :class:`~.VGroup` A group containing the elements: `dx_line`, `df_line`, and if applicable also :attr:`dx_label`, :attr:`df_label`, `secant_line`. """ group = VGroup() dx = dx or float(self.x_range[1] - self.x_range[0]) / 10 dy_line_color = dy_line_color or graph.get_color() p1 = self.input_to_graph_point(x, graph) p2 = self.input_to_graph_point(x + dx, graph) interim_point = p2[0] * RIGHT + p1[1] * UP group.dx_line = Line(p1, interim_point, color=dx_line_color) group.df_line = Line(interim_point, p2, color=dy_line_color) group.add(group.dx_line, group.df_line) labels = VGroup() if dx_label is not None: group.dx_label = self._create_label_tex(dx_label) labels.add(group.dx_label) group.add(group.dx_label) if dy_label is not None: group.df_label = self._create_label_tex(dy_label) labels.add(group.df_label) group.add(group.df_label) if len(labels) > 0: max_width = 0.8 * group.dx_line.width max_height = 0.8 * group.df_line.height if labels.width > max_width: labels.width = max_width if labels.height > max_height: labels.height = max_height if dx_label is not None: group.dx_label.next_to( group.dx_line, np.sign(dx) * DOWN, buff=group.dx_label.height / 2, ) group.dx_label.set_color(group.dx_line.get_color()) if dy_label is not None: group.df_label.next_to( group.df_line, np.sign(dx) * RIGHT, buff=group.df_label.height / 2, ) group.df_label.set_color(group.df_line.get_color()) if include_secant_line: group.secant_line = Line(p1, p2, color=secant_line_color) group.secant_line.scale( secant_line_length / group.secant_line.get_length(), ) group.add(group.secant_line) return group
[docs] def get_vertical_lines_to_graph( self, graph: ParametricFunction, x_range: Optional[Sequence[float]] = None, num_lines: int = 20, **kwargs, ) -> VGroup: """Obtains multiple lines from the x-axis to the curve. Examples -------- .. manim:: GetVerticalLinesToGraph :save_last_frame: class GetVerticalLinesToGraph(Scene): def construct(self): ax = Axes( x_range=[0, 8.0, 1], y_range=[-1, 1, 0.2], axis_config={"number_scale_value": 0.5}, ).add_coordinates() curve = ax.get_graph(lambda x: np.sin(x) / np.e ** 2 * x) lines = ax.get_vertical_lines_to_graph( curve, x_range=[0, 4], num_lines=30, color=BLUE ) self.add(ax, curve, lines) Parameters ---------- graph The graph along which the lines are placed. x_range A list containing the lower and and upper bounds of the lines: ``x_range = [x_min, x_max]``. num_lines The number of evenly spaced lines. kwargs Additional arguments to be passed to :meth:`~.CoordinateSystem.get_vertical_line` Returns ------- :class:`~.VGroup` The :class:`~.VGroup` of the evenly spaced lines. """ x_range = x_range if x_range is not None else self.x_range return VGroup( *( self.get_vertical_line(self.i2gp(x, graph), **kwargs) for x in np.linspace(x_range[0], x_range[1], num_lines) ) )
[docs] def get_T_label( self, x_val: float, graph: "ParametricFunction", label: Optional[Union[float, str, "Mobject"]] = None, label_color: Color = WHITE, triangle_size: float = MED_SMALL_BUFF, triangle_color: Color = WHITE, line_func: "Line" = Line, line_color: Color = YELLOW, ) -> VGroup: """Creates a labelled triangle marker with a vertical line from the x-axis to a curve at a given x-value. Parameters ---------- x_val The position along the curve at which the label, line and triangle will be constructed. graph The :class:`~.ParametricFunction` for which to construct the label. label The label of the vertical line and triangle. label_color The color of the label. triangle_size The size of the triangle. triangle_color The color of the triangle. line_func The function used to construct the vertical line. line_color The color of the vertical line. Examples -------- .. manim:: T_labelExample :save_last_frame: class T_labelExample(Scene): def construct(self): # defines the axes and linear function axes = Axes(x_range=[-1, 10], y_range=[-1, 10], x_length=9, y_length=6) func = axes.get_graph(lambda x: x, color=BLUE) # creates the T_label t_label = axes.get_T_label(x_val=4, graph=func, label=Tex("x-value")) self.add(axes, func, t_label) Returns ------- :class:`~.VGroup` A :class:`~.VGroup` of the label, triangle and vertical line mobjects. """ T_label_group = VGroup() triangle = RegularPolygon(n=3, start_angle=np.pi / 2, stroke_width=0).set_fill( color=triangle_color, opacity=1, ) triangle.height = triangle_size triangle.move_to(self.coords_to_point(x_val, 0), UP) if label is not None: t_label = self._create_label_tex(label).set_color(label_color) t_label.next_to(triangle, DOWN) T_label_group.add(t_label) v_line = self.get_vertical_line( self.i2gp(x_val, graph), color=line_color, line_func=line_func, ) T_label_group.add(triangle, v_line) return T_label_group
[docs]class Axes(VGroup, CoordinateSystem, metaclass=ConvertToOpenGL): """Creates a set of axes. Parameters ---------- x_range The :code:`[x_min, x_max, x_step]` values of the x-axis. y_range The :code:`[y_min, y_max, y_step]` values of the y-axis. x_length The length of the x-axis. y_length The length of the y-axis. axis_config Arguments to be passed to :class:`~.NumberLine` that influences both axes. x_axis_config Arguments to be passed to :class:`~.NumberLine` that influence the x-axis. y_axis_config Arguments to be passed to :class:`~.NumberLine` that influence the y-axis. tips Whether or not to include the tips on both axes. kwargs : Any Additional arguments to be passed to :class:`CoordinateSystem` and :class:`~.VGroup`. """ def __init__( self, x_range: Optional[Sequence[float]] = None, y_range: Optional[Sequence[float]] = None, x_length: Optional[float] = round(config.frame_width) - 2, y_length: Optional[float] = round(config.frame_height) - 2, axis_config: Optional[dict] = None, x_axis_config: Optional[dict] = None, y_axis_config: Optional[dict] = None, tips: bool = True, **kwargs, ): VGroup.__init__(self, **kwargs) CoordinateSystem.__init__(self, x_range, y_range, x_length, y_length) self.axis_config = { "include_tip": tips, "numbers_to_exclude": [0], "exclude_origin_tick": True, } self.x_axis_config = {} self.y_axis_config = {"rotation": 90 * DEGREES, "label_direction": LEFT} self._update_default_configs( (self.axis_config, self.x_axis_config, self.y_axis_config), (axis_config, x_axis_config, y_axis_config), ) self.x_axis_config = merge_dicts_recursively( self.axis_config, self.x_axis_config, ) self.y_axis_config = merge_dicts_recursively( self.axis_config, self.y_axis_config, ) self.x_axis = self._create_axis(self.x_range, self.x_axis_config, self.x_length) self.y_axis = self._create_axis(self.y_range, self.y_axis_config, self.y_length) # Add as a separate group in case various other # mobjects are added to self, as for example in # NumberPlane below self.axes = VGroup(self.x_axis, self.y_axis) self.add(*self.axes) # finds the middle-point on each axis lines_center_point = [((axis.x_max + axis.x_min) / 2) for axis in self.axes] self.shift(-self.coords_to_point(*lines_center_point)) @staticmethod def _update_default_configs(default_configs, passed_configs): for default_config, passed_config in zip(default_configs, passed_configs): if passed_config is not None: update_dict_recursively(default_config, passed_config) def _create_axis( self, range_terms: Sequence[float], axis_config: dict, length: float, ) -> NumberLine: """Creates an axis and dynamically adjusts its position depending on where 0 is located on the line. Parameters ---------- range_terms The range of the the axis : `(x_min, x_max, x_step)`. axis_config Additional parameters that are passed to :class:`NumberLine`. length The length of the axis. Returns ------- :class:`NumberLine` Returns a number line with the provided x and y axis range. """ axis_config["length"] = length axis = NumberLine(range_terms, **axis_config) # without the call to _origin_shift, graph does not exist when min > 0 or max < 0 # shifts the axis so that 0 is centered axis.shift(-axis.number_to_point(self._origin_shift(range_terms))) return axis
[docs] def coords_to_point(self, *coords: Sequence[float]) -> np.ndarray: """Accepts coordinates from the axes and returns a point with respect to the scene. Examples -------- .. manim:: CoordsToPointExample :save_last_frame: class CoordsToPointExample(Scene): def construct(self): ax = Axes().add_coordinates() # a dot with respect to the axes dot_axes = Dot(ax.coords_to_point(2, 2), color=GREEN) lines = ax.get_lines_to_point(ax.c2p(2,2)) # a dot with respect to the scene # the default plane corresponds to the coordinates of the scene. plane = NumberPlane() dot_scene = Dot((2,2,0), color=RED) self.add(plane, dot_scene, ax, dot_axes, lines) Parameters ---------- coords The coordinates. Each coord is passed as a separate argument: ``ax.coords_to_point(1, 2, 3)``. Returns ------- np.ndarray A point with respect to the scene's coordinate system. """ origin = self.x_axis.number_to_point(self._origin_shift(self.x_range)) result = np.array(origin) for axis, coord in zip(self.get_axes(), coords): result += axis.number_to_point(coord) - origin return result
[docs] def point_to_coords(self, point: Sequence[float]) -> Tuple[float]: """Accepts a point from the scene and returns its coordinates with respect to the axes. Examples -------- .. manim:: PointToCoordsExample :save_last_frame: class PointToCoordsExample(Scene): def construct(self): ax = Axes(x_range=[0, 10, 2]).add_coordinates() circ = Circle(radius=0.5).shift(UR * 2) # get the coordinates of the circle with respect to the axes coords = np.around(ax.point_to_coords(circ.get_right()), decimals=2) label = ( Matrix([[coords[0]], [coords[1]]]).scale(0.75).next_to(circ, RIGHT) ) self.add(ax, circ, label, Dot(circ.get_right())) Parameters ---------- point The point, i.e. ``RIGHT`` or ``[0, 1, 0]``. Returns ------- Tuple[float] The coordinates on the axes, i.e. ``(4.0, 7.0)``. """ return tuple(axis.point_to_number(point) for axis in self.get_axes())
[docs] def get_axes(self) -> VGroup: """Gets the axes. Returns ------- :class:`~.VGroup` A pair of axes. """ return self.axes
[docs] def get_line_graph( self, x_values: Iterable[float], y_values: Iterable[float], z_values: Optional[Iterable[float]] = None, line_color: Color = YELLOW, add_vertex_dots: bool = True, vertex_dot_radius: float = DEFAULT_DOT_RADIUS, vertex_dot_style: Optional[dict] = None, **kwargs, ) -> VDict: """Draws a line graph. The graph connects the vertices formed from zipping ``x_values``, ``y_values`` and ``z_values``. Also adds :class:`Dots <.Dot>` at the vertices if ``add_vertex_dots`` is set to ``True``. Parameters ---------- x_values Iterable of values along the x-axis. y_values Iterable of values along the y-axis. z_values Iterable of values (zeros if z_values is None) along the z-axis. line_color Color for the line graph. add_vertex_dots Whether or not to add :class:`~.Dot` at each vertex. vertex_dot_radius Radius for the :class:`~.Dot` at each vertex. vertex_dot_style Style arguments to be passed into :class:`~.Dot` at each vertex. kwargs Additional arguments to be passed into :class:`~.VMobject`. Examples -------- .. manim:: LineGraphExample :save_last_frame: class LineGraphExample(Scene): def construct(self): plane = NumberPlane( x_range = (0, 7), y_range = (0, 5), x_length = 7, axis_config={"include_numbers": True}, ) plane.center() line_graph = plane.get_line_graph( x_values = [0, 1.5, 2, 2.8, 4, 6.25], y_values = [1, 3, 2.25, 4, 2.5, 1.75], line_color=GOLD_E, vertex_dot_style=dict(stroke_width=3, fill_color=PURPLE), stroke_width = 4, ) self.add(plane, line_graph) Returns ------- :class:`~.VDict` A VDict containing both the line and dots (if specified). The line can be accessed with: ``line_graph["line_graph"]``. The dots can be accessed with: ``line_graph["vertex_dots"]``. """ x_values, y_values = map(np.array, (x_values, y_values)) if z_values is None: z_values = np.zeros(x_values.shape) line_graph = VDict() graph = VGroup(color=line_color, **kwargs) vertices = [ self.coords_to_point(x, y, z) for x, y, z in zip(x_values, y_values, z_values) ] graph.set_points_as_corners(vertices) line_graph["line_graph"] = graph if add_vertex_dots: vertex_dot_style = vertex_dot_style or {} vertex_dots = VGroup( *( Dot(point=vertex, radius=vertex_dot_radius, **vertex_dot_style) for vertex in vertices ) ) line_graph["vertex_dots"] = vertex_dots return line_graph
@staticmethod def _origin_shift(axis_range: Sequence[float]) -> float: """Determines how to shift graph mobjects to compensate when 0 is not on the axis. Parameters ---------- axis_range The range of the axis : ``(x_min, x_max, x_step)``. """ if axis_range[0] > 0: return axis_range[0] if axis_range[1] < 0: return axis_range[1] else: return 0
[docs]class ThreeDAxes(Axes): """A 3-dimensional set of axes. Parameters ---------- x_range The :code:`[x_min, x_max, x_step]` values of the x-axis. y_range The :code:`[y_min, y_max, y_step]` values of the y-axis. z_range The :code:`[z_min, z_max, z_step]` values of the z-axis. x_length The length of the x-axis. y_length The length of the y-axis. z_length The length of the z-axis. z_axis_config Arguments to be passed to :class:`~.NumberLine` that influence the z-axis. z_normal The direction of the normal. num_axis_pieces The number of pieces used to construct the axes. light_source The direction of the light source. depth Currently non-functional. gloss Currently non-functional. kwargs : Any Additional arguments to be passed to :class:`Axes`. """ def __init__( self, x_range: Optional[Sequence[float]] = (-6, 6, 1), y_range: Optional[Sequence[float]] = (-5, 5, 1), z_range: Optional[Sequence[float]] = (-4, 4, 1), x_length: Optional[float] = config.frame_height + 2.5, y_length: Optional[float] = config.frame_height + 2.5, z_length: Optional[float] = config.frame_height - 1.5, z_axis_config: Optional[dict] = None, z_normal: Sequence[float] = DOWN, num_axis_pieces: int = 20, light_source: Sequence[float] = 9 * DOWN + 7 * LEFT + 10 * OUT, # opengl stuff (?) depth=None, gloss=0.5, **kwargs, ): super().__init__( x_range=x_range, x_length=x_length, y_range=y_range, y_length=y_length, **kwargs, ) self.z_range = z_range self.z_length = z_length self.z_axis_config = {} self._update_default_configs((self.z_axis_config,), (z_axis_config,)) self.z_axis_config = merge_dicts_recursively( self.axis_config, self.z_axis_config, ) self.z_normal = z_normal self.num_axis_pieces = num_axis_pieces self.light_source = light_source self.dimension = 3 z_axis = self._create_axis(self.z_range, self.z_axis_config, self.z_length) z_axis.rotate_about_zero(-PI / 2, UP) z_axis.rotate_about_zero(angle_of_vector(self.z_normal)) z_axis.shift(self.x_axis.number_to_point(self._origin_shift(x_range))) self.axes.add(z_axis) self.add(z_axis) self.z_axis = z_axis if config.renderer != "opengl": self._add_3d_pieces() self._set_axis_shading() def _add_3d_pieces(self): for axis in self.axes: axis.pieces = VGroup(*axis.get_pieces(self.num_axis_pieces)) axis.add(axis.pieces) axis.set_stroke(width=0, family=False) axis.set_shade_in_3d(True) def _set_axis_shading(self): def make_func(axis): vect = self.light_source return lambda: ( axis.get_edge_center(-vect), axis.get_edge_center(vect), ) for axis in self: for submob in axis.family_members_with_points(): submob.get_gradient_start_and_end_points = make_func(axis) submob.get_unit_normal = lambda a: np.ones(3) submob.set_sheen(0.2)
[docs] def get_z_axis_label( self, label: Union[float, str, "Mobject"], edge: Sequence[float] = OUT, direction: Sequence[float] = RIGHT, buff: float = SMALL_BUFF, rotation=PI / 2, rotation_axis=RIGHT, **kwargs, ) -> "Mobject": """Generate an x-axis label. Examples -------- .. manim:: GetZAxisLabelExample :save_last_frame: class GetZAxisLabelExample(ThreeDScene): def construct(self): ax = ThreeDAxes() lab = ax.get_z_axis_label(Tex("$z$-label")) self.set_camera_orientation(phi=2*PI/5, theta=PI/5) self.add(ax, lab) Parameters ---------- label The label. Defaults to :class:`~.MathTex` for ``str`` and ``float`` inputs. edge The edge of the x-axis to which the label will be added, by default ``UR``. direction Allows for further positioning of the label from an edge, by default ``UR``. buff The distance of the label from the line. rotation The angle at which to rotate the label, by default ``PI/2``. rotation_axis The axis about which to rotate the label, by default ``RIGHT``. Returns ------- :class:`~.Mobject` The positioned label. """ positioned_label = self._get_axis_label( label, self.get_z_axis(), edge, direction, buff=buff, **kwargs ) positioned_label.rotate(rotation, axis=rotation_axis) return positioned_label
[docs]class NumberPlane(Axes): """Creates a cartesian plane with background lines. Parameters ---------- x_range The :code:`[x_min, x_max, x_step]` values of the plane in the horizontal direction. y_range The :code:`[y_min, y_max, y_step]` values of the plane in the vertical direction. x_length The width of the plane. y_length The height of the plane. background_line_style Arguments that influence the construction of the background lines of the plane. faded_line_style Similar to :attr:`background_line_style`, affects the construction of the scene's background lines. faded_line_ratio Determines the number of boxes within the background lines: :code:`2` = 4 boxes, :code:`3` = 9 boxes. make_smooth_after_applying_functions Currently non-functional. kwargs : Any Additional arguments to be passed to :class:`Axes`. .. note:: If :attr:`x_length` or :attr:`y_length` are not defined, the plane automatically adjusts its lengths based on the :attr:`x_range` and :attr:`y_range` values to set the ``unit_size`` to 1. Examples -------- .. manim:: NumberPlaneExample :save_last_frame: class NumberPlaneExample(Scene): def construct(self): number_plane = NumberPlane( x_range=[-10, 10, 1], y_range=[-10, 10, 1], background_line_style={ "stroke_color": TEAL, "stroke_width": 4, "stroke_opacity": 0.6 } ) self.add(number_plane) """ def __init__( self, x_range: Optional[Sequence[float]] = ( -config["frame_x_radius"], config["frame_x_radius"], 1, ), y_range: Optional[Sequence[float]] = ( -config["frame_y_radius"], config["frame_y_radius"], 1, ), x_length: Optional[float] = None, y_length: Optional[float] = None, background_line_style: Optional[dict] = None, faded_line_style: Optional[dict] = None, faded_line_ratio: int = 1, make_smooth_after_applying_functions=True, **kwargs, ): # configs self.axis_config = { "stroke_color": WHITE, "stroke_width": 2, "include_ticks": False, "include_tip": False, "line_to_number_buff": SMALL_BUFF, "label_direction": DR, "font_size": 24, } self.y_axis_config = {"label_direction": DR} self.background_line_style = { "stroke_color": BLUE_D, "stroke_width": 2, "stroke_opacity": 1, } self._update_default_configs( (self.axis_config, self.y_axis_config, self.background_line_style), ( kwargs.pop("axis_config", None), kwargs.pop("y_axis_config", None), background_line_style, ), ) # Defaults to a faded version of line_config self.faded_line_style = faded_line_style self.faded_line_ratio = faded_line_ratio self.make_smooth_after_applying_functions = make_smooth_after_applying_functions # init super().__init__( x_range=x_range, y_range=y_range, x_length=x_length, y_length=y_length, axis_config=self.axis_config, y_axis_config=self.y_axis_config, **kwargs, ) self._init_background_lines() def _init_background_lines(self): """Will init all the lines of NumberPlanes (faded or not)""" if self.faded_line_style is None: style = dict(self.background_line_style) # For anything numerical, like stroke_width # and stroke_opacity, chop it in half for key in style: if isinstance(style[key], numbers.Number): style[key] *= 0.5 self.faded_line_style = style self.background_lines, self.faded_lines = self._get_lines() self.background_lines.set_style( **self.background_line_style, ) self.faded_lines.set_style( **self.faded_line_style, ) self.add_to_back( self.faded_lines, self.background_lines, ) def _get_lines(self) -> Tuple[VGroup, VGroup]: """Generate all the lines, faded and not faded. Two sets of lines are generated: one parallel to the X-axis, and parallel to the Y-axis. Returns ------- Tuple[:class:`~.VGroup`, :class:`~.VGroup`] The first (i.e the non faded lines) and second (i.e the faded lines) sets of lines, respectively. """ x_axis = self.get_x_axis() y_axis = self.get_y_axis() x_lines1, x_lines2 = self._get_lines_parallel_to_axis( x_axis, y_axis, self.y_axis.x_step, self.faded_line_ratio, ) y_lines1, y_lines2 = self._get_lines_parallel_to_axis( y_axis, x_axis, self.x_axis.x_step, self.faded_line_ratio, ) # TODO this was added so that we can run tests on NumberPlane # In the future these attributes will be tacked onto self.background_lines self.x_lines = x_lines1 self.y_lines = y_lines1 lines1 = VGroup(*x_lines1, *y_lines1) lines2 = VGroup(*x_lines2, *y_lines2) return lines1, lines2 def _get_lines_parallel_to_axis( self, axis_parallel_to: NumberLine, axis_perpendicular_to: NumberLine, freq: float, ratio_faded_lines: int, ) -> Tuple[VGroup, VGroup]: """Generate a set of lines parallel to an axis. Parameters ---------- axis_parallel_to The axis with which the lines will be parallel. axis_perpendicular_to The axis with which the lines will be perpendicular. ratio_faded_lines The ratio between the space between faded lines and the space between non-faded lines. freq Frequency of non-faded lines (number of non-faded lines per graph unit). Returns ------- Tuple[:class:`~.VGroup`, :class:`~.VGroup`] The first (i.e the non-faded lines parallel to `axis_parallel_to`) and second (i.e the faded lines parallel to `axis_parallel_to`) sets of lines, respectively. """ line = Line(axis_parallel_to.get_start(), axis_parallel_to.get_end()) if ratio_faded_lines == 0: # don't show faded lines ratio_faded_lines = 1 # i.e. set ratio to 1 step = (1 / ratio_faded_lines) * freq lines1 = VGroup() lines2 = VGroup() unit_vector_axis_perp_to = axis_perpendicular_to.get_unit_vector() # min/max used in case range does not include 0. i.e. if (2,6): # the range becomes (0,4), not (0,6), to produce the correct number of lines ranges = ( np.arange( 0, min( axis_perpendicular_to.x_max - axis_perpendicular_to.x_min, axis_perpendicular_to.x_max, ), step, ), np.arange( 0, max( axis_perpendicular_to.x_min - axis_perpendicular_to.x_max, axis_perpendicular_to.x_min, ), -step, ), ) for inputs in ranges: for k, x in enumerate(inputs): new_line = line.copy() new_line.shift(unit_vector_axis_perp_to * x) if k % ratio_faded_lines == 0: lines1.add(new_line) else: lines2.add(new_line) return lines1, lines2 def get_vector(self, coords: Sequence[float], **kwargs): kwargs["buff"] = 0 return Arrow( self.coords_to_point(0, 0), self.coords_to_point(*coords), **kwargs ) def prepare_for_nonlinear_transform(self, num_inserted_curves: int = 50): for mob in self.family_members_with_points(): num_curves = mob.get_num_curves() if num_inserted_curves > num_curves: mob.insert_n_curves(num_inserted_curves - num_curves) return self
[docs]class PolarPlane(Axes): r"""Creates a polar plane with background lines. Parameters ---------- azimuth_step The number of divisions in the azimuth (also known as the `angular coordinate` or `polar angle`). If ``None`` is specified then it will use the default specified by ``azimuth_units``: - ``"PI radians"`` or ``"TAU radians"``: 20 - ``"degrees"``: 36 - ``"gradians"``: 40 - ``None``: 1 A non-integer value will result in a partial division at the end of the circle. size The diameter of the plane. radius_step The distance between faded radius lines. radius_max The maximum value of the radius. azimuth_units Specifies a default labelling system for the azimuth. Choices are: - ``"PI radians"``: Fractional labels in the interval :math:`\left[0, 2\pi\right]` with :math:`\pi` as a constant. - ``"TAU radians"``: Fractional labels in the interval :math:`\left[0, \tau\right]` (where :math:`\tau = 2\pi`) with :math:`\tau` as a constant. - ``"degrees"``: Decimal labels in the interval :math:`\left[0, 360\right]` with a degree (:math:`^{\circ}`) symbol. - ``"gradians"``: Decimal labels in the interval :math:`\left[0, 400\right]` with a superscript "g" (:math:`^{g}`). - ``None``: Decimal labels in the interval :math:`\left[0, 1\right]`. azimuth_compact_fraction If the ``azimuth_units`` choice has fractional labels, choose whether to combine the constant in a compact form :math:`\tfrac{xu}{y}` as opposed to :math:`\tfrac{x}{y}u`, where :math:`u` is the constant. azimuth_offset The angle offset of the azimuth, expressed in radians. azimuth_direction The direction of the azimuth. - ``"CW"``: Clockwise. - ``"CCW"``: Anti-clockwise. azimuth_label_buff The buffer for the azimuth labels. azimuth_label_font_size The font size of the azimuth labels. radius_config The axis config for the radius. Examples -------- .. manim:: PolarPlaneExample :ref_classes: PolarPlane :save_last_frame: class PolarPlaneExample(Scene): def construct(self): polarplane_pi = PolarPlane( azimuth_units="PI radians", size=6, azimuth_label_font_size=33.6, radius_config={"font_size": 33.6}, ).add_coordinates() self.add(polarplane_pi) """ @deprecated_params( params="azimuth_label_scale", since="v0.10.0", until="v0.11.0", message="Use azimuth_label_font_size instead. To convert old scale factors to font size, multiply by 48.", ) def __init__( self, radius_max: float = config["frame_y_radius"], size: Optional[float] = None, radius_step: float = 1, azimuth_step: Optional[float] = None, azimuth_units: Optional[str] = "PI radians", azimuth_compact_fraction: bool = True, azimuth_offset: float = 0, azimuth_direction: str = "CCW", azimuth_label_buff: float = SMALL_BUFF, azimuth_label_font_size: float = 24, radius_config: Optional[dict] = None, background_line_style: Optional[dict] = None, faded_line_style: Optional[dict] = None, faded_line_ratio: int = 1, make_smooth_after_applying_functions: bool = True, **kwargs, ): # deprecation azimuth_label_scale = kwargs.pop("azimuth_label_scale", None) if azimuth_label_scale: self.azimuth_label_font_size = ( azimuth_label_scale * DEFAULT_FONT_SIZE * 0.75 ) else: self.azimuth_label_font_size = azimuth_label_font_size # error catching if azimuth_units in ["PI radians", "TAU radians", "degrees", "gradians", None]: self.azimuth_units = azimuth_units else: raise ValueError( "Invalid azimuth units. Expected one of: PI radians, TAU radians, degrees, gradians or None.", ) if azimuth_direction in ["CW", "CCW"]: self.azimuth_direction = azimuth_direction else: raise ValueError("Invalid azimuth units. Expected one of: CW, CCW.") # configs self.radius_config = { "stroke_color": WHITE, "stroke_width": 2, "include_ticks": False, "include_tip": False, "line_to_number_buff": SMALL_BUFF, "label_direction": DL, "font_size": 24, } self.background_line_style = { "stroke_color": BLUE_D, "stroke_width": 2, "stroke_opacity": 1, } self.azimuth_step = ( ( { "PI radians": 20, "TAU radians": 20, "degrees": 36, "gradians": 40, None: 1, }[azimuth_units] ) if azimuth_step is None else azimuth_step ) self._update_default_configs( (self.radius_config, self.background_line_style), (radius_config, background_line_style), ) # Defaults to a faded version of line_config self.faded_line_style = faded_line_style self.faded_line_ratio = faded_line_ratio self.make_smooth_after_applying_functions = make_smooth_after_applying_functions self.azimuth_offset = azimuth_offset self.azimuth_label_buff = azimuth_label_buff # self.azimuth_label_font_size = azimuth_label_font_size <-uncomment when deprecation is done self.azimuth_compact_fraction = azimuth_compact_fraction # init super().__init__( x_range=np.array((-radius_max, radius_max, radius_step)), y_range=np.array((-radius_max, radius_max, radius_step)), x_length=size, y_length=size, axis_config=self.radius_config, **kwargs, ) self._init_background_lines() def _init_background_lines(self): """Will init all the lines of NumberPlanes (faded or not)""" if self.faded_line_style is None: style = dict(self.background_line_style) # For anything numerical, like stroke_width # and stroke_opacity, chop it in half for key in style: if isinstance(style[key], numbers.Number): style[key] *= 0.5 self.faded_line_style = style self.background_lines, self.faded_lines = self._get_lines() self.background_lines.set_style( **self.background_line_style, ) self.faded_lines.set_style( **self.faded_line_style, ) self.add_to_back( self.faded_lines, self.background_lines, ) def _get_lines(self) -> Tuple[VGroup, VGroup]: """Generate all the lines and circles, faded and not faded. Returns ------- Tuple[:class:`~.VGroup`, :class:`~.VGroup`] The first (i.e the non faded lines and circles) and second (i.e the faded lines and circles) sets of lines and circles, respectively. """ center = self.get_center_point() ratio_faded_lines = self.faded_line_ratio offset = self.azimuth_offset if ratio_faded_lines == 0: # don't show faded lines ratio_faded_lines = 1 # i.e. set ratio to 1 rstep = (1 / ratio_faded_lines) * self.x_axis.x_step astep = (1 / ratio_faded_lines) * (TAU * (1 / self.azimuth_step)) rlines1 = VGroup() rlines2 = VGroup() alines1 = VGroup() alines2 = VGroup() rinput = np.arange(0, self.x_axis.x_max + rstep, rstep) ainput = np.arange(0, TAU, astep) unit_vector = self.x_axis.get_unit_vector()[0] for k, x in enumerate(rinput): new_line = Circle(radius=x * unit_vector) if k % ratio_faded_lines == 0: alines1.add(new_line) else: alines2.add(new_line) line = Line(center, self.get_x_axis().get_end()) for k, x in enumerate(ainput): new_line = line.copy() new_line.rotate(x + offset, about_point=center) if k % ratio_faded_lines == 0: rlines1.add(new_line) else: rlines2.add(new_line) lines1 = VGroup(*rlines1, *alines1) lines2 = VGroup(*rlines2, *alines2) return lines1, lines2
[docs] def get_axes(self) -> VGroup: """Gets the axes. Returns ------- :class:`~.VGroup` A pair of axes. """ return self.axes
def get_vector(self, coords, **kwargs): kwargs["buff"] = 0 return Arrow( self.coords_to_point(0, 0), self.coords_to_point(*coords), **kwargs ) def prepare_for_nonlinear_transform(self, num_inserted_curves=50): for mob in self.family_members_with_points(): num_curves = mob.get_num_curves() if num_inserted_curves > num_curves: mob.insert_n_curves(num_inserted_curves - num_curves) return self
[docs] def polar_to_point(self, radius: float, azimuth: float) -> np.ndarray: r"""Gets a point from polar coordinates. Parameters ---------- radius The coordinate radius (:math:`r`). azimuth The coordinate azimuth (:math:`\theta`). Returns ------- numpy.ndarray The point. Examples -------- .. manim:: PolarToPointExample :ref_classes: PolarPlane Vector :save_last_frame: class PolarToPointExample(Scene): def construct(self): polarplane_pi = PolarPlane(azimuth_units="PI radians", size=6) polartopoint_vector = Vector(polarplane_pi.polar_to_point(3, PI/4)) self.add(polarplane_pi) self.add(polartopoint_vector) """ return self.coords_to_point(radius * np.cos(azimuth), radius * np.sin(azimuth))
[docs] def pr2pt(self, radius: float, azimuth: float) -> np.ndarray: """Abbreviation for :meth:`polar_to_point`""" return self.polar_to_point(radius, azimuth)
[docs] def point_to_polar(self, point: np.ndarray) -> Tuple[float, float]: r"""Gets polar coordinates from a point. Parameters ---------- point The point. Returns ------- Tuple[:class:`float`, :class:`float`] The coordinate radius (:math:`r`) and the coordinate azimuth (:math:`\theta`). """ x, y = self.point_to_coords(point) return np.sqrt(x ** 2 + y ** 2), np.arctan2(y, x)
[docs] def pt2pr(self, point: np.ndarray) -> Tuple[float, float]: """Abbreviation for :meth:`point_to_polar`""" return self.point_to_polar(point)
[docs] def get_coordinate_labels( self, r_values: Optional[Iterable[float]] = None, a_values: Optional[Iterable[float]] = None, **kwargs, ) -> VDict: """Gets labels for the coordinates Parameters ---------- r_values Iterable of values along the radius, by default None. a_values Iterable of values along the azimuth, by default None. Returns ------- VDict Labels for the radius and azimuth values. """ if r_values is None: r_values = [r for r in self.get_x_axis().get_tick_range() if r >= 0] if a_values is None: a_values = np.arange(0, 1, 1 / self.azimuth_step) r_mobs = self.get_x_axis().add_numbers(r_values) if self.azimuth_direction == "CCW": d = 1 elif self.azimuth_direction == "CW": d = -1 else: raise ValueError("Invalid azimuth direction. Expected one of: CW, CCW") a_points = [ { "label": i, "point": np.array( [ self.get_right()[0] * np.cos(d * (i * TAU) + self.azimuth_offset), self.get_right()[0] * np.sin(d * (i * TAU) + self.azimuth_offset), 0, ], ), } for i in a_values ] if self.azimuth_units == "PI radians" or self.azimuth_units == "TAU radians": a_tex = [ self.get_radian_label( i["label"], font_size=self.azimuth_label_font_size, ).next_to( i["point"], direction=i["point"], aligned_edge=i["point"], buff=self.azimuth_label_buff, ) for i in a_points ] elif self.azimuth_units == "degrees": a_tex = [ MathTex( f'{360 * i["label"]:g}' + r"^{\circ}", font_size=self.azimuth_label_font_size, ).next_to( i["point"], direction=i["point"], aligned_edge=i["point"], buff=self.azimuth_label_buff, ) for i in a_points ] elif self.azimuth_units == "gradians": a_tex = [ MathTex( f'{400 * i["label"]:g}' + r"^{g}", font_size=self.azimuth_label_font_size, ).next_to( i["point"], direction=i["point"], aligned_edge=i["point"], buff=self.azimuth_label_buff, ) for i in a_points ] elif self.azimuth_units is None: a_tex = [ MathTex( f'{i["label"]:g}', font_size=self.azimuth_label_font_size, ).next_to( i["point"], direction=i["point"], aligned_edge=i["point"], buff=self.azimuth_label_buff, ) for i in a_points ] a_mobs = VGroup(*a_tex) self.coordinate_labels = VGroup(r_mobs, a_mobs) return self.coordinate_labels
[docs] def add_coordinates( self, r_values: Optional[Iterable[float]] = None, a_values: Optional[Iterable[float]] = None, ): """Adds the coordinates. Parameters ---------- r_values Iterable of values along the radius, by default None. a_values Iterable of values along the azimuth, by default None. """ self.add(self.get_coordinate_labels(r_values, a_values)) return self
def get_radian_label(self, number, font_size=24, **kwargs): constant_label = {"PI radians": r"\pi", "TAU radians": r"\tau"}[ self.azimuth_units ] division = number * {"PI radians": 2, "TAU radians": 1}[self.azimuth_units] frac = fr.Fraction(division).limit_denominator(max_denominator=100) if frac.numerator == 0 & frac.denominator == 0: string = r"0" elif frac.numerator == 1 and frac.denominator == 1: string = constant_label elif frac.numerator == 1: if self.azimuth_compact_fraction: string = ( r"\tfrac{" + constant_label + r"}{" + str(frac.denominator) + "}" ) else: string = r"\tfrac{1}{" + str(frac.denominator) + "}" + constant_label elif frac.denominator == 1: string = str(frac.numerator) + constant_label else: if self.azimuth_compact_fraction: string = ( r"\tfrac{" + str(frac.numerator) + constant_label + r"}{" + str(frac.denominator) + r"}" ) else: string = ( r"\tfrac{" + str(frac.numerator) + r"}{" + str(frac.denominator) + r"}" + constant_label ) return MathTex(string, font_size=font_size, **kwargs)
[docs]class ComplexPlane(NumberPlane): """A :class:`~.NumberPlane` specialized for use with complex numbers. Examples -------- .. manim:: ComplexPlaneExample :save_last_frame: :ref_classes: Dot MathTex class ComplexPlaneExample(Scene): def construct(self): plane = ComplexPlane().add_coordinates() self.add(plane) d1 = Dot(plane.n2p(2 + 1j), color=YELLOW) d2 = Dot(plane.n2p(-3 - 2j), color=YELLOW) label1 = MathTex("2+i").next_to(d1, UR, 0.1) label2 = MathTex("-3-2i").next_to(d2, UR, 0.1) self.add( d1, label1, d2, label2, ) """ def __init__(self, **kwargs): super().__init__( **kwargs, )
[docs] def number_to_point(self, number: Union[float, complex]) -> np.ndarray: """Accepts a float/complex number and returns the equivalent point on the plane. Parameters ---------- number The number. Can be a float or a complex number. Returns ------- np.ndarray The point on the plane. """ number = complex(number) return self.coords_to_point(number.real, number.imag)
[docs] def n2p(self, number: Union[float, complex]) -> np.ndarray: """Abbreviation for :meth:`number_to_point`.""" return self.number_to_point(number)
[docs] def point_to_number(self, point: Sequence[float]) -> complex: """Accepts a point and returns a complex number equivalent to that point on the plane. Parameters ---------- point The point in manim's coordinate-system Returns ------- complex A complex number consisting of real and imaginary components. """ x, y = self.point_to_coords(point) return complex(x, y)
[docs] def p2n(self, point: Sequence[float]) -> complex: """Abbreviation for :meth:`point_to_number`.""" return self.point_to_number(point)
def _get_default_coordinate_values(self) -> List[Union[float, complex]]: """Generate a list containing the numerical values of the plane's labels. Returns ------- List[Union[float, complex]] A list of floats representing the x-axis and complex numbers representing the y-axis. """ x_numbers = self.get_x_axis().get_tick_range() y_numbers = self.get_y_axis().get_tick_range() y_numbers = [complex(0, y) for y in y_numbers if y != 0] return [*x_numbers, *y_numbers]
[docs] def get_coordinate_labels( self, *numbers: Iterable[Union[float, complex]], **kwargs ) -> VGroup: """Generates the :class:`~.DecimalNumber` mobjects for the coordinates of the plane. Parameters ---------- numbers An iterable of floats/complex numbers. Floats are positioned along the x-axis, complex numbers along the y-axis. kwargs Additional arguments to be passed to :meth:`~.NumberLine.get_number_mobject`, i.e. :class:`~.DecimalNumber`. Returns ------- :class:`~.VGroup` A :class:`~.VGroup` containing the positioned label mobjects. """ # TODO: Make this work the same as coord_sys.add_coordinates() if len(numbers) == 0: numbers = self._get_default_coordinate_values() self.coordinate_labels = VGroup() for number in numbers: z = complex(number) if abs(z.imag) > abs(z.real): axis = self.get_y_axis() value = z.imag kwargs["unit"] = "i" else: axis = self.get_x_axis() value = z.real number_mob = axis.get_number_mobject(value, **kwargs) self.coordinate_labels.add(number_mob) return self.coordinate_labels
[docs] def add_coordinates(self, *numbers: Iterable[Union[float, complex]], **kwargs): """Adds the labels produced from :meth:`~.NumberPlane.get_coordinate_labels` to the plane. Parameters ---------- numbers An iterable of floats/complex numbers. Floats are positioned along the x-axis, complex numbers along the y-axis. kwargs Additional arguments to be passed to :meth:`~.NumberLine.get_number_mobject`, i.e. :class:`~.DecimalNumber`. """ self.add(self.get_coordinate_labels(*numbers, **kwargs)) return self