Matplotlib plot canvas ====================== A :py:class:`anim.plot.canva` embeds a Matplotlib figure in an :py:class:`anim.window`. It follows the same animation cycle as a :py:class:`anim.plane.canva`, while exposing Matplotlib's object-oriented interface. Users do not need to import Qt or ``matplotlib.pyplot``. Creating a plot canvas ---------------------- A new plot canvas contains one empty axes by default: .. code-block:: python import anim W = anim.window('Plot') C = anim.plot.canva(W) W.add(C) C.axes.plot([0, 1, 2], [0, 1, 4]) W.show() The main Matplotlib objects are available directly on the canvas: ``C.figure`` The complete Matplotlib figure. Use it to create figure-level titles or additional axes. ``C.axes`` The default axes, or an array of axes when several subplots are requested. ``C.canvas`` The Matplotlib rendering canvas. Calling it directly is rarely necessary, because :py:meth:`anim.plot.canva.update` redraws it automatically. Configuring axes ---------------- Use the exposed axes just as in Matplotlib's object-oriented API: .. code-block:: python C = anim.plot.canva(W) C.axes.set_title('Position over time') C.axes.set_xlabel('Time (s)') C.axes.set_ylabel('Position (m)') C.axes.set_xlim(0, 10) C.axes.set_ylim(-1, 1) C.axes.grid(True, alpha=0.3) line, = C.axes.plot([], [], label='x(t)') C.axes.legend() Keeping the returned artist, such as ``line`` above, makes later updates simple and efficient. Modify the artist rather than clearing and rebuilding the complete axes on every frame. For standard axes, :py:meth:`matplotlib.axes.Axes.legend` automatically uses the axes background and foreground colors. A plain ``C.axes.legend()`` is therefore consistent with both dark and light plot styles. Explicit legend options remain possible and take priority over these defaults: .. code-block:: python C.axes.legend(facecolor='navy', labelcolor='white') Plot style ---------- The plot inherits its parent window style by default. The supported values are ``'dark'``, ``'light'`` and ``'white'``: .. code-block:: python W = anim.window('Dark plot', style='dark') inherited = anim.plot.canva(W) light_plot = anim.plot.canva(W, style='light') The style is applied only while the embedded figure is created. It does not change Matplotlib's global settings and therefore does not affect other plot canvas instances. Relative canvas size -------------------- ``width`` and ``height`` are positive integer weights used by the window grid. They are proportions, not pixels and not data-axis units. Their default value is 1, which gives a plot the same layout weight as other canvas types. .. code-block:: python W.add(anim.plane.canva(W), row=0, col=0) W.add(anim.plot.canva(W, width=2), row=0, col=1) Here the right column receives twice as much horizontal space as the left column. Likewise, ``height=2`` gives a row twice the vertical weight of a row whose weight is 1. Creating several axes --------------------- Use ``nrows`` and ``ncols`` for a regular grid of subplots. Any other keyword argument is forwarded directly to Matplotlib's subplot creation method: .. code-block:: python C = anim.plot.canva( W, nrows=2, ncols=1, sharex=True ) C.axes[0].set_ylabel('x') C.axes[1].set_ylabel('y') C.axes[1].set_xlabel('Time (s)') Advanced options such as ``sharey``, ``squeeze``, ``subplot_kw`` and ``gridspec_kw`` can be passed in the same way. By Matplotlib convention, ``C.axes`` is a single axes for a default 1 by 1 layout, but usually becomes a NumPy array when several subplots are created. The ``squeeze`` option can change that behavior. Updating a plot dynamically ---------------------------- Define a child class of :py:class:`anim.plot.canva`, keep references to the artists created in the constructor, then change their data in ``update``. Always finish with :py:meth:`anim.plot.canva.update` so the completed frame is drawn and reported to the window: .. code-block:: python import numpy as np import anim class LivePlot(anim.plot.canva): def __init__(self, window): super().__init__(window) self.line, = self.axes.plot([], []) self.axes.set_xlim(0, 10) self.axes.set_ylim(-1, 1) def update(self, t): times = np.arange(t.step + 1)*self.window.dt self.line.set_data(times, np.sin(times)) super().update(t) W = anim.window('Live plot') W.add(LivePlot) W.show() .. warning:: Do not blindly append one data point on every call to ``update`` unless the animation is guaranteed to move strictly forward and every step is visited exactly once. A backward step, a direct jump to another step, or a repeated update of the same step can otherwise create duplicate, out-of-order or stale plot data. Prefer deriving the displayed arrays from ``t.step`` or ``t.time`` on every update, as in the example above. If rebuilding all data is too expensive, use an explicitly reversible and idempotent history structure: updating the same step twice must produce the same result, and moving backward must undo or discard samples beyond the current step. The final call to :py:meth:`anim.plot.canva.update` performs a synchronous Matplotlib draw. This keeps the plot synchronized with other canvas panels and ensures that movie captures contain the fully updated frame.