Skip to content

Commit 85378e4

Browse files
committed
Implement line as new style artist
1 parent 4655614 commit 85378e4

File tree

5 files changed

+179
-99
lines changed

5 files changed

+179
-99
lines changed

data_prototype/artist.py

+3-77
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,10 @@
11
from typing import Sequence
22

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
93
import numpy as np
104

115
from .containers import DataContainer, ArrayContainer, DataUnion
126
from .description import Desc, desc_like
13-
from .conversion_edge import Edge, TransformEdge, FuncEdge, Graph
7+
from .conversion_edge import Edge, TransformEdge
148

159

1610
class Artist:
@@ -26,7 +20,8 @@ def __init__(
2620
edges = edges or []
2721
self._edges = list(edges)
2822

29-
def draw(self, renderer, edges: Sequence[Edge]) -> None: ...
23+
def draw(self, renderer, edges: Sequence[Edge]) -> None:
24+
return
3025

3126

3227
class CompatibilityArtist:
@@ -98,72 +93,3 @@ def draw(self, renderer, edges=None):
9893
)
9994

10095
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/line.py

+123
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
from typing import Sequence
2+
3+
import matplotlib.path as mpath
4+
import matplotlib.colors as mcolors
5+
import matplotlib.lines as mlines
6+
import matplotlib.markers as mmarkers
7+
import matplotlib.transforms as mtransforms
8+
import numpy as np
9+
10+
from .artist import Artist
11+
from .description import Desc
12+
from .conversion_edge import Edge, Graph, CoordinateEdge, DefaultEdge
13+
14+
15+
class Line(Artist):
16+
def __init__(self, container, edges=None, **kwargs):
17+
super().__init__(container, edges, **kwargs)
18+
19+
colordesc = Desc((), str, "display") # ... this needs thinking...
20+
floatdesc = Desc((), float, "display")
21+
strdesc = Desc((), str, "display")
22+
23+
self._edges += [
24+
CoordinateEdge.from_coords("xycoords", {"x": "auto", "y": "auto"}, "data"),
25+
CoordinateEdge.from_coords("color", {"color": Desc((), str)}, "display"),
26+
CoordinateEdge.from_coords(
27+
"linewidth", {"linewidth": Desc((), np.dtype("f8"))}, "display"
28+
),
29+
CoordinateEdge.from_coords(
30+
"linestyle", {"linestyle": Desc((), str)}, "display"
31+
),
32+
CoordinateEdge.from_coords(
33+
"markeredgecolor", {"markeredgecolor": Desc((), str)}, "display"
34+
),
35+
CoordinateEdge.from_coords(
36+
"markerfacecolor", {"markerfacecolor": Desc((), str)}, "display"
37+
),
38+
CoordinateEdge.from_coords(
39+
"markersize", {"markersize": Desc((), float)}, "display"
40+
),
41+
CoordinateEdge.from_coords(
42+
"markeredgewidth", {"markeredgewidth": Desc((), float)}, "display"
43+
),
44+
CoordinateEdge.from_coords("marker", {"marker": Desc((), str)}, "display"),
45+
DefaultEdge.from_default_value("color_def", "color", colordesc, "C0"),
46+
DefaultEdge.from_default_value("linewidth_def", "linewidth", floatdesc, 1),
47+
DefaultEdge.from_default_value("linestyle_def", "linestyle", strdesc, "-"),
48+
DefaultEdge.from_default_value(
49+
"mec_def", "markeredgecolor", colordesc, "C0"
50+
),
51+
DefaultEdge.from_default_value(
52+
"mfc_def", "markerfacecolor", colordesc, "C0"
53+
),
54+
DefaultEdge.from_default_value("ms_def", "markersize", floatdesc, 6),
55+
DefaultEdge.from_default_value("mew_def", "markeredgewidth", floatdesc, 1),
56+
DefaultEdge.from_default_value("marker_def", "marker", strdesc, "None"),
57+
]
58+
# Currently ignoring:
59+
# - cap/join style
60+
# - url
61+
# - antialiased
62+
# - snapping
63+
# - sketch
64+
# - gap color
65+
# - draw style (steps)
66+
# - fill style/alt_marker_path
67+
# - markevery
68+
# - non-str markers
69+
# Each individually pretty easy, but relatively rare features, focusing on common cases
70+
71+
def draw(self, renderer, edges: Sequence[Edge]) -> None:
72+
g = Graph(list(edges) + self._edges)
73+
desc = Desc(("N",), np.dtype("f8"), "display")
74+
colordesc = Desc((), str, "display") # ... this needs thinking...
75+
floatdesc = Desc((), float, "display")
76+
strdesc = Desc((), str, "display")
77+
78+
require = {
79+
"x": desc,
80+
"y": desc,
81+
"color": colordesc,
82+
"linewidth": floatdesc,
83+
"linestyle": strdesc,
84+
"markeredgecolor": colordesc,
85+
"markerfacecolor": colordesc,
86+
"markersize": floatdesc,
87+
"markeredgewidth": floatdesc,
88+
"marker": strdesc,
89+
}
90+
91+
conv = g.evaluator(self._container.describe(), require)
92+
query, _ = self._container.query(g)
93+
x, y, color, lw, ls, *marker = conv.evaluate(query).values()
94+
mec, mfc, ms, mew, mark = marker
95+
96+
# make the Path object
97+
path = mpath.Path(np.vstack([x, y]).T)
98+
# make an configure the graphic context
99+
gc = renderer.new_gc()
100+
gc.set_foreground(color)
101+
gc.set_linewidth(lw)
102+
gc.set_dashes(*mlines._scale_dashes(*mlines._get_dash_pattern(ls), lw))
103+
# add the line to the render buffer
104+
renderer.draw_path(gc, path, mtransforms.IdentityTransform())
105+
106+
if mark != "None" and ms > 0:
107+
gc = renderer.new_gc()
108+
gc.set_linewidth(mew)
109+
gc.set_foreground(mec)
110+
marker_ = mmarkers.MarkerStyle(mark)
111+
marker_path = marker_.get_path()
112+
marker_trans = marker_.get_transform()
113+
w = renderer.points_to_pixels(ms)
114+
marker_trans = marker_trans.scale(w)
115+
mfc = mcolors.to_rgba(mfc)
116+
renderer.draw_markers(
117+
gc,
118+
marker_path,
119+
marker_trans,
120+
path,
121+
mtransforms.IdentityTransform(),
122+
mfc,
123+
)

examples/first.py

+11-2
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,24 @@
1111
import numpy as np
1212
import pandas as pd
1313

14-
from data_prototype.artist import Line, CompatibilityArtist
14+
from data_prototype.artist import CompatibilityArtist
15+
from data_prototype.line import Line
1516
from data_prototype.containers import FuncContainer, SeriesContainer
1617

1718
fc = FuncContainer({"x": (("N",), lambda x: x), "y": (("N",), np.sin)})
1819
lw = Line(fc, linewidth=5, color="green", label="sin (function)")
1920

2021
th = np.linspace(0, 2 * np.pi, 16)
2122
sc = SeriesContainer(pd.Series(index=th, data=np.cos(th)), index_name="x", col_name="y")
22-
lw2 = Line(sc, linewidth=3, linestyle=":", color="blue", label="cos (pandas)")
23+
lw2 = Line(
24+
sc,
25+
linewidth=3,
26+
linestyle=":",
27+
color="C0",
28+
label="cos (pandas)",
29+
marker=".",
30+
markersize=12,
31+
)
2332

2433
fig, ax = plt.subplots()
2534
ax.add_artist(CompatibilityArtist(lw))

examples/mapped.py

+35-16
Original file line numberDiff line numberDiff line change
@@ -12,29 +12,46 @@
1212

1313
from matplotlib.colors import Normalize
1414

15-
from data_prototype.wrappers import LineWrapper, FormattedText
15+
from data_prototype.wrappers import FormattedText
16+
from data_prototype.artist import CompatibilityArtist as CA
17+
from data_prototype.line import Line
1618
from data_prototype.containers import ArrayContainer
19+
from data_prototype.description import Desc
1720
from data_prototype.conversion_node import FunctionConversionNode
21+
from data_prototype.conversion_edge import FuncEdge
22+
1823

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

24-
line_converter = FunctionConversionNode.from_funcs(
25-
{
26-
# arbitrary functions
27-
"lw": lambda lw: min(1 + lw, 5),
28-
# standard color mapping
29-
"color": lambda j: cmap(norm(j)),
30-
# categorical
31-
"ls": lambda cat: {"A": "-", "B": ":", "C": "--"}[cat[()]],
32-
},
33-
)
29+
line_edges = [
30+
FuncEdge.from_func(
31+
"lw",
32+
lambda lw: min(1 + lw, 5),
33+
{"lw": Desc((), str, "auto")},
34+
{"linewidth": Desc((), str, "display")},
35+
),
36+
# Probably should separate out norm/cmap step
37+
# Slight lie about color being a string here, because of limitations in impl
38+
FuncEdge.from_func(
39+
"cmap",
40+
lambda j: cmap(norm(j)),
41+
{"j": Desc((), str, "auto")},
42+
{"color": Desc((), str, "display")},
43+
),
44+
FuncEdge.from_func(
45+
"ls",
46+
lambda cat: {"A": "-", "B": ":", "C": "--"}[cat],
47+
{"cat": Desc((), str, "auto")},
48+
{"linestyle": Desc((), str, "display")},
49+
),
50+
]
3451

3552
text_converter = FunctionConversionNode.from_funcs(
3653
{
37-
"text": lambda j, cat: f"index={j[()]} class={cat[()]!r}",
54+
"text": lambda j, cat: f"index={j[()]} class={cat!r}",
3855
"y": lambda j: j,
3956
"x": lambda x: 2 * np.pi,
4057
},
@@ -53,13 +70,15 @@
5370
"y": np.sin(th + j * delta) + j,
5471
"j": np.asarray(j),
5572
"lw": np.asarray(j),
56-
"cat": np.asarray({0: "A", 1: "B", 2: "C"}[j % 3]),
73+
"cat": {0: "A", 1: "B", 2: "C"}[j % 3],
5774
}
5875
)
5976
ax.add_artist(
60-
LineWrapper(
61-
ac,
62-
line_converter,
77+
CA(
78+
Line(
79+
ac,
80+
line_edges,
81+
)
6382
)
6483
)
6584
ax.add_artist(

examples/widgets.py

+7-4
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,10 @@
1414
import matplotlib.pyplot as plt
1515
from matplotlib.widgets import Slider, Button
1616

17-
from data_prototype.artist import Line, CompatibilityArtist as CA
17+
from data_prototype.artist import CompatibilityArtist as CA
18+
from data_prototype.line import Line
1819
from data_prototype.containers import FuncContainer
20+
from data_prototype.description import Desc
1921
from data_prototype.conversion_edge import FuncEdge
2022

2123

@@ -126,11 +128,12 @@ def _query_hash(self, graph, parent_coordinates):
126128
FuncEdge.from_func(
127129
"color",
128130
lambda color: cmap((color / (2 * np.pi)) % 1),
129-
"user",
130-
"display",
131+
{"color": Desc((1,), np.float64)},
132+
{"color": Desc((), np.float64, "display")},
131133
)
132134
],
133-
linewidth=5,
135+
linewidth=5.0,
136+
linestyle="-",
134137
)
135138
ax.add_artist(CA(lw))
136139

0 commit comments

Comments
 (0)