plot-io 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- plot_io/__init__.py +66 -0
- plot_io/_backend.py +114 -0
- plot_io/axis.py +574 -0
- plot_io/colorbar.py +342 -0
- plot_io/decorations.py +496 -0
- plot_io/defaults.py +766 -0
- plot_io/density.py +97 -0
- plot_io/enums.py +63 -0
- plot_io/facets.py +184 -0
- plot_io/figure.py +331 -0
- plot_io/labels.py +63 -0
- plot_io/legend.py +498 -0
- plot_io/limits.py +92 -0
- plot_io/maps/__init__.py +365 -0
- plot_io/maps/data.py +333 -0
- plot_io/markers.py +117 -0
- plot_io/numbers.py +289 -0
- plot_io/occupancy.py +219 -0
- plot_io/palettes.py +142 -0
- plot_io/plot.py +435 -0
- plot_io/render.py +1122 -0
- plot_io/scales.py +93 -0
- plot_io/series/__init__.py +498 -0
- plot_io/series/area.py +181 -0
- plot_io/series/bar.py +294 -0
- plot_io/series/box.py +401 -0
- plot_io/series/catdata.py +23 -0
- plot_io/series/catpoints.py +182 -0
- plot_io/series/contour.py +294 -0
- plot_io/series/curves.py +128 -0
- plot_io/series/ecdf.py +143 -0
- plot_io/series/errorbar.py +111 -0
- plot_io/series/errors.py +100 -0
- plot_io/series/fill.py +168 -0
- plot_io/series/heatmap.py +304 -0
- plot_io/series/hexbin.py +216 -0
- plot_io/series/hist.py +379 -0
- plot_io/series/hist2d.py +163 -0
- plot_io/series/kde.py +228 -0
- plot_io/series/kde2d.py +153 -0
- plot_io/series/line.py +173 -0
- plot_io/series/pie.py +304 -0
- plot_io/series/points.py +341 -0
- plot_io/series/regression.py +250 -0
- plot_io/series/scatter.py +103 -0
- plot_io/series/stem.py +112 -0
- plot_io/series/violin.py +321 -0
- plot_io/settings.py +281 -0
- plot_io/shapes.py +57 -0
- plot_io/sharing.py +160 -0
- plot_io/slots.py +52 -0
- plot_io/stacking.py +63 -0
- plot_io/stats.py +140 -0
- plot_io/style.py +71 -0
- plot_io/styles/__init__.py +234 -0
- plot_io/styles/library.py +732 -0
- plot_io/tables.py +141 -0
- plot_io/text.py +109 -0
- plot_io/ticks.py +284 -0
- plot_io/times.py +199 -0
- plot_io/values.py +161 -0
- plot_io-0.1.0.dist-info/METADATA +102 -0
- plot_io-0.1.0.dist-info/RECORD +65 -0
- plot_io-0.1.0.dist-info/WHEEL +4 -0
- plot_io-0.1.0.dist-info/licenses/LICENSE +28 -0
plot_io/colorbar.py
ADDED
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
"""Colorbars: the color key of a mapped series or of a shared ``ColorMap``."""
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from pdf_io import LinearGradient
|
|
6
|
+
|
|
7
|
+
from .axis import Axis, AxisState
|
|
8
|
+
from .numbers import number_settings
|
|
9
|
+
from .scales import get_scale
|
|
10
|
+
from .settings import Setting, Settings, boolean, color, number, one_of, route, table, text
|
|
11
|
+
from .style import resolve_color
|
|
12
|
+
from .text import box, draw_text, measure
|
|
13
|
+
|
|
14
|
+
__all__ = ["Colorbar", "BarState"]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class Colorbar(Settings):
|
|
18
|
+
"""A colorbar: attached to a plot (``plot.colorbar(…)``) or in its own grid area
|
|
19
|
+
(``figure.colorbar(…, id=…)``). Its axis is ``colorbar.axis``."""
|
|
20
|
+
|
|
21
|
+
SETTINGS = table(
|
|
22
|
+
position=Setting(one_of("right", "left", "top", "bottom"),
|
|
23
|
+
doc="Side of the plot an attached bar takes."),
|
|
24
|
+
orientation=Setting(one_of("vertical", "horizontal"),
|
|
25
|
+
doc="Bar direction; default: vertical, or horizontal for bars "
|
|
26
|
+
"attached at the top or bottom."),
|
|
27
|
+
size=Setting(number(positive=True),
|
|
28
|
+
doc="Bar thickness, in the figure's unit; default 10 pt."),
|
|
29
|
+
length=Setting(number(lo=0.05, hi=1.0),
|
|
30
|
+
doc="Bar length as a fraction of the axes side (attached bars)."),
|
|
31
|
+
pad=Setting(number(lo=0),
|
|
32
|
+
doc="Gap between the axes and an attached bar, in the figure's unit."),
|
|
33
|
+
label=Setting(text, doc="Bar label: a string or rich content."),
|
|
34
|
+
extend=Setting(one_of("auto", "neither", "min", "max", "both"),
|
|
35
|
+
doc="Triangular ends in the under/over colors: `auto` shows them when "
|
|
36
|
+
"the limits clip the data."),
|
|
37
|
+
frame=Setting(boolean, doc="Outline the bar."),
|
|
38
|
+
frame_color=Setting(color, doc="Outline color."),
|
|
39
|
+
frame_width=Setting(number(lo=0), doc="Outline width (pt).", length=True),
|
|
40
|
+
font_size=Setting(number(positive=True), inherited=True, doc="Base font size (pt).", length=True),
|
|
41
|
+
font_family=Setting(str, inherited=True, doc="Font family."),
|
|
42
|
+
text_color=Setting(color, inherited=True, doc="Text color."),
|
|
43
|
+
**number_settings(),
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
def __init__(self, source: Any = None, plot: Any = None, figure: Any = None,
|
|
47
|
+
**settings: Any) -> None:
|
|
48
|
+
axis_kw = {k: settings.pop(k) for k in list(settings) if k in Axis.SETTINGS
|
|
49
|
+
and k not in Colorbar.SETTINGS}
|
|
50
|
+
own, = route(settings, [Colorbar])
|
|
51
|
+
self.source = source
|
|
52
|
+
self.plot = plot
|
|
53
|
+
self.figure = figure if figure is not None else (plot.figure if plot is not None else None)
|
|
54
|
+
self.id: Any = None
|
|
55
|
+
self.axis = Axis(self, "c")
|
|
56
|
+
super().__init__(**own)
|
|
57
|
+
if axis_kw:
|
|
58
|
+
self.axis.set(**axis_kw)
|
|
59
|
+
|
|
60
|
+
def _parent(self) -> Settings | None:
|
|
61
|
+
return self.plot if self.plot is not None else self.figure
|
|
62
|
+
|
|
63
|
+
def _points_per_unit(self) -> float:
|
|
64
|
+
return self.figure.points_per_unit if self.figure is not None else 72.0
|
|
65
|
+
|
|
66
|
+
def __repr__(self) -> str:
|
|
67
|
+
return f"Colorbar({self.source!r})"
|
|
68
|
+
|
|
69
|
+
def color_map(self) -> Any:
|
|
70
|
+
"""The ``ColorMap`` shown (``None`` when no source is found)."""
|
|
71
|
+
from .maps import ColorMap
|
|
72
|
+
|
|
73
|
+
src = self.source
|
|
74
|
+
if src is None and self.plot is not None:
|
|
75
|
+
for s in reversed(self.plot.series):
|
|
76
|
+
if s.color_sources():
|
|
77
|
+
src = s
|
|
78
|
+
break
|
|
79
|
+
if src is None:
|
|
80
|
+
return None
|
|
81
|
+
if isinstance(src, ColorMap):
|
|
82
|
+
return src
|
|
83
|
+
sources = src.color_sources()
|
|
84
|
+
if not sources:
|
|
85
|
+
raise ValueError(f"{src!r} has no color map to show")
|
|
86
|
+
return sources[0][0]
|
|
87
|
+
|
|
88
|
+
def auto_label(self) -> Any:
|
|
89
|
+
"""The label: the setting, else the source series' color column name, else the
|
|
90
|
+
name of a density it maps."""
|
|
91
|
+
label = self.get("label")
|
|
92
|
+
if label is not None:
|
|
93
|
+
return label
|
|
94
|
+
src = self.source
|
|
95
|
+
if src is None and self.plot is not None:
|
|
96
|
+
src = next((s for s in reversed(self.plot.series) if s.color_sources()), None)
|
|
97
|
+
if src is None or not hasattr(src, "SETTINGS"):
|
|
98
|
+
return None
|
|
99
|
+
cols = getattr(src, "columns", {})
|
|
100
|
+
for name in ("color", "line_color", "marker_color"):
|
|
101
|
+
if name in cols:
|
|
102
|
+
return cols[name]
|
|
103
|
+
from .density import Density
|
|
104
|
+
for name in ("color", "line_color", "marker_color"):
|
|
105
|
+
if name in src.SETTINGS and isinstance(src.get(name), Density):
|
|
106
|
+
return src.get(name).label()
|
|
107
|
+
return None
|
|
108
|
+
|
|
109
|
+
@property
|
|
110
|
+
def vertical(self) -> bool:
|
|
111
|
+
return self.get("orientation") == "vertical"
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
class BarState:
|
|
115
|
+
"""A colorbar while drawing."""
|
|
116
|
+
|
|
117
|
+
def __init__(self, cb: Colorbar, maps: dict[int, tuple], plot: Any) -> None:
|
|
118
|
+
self.cb = cb
|
|
119
|
+
self.cmap = cb.color_map()
|
|
120
|
+
if self.cmap is None:
|
|
121
|
+
raise ValueError("colorbar without a source: give a mapped series or a ColorMap")
|
|
122
|
+
flo, fhi, dlo, dhi = maps[id(self.cmap)]
|
|
123
|
+
self.state = AxisState(cb.axis)
|
|
124
|
+
scale = self.state.scale = get_scale(self.cmap.scale)
|
|
125
|
+
self.state.flo, self.state.fhi = flo, fhi
|
|
126
|
+
self.state.lo, self.state.hi = scale.inverse(flo), scale.inverse(fhi)
|
|
127
|
+
self.data = (scale.f(dlo), scale.f(dhi))
|
|
128
|
+
self.position = cb.get("position")
|
|
129
|
+
self.vertical = cb.vertical
|
|
130
|
+
self.thickness = cb.get("size") * cb._points_per_unit()
|
|
131
|
+
self.pad = cb.get("pad") * cb._points_per_unit()
|
|
132
|
+
ext = cb.get("extend")
|
|
133
|
+
if ext == "auto":
|
|
134
|
+
lo_clip = self.data[0] < flo - 1e-12 * abs(fhi - flo)
|
|
135
|
+
hi_clip = self.data[1] > fhi + 1e-12 * abs(fhi - flo)
|
|
136
|
+
ext = {(True, True): "both", (True, False): "min", (False, True): "max"}.get(
|
|
137
|
+
(lo_clip, hi_clip), "neither")
|
|
138
|
+
self.extend = ext
|
|
139
|
+
self.side = self._side()
|
|
140
|
+
|
|
141
|
+
def _side(self) -> str:
|
|
142
|
+
"""Side of the bar that carries ticks and labels."""
|
|
143
|
+
if self.cb.plot is not None:
|
|
144
|
+
return self.position
|
|
145
|
+
return "right" if self.vertical else "bottom"
|
|
146
|
+
|
|
147
|
+
# -- measurement ----------------------------------------------------------------
|
|
148
|
+
|
|
149
|
+
def _labels_extent(self, canvas: Any, length: float) -> tuple[float, float]:
|
|
150
|
+
"""Space outside the bar for ticks, labels and the bar label, and the overhang of
|
|
151
|
+
end labels along the bar."""
|
|
152
|
+
st = self.state
|
|
153
|
+
st.compute_ticks(canvas, length, not self.vertical)
|
|
154
|
+
axis = self.cb.axis
|
|
155
|
+
out = st.tick_out()[0]
|
|
156
|
+
font = st.tick_font()
|
|
157
|
+
labels = [t.label for t in st.ticks if t.major and t.label is not None]
|
|
158
|
+
across = out
|
|
159
|
+
over = 0.0
|
|
160
|
+
if labels:
|
|
161
|
+
sizes = [box(canvas, lab, font) for lab in labels]
|
|
162
|
+
across += axis.get("tick_pad") + max((w if self.vertical else h) for w, h in sizes)
|
|
163
|
+
over = max((h if self.vertical else w) for w, h in sizes) / 2
|
|
164
|
+
label = self.cb.auto_label()
|
|
165
|
+
if label is not None:
|
|
166
|
+
_, a, d = measure(canvas, label, st.label_font())
|
|
167
|
+
across += axis.get("label_pad") + a + d
|
|
168
|
+
return across, over
|
|
169
|
+
|
|
170
|
+
def measure_attached(self, canvas: Any, axes_len: float) -> float:
|
|
171
|
+
length = axes_len * self.cb.get("length")
|
|
172
|
+
across, _ = self._labels_extent(canvas, length)
|
|
173
|
+
return self.pad + self.thickness + across
|
|
174
|
+
|
|
175
|
+
def measure(self, canvas: Any, w: float, h: float) -> tuple[float, float, float, float]:
|
|
176
|
+
"""Margins of a bar in its own area."""
|
|
177
|
+
length = h if self.vertical else w
|
|
178
|
+
across, over = self._labels_extent(canvas, max(length, 1.0))
|
|
179
|
+
tri = self.thickness if self.extend != "neither" else 0.0
|
|
180
|
+
over = max(over, tri)
|
|
181
|
+
if self.vertical:
|
|
182
|
+
return (0.0, across, over, over)
|
|
183
|
+
return (over, over, 0.0, across)
|
|
184
|
+
|
|
185
|
+
def bar_rect(self, rect: tuple[float, float, float, float]) -> tuple:
|
|
186
|
+
x0, y0, x1, y1 = rect
|
|
187
|
+
if self.vertical:
|
|
188
|
+
return (x0, y0, min(x1, x0 + self.thickness), y1)
|
|
189
|
+
return (x0, y0, x1, min(y1, y0 + self.thickness))
|
|
190
|
+
|
|
191
|
+
# -- drawing --------------------------------------------------------------------
|
|
192
|
+
|
|
193
|
+
def draw_attached(self, canvas: Any, axes: tuple, offset: float, base: float) -> float:
|
|
194
|
+
x0, y0, x1, y1 = axes
|
|
195
|
+
frac = self.cb.get("length")
|
|
196
|
+
side = self.position
|
|
197
|
+
start = base + offset + self.pad
|
|
198
|
+
if side in ("left", "right"):
|
|
199
|
+
length = (y1 - y0) * frac
|
|
200
|
+
by0 = y0 + (y1 - y0 - length) / 2
|
|
201
|
+
bx0 = x1 + start if side == "right" else x0 - start - self.thickness
|
|
202
|
+
rect = (bx0, by0, bx0 + self.thickness, by0 + length)
|
|
203
|
+
else:
|
|
204
|
+
length = (x1 - x0) * frac
|
|
205
|
+
bx0 = x0 + (x1 - x0 - length) / 2
|
|
206
|
+
by0 = y1 + start if side == "bottom" else y0 - start - self.thickness
|
|
207
|
+
rect = (bx0, by0, bx0 + length, by0 + self.thickness)
|
|
208
|
+
self.draw_area(canvas, rect)
|
|
209
|
+
across, _ = self._labels_extent(canvas, length)
|
|
210
|
+
return self.pad + self.thickness + across
|
|
211
|
+
|
|
212
|
+
def draw_area(self, canvas: Any, rect: tuple[float, float, float, float]) -> None:
|
|
213
|
+
x0, y0, x1, y1 = rect
|
|
214
|
+
cmap = self.cmap
|
|
215
|
+
vertical = self.vertical
|
|
216
|
+
length = (y1 - y0) if vertical else (x1 - x0)
|
|
217
|
+
n = 256 if cmap.interpolation == "linear" else len(cmap.stops)
|
|
218
|
+
stops: list[tuple[float, Any, float]] = []
|
|
219
|
+
if cmap.interpolation == "linear":
|
|
220
|
+
for i in range(n):
|
|
221
|
+
t = i / (n - 1)
|
|
222
|
+
c = cmap(t)
|
|
223
|
+
stops.append((t, c[:3], c[3]))
|
|
224
|
+
else:
|
|
225
|
+
for i, (p, c) in enumerate(cmap.stops):
|
|
226
|
+
nxt = cmap.stops[i + 1][0] if i + 1 < len(cmap.stops) else 1.0
|
|
227
|
+
stops.append((p, c[:3], c[3]))
|
|
228
|
+
stops.append((nxt, c[:3], c[3]))
|
|
229
|
+
inv = self.cb.axis.get("invert")
|
|
230
|
+
if vertical:
|
|
231
|
+
g = LinearGradient(0, y0, 0, y1, stops) if inv else LinearGradient(0, y1, 0, y0, stops)
|
|
232
|
+
else:
|
|
233
|
+
g = LinearGradient(x1, 0, x0, 0, stops) if inv else LinearGradient(x0, 0, x1, 0, stops)
|
|
234
|
+
canvas.save()
|
|
235
|
+
canvas.fill_style = g
|
|
236
|
+
canvas.begin_path()
|
|
237
|
+
canvas.rect(x0, y0, x1 - x0, y1 - y0)
|
|
238
|
+
canvas.fill()
|
|
239
|
+
self._extensions(canvas, rect)
|
|
240
|
+
if self.cb.get("frame"):
|
|
241
|
+
canvas.stroke_style = resolve_color(self.cb.get("frame_color"), ())
|
|
242
|
+
canvas.line_width = self.cb.get("frame_width")
|
|
243
|
+
canvas.begin_path()
|
|
244
|
+
canvas.rect(x0, y0, x1 - x0, y1 - y0)
|
|
245
|
+
canvas.stroke()
|
|
246
|
+
canvas.restore()
|
|
247
|
+
self._axis(canvas, rect, length)
|
|
248
|
+
|
|
249
|
+
def _extensions(self, canvas: Any, rect: tuple) -> None:
|
|
250
|
+
if self.extend == "neither":
|
|
251
|
+
return
|
|
252
|
+
x0, y0, x1, y1 = rect
|
|
253
|
+
t = self.thickness
|
|
254
|
+
cmap = self.cmap
|
|
255
|
+
inv = self.cb.axis.get("invert")
|
|
256
|
+
ends = []
|
|
257
|
+
if self.extend in ("min", "both"):
|
|
258
|
+
ends.append(("min", cmap.under or cmap(0.0)))
|
|
259
|
+
if self.extend in ("max", "both"):
|
|
260
|
+
ends.append(("max", cmap.over or cmap(1.0)))
|
|
261
|
+
for which, c in ends:
|
|
262
|
+
at_start = (which == "min") != inv # start: bottom (vertical) or left
|
|
263
|
+
canvas.fill_style = c
|
|
264
|
+
canvas.begin_path()
|
|
265
|
+
if self.vertical:
|
|
266
|
+
if at_start:
|
|
267
|
+
canvas.move_to(x0, y1)
|
|
268
|
+
canvas.line_to(x1, y1)
|
|
269
|
+
canvas.line_to((x0 + x1) / 2, y1 + t)
|
|
270
|
+
else:
|
|
271
|
+
canvas.move_to(x0, y0)
|
|
272
|
+
canvas.line_to(x1, y0)
|
|
273
|
+
canvas.line_to((x0 + x1) / 2, y0 - t)
|
|
274
|
+
else:
|
|
275
|
+
if at_start:
|
|
276
|
+
canvas.move_to(x0, y0)
|
|
277
|
+
canvas.line_to(x0, y1)
|
|
278
|
+
canvas.line_to(x0 - t, (y0 + y1) / 2)
|
|
279
|
+
else:
|
|
280
|
+
canvas.move_to(x1, y0)
|
|
281
|
+
canvas.line_to(x1, y1)
|
|
282
|
+
canvas.line_to(x1 + t, (y0 + y1) / 2)
|
|
283
|
+
canvas.close_path()
|
|
284
|
+
canvas.fill()
|
|
285
|
+
|
|
286
|
+
def _axis(self, canvas: Any, rect: tuple, length: float) -> None:
|
|
287
|
+
st = self.state
|
|
288
|
+
if abs(st.length - length) > 0.01:
|
|
289
|
+
st.compute_ticks(canvas, length, not self.vertical)
|
|
290
|
+
axis = self.cb.axis
|
|
291
|
+
x0, y0, x1, y1 = rect
|
|
292
|
+
side = self.side
|
|
293
|
+
mapper = st.mapper(y1, y0) if self.vertical else st.mapper(x0, x1)
|
|
294
|
+
out, inside = st.tick_out()
|
|
295
|
+
ml = axis.get("minor_tick_length")
|
|
296
|
+
canvas.save()
|
|
297
|
+
canvas.stroke_style = resolve_color(axis.get("tick_color"), ())
|
|
298
|
+
canvas.line_width = axis.get("tick_width")
|
|
299
|
+
canvas.begin_path()
|
|
300
|
+
base = {"right": x1, "left": x0, "bottom": y1, "top": y0}[side]
|
|
301
|
+
other = {"right": x0, "left": x1, "bottom": y0, "top": y1}[side]
|
|
302
|
+
sign = 1.0 if side in ("right", "bottom") else -1.0
|
|
303
|
+
for t in st.ticks:
|
|
304
|
+
p = mapper(t.value)
|
|
305
|
+
o, i = (out, inside) if t.major else ((ml if out else 0.0), (ml if inside else 0.0))
|
|
306
|
+
ends = [(base - sign * i, base + sign * o)]
|
|
307
|
+
if axis.get("mirror") and i:
|
|
308
|
+
ends.append((other + sign * i, other))
|
|
309
|
+
for a, b in ends:
|
|
310
|
+
if self.vertical:
|
|
311
|
+
canvas.move_to(a, p)
|
|
312
|
+
canvas.line_to(b, p)
|
|
313
|
+
else:
|
|
314
|
+
canvas.move_to(p, a)
|
|
315
|
+
canvas.line_to(p, b)
|
|
316
|
+
canvas.stroke()
|
|
317
|
+
canvas.restore()
|
|
318
|
+
font = st.tick_font()
|
|
319
|
+
pos = base + sign * (out + axis.get("tick_pad"))
|
|
320
|
+
widest = 0.0
|
|
321
|
+
for t in st.ticks:
|
|
322
|
+
if not t.major or t.label is None:
|
|
323
|
+
continue
|
|
324
|
+
p = mapper(t.value)
|
|
325
|
+
w, h = box(canvas, t.label, font)
|
|
326
|
+
if self.vertical:
|
|
327
|
+
draw_text(canvas, t.label, pos, p, font, "left" if sign > 0 else "right", "center")
|
|
328
|
+
widest = max(widest, w)
|
|
329
|
+
else:
|
|
330
|
+
draw_text(canvas, t.label, p, pos, font, "center", "top" if sign > 0 else "bottom")
|
|
331
|
+
widest = max(widest, h)
|
|
332
|
+
label = self.cb.auto_label()
|
|
333
|
+
if label is None:
|
|
334
|
+
return
|
|
335
|
+
lf = st.label_font()
|
|
336
|
+
pos += sign * (widest + axis.get("label_pad"))
|
|
337
|
+
if self.vertical:
|
|
338
|
+
draw_text(canvas, label, pos, (y0 + y1) / 2, lf, "center",
|
|
339
|
+
"top" if sign > 0 else "bottom", 90.0)
|
|
340
|
+
else:
|
|
341
|
+
draw_text(canvas, label, (x0 + x1) / 2, pos, lf, "center",
|
|
342
|
+
"top" if sign > 0 else "bottom")
|