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,87 @@
1
+ """The drawing context: the canvas, the axes rectangle, the mappers, the plot's trained
2
+ colors, and the x↔y swap of horizontal layers (trained color and size axes are in the render,
3
+ :func:`plot_io.mapped.trained`)."""
4
+
5
+ import copy
6
+ from typing import Any
7
+ from ..canvas import Canvas
8
+
9
+ __all__ = ["DrawContext", "Frame"]
10
+
11
+
12
+ class Frame:
13
+ """The mappers of a layer's frame: ``u`` for its x, ``v`` for its y; ``flip``: its x is
14
+ the plot's y (horizontal layers)."""
15
+
16
+ __slots__ = ("u", "v", "flip")
17
+
18
+ def __init__(self, u: Any, v: Any, flip: bool) -> None:
19
+ self.u, self.v, self.flip = u, v, flip
20
+
21
+ def page(self, a: Any, b: Any) -> tuple[Any, Any]:
22
+ """Page ``(x, y)`` of frame page coordinates ``(a, b)`` (along u, along v)."""
23
+ return (b, a) if self.flip else (a, b)
24
+
25
+
26
+ class DrawContext:
27
+ """What a layer needs while drawing: the canvas (``None`` while laying out), the axes
28
+ rectangle and mappers, and the plot's colors (``cycle``, which ``"C<n>"`` refer to)."""
29
+
30
+ def __init__(self, canvas: Canvas | None, plot: Any, rect: tuple[float, float, float, float],
31
+ x: Any, y: Any, cycle: tuple) -> None:
32
+ self.canvas = canvas
33
+ self.plot = plot
34
+ self.rect = rect
35
+ self.x = x
36
+ self.y = y
37
+ x0, y0, x1, y1 = rect
38
+ self.xmap = x.mapper(x0, x1)
39
+ self.ymap = y.mapper(y1, y0)
40
+ self.cycle = cycle
41
+ self.figure_size = (x1, y1)
42
+ self.root: "DrawContext" = self # the plot's context (see for_call)
43
+ #: page boxes ``(x0, y0, x1, y1)`` point labels keep clear of (an inside legend)
44
+ self.avoid: list[tuple[float, float, float, float]] = []
45
+
46
+ def release(self) -> None:
47
+ """Drops the plot's caches (styles and lazy columns, with their per-point buffers)
48
+ once the plot is drawn, before the document is written."""
49
+ for k in ("_memo", "_lazy"):
50
+ self.__dict__.pop(k, None)
51
+
52
+ def color(self, value: Any) -> Any:
53
+ from ..color.values import resolve_color
54
+ from ..units import Fixed
55
+ if isinstance(value, Fixed): # a named color kept as given
56
+ value = value.in_context(self.plot)
57
+ return resolve_color(value, self.cycle)
58
+
59
+ def for_call(self, call: Any) -> "DrawContext":
60
+ """This context with mappers that apply the call's ``x_origin``/``x_scale``/
61
+ ``x_extent`` (and ``y_…``), or with the y mapper of its lane (``call.lane``);
62
+ itself when it has none. ``root`` is the plot's context."""
63
+ from ..transform import affine
64
+ tx, ty = affine(call, "x"), affine(call, "y")
65
+ lane = getattr(call, "lane", None)
66
+ if tx is None and ty is None and lane is None:
67
+ return self
68
+ root = self.root
69
+ c = copy.copy(root)
70
+ if tx is not None:
71
+ c.xmap = root.xmap.affine(*tx, "x")
72
+ if lane is not None:
73
+ c.ymap = lane(root)
74
+ elif ty is not None:
75
+ c.ymap = root.ymap.affine(*ty, "y")
76
+ return c
77
+
78
+ def frame(self, layer: Any) -> Frame:
79
+ """The mappers of ``layer``'s frame (swapped for a flipped layer)."""
80
+ if layer.flip:
81
+ return Frame(self.ymap, self.xmap, True)
82
+ return Frame(self.xmap, self.ymap, False)
83
+
84
+ def state(self, frame_role: str, layer: Any) -> Any:
85
+ """The axis state of a layer's frame axis (``"x"``: along its u)."""
86
+ role = layer.frame_role(frame_role)
87
+ return self.x if role == "x" else self.y
plot_io/layers/core.py ADDED
@@ -0,0 +1,446 @@
1
+ """Layers and calls: what a plot type's call becomes before it is drawn.
2
+
3
+ A **call** (``p.scatter(…)``, ``plt.Box(…)``) is a model object, a :class:`~plot_io.layers.Series`
4
+ holding data and settings. Each render turns it into a :class:`Call`: its **layers**, one
5
+ per geom it draws, its legend entries, and what the axes and maps need from it.
6
+
7
+ A **layer** draws one geom from a table:
8
+
9
+ * the table holds the output of the call's stat (:mod:`~plot_io.layers.stats`), columns
10
+ in data units (shared by the layers of one call that draw the same output: a density's
11
+ fill and its line); a layer's ``stat`` and ``position`` name what made and places it,
12
+ as labels (its ``repr``), and do not run anything;
13
+ * the layer's ``aes`` maps the geom's channels to columns of the table;
14
+ * positions (``stack``, ``dodge``, ``jitter``, ``swarm``, ``pack``), requested on the
15
+ tables, add or replace columns once per plot over all its layers
16
+ (:mod:`~plot_io.layers.positions`);
17
+ * ``paint`` gives the per-item aesthetics (colors, widths, markers), resolved once the
18
+ the plot's colors and the color maps are trained;
19
+ * ``flip`` swaps x and y for the geom: a horizontal layer is a vertical one drawn with
20
+ its frame's x on the plot's y axis.
21
+ """
22
+
23
+ import math
24
+ from array import array
25
+ from typing import Any, Callable
26
+
27
+ from vec_io import Path
28
+
29
+ from .. import kernels
30
+
31
+ __all__ = ["Table", "Layer", "Call", "Entry", "ROLES", "KEY_GLYPH", "part_values",
32
+ "draw_part"]
33
+
34
+ ROLES = ("x", "y")
35
+
36
+
37
+ class Table(dict):
38
+ """Columns of one stat output (names → arrays or lists). Columns whose values need
39
+ the placed axes (slot widths in px, swarm offsets) are functions of the drawing
40
+ context in ``lazy``; :meth:`Layer.columns` resolves them."""
41
+
42
+ def __init__(self, *args: Any, **kw: Any) -> None:
43
+ super().__init__(*args, **kw)
44
+ self.lazy: dict[str, Callable[[Any], Any]] = {}
45
+ #: position requests (see :mod:`~plot_io.layers.positions`): ``(group,
46
+ #: normalize)`` to stack, a ``Dodge`` to share category slots
47
+ self.stack: Any = None
48
+ self.dodge: Any = None
49
+ #: ``id`` of the table a resolved copy was made from (see :meth:`Layer.columns`)
50
+ self.origin = id(self)
51
+
52
+ def __repr__(self) -> str:
53
+ return f"Table({', '.join(self)})"
54
+
55
+
56
+ class Layer:
57
+ """One geom drawing columns of a table (see the module docstring)."""
58
+
59
+ __slots__ = ("geom", "table", "aes", "paint", "flip", "stat", "position", "overlay",
60
+ "occupy", "key", "options")
61
+
62
+ def __init__(self, geom: Any, table: Table, aes: dict[str, str] | None = None,
63
+ paint: Any = None, flip: bool = False, stat: str = "identity",
64
+ position: str = "identity", overlay: bool = False, occupy: Any = True,
65
+ key: bool = True, **options: Any) -> None:
66
+ self.geom = geom
67
+ self.table = table
68
+ self.aes = aes or {}
69
+ self.paint = paint
70
+ self.flip = flip
71
+ self.stat = stat
72
+ self.position = position
73
+ #: drawn after every layer of the plot, outside the axes clip (value labels)
74
+ self.overlay = overlay
75
+ #: legend placement: ``True`` (the geom's cells), ``False`` or ``"all"`` (the axes)
76
+ self.occupy = occupy
77
+ #: whether the call's legend symbol shows this layer
78
+ self.key = key
79
+ self.options = options
80
+
81
+ def __repr__(self) -> str:
82
+ return (f"Layer({self.stat} → {self.position} → {type(self.geom).__name__}"
83
+ f"{', flipped' if self.flip else ''})")
84
+
85
+ def col(self, channel: str) -> Any:
86
+ """The column a channel of the geom reads (``None`` when the table has none)."""
87
+ return self.table.get(self.aes.get(channel, channel))
88
+
89
+ def has(self, channel: str) -> bool:
90
+ return self.aes.get(channel, channel) in self.table
91
+
92
+ def columns(self, ctx: Any) -> "Layer":
93
+ """This layer with its lazy columns resolved for ``ctx`` (itself if it has none)."""
94
+ lazy = self.table.lazy
95
+ if not lazy:
96
+ return self
97
+ cache = ctx.root.__dict__.setdefault("_lazy", {})
98
+ key = id(self.table)
99
+ t = cache.get(key)
100
+ if t is None:
101
+ t = Table(self.table)
102
+ t.origin = self.table.origin
103
+ for name, fn in lazy.items():
104
+ t[name] = fn(ctx)
105
+ cache[key] = t
106
+ out = Layer.__new__(Layer)
107
+ for k in Layer.__slots__:
108
+ setattr(out, k, getattr(self, k))
109
+ out.table = t
110
+ return out
111
+
112
+ def frame_role(self, role: str) -> str:
113
+ """The geom's axis drawn on the plot's axis ``role``."""
114
+ if not self.flip:
115
+ return role
116
+ return "y" if role == "x" else "x"
117
+
118
+ def style(self, ctx: Any) -> Any:
119
+ """The resolved aesthetics: of a :class:`~plot_io.layers.paint.Paint` (a role), or
120
+ ``paint`` itself (fixed values)."""
121
+ p = self.paint
122
+ resolve = getattr(p, "resolve", None)
123
+ return resolve(ctx, False) if resolve is not None else (p or {})
124
+
125
+ def part(self) -> tuple[str, Any] | None:
126
+ """This layer's part of a legend symbol: ``(kind, options)`` (``None``: none)."""
127
+ return self.geom.part(self)
128
+
129
+
130
+ # ---------------------------------------------------------------------------
131
+ # Legend symbols as data
132
+ # ---------------------------------------------------------------------------
133
+
134
+ #: Whether a symbol part's values come from the paint resolved for the legend
135
+ #: (``glyph=True``) or as drawn on the axes.
136
+ KEY_GLYPH = {"marker": True, "line": False, "band": False, "rect": True, "polygon": True,
137
+ "rule": True, "gradient": True}
138
+
139
+
140
+ def part_values(kind: str, p: Any) -> Any:
141
+ """The values a symbol part draws, from its layer's resolved paint ``p`` (``None``:
142
+ nothing drawn)."""
143
+ if kind == "marker":
144
+ return p.key_values() if p else None
145
+ p = p or {}
146
+ if kind == "line":
147
+ c, lw = p.get("color"), p.get("width")
148
+ if c is None or not lw:
149
+ return None
150
+ return (c, lw, p.get("dash"), p.get("opacity"))
151
+ if kind == "band":
152
+ return (p.get("color"), p.get("opacity", 1.0), p.get("edge"), p.get("edge_width"))
153
+ if kind in ("rect", "polygon"):
154
+ return (p.get("fill"), p.get("stroke"), p.get("line_width"))
155
+ if kind == "rule":
156
+ c = p.get("color")
157
+ return None if c is None else (c, p.get("width"))
158
+ if kind == "gradient":
159
+ from ..colorbar import gradient_stops
160
+ m = p.get("map")
161
+ return None if m is None else tuple(gradient_stops(m, 16))
162
+ raise ValueError(f"unknown symbol part {kind!r}")
163
+
164
+
165
+ def draw_part(ctx: Any, kind: str, options: Any, v: Any, x: float, y: float, w: float,
166
+ h: float) -> None:
167
+ """Draw one symbol part (values ``v`` of :func:`part_values`) in box ``x, y, w, h``."""
168
+ if v is None:
169
+ return
170
+ from ..color.values import with_alpha
171
+ from ..shapes import swatch
172
+ canvas = ctx.canvas
173
+ o = dict(options)
174
+ if kind == "marker":
175
+ marker, size, fill, stroke, lw, opacity = v
176
+ canvas.draw_markers([x + w / 2], [y + h * o.get("at", 0.5)], marker=marker,
177
+ size=min(size, h * o.get("max", 1.1)), fill=fill, stroke=stroke,
178
+ stroke_width=lw, opacity=opacity)
179
+ elif kind == "line":
180
+ c, lw, dash, opacity = v
181
+ if o.get("top"):
182
+ x, y, w, h = swatch(x, y, w, h)
183
+ cy = y
184
+ else:
185
+ cy = y + h / 2
186
+ kw: dict[str, Any] = {}
187
+ if dash is not None:
188
+ kw["stroke_dasharray"] = dash
189
+ if opacity is not None:
190
+ kw["opacity"] = opacity
191
+ canvas.draw_lines([x, x + w], [cy, cy], stroke=c, stroke_width=lw, **kw)
192
+ elif kind == "band":
193
+ c, opacity, edge, ew = v
194
+ if o.get("form") == "band":
195
+ if c is not None:
196
+ canvas.fill = with_alpha(c, opacity)
197
+ canvas.draw_rect(x, y + h * 0.2, w, h * 0.6, stroke=None)
198
+ return
199
+ x, y, w, h = swatch(x, y, w, h)
200
+ if c is not None:
201
+ canvas.fill = with_alpha(c, opacity)
202
+ canvas.draw_rect(x, y, w, h, stroke=None)
203
+ if edge is not None and ew:
204
+ canvas.stroke = edge
205
+ canvas.stroke_width = ew
206
+ canvas.draw_rect(x, y, w, h, fill=None)
207
+ elif kind in ("rect", "polygon"):
208
+ fill, stroke, lw = v
209
+ form = o.get("form")
210
+ if form == "hexagon":
211
+ canvas.draw_markers([x + w / 2], [y + h / 2], marker="M0.5 -0.29 L0.5 0.29 L0 "
212
+ "0.58 L-0.5 0.29 L-0.5 -0.29 L0 -0.58 Z", size=h * 0.9,
213
+ fill=fill, stroke=None)
214
+ return
215
+ if form == "fill":
216
+ if fill is not None:
217
+ canvas.fill = fill
218
+ canvas.draw_rect(*swatch(x, y, w, h), stroke=None)
219
+ return
220
+ path = Path().rect(*swatch(x, y, w, h))
221
+ if fill is not None:
222
+ canvas.draw_path(path, fill=fill, stroke=None)
223
+ if stroke is not None and (lw is not None if kind == "rect" else lw):
224
+ canvas.stroke_width = lw
225
+ canvas.draw_path(path, fill=None, stroke=stroke)
226
+ elif kind == "gradient":
227
+ from vec_io import LinearGradient
228
+ sx, sy, sw, sh = swatch(x, y, w, h)
229
+ canvas.draw_rect(sx, sy, sw, sh, fill=LinearGradient(sx, 0, sx + sw, 0, list(v)), stroke=None)
230
+ elif kind == "rule":
231
+ c, lw = v
232
+ cx = x + w / 2
233
+ if o.get("form") == "stem":
234
+ canvas.draw_lines([cx, cx], [y + h, y + h * 0.35], stroke=c, stroke_width=lw)
235
+ return
236
+ canvas.draw_lines([cx, cx], [y, y + h], stroke=c, stroke_width=lw)
237
+ canvas.draw_segments([cx - 3, cx - 3], [y, y + h], [cx + 3, cx + 3], [y, y + h],
238
+ stroke=c, stroke_width=lw)
239
+
240
+
241
+ class Entry:
242
+ """A legend entry of a call: its label and symbol, as data.
243
+
244
+ ``symbol``: the parts of the call's key layers (``(kind, options)`` each, see
245
+ :func:`draw_part`); ``paint``: the paint of each part's layer, resolved to values once
246
+ per drawing context (:meth:`values`); ``colors``: a paint giving ``(fill, stroke)`` for
247
+ the legend's simple symbols. Two entries draw the same symbol when their symbols and
248
+ values are equal."""
249
+
250
+ __slots__ = ("label", "symbol", "paint", "colors")
251
+
252
+ def __init__(self, label: Any, symbol: Any, paint: Any, colors: Any = None) -> None:
253
+ self.label = label
254
+ self.symbol = symbol
255
+ self.paint = paint
256
+ self.colors = colors
257
+
258
+ def __repr__(self) -> str:
259
+ return f"Entry({self.label!r})"
260
+
261
+ def values(self, ctx: Any) -> tuple[Any, ...]:
262
+ """The values each part draws in ``ctx``."""
263
+ cache = ctx.root.__dict__.setdefault("_entry_values", {})
264
+ got = cache.get(id(self))
265
+ if got is None:
266
+ got = cache[id(self)] = tuple(
267
+ part_values(kind, p.resolve(ctx, KEY_GLYPH[kind]))
268
+ for (kind, _), p in zip(self.symbol, self.paint))
269
+ return got
270
+
271
+ def key(self, ctx: Any) -> Any:
272
+ """What the symbol draws, for comparing entries."""
273
+ return self.symbol, self.values(ctx)
274
+
275
+ # the legend's protocol (decorations have it too)
276
+
277
+ def legend_key_at(self, ctx: Any, i: int) -> Any:
278
+ return self.key(ctx)
279
+
280
+ def legend_glyph_at(self, ctx: Any, i: int, x: float, y: float, w: float,
281
+ h: float) -> None:
282
+ for (kind, options), v in zip(self.symbol, self.values(ctx)):
283
+ draw_part(ctx, kind, options, v, x, y, w, h)
284
+
285
+ def legend_colors_at(self, ctx: Any, i: int) -> tuple[Any, Any]:
286
+ return self.colors.resolve(ctx, True)
287
+
288
+
289
+ def _no_room(fonts: Any, role: str) -> list[tuple[float, float, int]]:
290
+ return []
291
+
292
+
293
+ class Call:
294
+ """A call of a plot type in a render: its layers and what the axes, the color and size
295
+ maps, the plot's colors and the legend need from it."""
296
+
297
+ def __init__(self, series: Any) -> None:
298
+ self.series = series
299
+ self.layers: list[Layer] = []
300
+ #: tables positioned but not drawn as such (histogram sets merged into one layer)
301
+ self.tables: list[Table] = []
302
+ self.entries: list[Entry] = []
303
+ #: the plot's colors taken when the call has no color of its own (0: none)
304
+ self.color_count = 0
305
+ #: the colors of a level call (:func:`plot_io.levels.assign`; ``None``: ``color_count``)
306
+ self.level: Any = None
307
+ #: ``(axis, values)`` pairs mapped on color and size axes (``values`` may be a
308
+ #: function, called at training: ``None`` until the axes are placed)
309
+ self.color_sources: list[tuple[Any, Any]] = []
310
+ self.size_sources: list[tuple[Any, Any]] = []
311
+ #: whether the call is drawn on axes (pies are not)
312
+ self.axes = True
313
+ #: per plot role: categories (categorical data), values included in the extent
314
+ #: beyond the layers' columns, an extent replacing the layers' (``f(scale)``),
315
+ #: values automatic padding does not pass, the column name of the axis title
316
+ self.cats: dict[str, list[Any]] = {}
317
+ self.extra: dict[str, Any] = {}
318
+ self.extent_of: dict[str, Callable[[Any], tuple[int, float, float]]] = {}
319
+ self.sticky_values: dict[str, list[float]] = {}
320
+ self.titles: dict[str, Any] = {}
321
+ #: plot roles that include 0 under ``include_zero="auto"``, that take no
322
+ #: automatic padding, inverted by default; ``role → tz`` of date data
323
+ self.zero: set[str] = set()
324
+ self.exact: set[str] = set()
325
+ self.invert: set[str] = set()
326
+ #: plot roles whose ticks are integers (rows and columns of a grid)
327
+ self.whole: set[str] = set()
328
+ self.times: dict[str, Any] = {}
329
+ #: per plot role: the scale the call asks of an axis without one (``"genome"``),
330
+ #: ticks it gives (``[(value, label)]``: named rows; the axis takes them when every
331
+ #: call on it gives some), roles whose axis it hides (drawn when any call does not)
332
+ self.scales: dict[str, str] = {}
333
+ self.ticks: dict[str, list[tuple[float, Any]]] = {}
334
+ #: plot roles whose given tick labels are left out where they overlap the last kept
335
+ self.thin: set[str] = set()
336
+ #: plot roles whose tick labels are leaf names (dendrograms): all drawn, rotated
337
+ #: and then smaller before any is left out (the axis' ``label_min_size``)
338
+ self.leaves: set[str] = set()
339
+ self.hide: set[str] = set()
340
+ #: the genome region the call reads (genome series), and per plot role where it
341
+ #: differs (a square Hi-C matrix: its ``region2`` on y)
342
+ self.region: Any = None
343
+ self.regions: dict[str, Any] = {}
344
+ #: ``lane(ctx) -> Mapper``: the y mapper of a call drawn in a lane of its own
345
+ #: (interval rows, :mod:`plot_io.genome.lanes`), from the plot's context
346
+ self.lane: Callable[[Any], Any] | None = None
347
+ #: ``room(fonts, role)``: value labels drawn past their values (bars)
348
+ self.room: Callable[[Any, str], list[tuple[float, float, int]]] = _no_room
349
+ #: ``overhang(fonts, w, h) -> (left, right)``: labels drawn past the axes (pies)
350
+ self.overhang: Callable[..., tuple[float, float]] | None = None
351
+ #: ``resolution(ctx) -> key``: for stats binned at the placed axes' resolution
352
+ #: (hexagon lattices), the key of the binning for the axes of a drawing context;
353
+ #: when it changes between layout rounds, the stats and the color axes they map
354
+ #: are computed again (see :mod:`plot_io.layout`)
355
+ self.resolution: Callable[[Any], Any] | None = None
356
+ #: steps of the preset that need the plot's positions (layers built from stacked
357
+ #: values), run after them
358
+ self.after: list[Callable[[], None]] = []
359
+
360
+ def __repr__(self) -> str:
361
+ return f"Call({self.series!r}, {len(self.layers)} layers)"
362
+
363
+ # -- what the axes need --------------------------------------------------------
364
+
365
+ def has_axis(self, role: str) -> bool:
366
+ return True
367
+
368
+ def categories(self, role: str) -> list[Any] | None:
369
+ return self.cats.get(role)
370
+
371
+ def extent(self, role: str, scale: Any) -> tuple[int, float, float]:
372
+ """``(count, low, high)`` of the call's values on axis ``role``, through the axis
373
+ scale's function: the union of its layers' geom extents and its extra values."""
374
+ fn = self.extent_of.get(role)
375
+ if fn is not None:
376
+ return fn(scale)
377
+ count, lo, hi = 0, math.inf, -math.inf
378
+ dropped = 0
379
+ for layer in self.layers:
380
+ if not layer.options.get("bounds", True):
381
+ continue
382
+ n, a, b = layer.geom.extent(layer, layer.frame_role(role), scale)
383
+ if n:
384
+ count += n
385
+ lo, hi = min(lo, a), max(hi, b)
386
+ if scale.base is not None:
387
+ dropped += layer.geom.dropped(layer, layer.frame_role(role), scale)
388
+ if dropped:
389
+ from ..spec import warn
390
+ kind = getattr(self.series, "kind", "series")
391
+ warn(f"{kind}: {dropped} value{'s' if dropped > 1 else ''} not positive on the "
392
+ f"log {role} axis, not drawn", key=("not positive", id(self), role))
393
+ extra = self.extra.get(role)
394
+ if extra is not None and len(extra):
395
+ n, a, b = kernels.extent(extra, scale.function)
396
+ if n:
397
+ count += n
398
+ lo, hi = min(lo, a), max(hi, b)
399
+ if not count:
400
+ return 0, math.nan, math.nan
401
+ return count, lo, hi
402
+
403
+ def values(self, role: str) -> Any:
404
+ """Every value on axis ``role`` (for quantile limits)."""
405
+ out = array("d")
406
+ for layer in self.layers:
407
+ if not layer.options.get("bounds", True):
408
+ continue
409
+ v = layer.geom.values(layer, layer.frame_role(role))
410
+ if v is not None:
411
+ out.extend(v)
412
+ extra = self.extra.get(role)
413
+ if extra is not None:
414
+ out.extend(extra)
415
+ return out
416
+
417
+ def sticky(self, role: str) -> list[float]:
418
+ return self.sticky_values.get(role, [])
419
+
420
+ def zero_default(self, role: str) -> bool:
421
+ return role in self.zero
422
+
423
+ def zero_padding(self, role: str) -> bool:
424
+ return role in self.exact
425
+
426
+ def inverts(self, role: str) -> bool:
427
+ return role in self.invert
428
+
429
+ def whole_ticks(self, role: str) -> bool:
430
+ return role in self.whole
431
+
432
+ def time_role(self, role: str) -> Any:
433
+ return self.times.get(role)
434
+
435
+ def scale_of(self, role: str) -> str | None:
436
+ return self.scales.get(role)
437
+
438
+ def region_of(self, role: str) -> Any:
439
+ """The genome region the call reads on axis ``role``."""
440
+ return self.regions.get(role, self.region)
441
+
442
+ def axis_title(self, role: str) -> Any:
443
+ return self.titles.get(role)
444
+
445
+ def label_room(self, fonts: Any, role: str) -> list[tuple[float, float, int]]:
446
+ return self.room(fonts, role)