Skip to content

Commit

Permalink
Introduce artist classes, starting with Line
Browse files Browse the repository at this point in the history
  • Loading branch information
ksunden committed Mar 8, 2024
1 parent a38c182 commit 959a7da
Show file tree
Hide file tree
Showing 7 changed files with 205 additions and 24 deletions.
169 changes: 169 additions & 0 deletions data_prototype/artist.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
from typing import Sequence


import matplotlib.path as mpath
import matplotlib.colors as mcolors
import matplotlib.lines as mlines
import matplotlib.path as mpath
import matplotlib.transforms as mtransforms
import numpy as np

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


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._edges = list(edges)

def draw(self, renderer, edges: Sequence[Edge]) -> None: ...


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.zorder = 2

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, edges=None):

if edges is None:
edges = []

if self.axes is not None:
desc: Desc = Desc(("N",), np.dtype("f8"), coordinates="data")
xy: dict[str, Desc] = {"x": desc, "y": desc}
edges.append(
TransformEdge(
"data",
xy,
desc_like(xy, coordinates="axes"),
transform=self.axes.transData - self.axes.transAxes,
)
)
edges.append(
TransformEdge(
"axes",
desc_like(xy, coordinates="axes"),
desc_like(xy, coordinates="display"),
transform=self.axes.transAxes,
)
)

self._artist.draw(renderer, edges)


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

defaults = ArrayContainer(
**{
"color": "C0", # TODO: interactions with cycler/rcparams?
"linewidth": 1,
"linestyle": "-",
}
)

self._container = DataUnion(defaults, self._container)
# These are a stand-in for units etc... just kind of placed here as no-ops
self._edges += [
FuncEdge.from_func(
"xvals", lambda x: x, "naive", "data", inverse=lambda x: x
),
FuncEdge.from_func(
"yvals", lambda y: y, "naive", "data", inverse=lambda y: y
),
]

def draw(self, renderer, edges: Sequence[Edge]) -> None:
g = Graph(list(edges) + self._edges)
desc = Desc(("N",), np.dtype("f8"), "display")
xy = {"x": desc, "y": desc}
conv = g.evaluator(self._container.describe(), xy)
query, _ = self._container.query(g)
x, y = conv.evaluate(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_foreground(mcolors.to_rgba(query["color"]), isRGBA=True)
gc.set_linewidth(query["linewidth"])
gc.set_dashes(*mlines._get_dash_pattern(query["linestyle"]))
# add the line to the render buffer
renderer.draw_path(gc, path, mtransforms.IdentityTransform())


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

defaults = ArrayContainer(
**{
"cmap": "viridis",
"norm": "linear",
}
)

self._container = DataUnion(defaults, self._container)
# These are a stand-in for units etc... just kind of placed here as no-ops
self._edges += [
FuncEdge.from_func(
"xvals", lambda x: x, "naive", "data", inverse=lambda x: x
),
FuncEdge.from_func(
"yvals", lambda y: y, "naive", "data", inverse=lambda y: y
),
]

def draw(self, renderer, edges: Sequence[Edge]) -> None:
g = Graph(list(edges) + self._edges)
...
2 changes: 0 additions & 2 deletions data_prototype/conversion_edge.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,7 @@ def from_edges(cls, name: str, edges: Sequence[Edge], output: dict[str, Desc]):

def evaluate(self, input: dict[str, Any]) -> dict[str, Any]:
for edge in self.edges:
print(input)
input |= edge.evaluate({k: input[k] for k in edge.input})
print(input)
return {k: input[k] for k in self.output}

@property
Expand Down
10 changes: 9 additions & 1 deletion data_prototype/description.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from dataclasses import dataclass
from typing import TypeAlias, Tuple, Union
from typing import TypeAlias, Tuple, Union, overload

import numpy as np

Expand Down Expand Up @@ -121,6 +121,14 @@ def compatible(a: dict[str, "Desc"], b: dict[str, "Desc"]) -> bool:
return True


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


def desc_like(desc, shape=None, dtype=None, coordinates=None):
if isinstance(desc, dict):
return {k: desc_like(v, shape, dtype, coordinates) for k, v in desc.items()}
Expand Down
5 changes: 3 additions & 2 deletions examples/animation.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@

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 Line, CompatibilityArtist as CA


class SinOfTime:
Expand Down Expand Up @@ -64,7 +65,7 @@ def update(frame, 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(
Expand Down
10 changes: 5 additions & 5 deletions examples/data_frame.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,14 @@
===============
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 Line, CompatibilityArtist as CA
from data_prototype.containers import DataFrameContainer

th = np.linspace(0, 4 * np.pi, 256)
Expand All @@ -34,9 +34,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)
Expand Down
12 changes: 6 additions & 6 deletions examples/first.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,26 +4,26 @@
=================
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 Line, CompatibilityArtist
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)")
lw = Line(fc, linewidth=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)")
lw2 = Line(sc, linewidth=3, linestyle=":", color="blue", label="cos (pandas)")

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

Expand Down
21 changes: 13 additions & 8 deletions examples/widgets.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@
import matplotlib.pyplot as plt
from matplotlib.widgets import Slider, Button

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


class SliderContainer(FuncContainer):
Expand Down Expand Up @@ -119,15 +119,20 @@ 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),
"user",
"display",
)
],
linewidth=5,
)
ax.add_artist(lw)
ax.add_artist(CA(lw))


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

0 comments on commit 959a7da

Please sign in to comment.