mpl_data_containers Documentation#

This is prototype development for the next generation of data structures for Matplotlib. This is the version “to throw away”. Everything in this repository should be considered experimental and used at you own risk.

Source : matplotlib/data-prototype

Examples#

Examples#

Examples used in the rapid prototyping of this project.

Errorbar graph

Errorbar graph

An simple scatter plot using ax.scatter

An simple scatter plot using ax.scatter

Circle

Circle

A re-binning histogram

A re-binning histogram

An simple scatter plot using PathCollectionWrapper

An simple scatter plot using PathCollectionWrapper

A functional 2D image

A functional 2D image

A functional line

A functional line

Show data frame

Show data frame

(Infinitly) Zoomable Mandelbrot Set

(Infinitly) Zoomable Mandelbrot Set

Custom bivariate colormap

Custom bivariate colormap

Using pint units with PathCollectionWrapper

Using pint units with PathCollectionWrapper

Simple patch artists

Simple patch artists

An animated line

An animated line

Dynamic Downsampling

Dynamic Downsampling

An animated lissajous ball

An animated lissajous ball

Mapping Line Properties

Mapping Line Properties

Slider

Slider

Errorbar graph#

Using containers.ArrayContainer and wrappers.ErrorbarWrapper to plot a graph with error bars.

errorbar
import matplotlib.pyplot as plt
import numpy as np


from mpl_data_containers.wrappers import ErrorbarWrapper
from mpl_data_containers.containers import ArrayContainer

x = np.arange(10)
y = x**2
yupper = y + np.sqrt(y)
ylower = y - np.sqrt(y)
xupper = x + 0.5
xlower = x - 0.5

ac = ArrayContainer(
    x=x, y=y, yupper=yupper, ylower=ylower, xlower=xlower, xupper=xupper
)


fig, ax = plt.subplots()

ew = ErrorbarWrapper(ac)
ax.add_artist(ew)
ax.set_xlim(0, 10)
ax.set_ylim(0, 100)
plt.show()

Gallery generated by Sphinx-Gallery

An simple scatter plot using ax.scatter#

This is a quick comparison between the current Matplotlib scatter and the version in mpl_data_containers/axes.py, which uses data containers and a conversion pipeline.

This is here to show what does work and what does not work with the current implementation of container-based artist drawing.

scatter with custom axes
import mpl_data_containers.axes  # side-effect registers projection # noqa

import matplotlib.pyplot as plt

fig = plt.figure()
newstyle = fig.add_subplot(2, 1, 1, projection="mpl-data-containers")
oldstyle = fig.add_subplot(2, 1, 2)

newstyle.scatter([0, 1, 2], [2, 5, 1])
oldstyle.scatter([0, 1, 2], [2, 5, 1])
newstyle.scatter([0, 1, 2], [3, 1, 2])
oldstyle.scatter([0, 1, 2], [3, 1, 2])


# Autoscaling not working
newstyle.set_xlim(oldstyle.get_xlim())
newstyle.set_ylim(oldstyle.get_ylim())

plt.show()

Gallery generated by Sphinx-Gallery

Circle#

Example of directly creating a Patch artist that is defined by a x, y, and path codes.

new patch
import matplotlib.pyplot as plt


from mpl_data_containers.artist import CompatibilityAxes
from mpl_data_containers.patches import Patch
from mpl_data_containers.containers import ArrayContainer

from matplotlib.path import Path

c = Path.unit_circle()

sc = ArrayContainer(None, x=c.vertices[:, 0], y=c.vertices[:, 1], codes=c.codes)
lw2 = Patch(sc, linewidth=3, linestyle=":", edgecolor="C5", alpha=1, hatch=None)

fig, nax = plt.subplots()
nax.set_aspect("equal")
ax = CompatibilityAxes(nax)
nax.add_artist(ax)
ax.add_artist(lw2, 2)
ax.set_xlim(-1.1, 1.1)
ax.set_ylim(-1.1, 1.1)

plt.show()

Gallery generated by Sphinx-Gallery

A re-binning histogram#

A containers.HistContainer which is used with wrappers.StepWrapper to provide a histogram which recomputes the bins based on a range selected.

full range, zoom to small peak
import matplotlib.pyplot as plt
import numpy as np

from mpl_data_containers.wrappers import StepWrapper
from mpl_data_containers.containers import HistContainer

hc = HistContainer(
    np.concatenate([np.random.randn(5000), 0.1 * np.random.randn(500) + 5]), 25
)


fig, (ax1, ax2) = plt.subplots(1, 2, layout="constrained")
for ax in (ax1, ax2):
    ax.add_artist(StepWrapper(hc, lw=0, color="green"))
    ax.set_ylim(0, 1)

ax1.set_xlim(-7, 7)
ax1.axvspan(4.5, 5.5, facecolor="none", zorder=-1, lw=5, edgecolor="k")
ax1.set_title("full range")

ax2.set_xlim(4.5, 5.5)
ax2.set_title("zoom to small peak")


plt.show()

Gallery generated by Sphinx-Gallery

An simple scatter plot using PathCollectionWrapper#

A quick scatter plot using containers.ArrayContainer and wrappers.PathCollectionWrapper.

simple scatter
import numpy as np

import matplotlib.pyplot as plt
import matplotlib.markers as mmarkers

from mpl_data_containers.containers import ArrayContainer

from mpl_data_containers.wrappers import PathCollectionWrapper

marker_obj = mmarkers.MarkerStyle("o")

cont = ArrayContainer(
    x=np.array([0, 1, 2]),
    y=np.array([1, 4, 2]),
    paths=np.array([marker_obj.get_path()]),
    sizes=np.array([12]),
    edgecolors=np.array(["k"]),
    facecolors=np.array(["C3"]),
)

fig, ax = plt.subplots()
ax.set_xlim(-0.5, 2.5)
ax.set_ylim(0, 5)
lw = PathCollectionWrapper(cont, offset_transform=ax.transData)
ax.add_artist(lw)
plt.show()

Gallery generated by Sphinx-Gallery

A functional 2D image#

A 2D image generated using containers.FuncContainer and wrappers.ImageWrapper.

2Dfunc
import matplotlib.pyplot as plt
import numpy as np

from mpl_data_containers.artist import CompatibilityAxes
from mpl_data_containers.image import Image
from mpl_data_containers.containers import FuncContainer

from matplotlib.colors import Normalize


fc = FuncContainer(
    {},
    xyfuncs={
        "x": ((2,), lambda x, y: [x[0], x[-1]]),
        "y": ((2,), lambda x, y: [y[0], y[-1]]),
        "image": (
            ("N", "M"),
            lambda x, y: np.sin(x).reshape(1, -1) * np.cos(y).reshape(-1, 1),
        ),
    },
)
norm = Normalize(vmin=-1, vmax=1)
im = Image(fc, norm=norm)

fig, nax = plt.subplots()
ax = CompatibilityAxes(nax)
nax.add_artist(ax)

ax.add_artist(im)
ax.set_xlim(-5, 5)
ax.set_ylim(-5, 5)
# fig.colorbar(im, ax=nax)
plt.show()

Gallery generated by Sphinx-Gallery

A functional line#

Demonstrating the differences between containers.FuncContainer and containers.SeriesContainer using artist.Line.

first
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd

from mpl_data_containers.artist import CompatibilityAxes
from mpl_data_containers.line import Line
from mpl_data_containers.containers import FuncContainer, SeriesContainer


fc = FuncContainer({"x": (("N",), lambda x: x), "y": (("N",), lambda x: np.sin(1 / x))})
lw = Line(fc, linewidth=5, color="green", label="sin(1/x) (function)")

th = np.linspace(0, 2 * np.pi, 16)
sc = SeriesContainer(pd.Series(index=th, data=np.cos(th)), index_name="x", col_name="y")
lw2 = Line(
    sc,
    linewidth=3,
    linestyle=":",
    color="C0",
    label="cos (pandas)",
    marker=".",
    markersize=12,
)

fig, nax = plt.subplots()
ax = CompatibilityAxes(nax)
nax.add_artist(ax)
ax.add_artist(lw, 3)
ax.add_artist(lw2, 2)
ax.set_xlim(0, np.pi * 4)
ax.set_ylim(-1.1, 1.1)

plt.show()

Gallery generated by Sphinx-Gallery

Show data frame#

Wrapping a pandas.DataFrame using containers.DataFrameContainer and artist.Line.

data frame
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd

from mpl_data_containers.artist import CompatibilityArtist as CA
from mpl_data_containers.line import Line
from mpl_data_containers.containers import DataFrameContainer

th = np.linspace(0, 4 * np.pi, 256)

dc1 = DataFrameContainer(
    pd.DataFrame({"x": th, "y": np.cos(th)}), index_name=None, col_names=lambda n: n
)

df = pd.DataFrame(
    {
        "cos": np.cos(th),
        "sin": np.sin(th),
    },
    index=th,
)


dc2 = DataFrameContainer(df, index_name="x", col_names={"sin": "y"})
dc3 = DataFrameContainer(df, index_name="x", col_names={"cos": "y"})


fig, (ax1, ax2) = plt.subplots(2, 1)
ax1.add_artist(CA(Line(dc1, linewidth=5, color="green", label="sin")))
ax2.add_artist(CA(Line(dc2, linewidth=5, color="green", label="sin")))
ax2.add_artist(CA(Line(dc3, linewidth=5, color="blue", label="cos")))
for ax in (ax1, ax2):
    ax.set_xlim(0, np.pi * 4)
    ax.set_ylim(-1.1, 1.1)

plt.show()

Gallery generated by Sphinx-Gallery

(Infinitly) Zoomable Mandelbrot Set#

A mandelbrot set which is computed using a containers.FuncContainer and represented using a wrappers.ImageWrapper.

The mandelbrot recomputes as it is zoomed in and/or panned.

mandelbrot
import matplotlib.pyplot as plt
import numpy as np

from mpl_data_containers.artist import CompatibilityAxes
from mpl_data_containers.image import Image
from mpl_data_containers.containers import FuncContainer

from matplotlib.colors import Normalize

maxiter = 75


def mandelbrot_set(X, Y, maxiter, *, horizon=3, power=2):
    C = X + Y[:, None] * 1j
    N = np.zeros_like(C, dtype=int)
    Z = np.zeros_like(C)
    for n in range(maxiter):
        mask = abs(Z) < horizon
        N += mask
        Z[mask] = Z[mask] ** power + C[mask]
    N[N == maxiter] = -1
    return Z, N


fc = FuncContainer(
    {},
    xyfuncs={
        "x": ((2,), lambda x, y: [x[0], x[-1]]),
        "y": ((2,), lambda x, y: [y[0], y[-1]]),
        "image": (("N", "M"), lambda x, y: mandelbrot_set(x, y, maxiter)[1]),
    },
)
cmap = plt.get_cmap()
cmap.set_under("w")
im = Image(fc, norm=Normalize(0, maxiter), cmap=cmap)

fig, nax = plt.subplots()
ax = CompatibilityAxes(nax)
nax.add_artist(ax)
ax.add_artist(im)
ax.set_xlim(-1, 1)
ax.set_ylim(-1, 1)

nax.set_aspect("equal")  # No equivalent yet

plt.show()

Total running time of the script: (0 minutes 1.369 seconds)

Gallery generated by Sphinx-Gallery

Custom bivariate colormap#

Using nu functions to account for two values when computing the color of each pixel.

mulivariate cmap
import matplotlib.pyplot as plt
import numpy as np

from mpl_data_containers.image import Image
from mpl_data_containers.artist import CompatibilityAxes
from mpl_data_containers.description import Desc
from mpl_data_containers.containers import FuncContainer
from mpl_data_containers.conversion_edge import FuncEdge

from matplotlib.colors import hsv_to_rgb


def func(x, y):
    return (
        (np.sin(x).reshape(1, -1) * np.cos(y).reshape(-1, 1)) ** 2,
        np.arctan2(np.cos(y).reshape(-1, 1), np.sin(x).reshape(1, -1)),
    )


def image_nu(image):
    saturation, angle = image
    hue = (angle + np.pi) / (2 * np.pi)
    value = np.ones_like(hue)
    return np.clip(hsv_to_rgb(np.stack([hue, saturation, value], axis=2)), 0, 1)


fc = FuncContainer(
    {},
    xyfuncs={
        "x": ((2,), lambda x, y: [x[0], x[-1]]),
        "y": ((2,), lambda x, y: [y[0], y[-1]]),
        "image": (("N", "M", 2), func),
    },
)

image_edges = FuncEdge.from_func(
    "image",
    image_nu,
    {"image": Desc(("M", "N", 2), "auto")},
    {"image": Desc(("M", "N", 3), "rgb")},
)

im = Image(fc, [image_edges])

fig, nax = plt.subplots()
ax = CompatibilityAxes(nax)
nax.add_artist(ax)
ax.add_artist(im)
ax.set_xlim(-5, 5)
ax.set_ylim(-5, 5)
plt.show()

Gallery generated by Sphinx-Gallery

Using pint units with PathCollectionWrapper#

Using third party units functionality in conjunction with Matplotlib Axes

units
import numpy as np
from collections import defaultdict

import matplotlib.pyplot as plt
import matplotlib.markers as mmarkers

from mpl_data_containers.artist import CompatibilityAxes
from mpl_data_containers.containers import ArrayContainer
from mpl_data_containers.conversion_edge import FuncEdge
from mpl_data_containers.description import Desc

from mpl_data_containers.line import Line

import pint

ureg = pint.UnitRegistry()
ureg.setup_matplotlib()

marker_obj = mmarkers.MarkerStyle("o")


coords = defaultdict(lambda: "auto")
coords["x"] = coords["y"] = "units"
cont = ArrayContainer(
    coords,
    x=np.array([0, 1, 2]) * ureg.m,
    y=np.array([1, 4, 2]) * ureg.m,
    paths=np.array([marker_obj.get_path()]),
    sizes=np.array([12]),
    edgecolors=np.array(["k"]),
    facecolors=np.array(["C3"]),
)

fig, nax = plt.subplots()
ax = CompatibilityAxes(nax)
nax.add_artist(ax)
ax.set_xlim(-0.5, 7)
ax.set_ylim(0, 5)

scalar = Desc((), "units")
unit_vector = Desc(("N",), "units")

xconv = FuncEdge.from_func(
    "xconv",
    lambda x, xunits: x.to(xunits).magnitude,
    {"x": unit_vector, "xunits": scalar},
    {"x": Desc(("N",), "data")},
)
yconv = FuncEdge.from_func(
    "yconv",
    lambda y, yunits: y.to(yunits).magnitude,
    {"y": unit_vector, "yunits": scalar},
    {"y": Desc(("N",), "data")},
)
lw = Line(cont, [xconv, yconv])

ax.add_artist(lw)
nax.xaxis.set_units(ureg.ft)
nax.yaxis.set_units(ureg.m)

plt.show()

Gallery generated by Sphinx-Gallery

Simple patch artists#

Draw two fully specified rectangle patches. Demonstrates patches.RectangleWrapper using containers.ArrayContainer.

simple patch
import numpy as np

import matplotlib.pyplot as plt
import matplotlib.patches as mpatches

from mpl_data_containers.containers import ArrayContainer
from mpl_data_containers.artist import CompatibilityAxes

from mpl_data_containers.patches import Rectangle

cont1 = ArrayContainer(
    lower_left_x=np.array(-3),
    lower_left_y=np.array(0),
    upper_right_x=np.array(-1),
    upper_right_y=np.array(3),
    edgecolor=np.array([0, 0, 0]),
    facecolor="green",
    linewidth=3,
    linestyle="-",
    antialiased=np.array([True]),
    fill=np.array([True]),
    capstyle=np.array(["round"]),
    joinstyle=np.array(["miter"]),
    alpha=np.array(0.5),
)

cont2 = ArrayContainer(
    lower_left_x=0,
    lower_left_y=np.array(1),
    upper_right_x=np.array(2),
    upper_right_y=np.array(5),
    angle=30,
    rotation_point_x=np.array(1),
    rotation_point_y=np.array(3.5),
    edgecolor=np.array([0.5, 0.2, 0]),
    facecolor="red",
    linewidth=6,
    linestyle="-",
    antialiased=np.array([True]),
    fill=np.array([True]),
    capstyle=np.array(["round"]),
    joinstyle=np.array(["miter"]),
)

fig, nax = plt.subplots()
ax = CompatibilityAxes(nax)
nax.add_artist(ax)
nax.set_xlim(-5, 5)
nax.set_ylim(0, 5)

rect = mpatches.Rectangle((4, 1), 2, 3, linewidth=6, edgecolor="black", angle=30)
nax.add_artist(rect)

rect1 = Rectangle(cont1, {})
rect2 = Rectangle(cont2, {})
ax.add_artist(rect1)
ax.add_artist(rect2)
nax.set_aspect(1)
plt.show()

Gallery generated by Sphinx-Gallery

An animated line#

An animated line using a custom container class, wrappers.LineWrapper, and wrappers.FormattedText.

import time
from typing import Dict, Tuple, Any, Union
from functools import partial

import numpy as np

import matplotlib.pyplot as plt
from matplotlib.animation import FuncAnimation

from mpl_data_containers.conversion_edge import Graph
from mpl_data_containers.description import Desc


from mpl_data_containers.artist import CompatibilityAxes
from mpl_data_containers.line import Line
from mpl_data_containers.text import Text
from mpl_data_containers.conversion_edge import FuncEdge


class SinOfTime:
    N = 1024
    # cycles per minutes
    scale = 10

    def describe(self):
        return {
            "x": Desc((self.N,)),
            "y": Desc((self.N,)),
            "phase": Desc(()),
            "time": Desc(()),
        }

    def query(
        self,
        graph: Graph,
        parent_coordinates: str = "axes",
    ) -> Tuple[Dict[str, Any], Union[str, int]]:
        th = np.linspace(0, 2 * np.pi, self.N)

        cur_time = time.time()

        phase = 2 * np.pi * (self.scale * cur_time % 60) / 60
        return {
            "x": th,
            "y": np.sin(th + phase),
            "phase": phase,
            "time": cur_time,
        }, hash(cur_time)


def update(frame, art):
    return art


sot_c = SinOfTime()
lw = Line(sot_c, linewidth=5, color="green", label="sin(time)")
fc = Text(
    sot_c,
    [
        FuncEdge.from_func(
            "text",
            lambda phase: f"ϕ={phase:.2f}",
            {"phase": Desc((), "auto")},
            {"text": Desc((), "display")},
        ),
    ],
    x=2 * np.pi,
    y=1,
    ha="right",
)
fig, nax = plt.subplots()
ax = CompatibilityAxes(nax)
nax.add_artist(ax)
ax.add_artist(lw)
ax.add_artist(fc)
ax.set_xlim(0, 2 * np.pi)
ax.set_ylim(-1.1, 1.1)
ani = FuncAnimation(
    fig,
    partial(update, art=(lw, fc)),
    frames=25,
    interval=1000 / 60,
    # TODO: blitting does not work because wrappers do not inherent from Artist
    # blit=True,
)

plt.show()

Total running time of the script: (0 minutes 3.453 seconds)

Gallery generated by Sphinx-Gallery

Dynamic Downsampling#

Generates a large image with three levels of detail.

When zoomed out, appears as a difference of 2D Gaussians. At medium zoom, a diagonal sinusoidal pattern is apparent. When zoomed in close, noise is visible.

The image is dynamically subsampled using a local mean which hides the finer details.

subsample
from typing import Tuple, Dict, Any, Union

import matplotlib as mpl
import matplotlib.pyplot as plt
from matplotlib.colors import Normalize

import numpy as np

from mpl_data_containers.description import Desc, desc_like
from mpl_data_containers.artist import CompatibilityArtist as CA
from mpl_data_containers.image import Image
from mpl_data_containers.containers import ArrayContainer

from skimage.transform import downscale_local_mean


x = y = np.linspace(-3, 3, 3000)
X, Y = np.meshgrid(x, y)
Z1 = np.exp(-(X**2) - Y**2) + 0.08 * np.sin(50 * (X + Y))
Z2 = np.exp(-((X - 1) ** 2) - (Y - 1) ** 2)
Z = (Z1 - Z2) * 2

Z += np.random.random(Z.shape) - 0.5


class Subsample:
    def describe(self):
        return {
            "x": Desc((2,)),
            "y": Desc((2,)),
            "image": Desc(("M", "N")),
        }

    def query(
        self,
        graph,
        parent_coordinates="axes",
    ) -> Tuple[Dict[str, Any], Union[str, int]]:
        desc = Desc(("N",), coordinates="data")
        xy = {"x": desc, "y": desc}
        data_lim = graph.evaluator(xy, desc_like(xy, coordinates="axes")).inverse

        pts = data_lim.evaluate({"x": (0, 1), "y": (0, 1)})
        x1, x2 = pts["x"]
        y1, y2 = pts["y"]

        xi1 = np.argmin(np.abs(x - x1))
        yi1 = np.argmin(np.abs(y - y1))
        xi2 = np.argmin(np.abs(x - x2))
        yi2 = np.argmin(np.abs(y - y2))

        xscale = int(np.ceil((xi2 - xi1) / 50))
        yscale = int(np.ceil((yi2 - yi1) / 50))

        return {
            "x": [x1, x2],
            "y": [y1, y2],
            "image": downscale_local_mean(Z[xi1:xi2, yi1:yi2], (xscale, yscale)),
        }, hash((x1, x2, y1, y2))


non_sub = ArrayContainer(**{"image": Z, "x": np.array([0, 1]), "y": np.array([0, 10])})

sub = Subsample()
cmap = mpl.colormaps["coolwarm"]
norm = Normalize(-2.2, 2.2)
im = Image(sub, cmap=cmap, norm=norm)

fig, ax = plt.subplots()
ax.add_artist(CA(im))
ax.set_xlim(-3, 3)
ax.set_ylim(-3, 3)
plt.show()

Gallery generated by Sphinx-Gallery

An animated lissajous ball#

Inspired by https://twitter.com/_brohrer_/status/1584681864648065027

An animated scatter plot using a custom container and wrappers.PathCollectionWrapper

import time
from typing import Dict, Tuple, Any, Union
from functools import partial

import numpy as np

import matplotlib.pyplot as plt
import matplotlib.markers as mmarkers
from matplotlib.animation import FuncAnimation

from mpl_data_containers.conversion_edge import Graph
from mpl_data_containers.description import Desc

from mpl_data_containers.wrappers import PathCollectionWrapper


class Lissajous:
    N = 1024
    # cycles per minutes
    scale = 2

    def describe(self):
        return {
            "x": Desc((self.N,)),
            "y": Desc((self.N,)),
            "time": Desc(()),
            "sizes": Desc(()),
            "paths": Desc(()),
            "edgecolors": Desc(()),
            "facecolors": Desc((self.N,)),
        }

    def query(
        self,
        graph: Graph,
        parent_coordinates: str = "axes",
    ) -> Tuple[Dict[str, Any], Union[str, int]]:
        def next_time():
            cur_time = time.time()
            cur_time = np.array(
                [cur_time, cur_time - 0.1, cur_time - 0.2, cur_time - 0.3]
            )

            phase = 15 * np.pi * (self.scale * cur_time % 60) / 150
            marker_obj = mmarkers.MarkerStyle("o")
            return {
                "x": np.cos(5 * phase),
                "y": np.sin(3 * phase),
                "sizes": np.array([256]),
                "paths": [
                    marker_obj.get_path().transformed(marker_obj.get_transform())
                ],
                "edgecolors": "k",
                "facecolors": ["#4682b4ff", "#82b446aa", "#46b48288", "#8246b433"],
                "time": cur_time[0],
            }, hash(cur_time[0])

        return next_time()


def update(frame, art):
    return art


sot_c = Lissajous()

fig, ax = plt.subplots()
ax.set_xlim(-1.1, 1.1)
ax.set_ylim(-1.1, 1.1)
lw = PathCollectionWrapper(sot_c, offset_transform=ax.transData)
ax.add_artist(lw)
# ax.set_xticks([])
# ax.set_yticks([])
ax.set_aspect(1)
ani = FuncAnimation(
    fig,
    partial(update, art=(lw,)),
    frames=60,
    interval=1000 / 100 * 15,
    # TODO: blitting does not work because wrappers do not inherent from Artist
    # blit=True,
)
plt.show()

Total running time of the script: (0 minutes 5.465 seconds)

Gallery generated by Sphinx-Gallery

Mapping Line Properties#

Leveraging the converter functions to transform users space data to visualization data.

mapped
import matplotlib.pyplot as plt
import numpy as np

from matplotlib.colors import Normalize

from mpl_data_containers.artist import CompatibilityAxes
from mpl_data_containers.line import Line
from mpl_data_containers.containers import ArrayContainer
from mpl_data_containers.description import Desc
from mpl_data_containers.conversion_edge import FuncEdge
from mpl_data_containers.text import Text


cmap = plt.colormaps["viridis"]
cmap.set_over("k")
cmap.set_under("r")
norm = Normalize(1, 8)

line_edges = [
    FuncEdge.from_func(
        "lw",
        lambda lw: min(1 + lw, 5),
        {"lw": Desc((), "auto")},
        {"linewidth": Desc((), "display")},
    ),
    # Probably should separate out norm/cmap step
    # Slight lie about color being a string here, because of limitations in impl
    FuncEdge.from_func(
        "cmap",
        lambda j: cmap(norm(j)),
        {"j": Desc((), "auto")},
        {"color": Desc((), "display")},
    ),
    FuncEdge.from_func(
        "ls",
        lambda cat: {"A": "-", "B": ":", "C": "--"}[cat],
        {"cat": Desc((), "auto")},
        {"linestyle": Desc((), "display")},
    ),
]

text_edges = [
    FuncEdge.from_func(
        "text",
        lambda j, cat: f"index={j[()]} class={cat!r}",
        {"j": Desc((), "auto"), "cat": Desc((), "auto")},
        {"text": Desc((), "display")},
    ),
    FuncEdge.from_func(
        "y",
        lambda j: j,
        {"j": Desc((), "auto")},
        {"y": Desc((), "data")},
    ),
    FuncEdge.from_func(
        "x",
        lambda: 2 * np.pi,
        {},
        {"x": Desc((), "data")},
    ),
]


th = np.linspace(0, 2 * np.pi, 128)
delta = np.pi / 9

fig, nax = plt.subplots()

ax = CompatibilityAxes(nax)
nax.add_artist(ax)

for j in range(10):
    ac = ArrayContainer(
        **{
            "x": th,
            "y": np.sin(th + j * delta) + j,
            "j": np.asarray(j),
            "lw": np.asarray(j),
            "cat": {0: "A", 1: "B", 2: "C"}[j % 3],
        }
    )
    ax.add_artist(
        Line(
            ac,
            line_edges,
        )
    )
    ax.add_artist(
        Text(
            ac,
            text_edges,
            x=2 * np.pi,
            # ha="right",
            # bbox={"facecolor": "gray", "alpha": 0.5},
        )
    )
ax.set_xlim(0, np.pi * 2)
ax.set_ylim(-1.1, 10.1)

plt.show()

Gallery generated by Sphinx-Gallery

Slider#

In this example, sliders are used to control the frequency and amplitude of a sine wave.

widgets
import inspect

import numpy as np
import matplotlib.pyplot as plt
from matplotlib.widgets import Slider, Button

from mpl_data_containers.artist import CompatibilityArtist as CA
from mpl_data_containers.line import Line
from mpl_data_containers.containers import FuncContainer
from mpl_data_containers.description import Desc
from mpl_data_containers.conversion_edge import FuncEdge


class SliderContainer(FuncContainer):
    def __init__(self, xfuncs, /, **sliders):
        self._sliders = sliders
        for slider in sliders.values():
            slider.on_changed(
                lambda _, sld=slider: sld.ax.figure.canvas.draw_idle(),
            )

        def get_needed_keys(f, offset=1):
            return tuple(inspect.signature(f).parameters)[offset:]

        super().__init__(
            {
                k: (
                    s,
                    # this line binds the correct sliders to the functions
                    # and makes lambdas that match the API FuncContainer needs
                    lambda x, keys=get_needed_keys(f), f=f: f(
                        x, *(sliders[k].val for k in keys)
                    ),
                )
                for k, (s, f) in xfuncs.items()
            },
        )

    def _query_hash(self, graph, parent_coordinates):
        key = super()._query_hash(graph, parent_coordinates)
        # inject the slider values into the hashing logic
        return hash((key, tuple(s.val for s in self._sliders.values())))


# Define initial parameters
init_amplitude = 5
init_frequency = 3

# Create the figure and the line that we will manipulate
fig, ax = plt.subplots()
ax.set_xlim(0, 1)
ax.set_ylim(-7, 7)

ax.set_xlabel("Time [s]")

# adjust the main plot to make room for the sliders
fig.subplots_adjust(left=0.25, bottom=0.25, right=0.75)

# Make a horizontal slider to control the frequency.
axfreq = fig.add_axes([0.25, 0.1, 0.65, 0.03])
freq_slider = Slider(
    ax=axfreq,
    label="Frequency [Hz]",
    valmin=0.1,
    valmax=30,
    valinit=init_frequency,
)

# Make a vertically oriented slider to control the amplitude
axamp = fig.add_axes([0.1, 0.25, 0.0225, 0.63])
amp_slider = Slider(
    ax=axamp,
    label="Amplitude",
    valmin=0,
    valmax=10,
    valinit=init_amplitude,
    orientation="vertical",
)

# Make a vertically oriented slider to control the phase
axphase = fig.add_axes([0.85, 0.25, 0.0225, 0.63])
phase_slider = Slider(
    ax=axphase,
    label="Phase [rad]",
    valmin=-2 * np.pi,
    valmax=2 * np.pi,
    valinit=0,
    orientation="vertical",
)

# pick a cyclic color map
cmap = plt.get_cmap("twilight")

# set up the data container
fc = SliderContainer(
    {
        # the x data does not need the sliders values
        "x": (("N",), lambda t: t),
        "y": (
            ("N",),
            # the y data needs all three sliders
            lambda t, amplitude, frequency, phase: amplitude
            * np.sin(2 * np.pi * frequency * t + phase),
        ),
        # the color data has to take the x (because reasons), but just
        # needs the phase
        "color": ((1,), lambda _, phase: phase),
    },
    # bind the sliders to the data container
    amplitude=amp_slider,
    frequency=freq_slider,
    phase=phase_slider,
)
lw = Line(
    fc,
    # color map phase (scaled to 2pi and wrapped to [0, 1])
    [
        FuncEdge.from_func(
            "color",
            lambda color: cmap((color / (2 * np.pi)) % 1),
            {"color": Desc((1,))},
            {"color": Desc((), "display")},
        )
    ],
    linewidth=5.0,
    linestyle="-",
)
ax.add_artist(CA(lw))


# Create a `matplotlib.widgets.Button` to reset the sliders to initial values.
resetax = fig.add_axes([0.8, 0.025, 0.1, 0.04])
button = Button(resetax, "Reset", hovercolor="0.975")
button.on_clicked(
    lambda _: [sld.reset() for sld in (freq_slider, amp_slider, phase_slider)]
)

plt.show()

Gallery generated by Sphinx-Gallery

Gallery generated by Sphinx-Gallery

API#

Containers#

class mpl_data_containers.containers.ArrayContainer(coordinates: dict[str, str] | None = None, /, **data)#
describe() Dict[str, Desc]#
query(graph: Graph, parent_coordinates: str = 'axes') Tuple[Dict[str, Any], str | int]#
update(**data)#
class mpl_data_containers.containers.DataContainer(*args, **kwargs)#
describe() Dict[str, Desc]#

Describe the data a query will return

Returns:
Dict[str, Desc]
query(graph: Graph, parent_coordinates: str = 'axes', /) Tuple[Dict[str, Any], str | int]#

Query the data container for data.

We are given the data limits and the screen size so that we have an estimate of how finely (or not) we need to sample the data we wrapping.

Parameters:
coord_transformmatplotlib.transform.Transform

Must go from axes fraction space -> data space

size2 integers

xpixels, ypixels

The size in screen / render units that we have to fill.

Returns:
dataDict[str, Any]

The values are really array-likes, but 🤷 how to spell that in typing given that the dimension and type will depend on the key / how it is set up and the size may depend on the input values

cache_keystr

This is a key that clients can use to cache down-stream computations on this data.

class mpl_data_containers.containers.DataFrameContainer(df: DataFrame, *, col_names: Callable[[str], str] | Dict[str, str], index_name: str | None = None)#
describe() Dict[str, Desc]#
query(graph: Graph, parent_coordinates: str = 'axes') Tuple[Dict[str, Any], str | int]#
class mpl_data_containers.containers.DataUnion(*data: DataContainer)#
describe()#
query(graph: Graph, parent_coordinates: str = 'axes') Tuple[Dict[str, Any], str | int]#
class mpl_data_containers.containers.FuncContainer(xfuncs: Dict[str, Tuple[Tuple[int | str, ...], Callable[[Any], Any]]] | None = None, yfuncs: Dict[str, Tuple[Tuple[int | str, ...], Callable[[Any], Any]]] | None = None, xyfuncs: Dict[str, Tuple[Tuple[int | str, ...], Callable[[Any, Any], Any]]] | None = None)#
describe() Dict[str, Desc]#
query(graph: Graph, parent_coordinates: str = 'axes') Tuple[Dict[str, Any], str | int]#
class mpl_data_containers.containers.HistContainer(raw_data, num_bins: int)#
describe() Dict[str, Desc]#
query(graph: Graph, parent_coordinates: str = 'axes') Tuple[Dict[str, Any], str | int]#
exception mpl_data_containers.containers.NoNewKeys#
class mpl_data_containers.containers.RandomContainer(**shapes)#
describe() Dict[str, Desc]#
query(graph: Graph, parent_coordinates: str = 'axes') Tuple[Dict[str, Any], str | int]#
class mpl_data_containers.containers.ReNamer(data: DataContainer, mapping: Dict[str, str])#
describe()#
query(graph: Graph, parent_coordinates: str = 'axes') Tuple[Dict[str, Any], str | int]#
class mpl_data_containers.containers.SeriesContainer(series: Series, *, index_name: str, col_name: str)#
describe() Dict[str, Desc]#
query(graph: Graph, parent_coordinates: str = 'axes') Tuple[Dict[str, Any], str | int]#
class mpl_data_containers.containers.WebServiceContainer#
query(graph: Graph, parent_coordinates: str = 'axes') Tuple[Dict[str, Any], str | int]#

Wrappers#

class mpl_data_containers.wrappers.ErrorbarWrapper(data: DataContainer, converters=None, /, **kwargs)#
draw(renderer)#

Draw the Artist (and its children) using the given renderer.

This has no effect if the artist is not visible (.Artist.get_visible returns False).

Parameters:
renderer~matplotlib.backend_bases.RendererBase subclass.

Notes

This method is overridden in the Artist subclasses.

expected_keys: set = {'xlower', 'xupper', 'ylower', 'yupper'}#
required_keys: set = {'x', 'y'}#
set(*, agg_filter=<UNSET>, alpha=<UNSET>, animated=<UNSET>, clip_box=<UNSET>, clip_on=<UNSET>, clip_path=<UNSET>, gid=<UNSET>, in_layout=<UNSET>, label=<UNSET>, mouseover=<UNSET>, path_effects=<UNSET>, picker=<UNSET>, rasterized=<UNSET>, sketch_params=<UNSET>, snap=<UNSET>, transform=<UNSET>, url=<UNSET>, visible=<UNSET>, zorder=<UNSET>)#

Set multiple properties at once.

Supported properties are

Properties:

agg_filter: a filter function, which takes a (m, n, 3) float array and a dpi value, and returns a (m, n, 3) array and two offsets from the bottom left corner of the image alpha: float or None animated: unknown clip_box: unknown clip_on: bool clip_path: unknown figure: unknown gid: str in_layout: bool label: object mouseover: bool path_effects: list of .AbstractPathEffect picker: unknown rasterized: bool sketch_params: unknown snap: unknown transform: unknown url: str visible: bool zorder: float

class mpl_data_containers.wrappers.FormattedText(data: DataContainer, converters=None, /, **kwargs)#
draw(renderer)#
class mpl_data_containers.wrappers.ImageWrapper(data: DataContainer, converters=None, /, cmap=None, norm=None, **kwargs)#
draw(renderer)#
required_keys: set = {'image', 'xextent', 'yextent'}#
class mpl_data_containers.wrappers.LineWrapper(data: DataContainer, converters=None, /, **kwargs)#
draw(renderer)#
required_keys: set = {'x', 'y'}#
class mpl_data_containers.wrappers.MultiProxyWrapper(data, converters: ConversionNode | list[ConversionNode] | None, **kwargs)#
draw(renderer)#

Draw the Artist (and its children) using the given renderer.

This has no effect if the artist is not visible (.Artist.get_visible returns False).

Parameters:
renderer~matplotlib.backend_bases.RendererBase subclass.

Notes

This method is overridden in the Artist subclasses.

get_children()#

Return a list of the child .Artists of this .Artist.

set(*, agg_filter=<UNSET>, alpha=<UNSET>, animated=<UNSET>, clip_box=<UNSET>, clip_on=<UNSET>, clip_path=<UNSET>, gid=<UNSET>, in_layout=<UNSET>, label=<UNSET>, mouseover=<UNSET>, path_effects=<UNSET>, picker=<UNSET>, rasterized=<UNSET>, sketch_params=<UNSET>, snap=<UNSET>, transform=<UNSET>, url=<UNSET>, visible=<UNSET>, zorder=<UNSET>)#

Set multiple properties at once.

Supported properties are

Properties:

agg_filter: a filter function, which takes a (m, n, 3) float array and a dpi value, and returns a (m, n, 3) array and two offsets from the bottom left corner of the image alpha: float or None animated: bool clip_box: ~matplotlib.transforms.BboxBase or None clip_on: bool clip_path: Patch or (Path, Transform) or None figure: ~matplotlib.figure.Figure or ~matplotlib.figure.SubFigure gid: str in_layout: bool label: object mouseover: bool path_effects: list of .AbstractPathEffect picker: None or bool or float or callable rasterized: bool sketch_params: (scale: float, length: float, randomness: float) snap: bool or None transform: ~matplotlib.transforms.Transform url: str visible: bool zorder: float

set_animated(*args, **kwargs)#

broadcasts set_animated to children

set_clip_box(*args, **kwargs)#

broadcasts set_clip_box to children

set_clip_path(*args, **kwargs)#

broadcasts set_clip_path to children

set_figure(*args, **kwargs)#

broadcasts set_figure to children

set_picker(*args, **kwargs)#

broadcasts set_picker to children

set_sketch_params(*args, **kwargs)#

broadcasts set_sketch_params to children

set_snap(*args, **kwargs)#

broadcasts set_snap to children

set_transform(*args, **kwargs)#

broadcasts set_transform to children

class mpl_data_containers.wrappers.PathCollectionWrapper(data: DataContainer, converters=None, /, **kwargs)#
draw(renderer)#
required_keys: set = {'edgecolors', 'facecolors', 'paths', 'sizes', 'x', 'y'}#
class mpl_data_containers.wrappers.ProxyWrapper(data, converters: ConversionNode | list[ConversionNode] | None, **kwargs)#
class mpl_data_containers.wrappers.ProxyWrapperBase(data, converters: ConversionNode | list[ConversionNode] | None, **kwargs)#
axes: _Axes#
data: DataContainer#
draw(renderer)#
expected_keys: set = {}#
required_keys: set = {}#
stale: bool#
class mpl_data_containers.wrappers.StepWrapper(data: DataContainer, converters=None, /, **kwargs)#
draw(renderer)#
required_keys: set = {'density', 'edges'}#
class mpl_data_containers.patches.Patch(container, edges=None, **kwargs)#
draw(renderer, graph: Graph) None#
class mpl_data_containers.patches.PatchWrapper(data: DataContainer, converters=None, /, **kwargs)#
draw(renderer)#
required_keys: set = {'antialiased', 'capstyle', 'edgecolor', 'facecolor', 'fill', 'hatch', 'joinstyle', 'linestyle', 'linewidth'}#
class mpl_data_containers.patches.Rectangle(container, edges=None, **kwargs)#
class mpl_data_containers.patches.RectangleContainer(*args, **kwargs)#
class mpl_data_containers.patches.RectangleWrapper(data: DataContainer, converters=None, /, **kwargs)#
required_keys: set = {'angle', 'antialiased', 'capstyle', 'edgecolor', 'facecolor', 'fill', 'hatch', 'height', 'joinstyle', 'linestyle', 'linewidth', 'rotation_point', 'width', 'x', 'y'}#
class mpl_data_containers.patches.RegularPolygon(container, edges=None, **kwargs)#

Backmatter#

Installation#

At the command line:

$ pip install git+https://github.com/matplotlib/data-prototype.git@main

Release History#

Initial Release (YYYY-MM-DD)#

Minimum Version of Python and NumPy#

  • This project supports at least the minor versions of Python initially released 42 months prior to a planned project release date.

  • The project will always support at least the 2 latest minor versions of Python.

  • The project will support minor versions of numpy initially released in the 24 months prior to a planned project release date or the oldest version that supports the minimum Python version (whichever is higher).

  • The project will always support at least the 3 latest minor versions of NumPy.

The minimum supported version of Python will be set to python_requires in setup. All supported minor versions of Python will be in the test matrix and have binary artifacts built for releases.

The project should adjust upward the minimum Python and NumPy version support on every minor and major release, but never on a patch release.

This is consistent with NumPy NEP 29.