Skip to content

Introduce artist classes, starting with Line #40

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
Jun 6, 2024
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
236 changes: 236 additions & 0 deletions data_prototype/artist.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,236 @@
from bisect import insort
from typing import Sequence

import numpy as np

from .containers import DataContainer, ArrayContainer, DataUnion
from .description import Desc, desc_like
from .conversion_edge import Edge, Graph, TransformEdge


class Artist:
required_keys: dict[str, Desc]

# defaults?
def __init__(
self, container: DataContainer, edges: Sequence[Edge] | None = None, **kwargs
):
kwargs_cont = ArrayContainer(**kwargs)
self._container = DataUnion(container, kwargs_cont)

edges = edges or []
self._visible = True
self._graph = Graph(edges)
self._clip_box: DataContainer = ArrayContainer(
{"x": "parent", "y": "parent"},
**{"x": np.asarray([0, 1]), "y": np.asarray([0, 1])}
)

def draw(self, renderer, graph: Graph) -> None:
return

def set_clip_box(self, container: DataContainer) -> None:
self._clip_box = container

def get_clip_box(self, container: DataContainer) -> DataContainer:
return self._clip_box

def get_visible(self):
return self._visible

def set_visible(self, visible):
self._visible = visible


class CompatibilityArtist:
"""A compatibility shim to ducktype as a classic Matplotlib Artist.
At this time features are implemented on an "as needed" basis, and many
are only implemented insofar as they do not fail, not necessarily providing
full functionality of a full MPL Artist.
The idea is to keep the new Artist class as minimal as possible.
As features are added this may shrink.
The main thing we are trying to avoid is the reliance on the axes/figure
Ultimately for useability, whatever remains shimmed out here may be rolled in as
some form of gaurded option to ``Artist`` itself, but a firm dividing line is
useful for avoiding accidental dependency.
"""

def __init__(self, artist: Artist):
self._artist = artist

self._axes = None
self.figure = None
self._clippath = None
self._visible = True
self.zorder = 2
self._graph = Graph([])

@property
def axes(self):
return self._axes

@axes.setter
def axes(self, ax):
self._axes = ax

if self._axes is None:
self._graph = Graph([])
return

desc: Desc = Desc(("N",), coordinates="data")
xy: dict[str, Desc] = {"x": desc, "y": desc}
self._graph = Graph(
[
TransformEdge(
"data",
xy,
desc_like(xy, coordinates="axes"),
transform=self._axes.transData - self._axes.transAxes,
),
TransformEdge(
"axes",
desc_like(xy, coordinates="axes"),
desc_like(xy, coordinates="display"),
transform=self._axes.transAxes,
),
],
aliases=(("parent", "axes"),),
)

def set_figure(self, fig):
self.figure = fig

def is_transform_set(self):
return True

def get_mouseover(self):
return False

def get_clip_path(self):
self._clippath

def set_clip_path(self, path):
self._clippath = path

def get_animated(self):
return False

def get_visible(self):
return self._visible

def set_visible(self, visible):
self._visible = visible

def draw(self, renderer, graph=None):
if not self.get_visible():
return

if graph is None:
graph = Graph([])
self._artist.draw(renderer, graph + self._graph)


class CompatibilityAxes:
"""A compatibility shim to add to traditional matplotlib axes.
At this time features are implemented on an "as needed" basis, and many
are only implemented insofar as they do not fail, not necessarily providing
full functionality of a full MPL Artist.
The idea is to keep the new Artist class as minimal as possible.
As features are added this may shrink.
The main thing we are trying to avoid is the reliance on the axes/figure
Ultimately for useability, whatever remains shimmed out here may be rolled in as
some form of gaurded option to ``Artist`` itself, but a firm dividing line is
useful for avoiding accidental dependency.
"""

def __init__(self, axes):
self._axes = axes
self.figure = None
self._clippath = None
self._visible = True
self.zorder = 2
self._children: list[tuple[float, Artist]] = []

@property
def axes(self):
return self._axes

@axes.setter
def axes(self, ax):
self._axes = ax

if self._axes is None:
self._graph = Graph([])
return

desc: Desc = Desc(("N",), coordinates="data")
xy: dict[str, Desc] = {"x": desc, "y": desc}
self._graph = Graph(
[
TransformEdge(
"data",
xy,
desc_like(xy, coordinates="axes"),
transform=self._axes.transData - self._axes.transAxes,
),
TransformEdge(
"axes",
desc_like(xy, coordinates="axes"),
desc_like(xy, coordinates="display"),
transform=self._axes.transAxes,
),
],
aliases=(("parent", "axes"),),
)

def set_figure(self, fig):
self.figure = fig

def is_transform_set(self):
return True

def get_mouseover(self):
return False

def get_clip_path(self):
self._clippath

def set_clip_path(self, path):
self._clippath = path

def get_animated(self):
return False

def draw(self, renderer, graph=None):
if not self.visible:
return
if graph is None:
graph = Graph([])

graph = graph + self._graph

for _, c in self._children:
c.draw(renderer, graph)

def add_artist(self, artist, zorder=1):
insort(self._children, (zorder, artist), key=lambda x: x[0])

def set_xlim(self, min_=None, max_=None):
self.axes.set_xlim(min_, max_)

def set_ylim(self, min_=None, max_=None):
self.axes.set_ylim(min_, max_)

def get_visible(self):
return self._visible

def set_visible(self, visible):
self._visible = visible
27 changes: 14 additions & 13 deletions data_prototype/containers.py
Original file line number Diff line number Diff line change
@@ -82,14 +82,15 @@ class NoNewKeys(ValueError): ...


class ArrayContainer:
def __init__(self, **data):
def __init__(self, coordinates: dict[str, str] | None = None, /, **data):
coordinates = coordinates or {}
self._data = data
self._cache_key = str(uuid.uuid4())
self._desc = {
k: (
Desc(v.shape, v.dtype)
Desc(v.shape, coordinates.get(k, "auto"))
if isinstance(v, np.ndarray)
else Desc((), type(v))
else Desc(())
)
for k, v in data.items()
}
@@ -117,7 +118,7 @@ def update(self, **data):

class RandomContainer:
def __init__(self, **shapes):
self._desc = {k: Desc(s, np.dtype(float)) for k, s in shapes.items()}
self._desc = {k: Desc(s) for k, s in shapes.items()}

def query(
self,
@@ -171,7 +172,7 @@ def __init__(
def _split(input_dict):
out = {}
for k, (shape, func) in input_dict.items():
self._desc[k] = Desc(shape, np.dtype(float))
self._desc[k] = Desc(shape)
out[k] = func
return out

@@ -196,7 +197,7 @@ def query(
# if hash_key in self._cache:
# return self._cache[hash_key], hash_key

desc = Desc(("N",), np.dtype("f8"))
desc = Desc(("N",))
xy = {"x": desc, "y": desc}
data_lim = graph.evaluator(
desc_like(xy, coordinates="data"),
@@ -243,8 +244,8 @@ def __init__(self, raw_data, num_bins: int):
self._raw_data = raw_data
self._num_bins = num_bins
self._desc = {
"edges": Desc((num_bins + 1 + 2,), np.dtype(float)),
"density": Desc((num_bins + 2,), np.dtype(float)),
"edges": Desc((num_bins + 1 + 2,)),
"density": Desc((num_bins + 2,)),
}
self._full_range = (raw_data.min(), raw_data.max())
self._cache: MutableMapping[Union[str, int], Any] = LFUCache(64)
@@ -256,7 +257,7 @@ def query(
) -> Tuple[Dict[str, Any], Union[str, int]]:
dmin, dmax = self._full_range

desc = Desc(("N",), np.dtype("f8"))
desc = Desc(("N",))
xy = {"x": desc, "y": desc}
data_lim = graph.evaluator(
desc_like(xy, coordinates="data"),
@@ -302,8 +303,8 @@ def __init__(self, series: pd.Series, *, index_name: str, col_name: str):
self._index_name = index_name
self._col_name = col_name
self._desc = {
index_name: Desc((len(series),), series.index.dtype),
col_name: Desc((len(series),), series.dtype),
index_name: Desc((len(series),)),
col_name: Desc((len(series),)),
}
self._hash_key = str(uuid.uuid4())

@@ -343,9 +344,9 @@ def __init__(

self._desc: Dict[str, Desc] = {}
if self._index_name is not None:
self._desc[self._index_name] = Desc((len(df),), df.index.dtype)
self._desc[self._index_name] = Desc((len(df),))
for col, out in self._col_name_dict.items():
self._desc[out] = Desc((len(df),), df[col].dtype)
self._desc[out] = Desc((len(df),))

self._hash_key = str(uuid.uuid4())

326 changes: 255 additions & 71 deletions data_prototype/conversion_edge.py

Large diffs are not rendered by default.

44 changes: 32 additions & 12 deletions data_prototype/description.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
from dataclasses import dataclass
from typing import TypeAlias, Tuple, Union

import numpy as np
from typing import TypeAlias, Tuple, Union, overload


ShapeSpec: TypeAlias = Tuple[Union[str, int], ...]
@@ -16,8 +14,7 @@ class Desc:
# - what is the relative size to the other variable values (N vs N+1)
# We are probably going to have to implement a DSL for this (😞)
shape: ShapeSpec
dtype: np.dtype
coordinates: str = "naive"
coordinates: str = "auto"

@staticmethod
def validate_shapes(
@@ -106,28 +103,51 @@ def validate_shapes(
return None

@staticmethod
def compatible(a: dict[str, "Desc"], b: dict[str, "Desc"]) -> bool:
def compatible(
a: dict[str, "Desc"],
b: dict[str, "Desc"],
aliases: tuple[tuple[str, str], ...] = (),
) -> bool:
"""Determine if ``a`` is a valid input for ``b``.
Note: ``a`` _may_ have additional keys.
"""

def resolve_aliases(coord):
while True:
for coa, cob in aliases:
if coord == coa:
coord = cob
break
else:
break
return coord

try:
Desc.validate_shapes(b, a)
except (KeyError, ValueError):
return False
for k, v in b.items():
if a[k].coordinates != v.coordinates:
if resolve_aliases(a[k].coordinates) != resolve_aliases(v.coordinates):
return False
return True


def desc_like(desc, shape=None, dtype=None, coordinates=None):
@overload
def desc_like(desc: Desc, shape=None, coordinates=None) -> Desc: ...


@overload
def desc_like(
desc: dict[str, Desc], shape=None, coordinates=None
) -> dict[str, Desc]: ...


def desc_like(desc, shape=None, coordinates=None):
if isinstance(desc, dict):
return {k: desc_like(v, shape, dtype, coordinates) for k, v in desc.items()}
return {k: desc_like(v, shape, coordinates) for k, v in desc.items()}
if shape is None:
shape = desc.shape
if dtype is None:
dtype = desc.dtype
if coordinates is None:
coordinates = desc.coordinates
return Desc(shape, dtype, coordinates)
return Desc(shape, coordinates)
113 changes: 113 additions & 0 deletions data_prototype/image.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import numpy as np

import matplotlib as mpl
import matplotlib.colors as mcolors
import matplotlib.transforms as mtransforms

from .artist import Artist
from .description import Desc, desc_like
from .conversion_edge import FuncEdge, Graph, CoordinateEdge


def _interpolate_nearest(image, x, y):
magnification = 1 # TODO
l, r = x
width = int(((round(r) + 0.5) - (round(l) - 0.5)) * magnification)

xpix = np.digitize(np.arange(width), np.linspace(0, r - l, image.shape[1] + 1))

b, t = y
height = int(((round(t) + 0.5) - (round(b) - 0.5)) * magnification)
ypix = np.digitize(np.arange(height), np.linspace(0, t - b, image.shape[0] + 1))

out = np.empty((height, width, 4))

out[:, :, :] = image[
ypix[:, None].clip(0, image.shape[0] - 1),
xpix[None, :].clip(0, image.shape[1] - 1),
:,
]
return out


class Image(Artist):
def __init__(self, container, edges=None, norm=None, cmap=None, **kwargs):
super().__init__(container, edges, **kwargs)
if norm is None:
norm = mcolors.Normalize()
if cmap is None:
cmap = mpl.colormaps["viridis"]
self.norm = norm
self.cmap = cmap

arrdesc = Desc(("M", "N"))

self._interpolation_edge = FuncEdge.from_func(
"interpolate_nearest_rgba",
_interpolate_nearest,
{
"image": Desc(("M", "N", 4), coordinates="rgba"),
"x": Desc(("X",), coordinates="display"),
"y": Desc(("Y",), coordinates="display"),
},
{"image": Desc(("O", "P", 4), coordinates="rgba_resampled")},
)

self._edges += [
CoordinateEdge.from_coords("xycoords", {"x": "auto", "y": "auto"}, "data"),
CoordinateEdge.from_coords(
"image_coords", {"image": Desc(("M", "N"), "auto")}, "data"
),
FuncEdge.from_func(
"image_norm",
lambda image: self.norm(image),
{"image": desc_like(arrdesc, coordinates="data_resampled")},
{"image": desc_like(arrdesc, coordinates="norm")},
),
FuncEdge.from_func(
"image_cmap",
lambda image: self.cmap(image),
{"image": desc_like(arrdesc, coordinates="norm")},
{"image": Desc(("M", "N", 4), coordinates="rgba")},
),
FuncEdge.from_func(
"image_display",
lambda image: (image * 255).astype(np.uint8),
{"image": Desc(("O", "P", 4), "rgba_resampled")},
{"image": Desc(("O", "P", 4), "display")},
),
self._interpolation_edge,
]

self._graph = Graph(self._edges, (("data", "data_resampled"),))

def draw(self, renderer, graph: Graph) -> None:
if not self.get_visible():
return
g = graph + self._graph
conv = g.evaluator(
self._container.describe(),
{
"image": Desc(("O", "P", 4), "display"),
"x": Desc(("X",), "display"),
"y": Desc(("Y",), "display"),
},
)
query, _ = self._container.query(g)
evald = conv.evaluate(query)
image = evald["image"]
x = evald["x"]
y = evald["y"]

clip_conv = g.evaluator(
self._clip_box.describe(),
{"x": Desc(("N",), "display"), "y": Desc(("N",), "display")},
)
clip_query, _ = self._clip_box.query(g)
clipx, clipy = clip_conv.evaluate(clip_query).values()

gc = renderer.new_gc()
gc.set_clip_rectangle(
mtransforms.Bbox.from_extents(clipx[0], clipy[0], clipx[1], clipy[1])
)
renderer.draw_image(gc, x[0], y[0], image) # TODO vector backend transforms
125 changes: 125 additions & 0 deletions data_prototype/line.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import matplotlib.path as mpath
import matplotlib.colors as mcolors
import matplotlib.lines as mlines
import matplotlib.markers as mmarkers
import matplotlib.transforms as mtransforms
import numpy as np

from .artist import Artist
from .description import Desc
from .conversion_edge import Graph, CoordinateEdge, DefaultEdge


class Line(Artist):
def __init__(self, container, edges=None, **kwargs):
super().__init__(container, edges, **kwargs)

scalar = Desc((), "display") # ... this needs thinking...

edges = [
CoordinateEdge.from_coords("xycoords", {"x": "auto", "y": "auto"}, "data"),
CoordinateEdge.from_coords("color", {"color": Desc(())}, "display"),
CoordinateEdge.from_coords("linewidth", {"linewidth": Desc(())}, "display"),
CoordinateEdge.from_coords("linestyle", {"linestyle": Desc(())}, "display"),
CoordinateEdge.from_coords(
"markeredgecolor", {"markeredgecolor": Desc(())}, "display"
),
CoordinateEdge.from_coords(
"markerfacecolor", {"markerfacecolor": Desc(())}, "display"
),
CoordinateEdge.from_coords(
"markersize", {"markersize": Desc(())}, "display"
),
CoordinateEdge.from_coords(
"markeredgewidth", {"markeredgewidth": Desc(())}, "display"
),
CoordinateEdge.from_coords("marker", {"marker": Desc(())}, "display"),
DefaultEdge.from_default_value("color_def", "color", scalar, "C0"),
DefaultEdge.from_default_value("linewidth_def", "linewidth", scalar, 1),
DefaultEdge.from_default_value("linestyle_def", "linestyle", scalar, "-"),
DefaultEdge.from_default_value("mec_def", "markeredgecolor", scalar, "C0"),
DefaultEdge.from_default_value("mfc_def", "markerfacecolor", scalar, "C0"),
DefaultEdge.from_default_value("ms_def", "markersize", scalar, 6),
DefaultEdge.from_default_value("mew_def", "markeredgewidth", scalar, 1),
DefaultEdge.from_default_value("marker_def", "marker", scalar, "None"),
]
self._graph = self._graph + Graph(edges)
# Currently ignoring:
# - cap/join style
# - url
# - antialiased
# - snapping
# - sketch
# - gap color
# - draw style (steps)
# - fill style/alt_marker_path
# - markevery
# - non-str markers
# Each individually pretty easy, but relatively rare features, focusing on common cases

def draw(self, renderer, graph: Graph) -> None:
if not self.get_visible():
return
g = graph + self._graph
desc = Desc(("N",), "display")
scalar = Desc((), "display") # ... this needs thinking...

require = {
"x": desc,
"y": desc,
"color": scalar,
"linewidth": scalar,
"linestyle": scalar,
"markeredgecolor": scalar,
"markerfacecolor": scalar,
"markersize": scalar,
"markeredgewidth": scalar,
"marker": scalar,
}

conv = g.evaluator(self._container.describe(), require)
query, _ = self._container.query(g)
x, y, color, lw, ls, *marker = conv.evaluate(query).values()
mec, mfc, ms, mew, mark = marker

clip_conv = g.evaluator(
self._clip_box.describe(),
{"x": Desc(("N",), "display"), "y": Desc(("N",), "display")},
)
clip_query, _ = self._clip_box.query(g)
clipx, clipy = clip_conv.evaluate(clip_query).values()

# make the Path object
path = mpath.Path(np.vstack([x, y]).T)
# make an configure the graphic context
gc = renderer.new_gc()
gc.set_clip_rectangle(
mtransforms.Bbox.from_extents(clipx[0], clipy[0], clipx[1], clipy[1])
)
gc.set_foreground(color)
gc.set_linewidth(lw)
gc.set_dashes(*mlines._scale_dashes(*mlines._get_dash_pattern(ls), lw))
# add the line to the render buffer
renderer.draw_path(gc, path, mtransforms.IdentityTransform())

if mark != "None" and ms > 0:
gc = renderer.new_gc()
gc.set_clip_rectangle(
mtransforms.Bbox.from_extents(clipx[0], clipy[0], clipx[1], clipy[1])
)
gc.set_linewidth(mew)
gc.set_foreground(mec)
marker_ = mmarkers.MarkerStyle(mark)
marker_path = marker_.get_path()
marker_trans = marker_.get_transform()
w = renderer.points_to_pixels(ms)
marker_trans = marker_trans.scale(w)
mfc = mcolors.to_rgba(mfc)
renderer.draw_markers(
gc,
marker_path,
marker_trans,
path,
mtransforms.IdentityTransform(),
mfc,
)
2 changes: 1 addition & 1 deletion data_prototype/wrappers.py
Original file line number Diff line number Diff line change
@@ -132,7 +132,7 @@ def _query_and_transform(self, renderer) -> Dict[str, Any]:

# actually query the underlying data. This returns both the (raw) data
# and key to use for caching.
desc = Desc(("N",), np.dtype("f8"), coordinates="data")
desc = Desc(("N",), coordinates="data")
xy = {"x": desc, "y": desc}
edges = [
TransformEdge(
33 changes: 16 additions & 17 deletions examples/animation.py
Original file line number Diff line number Diff line change
@@ -22,7 +22,9 @@

from data_prototype.conversion_node import FunctionConversionNode

from data_prototype.wrappers import LineWrapper, FormattedText
from data_prototype.wrappers import FormattedText
from data_prototype.artist import CompatibilityArtist as CA
from data_prototype.line import Line


class SinOfTime:
@@ -32,10 +34,10 @@ class SinOfTime:

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

def query(
@@ -45,26 +47,23 @@ def query(
) -> Tuple[Dict[str, Any], Union[str, int]]:
th = np.linspace(0, 2 * np.pi, self.N)

def next_time():
cur_time = time.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()
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 = LineWrapper(sot_c, lw=5, color="green", label="sin(time)")
lw = CA(Line(sot_c, linewidth=5, color="green", label="sin(time)"))
fc = FormattedText(
sot_c,
FunctionConversionNode.from_funcs(
11 changes: 6 additions & 5 deletions examples/data_frame.py
Original file line number Diff line number Diff line change
@@ -4,14 +4,15 @@
===============
Wrapping a :class:`pandas.DataFrame` using :class:`.containers.DataFrameContainer`
and :class:`.wrappers.LineWrapper`.
and :class:`.artist.Line`.
"""

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

from data_prototype.wrappers import LineWrapper
from data_prototype.artist import CompatibilityArtist as CA
from data_prototype.line import Line
from data_prototype.containers import DataFrameContainer

th = np.linspace(0, 4 * np.pi, 256)
@@ -34,9 +35,9 @@


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"))
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)
29 changes: 20 additions & 9 deletions examples/first.py
Original file line number Diff line number Diff line change
@@ -4,27 +4,38 @@
=================
Demonstrating the differences between :class:`.containers.FuncContainer` and
:class:`.containers.SeriesContainer` using :class:`.wrappers.LineWrapper`.
:class:`.containers.SeriesContainer` using :class:`.artist.Line`.
"""

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

from data_prototype.wrappers import LineWrapper
from data_prototype.artist import CompatibilityAxes
from data_prototype.line import Line
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)")

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 = LineWrapper(sc, lw=3, color="blue", label="cos (pandas)")
lw2 = Line(
sc,
linewidth=3,
linestyle=":",
color="C0",
label="cos (pandas)",
marker=".",
markersize=12,
)

fig, ax = plt.subplots()
ax.add_artist(lw)
ax.add_artist(lw2)
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()
14 changes: 7 additions & 7 deletions examples/lissajous.py
Original file line number Diff line number Diff line change
@@ -32,13 +32,13 @@ class Lissajous:

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),
"x": Desc((self.N,)),
"y": Desc((self.N,)),
"time": Desc(()),
"sizes": Desc(()),
"paths": Desc(()),
"edgecolors": Desc(()),
"facecolors": Desc((self.N,)),
}

def query(
51 changes: 35 additions & 16 deletions examples/mapped.py
Original file line number Diff line number Diff line change
@@ -12,29 +12,46 @@

from matplotlib.colors import Normalize

from data_prototype.wrappers import LineWrapper, FormattedText
from data_prototype.wrappers import FormattedText
from data_prototype.artist import CompatibilityArtist as CA
from data_prototype.line import Line
from data_prototype.containers import ArrayContainer
from data_prototype.description import Desc
from data_prototype.conversion_node import FunctionConversionNode
from data_prototype.conversion_edge import FuncEdge


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[()]],
},
)
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_converter = FunctionConversionNode.from_funcs(
{
"text": lambda j, cat: f"index={j[()]} class={cat[()]!r}",
"text": lambda j, cat: f"index={j[()]} class={cat!r}",
"y": lambda j: j,
"x": lambda x: 2 * np.pi,
},
@@ -53,13 +70,15 @@
"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]),
"cat": {0: "A", 1: "B", 2: "C"}[j % 3],
}
)
ax.add_artist(
LineWrapper(
ac,
line_converter,
CA(
Line(
ac,
line_edges,
)
)
)
ax.add_artist(
22 changes: 13 additions & 9 deletions examples/subsample.py
Original file line number Diff line number Diff line change
@@ -21,7 +21,9 @@
import numpy as np

from data_prototype.description import Desc, desc_like
from data_prototype.wrappers import ImageWrapper
from data_prototype.artist import CompatibilityArtist as CA
from data_prototype.image import Image
from data_prototype.containers import ArrayContainer

from skimage.transform import downscale_local_mean

@@ -38,17 +40,17 @@
class Subsample:
def describe(self):
return {
"xextent": Desc([2], float),
"yextent": Desc([2], float),
"image": Desc([], float),
"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",), np.dtype("f8"), coordinates="data")
desc = Desc(("N",), coordinates="data")
xy = {"x": desc, "y": desc}
data_lim = graph.evaluator(xy, desc_like(xy, coordinates="axes")).inverse

@@ -65,19 +67,21 @@ def query(
yscale = int(np.ceil((yi2 - yi1) / 50))

return {
"xextent": [x1, x2],
"yextent": [y1, y2],
"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 = ImageWrapper(sub, cmap=cmap, norm=norm)
im = Image(sub, cmap=cmap, norm=norm)

fig, ax = plt.subplots()
ax.add_artist(im)
ax.add_artist(CA(im))
ax.set_xlim(-3, 3)
ax.set_ylim(-3, 3)
plt.show()
24 changes: 16 additions & 8 deletions examples/widgets.py
Original file line number Diff line number Diff line change
@@ -14,9 +14,11 @@
import matplotlib.pyplot as plt
from matplotlib.widgets import Slider, Button

from data_prototype.wrappers import LineWrapper
from data_prototype.artist import CompatibilityArtist as CA
from data_prototype.line import Line
from data_prototype.containers import FuncContainer
from data_prototype.conversion_node import FunctionConversionNode
from data_prototype.description import Desc
from data_prototype.conversion_edge import FuncEdge


class SliderContainer(FuncContainer):
@@ -119,15 +121,21 @@ def _query_hash(self, graph, parent_coordinates):
frequency=freq_slider,
phase=phase_slider,
)
lw = LineWrapper(
lw = Line(
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,
[
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(lw)
ax.add_artist(CA(lw))


# Create a `matplotlib.widgets.Button` to reset the sliders to initial values.