Skip to content

Commit 959a7da

Browse files
committed
Introduce artist classes, starting with Line
1 parent a38c182 commit 959a7da

File tree

7 files changed

+205
-24
lines changed

7 files changed

+205
-24
lines changed

data_prototype/artist.py

Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
from typing import Sequence
2+
3+
4+
import matplotlib.path as mpath
5+
import matplotlib.colors as mcolors
6+
import matplotlib.lines as mlines
7+
import matplotlib.path as mpath
8+
import matplotlib.transforms as mtransforms
9+
import numpy as np
10+
11+
from .containers import DataContainer, ArrayContainer, DataUnion
12+
from .description import Desc, desc_like
13+
from .conversion_edge import Edge, TransformEdge, FuncEdge, Graph
14+
15+
16+
class Artist:
17+
required_keys: dict[str, Desc]
18+
19+
# defaults?
20+
def __init__(
21+
self, container: DataContainer, edges: Sequence[Edge] | None = None, **kwargs
22+
):
23+
kwargs_cont = ArrayContainer(**kwargs)
24+
self._container = DataUnion(container, kwargs_cont)
25+
26+
edges = edges or []
27+
self._edges = list(edges)
28+
29+
def draw(self, renderer, edges: Sequence[Edge]) -> None: ...
30+
31+
32+
class CompatibilityArtist:
33+
"""A compatibility shim to ducktype as a classic Matplotlib Artist.
34+
35+
At this time features are implemented on an "as needed" basis, and many
36+
are only implemented insofar as they do not fail, not necessarily providing
37+
full functionality of a full MPL Artist.
38+
39+
The idea is to keep the new Artist class as minimal as possible.
40+
As features are added this may shrink.
41+
42+
The main thing we are trying to avoid is the reliance on the axes/figure
43+
44+
Ultimately for useability, whatever remains shimmed out here may be rolled in as
45+
some form of gaurded option to ``Artist`` itself, but a firm dividing line is
46+
useful for avoiding accidental dependency.
47+
"""
48+
49+
def __init__(self, artist: Artist):
50+
self._artist = artist
51+
52+
self.axes = None
53+
self.figure = None
54+
self._clippath = None
55+
self.zorder = 2
56+
57+
def set_figure(self, fig):
58+
self.figure = fig
59+
60+
def is_transform_set(self):
61+
return True
62+
63+
def get_mouseover(self):
64+
return False
65+
66+
def get_clip_path(self):
67+
self._clippath
68+
69+
def set_clip_path(self, path):
70+
self._clippath = path
71+
72+
def get_animated(self):
73+
return False
74+
75+
def draw(self, renderer, edges=None):
76+
77+
if edges is None:
78+
edges = []
79+
80+
if self.axes is not None:
81+
desc: Desc = Desc(("N",), np.dtype("f8"), coordinates="data")
82+
xy: dict[str, Desc] = {"x": desc, "y": desc}
83+
edges.append(
84+
TransformEdge(
85+
"data",
86+
xy,
87+
desc_like(xy, coordinates="axes"),
88+
transform=self.axes.transData - self.axes.transAxes,
89+
)
90+
)
91+
edges.append(
92+
TransformEdge(
93+
"axes",
94+
desc_like(xy, coordinates="axes"),
95+
desc_like(xy, coordinates="display"),
96+
transform=self.axes.transAxes,
97+
)
98+
)
99+
100+
self._artist.draw(renderer, edges)
101+
102+
103+
class Line(Artist):
104+
def __init__(self, container, edges=None, **kwargs):
105+
super().__init__(container, edges, **kwargs)
106+
107+
defaults = ArrayContainer(
108+
**{
109+
"color": "C0", # TODO: interactions with cycler/rcparams?
110+
"linewidth": 1,
111+
"linestyle": "-",
112+
}
113+
)
114+
115+
self._container = DataUnion(defaults, self._container)
116+
# These are a stand-in for units etc... just kind of placed here as no-ops
117+
self._edges += [
118+
FuncEdge.from_func(
119+
"xvals", lambda x: x, "naive", "data", inverse=lambda x: x
120+
),
121+
FuncEdge.from_func(
122+
"yvals", lambda y: y, "naive", "data", inverse=lambda y: y
123+
),
124+
]
125+
126+
def draw(self, renderer, edges: Sequence[Edge]) -> None:
127+
g = Graph(list(edges) + self._edges)
128+
desc = Desc(("N",), np.dtype("f8"), "display")
129+
xy = {"x": desc, "y": desc}
130+
conv = g.evaluator(self._container.describe(), xy)
131+
query, _ = self._container.query(g)
132+
x, y = conv.evaluate(query).values()
133+
134+
# make the Path object
135+
path = mpath.Path(np.vstack([x, y]).T)
136+
# make an configure the graphic context
137+
gc = renderer.new_gc()
138+
gc.set_foreground(mcolors.to_rgba(query["color"]), isRGBA=True)
139+
gc.set_linewidth(query["linewidth"])
140+
gc.set_dashes(*mlines._get_dash_pattern(query["linestyle"]))
141+
# add the line to the render buffer
142+
renderer.draw_path(gc, path, mtransforms.IdentityTransform())
143+
144+
145+
class Image(Artist):
146+
def __init__(self, container, edges=None, **kwargs):
147+
super().__init__(container, edges, **kwargs)
148+
149+
defaults = ArrayContainer(
150+
**{
151+
"cmap": "viridis",
152+
"norm": "linear",
153+
}
154+
)
155+
156+
self._container = DataUnion(defaults, self._container)
157+
# These are a stand-in for units etc... just kind of placed here as no-ops
158+
self._edges += [
159+
FuncEdge.from_func(
160+
"xvals", lambda x: x, "naive", "data", inverse=lambda x: x
161+
),
162+
FuncEdge.from_func(
163+
"yvals", lambda y: y, "naive", "data", inverse=lambda y: y
164+
),
165+
]
166+
167+
def draw(self, renderer, edges: Sequence[Edge]) -> None:
168+
g = Graph(list(edges) + self._edges)
169+
...

data_prototype/conversion_edge.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,9 +42,7 @@ def from_edges(cls, name: str, edges: Sequence[Edge], output: dict[str, Desc]):
4242

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

5048
@property

data_prototype/description.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
from dataclasses import dataclass
2-
from typing import TypeAlias, Tuple, Union
2+
from typing import TypeAlias, Tuple, Union, overload
33

44
import numpy as np
55

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

123123

124+
@overload
125+
def desc_like(desc: Desc, shape=None, dtype=None, coordinates=None) -> Desc: ...
126+
@overload
127+
def desc_like(
128+
desc: dict[str, Desc], shape=None, dtype=None, coordinates=None
129+
) -> dict[str, Desc]: ...
130+
131+
124132
def desc_like(desc, shape=None, dtype=None, coordinates=None):
125133
if isinstance(desc, dict):
126134
return {k: desc_like(v, shape, dtype, coordinates) for k, v in desc.items()}

examples/animation.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@
2222

2323
from data_prototype.conversion_node import FunctionConversionNode
2424

25-
from data_prototype.wrappers import LineWrapper, FormattedText
25+
from data_prototype.wrappers import FormattedText
26+
from data_prototype.artist import Line, CompatibilityArtist as CA
2627

2728

2829
class SinOfTime:
@@ -64,7 +65,7 @@ def update(frame, art):
6465

6566

6667
sot_c = SinOfTime()
67-
lw = LineWrapper(sot_c, lw=5, color="green", label="sin(time)")
68+
lw = CA(Line(sot_c, linewidth=5, color="green", label="sin(time)"))
6869
fc = FormattedText(
6970
sot_c,
7071
FunctionConversionNode.from_funcs(

examples/data_frame.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,14 @@
44
===============
55
66
Wrapping a :class:`pandas.DataFrame` using :class:`.containers.DataFrameContainer`
7-
and :class:`.wrappers.LineWrapper`.
7+
and :class:`.artist.Line`.
88
"""
99

1010
import matplotlib.pyplot as plt
1111
import numpy as np
1212
import pandas as pd
1313

14-
from data_prototype.wrappers import LineWrapper
14+
from data_prototype.artist import Line, CompatibilityArtist as CA
1515
from data_prototype.containers import DataFrameContainer
1616

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

3535

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

examples/first.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,26 +4,26 @@
44
=================
55
66
Demonstrating the differences between :class:`.containers.FuncContainer` and
7-
:class:`.containers.SeriesContainer` using :class:`.wrappers.LineWrapper`.
7+
:class:`.containers.SeriesContainer` using :class:`.artist.Line`.
88
"""
99

1010
import matplotlib.pyplot as plt
1111
import numpy as np
1212
import pandas as pd
1313

14-
from data_prototype.wrappers import LineWrapper
14+
from data_prototype.artist import Line, CompatibilityArtist
1515
from data_prototype.containers import FuncContainer, SeriesContainer
1616

1717
fc = FuncContainer({"x": (("N",), lambda x: x), "y": (("N",), np.sin)})
18-
lw = LineWrapper(fc, lw=5, color="green", label="sin (function)")
18+
lw = Line(fc, linewidth=5, color="green", label="sin (function)")
1919

2020
th = np.linspace(0, 2 * np.pi, 16)
2121
sc = SeriesContainer(pd.Series(index=th, data=np.cos(th)), index_name="x", col_name="y")
22-
lw2 = LineWrapper(sc, lw=3, color="blue", label="cos (pandas)")
22+
lw2 = Line(sc, linewidth=3, linestyle=":", color="blue", label="cos (pandas)")
2323

2424
fig, ax = plt.subplots()
25-
ax.add_artist(lw)
26-
ax.add_artist(lw2)
25+
ax.add_artist(CompatibilityArtist(lw))
26+
ax.add_artist(CompatibilityArtist(lw2))
2727
ax.set_xlim(0, np.pi * 4)
2828
ax.set_ylim(-1.1, 1.1)
2929

examples/widgets.py

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,9 @@
1414
import matplotlib.pyplot as plt
1515
from matplotlib.widgets import Slider, Button
1616

17-
from data_prototype.wrappers import LineWrapper
17+
from data_prototype.artist import Line, CompatibilityArtist as CA
1818
from data_prototype.containers import FuncContainer
19-
from data_prototype.conversion_node import FunctionConversionNode
19+
from data_prototype.conversion_edge import FuncEdge
2020

2121

2222
class SliderContainer(FuncContainer):
@@ -119,15 +119,20 @@ def _query_hash(self, graph, parent_coordinates):
119119
frequency=freq_slider,
120120
phase=phase_slider,
121121
)
122-
lw = LineWrapper(
122+
lw = Line(
123123
fc,
124124
# color map phase (scaled to 2pi and wrapped to [0, 1])
125-
FunctionConversionNode.from_funcs(
126-
{"color": lambda color: cmap((color / (2 * np.pi)) % 1)}
127-
),
128-
lw=5,
125+
[
126+
FuncEdge.from_func(
127+
"color",
128+
lambda color: cmap((color / (2 * np.pi)) % 1),
129+
"user",
130+
"display",
131+
)
132+
],
133+
linewidth=5,
129134
)
130-
ax.add_artist(lw)
135+
ax.add_artist(CA(lw))
131136

132137

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

0 commit comments

Comments
 (0)