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.
Files changed (130) hide show
  1. plot_io/__init__.py +73 -0
  2. plot_io/_methods.py +1691 -0
  3. plot_io/_native/__init__.py +199 -0
  4. plot_io/_native/__main__.py +197 -0
  5. plot_io/_native/_api.py +457 -0
  6. plot_io/_native/bin/windows-arm64.dll +0 -0
  7. plot_io/_native/kernels.py +335 -0
  8. plot_io/_native/manifest.json +49 -0
  9. plot_io/axis.py +1347 -0
  10. plot_io/boxes.py +1156 -0
  11. plot_io/canvas.py +389 -0
  12. plot_io/color/__init__.py +3 -0
  13. plot_io/color/data.py +1646 -0
  14. plot_io/color/maps.py +922 -0
  15. plot_io/color/values.py +295 -0
  16. plot_io/color/variants.py +1246 -0
  17. plot_io/colorbar.py +1426 -0
  18. plot_io/columns.py +614 -0
  19. plot_io/decorations.py +920 -0
  20. plot_io/defaults.py +809 -0
  21. plot_io/density.py +113 -0
  22. plot_io/ext.py +68 -0
  23. plot_io/facets.py +240 -0
  24. plot_io/figure.py +788 -0
  25. plot_io/formatters.py +97 -0
  26. plot_io/genome/__init__.py +17 -0
  27. plot_io/genome/adapters.py +356 -0
  28. plot_io/genome/combine.py +123 -0
  29. plot_io/genome/contacts.py +508 -0
  30. plot_io/genome/intervals.py +750 -0
  31. plot_io/genome/lanes.py +191 -0
  32. plot_io/genome/layout.py +184 -0
  33. plot_io/genome/links.py +364 -0
  34. plot_io/genome/pack.py +27 -0
  35. plot_io/genome/readers.py +145 -0
  36. plot_io/genome/regions.py +228 -0
  37. plot_io/genome/scale.py +349 -0
  38. plot_io/genome/series.py +344 -0
  39. plot_io/genome/signal.py +621 -0
  40. plot_io/genome/sites.py +782 -0
  41. plot_io/genome/sources.py +459 -0
  42. plot_io/genome/testing.py +161 -0
  43. plot_io/geometry.py +289 -0
  44. plot_io/groups.py +308 -0
  45. plot_io/guides.py +503 -0
  46. plot_io/helpers/__init__.py +17 -0
  47. plot_io/helpers/clustermap.py +202 -0
  48. plot_io/helpers/dendrogram.py +172 -0
  49. plot_io/helpers/grids.py +304 -0
  50. plot_io/helpers/qq.py +334 -0
  51. plot_io/helpers/raincloud.py +249 -0
  52. plot_io/helpers/tree.py +214 -0
  53. plot_io/helpers/volcano.py +505 -0
  54. plot_io/hints.py +125 -0
  55. plot_io/kernels.py +740 -0
  56. plot_io/labels.py +566 -0
  57. plot_io/layers/__init__.py +100 -0
  58. plot_io/layers/base.py +630 -0
  59. plot_io/layers/context.py +87 -0
  60. plot_io/layers/core.py +446 -0
  61. plot_io/layers/geoms.py +1362 -0
  62. plot_io/layers/grammar.py +586 -0
  63. plot_io/layers/paint.py +614 -0
  64. plot_io/layers/pipeline.py +170 -0
  65. plot_io/layers/positions.py +431 -0
  66. plot_io/layers/stats.py +1088 -0
  67. plot_io/layout.py +1381 -0
  68. plot_io/legend.py +605 -0
  69. plot_io/levels.py +509 -0
  70. plot_io/limits.py +124 -0
  71. plot_io/lines.py +42 -0
  72. plot_io/mapped.py +522 -0
  73. plot_io/markers.py +110 -0
  74. plot_io/markup.py +128 -0
  75. plot_io/numbers.py +354 -0
  76. plot_io/occupancy.py +295 -0
  77. plot_io/plot.py +597 -0
  78. plot_io/presets/__init__.py +51 -0
  79. plot_io/presets/_util.py +153 -0
  80. plot_io/presets/area.py +145 -0
  81. plot_io/presets/bar.py +261 -0
  82. plot_io/presets/box.py +400 -0
  83. plot_io/presets/catpoints.py +177 -0
  84. plot_io/presets/contour.py +353 -0
  85. plot_io/presets/curves.py +30 -0
  86. plot_io/presets/dots.py +355 -0
  87. plot_io/presets/ecdf.py +97 -0
  88. plot_io/presets/errorbar.py +106 -0
  89. plot_io/presets/errors.py +296 -0
  90. plot_io/presets/fill.py +74 -0
  91. plot_io/presets/heatmap.py +322 -0
  92. plot_io/presets/hexbin.py +198 -0
  93. plot_io/presets/hist.py +391 -0
  94. plot_io/presets/hist2d.py +87 -0
  95. plot_io/presets/kde.py +162 -0
  96. plot_io/presets/kde2d.py +147 -0
  97. plot_io/presets/line.py +143 -0
  98. plot_io/presets/pie.py +502 -0
  99. plot_io/presets/regression.py +115 -0
  100. plot_io/presets/scatter.py +194 -0
  101. plot_io/presets/stem.py +100 -0
  102. plot_io/presets/violin.py +323 -0
  103. plot_io/py.typed +0 -0
  104. plot_io/registry.py +82 -0
  105. plot_io/render.py +480 -0
  106. plot_io/rows.py +491 -0
  107. plot_io/scales.py +103 -0
  108. plot_io/seams.py +138 -0
  109. plot_io/settings.py +807 -0
  110. plot_io/shapes.py +66 -0
  111. plot_io/sharing.py +232 -0
  112. plot_io/sizemaps.py +97 -0
  113. plot_io/spec.py +240 -0
  114. plot_io/states.py +216 -0
  115. plot_io/styles/__init__.py +307 -0
  116. plot_io/styles/library.py +397 -0
  117. plot_io/tables.py +138 -0
  118. plot_io/text.py +412 -0
  119. plot_io/ticks.py +400 -0
  120. plot_io/times.py +287 -0
  121. plot_io/titles.py +225 -0
  122. plot_io/transform.py +114 -0
  123. plot_io/types.py +13604 -0
  124. plot_io/units.py +768 -0
  125. plot_io/validators.py +1029 -0
  126. plot_io/values.py +190 -0
  127. plot_io-0.12.0.dist-info/METADATA +215 -0
  128. plot_io-0.12.0.dist-info/RECORD +130 -0
  129. plot_io-0.12.0.dist-info/WHEEL +4 -0
  130. plot_io-0.12.0.dist-info/licenses/LICENSE +28 -0
@@ -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