plot-io 0.12.0__py3-none-win_arm64.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 +73 -0
- plot_io/_methods.py +1691 -0
- plot_io/_native/__init__.py +199 -0
- plot_io/_native/__main__.py +197 -0
- plot_io/_native/_api.py +457 -0
- plot_io/_native/bin/windows-arm64.dll +0 -0
- plot_io/_native/kernels.py +335 -0
- plot_io/_native/manifest.json +49 -0
- plot_io/axis.py +1347 -0
- plot_io/boxes.py +1156 -0
- plot_io/canvas.py +389 -0
- plot_io/color/__init__.py +3 -0
- plot_io/color/data.py +1646 -0
- plot_io/color/maps.py +922 -0
- plot_io/color/values.py +295 -0
- plot_io/color/variants.py +1246 -0
- plot_io/colorbar.py +1426 -0
- plot_io/columns.py +614 -0
- plot_io/decorations.py +920 -0
- plot_io/defaults.py +809 -0
- plot_io/density.py +113 -0
- plot_io/ext.py +68 -0
- plot_io/facets.py +240 -0
- plot_io/figure.py +788 -0
- plot_io/formatters.py +97 -0
- plot_io/genome/__init__.py +17 -0
- plot_io/genome/adapters.py +356 -0
- plot_io/genome/combine.py +123 -0
- plot_io/genome/contacts.py +508 -0
- plot_io/genome/intervals.py +750 -0
- plot_io/genome/lanes.py +191 -0
- plot_io/genome/layout.py +184 -0
- plot_io/genome/links.py +364 -0
- plot_io/genome/pack.py +27 -0
- plot_io/genome/readers.py +145 -0
- plot_io/genome/regions.py +228 -0
- plot_io/genome/scale.py +349 -0
- plot_io/genome/series.py +344 -0
- plot_io/genome/signal.py +621 -0
- plot_io/genome/sites.py +782 -0
- plot_io/genome/sources.py +459 -0
- plot_io/genome/testing.py +161 -0
- plot_io/geometry.py +289 -0
- plot_io/groups.py +308 -0
- plot_io/guides.py +503 -0
- plot_io/helpers/__init__.py +17 -0
- plot_io/helpers/clustermap.py +202 -0
- plot_io/helpers/dendrogram.py +172 -0
- plot_io/helpers/grids.py +304 -0
- plot_io/helpers/qq.py +334 -0
- plot_io/helpers/raincloud.py +249 -0
- plot_io/helpers/tree.py +214 -0
- plot_io/helpers/volcano.py +505 -0
- plot_io/hints.py +125 -0
- plot_io/kernels.py +740 -0
- plot_io/labels.py +566 -0
- plot_io/layers/__init__.py +100 -0
- plot_io/layers/base.py +630 -0
- plot_io/layers/context.py +87 -0
- plot_io/layers/core.py +446 -0
- plot_io/layers/geoms.py +1362 -0
- plot_io/layers/grammar.py +586 -0
- plot_io/layers/paint.py +614 -0
- plot_io/layers/pipeline.py +170 -0
- plot_io/layers/positions.py +431 -0
- plot_io/layers/stats.py +1088 -0
- plot_io/layout.py +1381 -0
- plot_io/legend.py +605 -0
- plot_io/levels.py +509 -0
- plot_io/limits.py +124 -0
- plot_io/lines.py +42 -0
- plot_io/mapped.py +522 -0
- plot_io/markers.py +110 -0
- plot_io/markup.py +128 -0
- plot_io/numbers.py +354 -0
- plot_io/occupancy.py +295 -0
- plot_io/plot.py +597 -0
- plot_io/presets/__init__.py +51 -0
- plot_io/presets/_util.py +153 -0
- plot_io/presets/area.py +145 -0
- plot_io/presets/bar.py +261 -0
- plot_io/presets/box.py +400 -0
- plot_io/presets/catpoints.py +177 -0
- plot_io/presets/contour.py +353 -0
- plot_io/presets/curves.py +30 -0
- plot_io/presets/dots.py +355 -0
- plot_io/presets/ecdf.py +97 -0
- plot_io/presets/errorbar.py +106 -0
- plot_io/presets/errors.py +296 -0
- plot_io/presets/fill.py +74 -0
- plot_io/presets/heatmap.py +322 -0
- plot_io/presets/hexbin.py +198 -0
- plot_io/presets/hist.py +391 -0
- plot_io/presets/hist2d.py +87 -0
- plot_io/presets/kde.py +162 -0
- plot_io/presets/kde2d.py +147 -0
- plot_io/presets/line.py +143 -0
- plot_io/presets/pie.py +502 -0
- plot_io/presets/regression.py +115 -0
- plot_io/presets/scatter.py +194 -0
- plot_io/presets/stem.py +100 -0
- plot_io/presets/violin.py +323 -0
- plot_io/py.typed +0 -0
- plot_io/registry.py +82 -0
- plot_io/render.py +480 -0
- plot_io/rows.py +491 -0
- plot_io/scales.py +103 -0
- plot_io/seams.py +138 -0
- plot_io/settings.py +807 -0
- plot_io/shapes.py +66 -0
- plot_io/sharing.py +232 -0
- plot_io/sizemaps.py +97 -0
- plot_io/spec.py +240 -0
- plot_io/states.py +216 -0
- plot_io/styles/__init__.py +307 -0
- plot_io/styles/library.py +397 -0
- plot_io/tables.py +138 -0
- plot_io/text.py +412 -0
- plot_io/ticks.py +400 -0
- plot_io/times.py +287 -0
- plot_io/titles.py +225 -0
- plot_io/transform.py +114 -0
- plot_io/types.py +13604 -0
- plot_io/units.py +768 -0
- plot_io/validators.py +1029 -0
- plot_io/values.py +190 -0
- plot_io-0.12.0.dist-info/METADATA +215 -0
- plot_io-0.12.0.dist-info/RECORD +130 -0
- plot_io-0.12.0.dist-info/WHEEL +4 -0
- plot_io-0.12.0.dist-info/licenses/LICENSE +28 -0
plot_io/layers/paint.py
ADDED
|
@@ -0,0 +1,614 @@
|
|
|
1
|
+
"""Aesthetics resolved after training: series colors, mapped colors and sizes (through the
|
|
2
|
+
series' color and size axes, :mod:`plot_io.mapped`), markers.
|
|
3
|
+
|
|
4
|
+
A layer names its paint by a **role** (:class:`Paint`): ``marker`` (a :class:`MarkerStyle`),
|
|
5
|
+
``stroke`` (lines and segments: color, width, dash), ``band`` (filled bands), ``key`` (a
|
|
6
|
+
legend entry's ``(fill, stroke)``), ``text`` (labels), ``gradient`` (a color map's legend
|
|
7
|
+
symbol), with options. A role's colors come from a **source**
|
|
8
|
+
(:func:`color_of`): the series color, it at ``opacity``, a color setting, or a method of the
|
|
9
|
+
series giving the colors of its sets or items. Presets add roles with :func:`role`.
|
|
10
|
+
|
|
11
|
+
:class:`MarkerStyle` resolves the marker settings of a call (with a prefix: ``marker_`` on
|
|
12
|
+
lines, error bars, stems, regressions and box outliers) into the arguments of vec_io's
|
|
13
|
+
``draw_markers``.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import array
|
|
17
|
+
from typing import Any
|
|
18
|
+
|
|
19
|
+
from vec_io.arrays import doubles
|
|
20
|
+
|
|
21
|
+
from ..canvas import Canvas
|
|
22
|
+
from .. import kernels
|
|
23
|
+
from ..markers import area_factor, resolve
|
|
24
|
+
from ..settings import Setting, number
|
|
25
|
+
from ..validators import Custom, typed
|
|
26
|
+
from ..color.values import resolve_color, with_alpha
|
|
27
|
+
from ..lines import dash_of
|
|
28
|
+
from ..spec import auto_colors as _auto_colors
|
|
29
|
+
from ..values import Values, is_computed, paint, size_value
|
|
30
|
+
|
|
31
|
+
__all__ = ["Paint", "ROLES", "role", "color_of", "memo", "paint", "colors_only", "size_value", "MarkerStyle", "permuted",
|
|
32
|
+
"order_settings", "order_index", "density_points", "xy_density_points",
|
|
33
|
+
"density_stats", "faded",
|
|
34
|
+
"color_map_of", "point_values", "base_color", "base_colors", "auto_colors",
|
|
35
|
+
"COLOR_SETTINGS", "SIZE_SETTINGS", "color_sources", "size_sources"]
|
|
36
|
+
|
|
37
|
+
#: Settings that may hold colors to map (numbers or a ``Density``).
|
|
38
|
+
COLOR_SETTINGS = ("color", "edge_color", "marker_color", "marker_edge_color")
|
|
39
|
+
#: Settings that may hold sizes to map.
|
|
40
|
+
SIZE_SETTINGS = ("size",)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
# ---------------------------------------------------------------------------
|
|
44
|
+
# Paint roles
|
|
45
|
+
# ---------------------------------------------------------------------------
|
|
46
|
+
|
|
47
|
+
#: The paint roles: ``name → fn(series, ctx, glyph, **options)``.
|
|
48
|
+
ROLES: dict[str, Any] = {}
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def role(name: str) -> Any:
|
|
52
|
+
"""Register a paint role: ``fn(series, ctx, glyph, **options)`` gives the resolved
|
|
53
|
+
aesthetics of a layer (``glyph``: for its legend symbol)."""
|
|
54
|
+
def add(fn: Any) -> Any:
|
|
55
|
+
ROLES[name] = fn
|
|
56
|
+
return fn
|
|
57
|
+
return add
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class Paint:
|
|
61
|
+
"""A layer's paint: role ``name`` of ``series`` with ``options``, resolved in a drawing
|
|
62
|
+
context (:meth:`resolve`, through :meth:`~plot_io.layers.core.Layer.style`)."""
|
|
63
|
+
|
|
64
|
+
__slots__ = ("series", "name", "options")
|
|
65
|
+
|
|
66
|
+
def __init__(self, series: Any, name: str, **options: Any) -> None:
|
|
67
|
+
if name not in ROLES:
|
|
68
|
+
raise ValueError(f"{series.kind}: unknown paint role {name!r} (roles: "
|
|
69
|
+
f"{', '.join(sorted(ROLES))})")
|
|
70
|
+
self.series = series
|
|
71
|
+
self.name = name
|
|
72
|
+
self.options = options
|
|
73
|
+
|
|
74
|
+
def __repr__(self) -> str:
|
|
75
|
+
return f"Paint({self.name!r})"
|
|
76
|
+
|
|
77
|
+
def resolve(self, ctx: Any, glyph: bool) -> Any:
|
|
78
|
+
return ROLES[self.name](self.series, ctx, glyph, **self.options)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def color_of(series: Any, ctx: Any, source: Any, index: int | None = None) -> Any:
|
|
82
|
+
"""A color from ``source``: ``None``; ``"base"`` (the series color); ``"faded"`` (it
|
|
83
|
+
at ``opacity``); ``"name()"``, a method of the series called with ``ctx`` (a color, or
|
|
84
|
+
one per set: item ``index``); else a color setting's name."""
|
|
85
|
+
if source is None:
|
|
86
|
+
return None
|
|
87
|
+
if source == "base":
|
|
88
|
+
lv = series._lv
|
|
89
|
+
if lv is not None and lv.color is not None and lv.parts[0][0] is series:
|
|
90
|
+
return lv.item_colors(series, ctx) # each point by its color level
|
|
91
|
+
return base_color(series, ctx)
|
|
92
|
+
if source == "faded":
|
|
93
|
+
return with_alpha(base_color(series, ctx), series.resolve("opacity"))
|
|
94
|
+
if source.endswith("()"):
|
|
95
|
+
v = getattr(series, source[:-2])(ctx)
|
|
96
|
+
if index is None:
|
|
97
|
+
return v
|
|
98
|
+
return v[index] if index < len(v) else None
|
|
99
|
+
return ctx.color(series.resolve(source))
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def memo(ctx: Any, key: Any, compute: Any) -> Any:
|
|
103
|
+
"""``compute()`` once per drawing context of a plot (styles shared by a call's
|
|
104
|
+
layers)."""
|
|
105
|
+
cache = ctx.root.__dict__.setdefault("_memo", {})
|
|
106
|
+
if key not in cache:
|
|
107
|
+
cache[key] = compute()
|
|
108
|
+
return cache[key]
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
@role("marker")
|
|
112
|
+
def _marker_role(s: Any, ctx: Any, glyph: bool, prefix: str = "", color: Any = "base",
|
|
113
|
+
n: int = 0, order: bool = False, once: bool = False,
|
|
114
|
+
when: Any = None) -> Any:
|
|
115
|
+
"""Markers (:class:`MarkerStyle`) of settings ``prefix…``; ``order``: in
|
|
116
|
+
``draw_order``; ``once``: once per drawing context; ``when``: a condition
|
|
117
|
+
(:func:`~plot_io.layers.grammar.holds`; else no markers)."""
|
|
118
|
+
from .grammar import holds
|
|
119
|
+
if not holds(s, when):
|
|
120
|
+
return None
|
|
121
|
+
|
|
122
|
+
def make() -> MarkerStyle:
|
|
123
|
+
return MarkerStyle(s, ctx, prefix, color_of(s, ctx, color), n, glyph,
|
|
124
|
+
order_index(s, n) if order and not glyph else None)
|
|
125
|
+
|
|
126
|
+
st = memo(ctx, (id(s), prefix, glyph), make) if once else make()
|
|
127
|
+
st.simplify = s.resolve("simplify")
|
|
128
|
+
return st
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
@role("stroke")
|
|
132
|
+
def _stroke_role(s: Any, ctx: Any, glyph: bool, color: Any = "base", index: Any = None,
|
|
133
|
+
width: str = "line_width", dash: bool = False, opacity: bool = False,
|
|
134
|
+
extra: tuple[str, ...] = (), fixed: Any = None, drop_zero: bool = False,
|
|
135
|
+
empty: bool = False) -> dict[str, Any]:
|
|
136
|
+
"""Lines and segments: ``color`` (a source, :func:`color_of`), the ``width`` setting,
|
|
137
|
+
the dash of ``line_style`` (``dash``), ``opacity`` (1 when unset); drawn on the axes
|
|
138
|
+
(not ``glyph``): the ``extra`` settings and ``fixed`` values, no color when the width
|
|
139
|
+
is 0 (``drop_zero``) or the set is ``empty``."""
|
|
140
|
+
w = s.resolve(width)
|
|
141
|
+
p = {"color": color_of(s, ctx, color, index), "width": w}
|
|
142
|
+
if dash:
|
|
143
|
+
p["dash"] = dash_of(s.resolve("line_style"), w)
|
|
144
|
+
if opacity:
|
|
145
|
+
o = s.resolve("opacity")
|
|
146
|
+
p["opacity"] = 1.0 if o is None else o
|
|
147
|
+
if not glyph:
|
|
148
|
+
if (drop_zero and not w) or empty:
|
|
149
|
+
p["color"] = None
|
|
150
|
+
for k in extra:
|
|
151
|
+
p[k] = s.resolve(k)
|
|
152
|
+
if fixed:
|
|
153
|
+
p.update(fixed)
|
|
154
|
+
return p
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
@role("band")
|
|
158
|
+
def _band_role(s: Any, ctx: Any, glyph: bool, color: Any = "base", index: Any = None,
|
|
159
|
+
opacity: str | float | None = None, edges: bool = False,
|
|
160
|
+
extra: tuple[str, ...] = ()) -> dict[str, Any]:
|
|
161
|
+
"""Filled bands: ``color`` (a source), ``opacity`` (a number, or the setting named), the
|
|
162
|
+
``color_below`` and outline settings (``edges``), the ``extra`` settings."""
|
|
163
|
+
p = {"color": color_of(s, ctx, color, index)}
|
|
164
|
+
if opacity is not None:
|
|
165
|
+
p["opacity"] = s.resolve(opacity) if isinstance(opacity, str) else opacity
|
|
166
|
+
if edges:
|
|
167
|
+
p["below"] = ctx.color(s.resolve("color_below"))
|
|
168
|
+
p["edge"] = ctx.color(s.resolve("edge_color"))
|
|
169
|
+
p["edge_width"] = s.resolve("edge_width")
|
|
170
|
+
for k in extra:
|
|
171
|
+
p[k] = s.resolve(k)
|
|
172
|
+
return p
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
@role("key")
|
|
176
|
+
def _key_role(s: Any, ctx: Any, glyph: bool, fill: Any = None, stroke: Any = None,
|
|
177
|
+
index: Any = None, marker: Any = None, when: Any = None) -> tuple[Any, Any]:
|
|
178
|
+
"""A legend entry's ``(fill, stroke)``: two sources (:func:`color_of`); ``"marker"``:
|
|
179
|
+
the first colors of the ``marker`` paint's legend markers; ``"symbol"``: the colors of
|
|
180
|
+
the ``marker`` paint's legend symbol (rectangles); ``when``: a setting whose width
|
|
181
|
+
must be non-zero for the stroke."""
|
|
182
|
+
def one(source: Any, which: str) -> Any:
|
|
183
|
+
if source == "marker":
|
|
184
|
+
return marker.resolve(ctx, True).first_color(which)
|
|
185
|
+
if source == "symbol":
|
|
186
|
+
return marker.resolve(ctx, True)[which]
|
|
187
|
+
return color_of(s, ctx, source, index)
|
|
188
|
+
if when is not None and not s.resolve(when):
|
|
189
|
+
stroke = None
|
|
190
|
+
return one(fill, "fill"), one(stroke, "stroke")
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
@role("text")
|
|
194
|
+
def _text_role(s: Any, ctx: Any, glyph: bool, font: Any = None, **extra: Any) -> dict[str, Any]:
|
|
195
|
+
"""Labels: ``font(ctx)``, and the ``extra`` values (functions of ``ctx`` called)."""
|
|
196
|
+
return {"font": font(ctx), **{k: (v(ctx) if callable(v) else v) for k, v in extra.items()}}
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
@role("gradient")
|
|
200
|
+
def _gradient_role(s: Any, ctx: Any, glyph: bool, cmap: Any = None) -> dict[str, Any]:
|
|
201
|
+
"""A color map drawn as a gradient (``cmap()``: the map, or ``None``)."""
|
|
202
|
+
return {"map": cmap()}
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
# ---------------------------------------------------------------------------
|
|
206
|
+
# Series colors and maps
|
|
207
|
+
# ---------------------------------------------------------------------------
|
|
208
|
+
|
|
209
|
+
def auto_colors(series: Any, ctx: Any) -> list[Any]:
|
|
210
|
+
"""The plot's colors the series took in training (one per value set)."""
|
|
211
|
+
cols = _auto_colors(series) or [None]
|
|
212
|
+
return [ctx.color(c) for c in cols]
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def base_colors(series: Any, ctx: Any, keys: list[Any], what: str = "sets",
|
|
216
|
+
items: bool = False, saturation: bool = True) -> list[Any]:
|
|
217
|
+
"""The base color of each set (or each item, ``items``) named by ``keys``, before
|
|
218
|
+
opacity: numbers through the series' color axis (items), the colors given (one, a
|
|
219
|
+
list, a dict by set or item), else the plot's colors (each set its color; items the series
|
|
220
|
+
color); ``saturation`` applied when the series has it."""
|
|
221
|
+
from ..color.values import desaturate
|
|
222
|
+
s = series
|
|
223
|
+
c = s.resolve("color")
|
|
224
|
+
n = len(keys)
|
|
225
|
+
cols: list[Any]
|
|
226
|
+
if items and isinstance(c, Values):
|
|
227
|
+
from ..mapped import color_axis_of, unit_of
|
|
228
|
+
from ..presets._util import per_category
|
|
229
|
+
axis = color_axis_of(s)
|
|
230
|
+
cm, t = axis.color_map(), unit_of(axis)
|
|
231
|
+
cols = [cm(t(v)) for v in per_category(s, keys, c.data, what)]
|
|
232
|
+
elif isinstance(c, dict) and items:
|
|
233
|
+
cols = [ctx.color(c[k]) if c.get(k) is not None else None for k in keys]
|
|
234
|
+
elif isinstance(c, dict):
|
|
235
|
+
unknown = [k for k in c if k not in keys]
|
|
236
|
+
if unknown:
|
|
237
|
+
raise ValueError(f"color: no {what[:-1]} {unknown[0]!r}")
|
|
238
|
+
auto = auto_colors(s, ctx) if len(c) < n else []
|
|
239
|
+
cols = [ctx.color(c[k]) if k in c else (auto[i] if i < len(auto) else None)
|
|
240
|
+
for i, k in enumerate(keys)]
|
|
241
|
+
elif isinstance(c, list):
|
|
242
|
+
if len(c) != n:
|
|
243
|
+
raise ValueError(f"{len(c)} colors for {n} {what}")
|
|
244
|
+
cols = [ctx.color(x) for x in c]
|
|
245
|
+
elif items:
|
|
246
|
+
cols = [base_color(s, ctx)] * n
|
|
247
|
+
elif c is not None:
|
|
248
|
+
cols = [ctx.color(c)] * n
|
|
249
|
+
else:
|
|
250
|
+
cols = auto_colors(s, ctx)
|
|
251
|
+
sat = s.resolve("saturation") if saturation and "saturation" in s.SETTINGS else 1.0
|
|
252
|
+
if sat != 1.0:
|
|
253
|
+
cols = [desaturate(x, sat) if x is not None else None for x in cols]
|
|
254
|
+
return cols
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def base_color(series: Any, ctx: Any) -> Any:
|
|
258
|
+
"""The series color: its own, or the plot's color it was given."""
|
|
259
|
+
c = series.resolve("color") if "color" in series.SETTINGS else None
|
|
260
|
+
if c is None:
|
|
261
|
+
c = (_auto_colors(series) or [None])[0]
|
|
262
|
+
return ctx.color(c)
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def color_map_of(series: Any) -> Any:
|
|
266
|
+
"""The gradient numbers in the series' colors are drawn with (its color axis')."""
|
|
267
|
+
from ..mapped import color_axis_of
|
|
268
|
+
return color_axis_of(series).color_map()
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def point_values(series: Any, v: Any) -> Any:
|
|
272
|
+
"""The numbers of a value setting: given ``Values``, sizes, or a computed ``Density``."""
|
|
273
|
+
if isinstance(v, Values):
|
|
274
|
+
return v.data
|
|
275
|
+
if is_computed(v):
|
|
276
|
+
return series.cached(("density", id(v)), lambda: v.compute(series))
|
|
277
|
+
return v
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def color_sources(series: Any) -> list[tuple[Any, Any]]:
|
|
281
|
+
"""``(color axis, values)`` of the series' color settings holding numbers or a
|
|
282
|
+
density."""
|
|
283
|
+
from ..mapped import color_axis_of
|
|
284
|
+
out = []
|
|
285
|
+
for name in COLOR_SETTINGS:
|
|
286
|
+
if name in series.SETTINGS:
|
|
287
|
+
v = series.resolve(name)
|
|
288
|
+
if isinstance(v, Values) or is_computed(v):
|
|
289
|
+
out.append((color_axis_of(series), point_values(series, v)))
|
|
290
|
+
return out
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def size_sources(series: Any) -> list[tuple[Any, Any]]:
|
|
294
|
+
"""``(size axis, values)`` of the series' size settings mapping values."""
|
|
295
|
+
from ..mapped import size_axis_of
|
|
296
|
+
out = []
|
|
297
|
+
for name in SIZE_SETTINGS:
|
|
298
|
+
axis = size_axis_of(series, name)
|
|
299
|
+
if axis is not None:
|
|
300
|
+
out.append((axis, point_values(series, series.resolve(name))))
|
|
301
|
+
return out
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def colors_only(v: Any) -> Any:
|
|
305
|
+
"""A color or a list of colors (series whose colors are per value set, not mapped)."""
|
|
306
|
+
out = paint(v)
|
|
307
|
+
if isinstance(out, Values) or is_computed(out):
|
|
308
|
+
raise ValueError("expected a color or a list of colors (values to map are not "
|
|
309
|
+
"accepted here)")
|
|
310
|
+
return out
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
def permuted(v: Any, idx: Any) -> Any:
|
|
314
|
+
"""A per-point column (list, ``array('d')``, ``(n, 4)`` buffer) reordered by ``idx``
|
|
315
|
+
(gathered in C for numeric columns); single values unchanged."""
|
|
316
|
+
if isinstance(v, list):
|
|
317
|
+
return [v[i] for i in idx]
|
|
318
|
+
if isinstance(v, array.array):
|
|
319
|
+
n = len(idx)
|
|
320
|
+
if len(v) == 4 * n and n != 4: # flat RGBA
|
|
321
|
+
return kernels.take(memoryview(v).cast("B").cast("d", (len(v) // 4, 4)), idx)
|
|
322
|
+
return kernels.take(v, idx)
|
|
323
|
+
if isinstance(v, memoryview) and v.ndim == 2:
|
|
324
|
+
m: Any = v
|
|
325
|
+
k = m.shape[1]
|
|
326
|
+
out_mv: Any = memoryview(kernels.take(m, idx)).cast("B")
|
|
327
|
+
return out_mv.cast("d", (len(idx), k))
|
|
328
|
+
return v
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
def order_index(series: Any, n: int) -> Any:
|
|
332
|
+
"""The drawing order of a series' ``draw_order`` setting, or ``None`` (call order)."""
|
|
333
|
+
from ..density import Density
|
|
334
|
+
o = series.resolve("draw_order")
|
|
335
|
+
if o is None:
|
|
336
|
+
return None
|
|
337
|
+
descending = False
|
|
338
|
+
if isinstance(o, str):
|
|
339
|
+
descending = o.startswith("-")
|
|
340
|
+
dens = next((series.resolve(k) for k in ("color", "size", "opacity")
|
|
341
|
+
if k in series.SETTINGS and isinstance(series.resolve(k), Density)), None)
|
|
342
|
+
o = dens if dens is not None else series.cached("order_density", Density)
|
|
343
|
+
if isinstance(o, Density):
|
|
344
|
+
keys = point_values(series, o)
|
|
345
|
+
elif isinstance(o, Values):
|
|
346
|
+
keys = o.data
|
|
347
|
+
else:
|
|
348
|
+
keys = o
|
|
349
|
+
if len(keys) != n:
|
|
350
|
+
raise ValueError(f"draw_order: {len(keys)} order keys for {n} points")
|
|
351
|
+
return kernels.argsort(keys, descending=descending)
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
@typed("Literal['density', '-density'] | Density | Column")
|
|
355
|
+
def order_value(v: Any) -> Any:
|
|
356
|
+
"""``draw_order``: ``"density"``, ``"-density"``, a ``Density`` or sort keys per
|
|
357
|
+
point."""
|
|
358
|
+
from ..density import Density
|
|
359
|
+
if isinstance(v, Density):
|
|
360
|
+
return v
|
|
361
|
+
if isinstance(v, str):
|
|
362
|
+
if v.strip().lower() in ("density", "-density"):
|
|
363
|
+
return v.strip().lower()
|
|
364
|
+
raise ValueError(f"expected 'density', '-density' or sort keys, got {v!r}")
|
|
365
|
+
if isinstance(v, (bytes, bytearray)):
|
|
366
|
+
raise ValueError(f"expected 'density', '-density' or sort keys, got {v!r}")
|
|
367
|
+
return doubles(v.to_numpy() if hasattr(v, "to_numpy") else v)
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
_OPACITY = number(0, 1)
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
@typed("SupportsFloat | Literal['density'] | Density")
|
|
374
|
+
def _opacity_value(v: Any) -> Any:
|
|
375
|
+
from ..density import Density
|
|
376
|
+
if isinstance(v, Density):
|
|
377
|
+
return v
|
|
378
|
+
if isinstance(v, str) and v.strip().lower() == "density":
|
|
379
|
+
return Density()
|
|
380
|
+
return _OPACITY(v)
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
#: ``opacity``: a number in [0, 1], or ``"density"`` / a ``Density``.
|
|
384
|
+
opacity_value = Custom(_opacity_value, like=_OPACITY)
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
def order_settings() -> dict[str, Setting]:
|
|
388
|
+
"""``draw_order``, ``opacity`` (numbers or a density) and ``opacity_range``."""
|
|
389
|
+
from ..settings import pair
|
|
390
|
+
return {
|
|
391
|
+
"draw_order": Setting(order_value,
|
|
392
|
+
doc="Drawing order: `\"density\"` (densest drawn last), "
|
|
393
|
+
"`\"-density\"`, or sort keys per point (ascending); "
|
|
394
|
+
"default: call order."),
|
|
395
|
+
"opacity": Setting(opacity_value, inherited=True,
|
|
396
|
+
doc="Opacity, multiplying the colors' own; or `\"density\"` / a "
|
|
397
|
+
"`Density`, mapped linearly to `opacity_range`."),
|
|
398
|
+
"opacity_range": Setting(pair(number(0, 1)),
|
|
399
|
+
doc="Opacities of the least and most dense points when "
|
|
400
|
+
"`opacity` is a density."),
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
def density_points(series: Any, xs: Any, ys: Any) -> tuple[Any, Any]:
|
|
405
|
+
"""Points ``(xs, ys)`` on the plot's axis positions (log axes in log space)."""
|
|
406
|
+
from ..scales import get_scale
|
|
407
|
+
out = []
|
|
408
|
+
for v, axis in ((xs, series.plot.x_axis), (ys, series.plot.y_axis)):
|
|
409
|
+
name = axis.resolve("scale")
|
|
410
|
+
fn = get_scale(name, axis.resolve("linthresh")).function \
|
|
411
|
+
if name not in (None, "category", "time") else None
|
|
412
|
+
out.append(kernels.scaled(v, 1.0, 0.0, fn) if fn else doubles(v))
|
|
413
|
+
return out[0], out[1]
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
def xy_density_points(series: Any, xs: Any, ys: Any) -> tuple[Any, Any]:
|
|
417
|
+
""":func:`density_points` of an ``(x, y)`` series, whose ``orientation="horizontal"``
|
|
418
|
+
puts x on the plot's y axis."""
|
|
419
|
+
if "orientation" in series.SETTINGS and series.resolve("orientation") == "horizontal":
|
|
420
|
+
return density_points(series, ys, xs)
|
|
421
|
+
return density_points(series, xs, ys)
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
def density_stats(series: Any) -> dict[str, Any]:
|
|
425
|
+
"""``{"density": values}`` when a setting of the series maps a density."""
|
|
426
|
+
from ..density import Density
|
|
427
|
+
for k in ("color", "edge_color", "size", "opacity", "draw_order"):
|
|
428
|
+
if k in series.SETTINGS and isinstance(series.resolve(k), Density):
|
|
429
|
+
return {"density": point_values(series, series.resolve(k))}
|
|
430
|
+
if "draw_order" in series.SETTINGS and isinstance(series.resolve("draw_order"), str):
|
|
431
|
+
return {"density": point_values(series, series.cached("order_density", Density))}
|
|
432
|
+
return {}
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
def faded(paint: Any, opacity: float) -> Any:
|
|
436
|
+
"""A resolved paint (a color, a list of colors or flat RGBA values) at ``opacity``; a
|
|
437
|
+
list's colors computed once per distinct color."""
|
|
438
|
+
if isinstance(paint, array.array):
|
|
439
|
+
out = array.array("d", paint)
|
|
440
|
+
out[3::4] = kernels.scaled(out[3::4], opacity)
|
|
441
|
+
return out
|
|
442
|
+
if isinstance(paint, list):
|
|
443
|
+
done: dict[Any, Any] = {}
|
|
444
|
+
colors: list[Any] = []
|
|
445
|
+
for c in paint:
|
|
446
|
+
if c is None:
|
|
447
|
+
colors.append(None)
|
|
448
|
+
continue
|
|
449
|
+
k = c if isinstance(c, (str, tuple)) else id(c)
|
|
450
|
+
if k not in done:
|
|
451
|
+
done[k] = with_alpha(c, opacity)
|
|
452
|
+
colors.append(done[k])
|
|
453
|
+
return colors
|
|
454
|
+
if isinstance(paint, memoryview): # (n, 4) colors from a ColorMap
|
|
455
|
+
flat = faded(array.array("d", paint.cast("B").cast("d")), opacity)
|
|
456
|
+
return memoryview(flat).cast("B").cast("d", (len(flat) // 4, 4))
|
|
457
|
+
return with_alpha(paint, opacity)
|
|
458
|
+
|
|
459
|
+
|
|
460
|
+
class MarkerStyle:
|
|
461
|
+
"""Resolved marker arguments for ``Canvas.draw_markers`` (the paint of a ``Points``
|
|
462
|
+
layer)."""
|
|
463
|
+
|
|
464
|
+
def __init__(self, series: Any, ctx: Any, prefix: str = "", default_color: Any = None,
|
|
465
|
+
n: int = 0, glyph: bool = False, order: Any = None) -> None:
|
|
466
|
+
self.glyph_only = glyph
|
|
467
|
+
self.order = None if glyph else order
|
|
468
|
+
self.simplify: Any = None
|
|
469
|
+
g = series.resolve
|
|
470
|
+
p = prefix
|
|
471
|
+
size_name = "size" if not p else "marker_size"
|
|
472
|
+
color_name = "color" if not p else "marker_color"
|
|
473
|
+
opacity = g("opacity")
|
|
474
|
+
if is_computed(opacity) and glyph:
|
|
475
|
+
opacity = series.resolve("opacity_range")[1]
|
|
476
|
+
elif is_computed(opacity):
|
|
477
|
+
lo, hi = series.resolve("opacity_range")
|
|
478
|
+
vals = self._take(point_values(series, opacity))
|
|
479
|
+
_, vmin, vmax = kernels.extent(vals, None)
|
|
480
|
+
span = (vmax - vmin) if vmax > vmin else 1.0
|
|
481
|
+
k = (hi - lo) / span
|
|
482
|
+
opacity = kernels.scaled(vals, k, lo - k * vmin, None, nan=0.0)
|
|
483
|
+
self.opacity = 1.0 if opacity is None else opacity
|
|
484
|
+
marker = g("marker")
|
|
485
|
+
basis = g("size_basis")
|
|
486
|
+
size = g(size_name)
|
|
487
|
+
if isinstance(marker, list):
|
|
488
|
+
marker = self._take(marker)
|
|
489
|
+
if isinstance(size, array.array):
|
|
490
|
+
size = self._take(size)
|
|
491
|
+
if isinstance(marker, list) and glyph:
|
|
492
|
+
marker = marker[0]
|
|
493
|
+
if isinstance(marker, list):
|
|
494
|
+
if len(marker) != n:
|
|
495
|
+
raise ValueError(f"{len(marker)} markers for {n} points")
|
|
496
|
+
resolved = [resolve(m) for m in marker]
|
|
497
|
+
self.marker: Any = [r[0] for r in resolved]
|
|
498
|
+
factor: Any = array.array("d", [area_factor(m) if basis == "area" else 1.0
|
|
499
|
+
for m in marker])
|
|
500
|
+
else:
|
|
501
|
+
self.marker = resolve(marker)[0]
|
|
502
|
+
factor = area_factor(marker) if basis == "area" else 1.0
|
|
503
|
+
from ..mapped import size_axis_of
|
|
504
|
+
size_axis = size_axis_of(series, size_name)
|
|
505
|
+
if size_axis is not None and glyph:
|
|
506
|
+
size = sum(size_axis.size_map().diameters(series)) / 2
|
|
507
|
+
elif size_axis is not None:
|
|
508
|
+
from ..mapped import trained
|
|
509
|
+
values = self._take(point_values(series, size))
|
|
510
|
+
st = trained(size_axis)
|
|
511
|
+
assert st.scale is not None
|
|
512
|
+
size = size_axis.size_map().map(values, st.flo, st.fhi, st.scale.function, series)
|
|
513
|
+
if isinstance(size, array.array) and glyph:
|
|
514
|
+
finite = sorted(v for v in size[::max(1, len(size) // 1000)] if v == v)
|
|
515
|
+
size = finite[len(finite) // 2] if finite else 4.0
|
|
516
|
+
if isinstance(size, array.array):
|
|
517
|
+
if len(size) != n:
|
|
518
|
+
raise ValueError(f"{series.kind} setting {size_name!r}: {len(size)} sizes "
|
|
519
|
+
f"for {n} points")
|
|
520
|
+
if isinstance(factor, array.array):
|
|
521
|
+
size = array.array("d", [s * f for s, f in zip(size, factor)])
|
|
522
|
+
elif factor != 1.0:
|
|
523
|
+
size = kernels.scaled(size, factor)
|
|
524
|
+
elif isinstance(factor, array.array):
|
|
525
|
+
size = array.array("d", [size * f for f in factor])
|
|
526
|
+
else:
|
|
527
|
+
size = size * factor
|
|
528
|
+
self.size = size
|
|
529
|
+
fill = g(color_name)
|
|
530
|
+
if fill is None:
|
|
531
|
+
fill = default_color
|
|
532
|
+
self.fill = self._paint(fill, series, ctx, n, color_name)
|
|
533
|
+
edge = g(f"{p}edge_color")
|
|
534
|
+
filled = g(f"{p}fill") is True
|
|
535
|
+
opacity = g(f"{p}fill_opacity") if filled else 0.0
|
|
536
|
+
opaque = filled and (opacity is None or opacity >= 1)
|
|
537
|
+
if edge == "auto": # white around opaque markers
|
|
538
|
+
edge = "white" if opaque else None
|
|
539
|
+
self.stroke = self._paint(edge, series, ctx, n, f"{p}edge_color")
|
|
540
|
+
self.line_width = g(f"{p}edge_width")
|
|
541
|
+
if not opaque and self.fill is not None:
|
|
542
|
+
if self.stroke is None:
|
|
543
|
+
self.stroke = self.fill
|
|
544
|
+
self.fill = faded(self.fill, opacity) if filled else None
|
|
545
|
+
if self.stroke is None or not self.line_width:
|
|
546
|
+
self.stroke, self.line_width = None, None
|
|
547
|
+
|
|
548
|
+
def _paint(self, value: Any, series: Any, ctx: Any, n: int, name: str) -> Any:
|
|
549
|
+
if value is None:
|
|
550
|
+
return None
|
|
551
|
+
mapped = isinstance(value, Values) or is_computed(value)
|
|
552
|
+
if mapped and self.glyph_only:
|
|
553
|
+
return color_map_of(series)(0.5)
|
|
554
|
+
if isinstance(value, list) and self.glyph_only:
|
|
555
|
+
return resolve_color(next((c for c in value if c is not None), None), ctx.cycle)
|
|
556
|
+
if isinstance(value, memoryview) and self.glyph_only:
|
|
557
|
+
return tuple(value[0, k] for k in range(4)) if len(value) else None
|
|
558
|
+
if mapped:
|
|
559
|
+
values = self._take(point_values(series, value))
|
|
560
|
+
if len(values) != n:
|
|
561
|
+
raise ValueError(f"{series.kind}: {name} has {len(values)} color values for "
|
|
562
|
+
f"{n} points")
|
|
563
|
+
from ..mapped import color_axis_of, colors_of
|
|
564
|
+
return colors_of(color_axis_of(series), values)
|
|
565
|
+
if isinstance(value, memoryview):
|
|
566
|
+
if len(value) != n:
|
|
567
|
+
raise ValueError(f"{series.kind}: {name} has {len(value)} colors for {n} points")
|
|
568
|
+
return self._take(value)
|
|
569
|
+
if isinstance(value, list):
|
|
570
|
+
if len(value) != n:
|
|
571
|
+
raise ValueError(f"{series.kind}: {name} has {len(value)} colors for {n} points")
|
|
572
|
+
return [resolve_color(c, ctx.cycle) for c in self._take(value)]
|
|
573
|
+
return resolve_color(value, ctx.cycle)
|
|
574
|
+
|
|
575
|
+
def first_color(self, which: str = "fill") -> Any:
|
|
576
|
+
v = self.fill if which == "fill" else self.stroke
|
|
577
|
+
if v is None or isinstance(v, (str, tuple)):
|
|
578
|
+
return v
|
|
579
|
+
if isinstance(v, list):
|
|
580
|
+
return next((c for c in v if c is not None), None)
|
|
581
|
+
if isinstance(v, array.array): # flat RGBA
|
|
582
|
+
return tuple(v[:4]) if len(v) >= 4 else None
|
|
583
|
+
if isinstance(v, memoryview) and v.ndim == 2:
|
|
584
|
+
m: Any = v
|
|
585
|
+
return tuple(m[0, k] for k in range(m.shape[1])) if len(m) else None
|
|
586
|
+
try:
|
|
587
|
+
return tuple(v[0])
|
|
588
|
+
except (TypeError, IndexError):
|
|
589
|
+
return None
|
|
590
|
+
|
|
591
|
+
def _take(self, v: Any) -> Any:
|
|
592
|
+
"""A per-point input in drawing order (``draw_order``), before colors are mapped."""
|
|
593
|
+
return v if self.order is None or v is None else permuted(v, self.order)
|
|
594
|
+
|
|
595
|
+
def draw(self, canvas: Canvas, xs: Any, ys: Any, simplify: Any) -> None:
|
|
596
|
+
if self.fill is None and self.stroke is None:
|
|
597
|
+
return
|
|
598
|
+
canvas.draw_markers(xs, ys, marker=self.marker, size=self.size, fill=self.fill,
|
|
599
|
+
stroke=self.stroke, stroke_width=self.line_width,
|
|
600
|
+
opacity=self.opacity, simplify=simplify)
|
|
601
|
+
|
|
602
|
+
def key_values(self) -> tuple[Any, ...] | None:
|
|
603
|
+
"""``(marker, size, fill, stroke, line_width, opacity)`` of the marker a legend
|
|
604
|
+
symbol draws (the median size, the first colors); ``None`` when it has no color."""
|
|
605
|
+
size = self.size
|
|
606
|
+
if not isinstance(size, (int, float)):
|
|
607
|
+
finite = [s for s in size if s == s]
|
|
608
|
+
size = sorted(finite)[len(finite) // 2] if finite else 4.0
|
|
609
|
+
marker = self.marker[0] if isinstance(self.marker, list) else self.marker
|
|
610
|
+
fill = self.first_color("fill")
|
|
611
|
+
stroke = self.first_color("stroke")
|
|
612
|
+
if fill is None and stroke is None:
|
|
613
|
+
return None
|
|
614
|
+
return marker, size, fill, stroke, self.line_width, self.opacity
|