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

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

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

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 4.574 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()

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

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, FormatedText
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(
        FormatedText(
            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.wrappers import ImageWrapper
from data_prototype.containers import _MatplotlibTransform, Desc

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,
        coord_transform: _MatplotlibTransform,
        size: Tuple[int, int],
    ) -> Tuple[Dict[str, Any], Union[str, int]]:
        (x1, y1), (x2, y2) = coord_transform.transform([[0, 0], [1, 1]])

        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()

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

Gallery generated by Sphinx-Gallery

An animated line#

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

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.containers import _MatplotlibTransform, Desc
from data_prototype.conversion_node import FunctionConversionNode

from data_prototype.wrappers import LineWrapper, FormatedText


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,
        coord_transformtransform: _MatplotlibTransform,
        size: Tuple[int, int],
    ) -> 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 = FormatedText(
    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 5.851 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