data_prototype 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

A re-binning histogram

A re-binning histogram

An simple scatter plot using PathCollectionWrapper

An simple scatter plot using PathCollectionWrapper

A functional line

A functional line

A functional 2D image

A functional 2D image

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

Mapping Line Properties

Mapping Line Properties

Dynamic Downsampling

Dynamic Downsampling

An animated line

An animated line

An animated lissajous ball

An animated lissajous ball

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 data_prototype.wrappers import ErrorbarWrapper
from data_prototype.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 data_prototype/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 data_prototype.axes  # side-effect registers projection # noqa

import matplotlib.pyplot as plt

fig = plt.figure()
newstyle = fig.add_subplot(2, 1, 1, projection="data-prototype")
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

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 data_prototype.wrappers import StepWrapper
from data_prototype.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 data_prototype.containers import ArrayContainer

from data_prototype.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 line#

Demonstrating the differences between containers.FuncContainer and containers.SeriesContainer using wrappers.LineWrapper.

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

from data_prototype.wrappers import LineWrapper
from data_prototype.containers import FuncContainer, SeriesContainer

fc = FuncContainer({"x": (("N",), lambda x: x), "y": (("N",), np.sin)})
lw = LineWrapper(fc, lw=5, color="green", label="sin (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 = LineWrapper(sc, lw=3, color="blue", label="cos (pandas)")

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

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 data_prototype.wrappers import ImageWrapper
from data_prototype.containers import FuncContainer

from matplotlib.colors import Normalize


fc = FuncContainer(
    {},
    xyfuncs={
        "xextent": ((2,), lambda x, y: [x[0], x[-1]]),
        "yextent": ((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 = ImageWrapper(fc, norm=norm)

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

Gallery generated by Sphinx-Gallery

Show data frame#

Wrapping a pandas.DataFrame using containers.DataFrameContainer and wrappers.LineWrapper.

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

from data_prototype.wrappers import LineWrapper
from data_prototype.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(LineWrapper(dc1, lw=5, color="green", label="sin"))
ax2.add_artist(LineWrapper(dc2, lw=5, color="green", label="sin"))
ax2.add_artist(LineWrapper(dc3, lw=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 data_prototype.wrappers import ImageWrapper
from data_prototype.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):
        I = abs(Z) < horizon
        N += I
        Z[I] = Z[I] ** power + C[I]
    N[N == maxiter] = -1
    return Z, N


fc = FuncContainer(
    {},
    xyfuncs={
        "xextent": ((2,), lambda x, y: [x[0], x[-1]]),
        "yextent": ((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 = ImageWrapper(fc, norm=Normalize(0, maxiter), cmap=cmap)

fig, ax = plt.subplots()
ax.add_artist(im)
ax.set_xlim(-1, 1)
ax.set_ylim(-1, 1)
ax.set_aspect("equal")
fig.colorbar(im)
plt.show()

Total running time of the script: (0 minutes 1.536 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 data_prototype.wrappers import ImageWrapper
from data_prototype.containers import FuncContainer
from data_prototype.conversion_node import FunctionConversionNode

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={
        "xextent": ((2,), lambda x, y: [x[0], x[-1]]),
        "yextent": ((2,), lambda x, y: [y[0], y[-1]]),
        "image": (("N", "M", 2), func),
    },
)

im = ImageWrapper(fc, FunctionConversionNode.from_funcs({"image": image_nu}))

fig, ax = plt.subplots()
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

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

from data_prototype.containers import ArrayContainer
from data_prototype.conversion_node import DelayedConversionNode

from data_prototype.wrappers import PathCollectionWrapper

import pint

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

marker_obj = mmarkers.MarkerStyle("o")

cont = ArrayContainer(
    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, ax = plt.subplots()
ax.set_xlim(-0.5, 7)
ax.set_ylim(0, 5)

# DelayedConversionNode is used to identify the keys which undergo unit transformations
# The actual method which does conversions in this example is added by the
# `Axis`/`Axes`, but `PathCollectionWrapper` does not natively interact with the units.
xconv = DelayedConversionNode.from_keys(("x",), converter_key="xunits")
yconv = DelayedConversionNode.from_keys(("y",), converter_key="yunits")
lw = PathCollectionWrapper(cont, [xconv, yconv], offset_transform=ax.transData)
ax.add_artist(lw)
ax.xaxis.set_units(ureg.feet)
ax.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

from data_prototype.containers import ArrayContainer

from data_prototype.patches import RectangleWrapper

cont1 = ArrayContainer(
    x=np.array([-3]),
    y=np.array([0]),
    width=np.array([2]),
    height=np.array([3]),
    angle=np.array([0]),
    rotation_point=np.array(["center"]),
    edgecolor=np.array([0, 0, 0]),
    facecolor=np.array([0.0, 0.7, 0, 0.5]),
    linewidth=np.array([3]),
    linestyle=np.array(["-"]),
    antialiased=np.array([True]),
    hatch=np.array(["*"]),
    fill=np.array([True]),
    capstyle=np.array(["round"]),
    joinstyle=np.array(["miter"]),
)

cont2 = ArrayContainer(
    x=np.array([0]),
    y=np.array([1]),
    width=np.array([2]),
    height=np.array([3]),
    angle=np.array([30]),
    rotation_point=np.array(["center"]),
    edgecolor=np.array([0, 0, 0]),
    facecolor=np.array([0.7, 0, 0]),
    linewidth=np.array([6]),
    linestyle=np.array(["-"]),
    antialiased=np.array([True]),
    hatch=np.array([""]),
    fill=np.array([True]),
    capstyle=np.array(["butt"]),
    joinstyle=np.array(["round"]),
)

fig, ax = plt.subplots()
ax.set_xlim(-5, 5)
ax.set_ylim(0, 5)
rect1 = RectangleWrapper(cont1, {})
rect2 = RectangleWrapper(cont2, {})
ax.add_artist(rect1)
ax.add_artist(rect2)
ax.set_aspect(1)
plt.show()

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 data_prototype.wrappers import LineWrapper, FormattedText
from data_prototype.containers import ArrayContainer
from data_prototype.conversion_node import FunctionConversionNode

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

line_converter = FunctionConversionNode.from_funcs(
    {
        # arbitrary functions
        "lw": lambda lw: min(1 + lw, 5),
        # standard color mapping
        "color": lambda j: cmap(norm(j)),
        # categorical
        "ls": lambda cat: {"A": "-", "B": ":", "C": "--"}[cat[()]],
    },
)

text_converter = FunctionConversionNode.from_funcs(
    {
        "text": lambda j, cat: f"index={j[()]} class={cat[()]!r}",
        "y": lambda j: j,
        "x": lambda x: 2 * np.pi,
    },
)


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

fig, ax = plt.subplots()

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": np.asarray({0: "A", 1: "B", 2: "C"}[j % 3]),
        }
    )
    ax.add_artist(
        LineWrapper(
            ac,
            line_converter,
        )
    )
    ax.add_artist(
        FormattedText(
            ac,
            text_converter,
            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

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 data_prototype.description import Desc, desc_like
from data_prototype.wrappers import ImageWrapper

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 {
            "xextent": Desc([2], float),
            "yextent": Desc([2], float),
            "image": Desc([], float),
        }

    def query(
        self,
        graph,
        parent_coordinates="axes",
    ) -> Tuple[Dict[str, Any], Union[str, int]]:
        desc = Desc(("N",), np.dtype("f8"), 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 {
            "xextent": [x1, x2],
            "yextent": [y1, y2],
            "image": downscale_local_mean(Z[xi1:xi2, yi1:yi2], (xscale, yscale)),
        }, hash((x1, x2, y1, y2))


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

fig, ax = plt.subplots()
ax.add_artist(im)
ax.set_xlim(-3, 3)
ax.set_ylim(-3, 3)
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 data_prototype.conversion_edge import Graph
from data_prototype.description import Desc

from data_prototype.conversion_node import FunctionConversionNode

from data_prototype.wrappers import LineWrapper, FormattedText


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

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

    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)

        def next_time():
            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)

        return next_time()


def update(frame, art):
    return art


sot_c = SinOfTime()
lw = LineWrapper(sot_c, lw=5, color="green", label="sin(time)")
fc = FormattedText(
    sot_c,
    FunctionConversionNode.from_funcs(
        {"text": lambda phase: f"ϕ={phase:.2f}", "x": lambda: 2 * np.pi, "y": lambda: 1}
    ),
    ha="right",
)
fig, ax = plt.subplots()
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.246 seconds)

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 data_prototype.conversion_edge import Graph
from data_prototype.description import Desc

from data_prototype.wrappers import PathCollectionWrapper


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

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

    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 6.889 seconds)

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 data_prototype.wrappers import LineWrapper
from data_prototype.containers import FuncContainer
from data_prototype.conversion_node import FunctionConversionNode


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 = LineWrapper(
    fc,
    # color map phase (scaled to 2pi and wrapped to [0, 1])
    FunctionConversionNode.from_funcs(
        {"color": lambda color: cmap((color / (2 * np.pi)) % 1)}
    ),
    lw=5,
)
ax.add_artist(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 data_prototype.containers.ArrayContainer(**data)#
describe() Dict[str, Desc]#
query(graph: Graph, parent_coordinates: str = 'axes') Tuple[Dict[str, Any], str | int]#
update(**data)#
class data_prototype.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 data_prototype.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 data_prototype.containers.DataUnion(*data: DataContainer)#
describe()#
query(graph: Graph, parent_coordinates: str = 'axes') Tuple[Dict[str, Any], str | int]#
class data_prototype.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 data_prototype.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 data_prototype.containers.NoNewKeys#
class data_prototype.containers.RandomContainer(**shapes)#
describe() Dict[str, Desc]#
query(graph: Graph, parent_coordinates: str = 'axes') Tuple[Dict[str, Any], str | int]#
class data_prototype.containers.ReNamer(data: DataContainer, mapping: Dict[str, str])#
describe()#
query(graph: Graph, parent_coordinates: str = 'axes') Tuple[Dict[str, Any], str | int]#
class data_prototype.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 data_prototype.containers.WebServiceContainer#
query(graph: Graph, parent_coordinates: str = 'axes') Tuple[Dict[str, Any], str | int]#

Wrappers#

class data_prototype.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: scalar 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 data_prototype.wrappers.FormattedText(data: DataContainer, converters=None, /, **kwargs)#
draw(renderer)#
class data_prototype.wrappers.ImageWrapper(data: DataContainer, converters=None, /, cmap=None, norm=None, **kwargs)#
draw(renderer)#
required_keys: set = {'image', 'xextent', 'yextent'}#
class data_prototype.wrappers.LineWrapper(data: DataContainer, converters=None, /, **kwargs)#
draw(renderer)#
required_keys: set = {'x', 'y'}#
class data_prototype.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: scalar 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 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 data_prototype.wrappers.PathCollectionWrapper(data: DataContainer, converters=None, /, **kwargs)#
draw(renderer)#
required_keys: set = {'edgecolors', 'facecolors', 'paths', 'sizes', 'x', 'y'}#
class data_prototype.wrappers.ProxyWrapper(data, converters: ConversionNode | list[ConversionNode] | None, **kwargs)#
class data_prototype.wrappers.ProxyWrapperBase(data, converters: ConversionNode | list[ConversionNode] | None, **kwargs)#
axes: _Axes#
data: DataContainer#
draw(renderer)#
expected_keys: set = {}#
required_keys: set = {}#
stale: bool#
class data_prototype.wrappers.StepWrapper(data: DataContainer, converters=None, /, **kwargs)#
draw(renderer)#
required_keys: set = {'density', 'edges'}#
class data_prototype.patches.PatchWrapper(data: DataContainer, converters=None, /, **kwargs)#
draw(renderer)#
required_keys: set = {'antialiased', 'capstyle', 'edgecolor', 'facecolor', 'fill', 'hatch', 'joinstyle', 'linestyle', 'linewidth'}#
class data_prototype.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'}#

Backmatter#

Installation#

At the command line:

$ pip install git+https://github.com/tacaswell/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.