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
plot_io/guides.py ADDED
@@ -0,0 +1,503 @@
1
+ """Guides (legends, colorbars, size keys) and their places: inside the axes at an anchor
2
+ (``inside=``) or outside on a side (``side=``); the ``legend="…"`` shorthand; one
3
+ :class:`GuideState` per guide while drawing, placed by where it is."""
4
+
5
+ import math
6
+ from typing import Any
7
+
8
+ from .occupancy import N
9
+ from .settings import Settings
10
+ from .titles import SIDES, align_of
11
+
12
+ __all__ = ["ANCHORS", "anchor", "where_of", "placement", "set_place", "anchor_box", "along_at",
13
+ "GuideState", "marked", "best_anchor", "place_inside"]
14
+
15
+ #: Places of a guide inside the axes (``inside=``); ``"auto"`` chooses one (a legend may go
16
+ #: outside), ``"best"`` chooses one too (outside only when every place covers a marker).
17
+ ANCHORS = ("auto", "best", "top_right", "top_left", "bottom_right", "bottom_left", "top", "bottom",
18
+ "center_right", "center_left")
19
+
20
+
21
+ def _word(v: Any) -> str:
22
+ s = str(getattr(v, "value", v)).strip().lower().replace("-", " ")
23
+ return "_".join(s.replace("_", " ").split())
24
+
25
+
26
+ def anchor(v: Any) -> str:
27
+ s = _word(v)
28
+ if s not in ANCHORS:
29
+ raise ValueError(f"expected one of {', '.join(map(repr, ANCHORS))}, got {v!r}")
30
+ return s
31
+
32
+
33
+ def where_of(v: Any) -> dict[str, Any]:
34
+ """The shorthand of a guide's place (``legend="top"``, ``add_legend("top_left")``) as
35
+ settings: a side name is ``side=``, a corner, a centre or ``"auto"`` is ``inside=``,
36
+ ``"none"`` hides it."""
37
+ s = _word(v)
38
+ if s in SIDES:
39
+ return {"side": s}
40
+ if s in ANCHORS:
41
+ return {"inside": s}
42
+ if s == "none":
43
+ return {"visible": False}
44
+ raise ValueError(f"expected a side ({', '.join(SIDES)}), a place inside the axes "
45
+ f"({', '.join(ANCHORS)}) or 'none', got {v!r}")
46
+
47
+
48
+ def placement(guide: Settings, side_first: bool) -> tuple[str, Any]:
49
+ """``("inside", anchor)`` or ``("side", side)`` of a guide: the one of ``inside`` and
50
+ ``side`` set on it (setting one unsets the other), else from the style the one not
51
+ ``None`` (``side_first``: ``side`` when both are)."""
52
+ if guide.is_set("side"):
53
+ return "side", guide.resolve("side")
54
+ if guide.is_set("inside"):
55
+ return "inside", guide.resolve("inside")
56
+ side, inside = guide.resolve("side"), guide.resolve("inside")
57
+ if side is not None and (side_first or inside is None):
58
+ return "side", side
59
+ return "inside", inside
60
+
61
+
62
+ def set_place(guide: Settings, settings: dict[str, Any]) -> None:
63
+ """After ``guide.set(**settings)``: ``inside=`` unsets ``side`` and ``side=`` unsets
64
+ ``inside`` (the one given last wins)."""
65
+ for given, other in (("inside", "side"), ("side", "inside")):
66
+ if settings.get(given) is not None and settings.get(other) is None:
67
+ guide._values.pop(other, None)
68
+
69
+
70
+ def along_at(lo: float, hi: float, size: float, align: float) -> float:
71
+ """Where a box ``size`` long starts along ``lo``–``hi`` aligned at ``align`` (0 the
72
+ start, 0.5 the centre, 1 the end)."""
73
+ if align == 0.0:
74
+ return lo
75
+ if align == 1.0:
76
+ return hi - size
77
+ return (lo + hi - size) / 2
78
+
79
+
80
+ def anchor_box(rect: tuple[float, float, float, float], where: str, w: float, h: float,
81
+ px: float, py: float) -> tuple[float, float]:
82
+ """Top-left corner of a ``w`` × ``h`` box at anchor ``where`` inside ``rect``, ``px``
83
+ and ``py`` from its edges (a centre: centred)."""
84
+ x0, y0, x1, y1 = rect
85
+ vertical, _, horizontal = where.partition("_")
86
+ if where in ("top", "bottom", "center"):
87
+ vertical, horizontal = where, "center"
88
+ x = {"left": x0 + px, "right": x1 - px - w, "center": (x0 + x1 - w) / 2}[horizontal]
89
+ y = {"top": y0 + py, "bottom": y1 - py - h, "center": (y0 + y1 - h) / 2}[vertical]
90
+ return x, y
91
+
92
+
93
+
94
+
95
+ # ---------------------------------------------------------------------------
96
+ # Inside the axes: the occupancy grid
97
+ # ---------------------------------------------------------------------------
98
+
99
+ #: Inside candidates of ``inside="auto"`` and ``"best"``, in order of preference (ties go to the first).
100
+ CANDIDATES = ("top_right", "top_left", "bottom_left", "bottom_right", "top", "bottom",
101
+ "center_right", "center_left")
102
+ #: Inside candidates of ``"best"``: matplotlib's ``loc="best"`` places, in its order.
103
+ BEST_CANDIDATES = ("top_right", "top_left", "bottom_left", "bottom_right", "center_right",
104
+ "center_left", "bottom", "top", "center")
105
+ #: An inside place covering no marker but other drawn cells is taken when it covers at most
106
+ #: this fraction of them (else ``"auto"`` puts the legend outside right).
107
+ OUTSIDE_THRESHOLD = 0.02
108
+ #: The anchors chosen when drawing (the others are fixed places).
109
+ CHOSEN = ("auto", "best")
110
+
111
+
112
+ def _drawn(ps: Any, ctx: Any, fonts: Any = None) -> Any:
113
+ """The occupancy grid of what a plot and its twins draw on the placed axes (``ctx``):
114
+ series, the row label inside the axes, and with ``fonts`` decorations."""
115
+ from .layers.context import DrawContext
116
+ from .occupancy import Occupancy
117
+ from .rows import occupy_label
118
+ from .states import occupy
119
+ occ = Occupancy(ctx.rect)
120
+ contexts = [(ps, ctx)] + [(t, DrawContext(ctx.canvas, t.plot, ctx.rect, t.x, t.y,
121
+ t.cycle)) for t in getattr(ps, "twins", [])]
122
+ for st, c in contexts:
123
+ for call in st.calls:
124
+ occupy(call, c, occ)
125
+ occupy_label(ps.plot, ctx, occ)
126
+ if fonts is not None:
127
+ for st, c in contexts:
128
+ for d in st.plot.decorations:
129
+ d.occupy(fonts, c, occ)
130
+ return occ
131
+
132
+
133
+ def marked(ps: Any, ctx: Any, fonts: Any = None) -> Any:
134
+ """The occupancy grid a plot's legend chooses on: what is drawn (:func:`_drawn`) and
135
+ its colorbars inside the axes (placed in order with ``fonts``, else where last
136
+ placed)."""
137
+ occ = _drawn(ps, ctx, fonts)
138
+ if fonts is not None:
139
+ place_inside(ps, fonts, ctx, occ)
140
+ else:
141
+ for b in ps.bars:
142
+ if b.inside is not None and b.inside_box is not None:
143
+ occ.rects(*([v] for v in b.inside_box), solid=True)
144
+ return occ
145
+
146
+
147
+ def best_anchor(occ: Any, rect: tuple, w: float, h: float, px: float, py: float,
148
+ candidates: tuple[str, ...] = CANDIDATES) -> tuple[str, float]:
149
+ """The first of ``candidates`` where a ``w`` × ``h`` box covers no marked cell and
150
+ meets no marker, text or inside guide, else the one covering the fewest (the first of
151
+ equals); with the fraction it covers (a box meeting one in no marked cell: one
152
+ cell's)."""
153
+ best, best_f = candidates[0], math.inf
154
+ for pos in candidates:
155
+ box = (*anchor_box(rect, pos, w, h, px, py), w, h)
156
+ f = occ.covered(*box)
157
+ if f == 0.0:
158
+ if not occ.solid_in(*box):
159
+ return pos, 0.0
160
+ f = 1.0 / (N * N)
161
+ if f < best_f - 1e-12:
162
+ best, best_f = pos, f
163
+ return best, best_f
164
+
165
+
166
+ def place_inside(ps: Any, fonts: Any, ctx: Any, occ: Any = None) -> list[Any]:
167
+ """The geometries of a plot's colorbars inside its axes, in order; ``"auto"`` ones
168
+ choose on the occupancy grid ``occ`` (default: :func:`marked` without them), each
169
+ marking it once placed."""
170
+ bars = [b for b in ps.bars if b.inside is not None]
171
+ if not bars:
172
+ return []
173
+ if occ is None and any(b.inside in CHOSEN for b in bars):
174
+ occ = _drawn(ps, ctx, fonts)
175
+ out = []
176
+ for b in bars:
177
+ out.append(b.place(fonts, ctx.rect, occ=occ))
178
+ if occ is not None and b.inside_box is not None:
179
+ occ.rects(*([v] for v in b.inside_box), solid=True)
180
+ return out
181
+
182
+
183
+ # ---------------------------------------------------------------------------
184
+ # The guide state
185
+ # ---------------------------------------------------------------------------
186
+
187
+ #: What a :class:`GuideState` holds itself (the rest is its content's).
188
+ _OWN = frozenset({"content", "spot", "choice", "free", "moved", "inside_box"})
189
+
190
+
191
+ class GuideState:
192
+ """A guide while drawing: its content and its place.
193
+
194
+ ``content``: what it shows and how it lays that out, its attributes read through the
195
+ guide: a legend's entries and size keys (:class:`plot_io.legend.Entries`), a colorbar
196
+ or size key (:class:`plot_io.colorbar.Bar`), or a size legend's rows
197
+ (:class:`plot_io.legend.SizeRows`).
198
+
199
+ ``spot``, its place in the one vocabulary: ``("inside", anchor)`` inside its plot's
200
+ axes (``"auto"`` included), ``("side", side)`` in a band on a side of its plot,
201
+ ``("lanes", side)`` in the lanes beside a set of plots, ``("area", None)`` in its own
202
+ grid area. :meth:`measure` and :meth:`place` lay the content out for it."""
203
+
204
+ def __init__(self, content: Any, spot: tuple[str, Any]) -> None:
205
+ self.content = content
206
+ self.spot = spot
207
+ #: ``inside="auto"``: the anchor chosen once the axes are placed
208
+ self.choice: str | None = None
209
+ #: ``"inside"`` (a free place, not an anchor): where, as fractions of the room
210
+ self.free: tuple[float, float] = (0.0, 0.0)
211
+ #: an inside legend larger than its axes, drawn on their right side instead
212
+ self.moved = False
213
+ #: inside the axes: its box where last placed
214
+ self.inside_box: tuple[float, float, float, float] | None = None
215
+ content.guide = self
216
+
217
+ def __getattr__(self, name: str) -> Any:
218
+ if name == "content":
219
+ raise AttributeError(name)
220
+ return getattr(self.content, name)
221
+
222
+ def __setattr__(self, name: str, value: Any) -> None:
223
+ if name not in _OWN:
224
+ raise AttributeError(f"GuideState.{name}: set on its content")
225
+ object.__setattr__(self, name, value)
226
+
227
+ @classmethod
228
+ def for_plot(cls, ps: Any) -> "GuideState":
229
+ """A plot's legend (a twin's: empty, its main plot's shows its entries)."""
230
+ from .legend import Entries
231
+ return cls(Entries.of_plot(ps), ps.plot.legend.placement())
232
+
233
+ @classmethod
234
+ def for_figure_legend(cls, legend: Any, states: list[Any]) -> "GuideState":
235
+ """A figure legend (:class:`~plot_io.plot.FigureLegend`) of the plots ``states``."""
236
+ from .legend import Entries
237
+ return cls(Entries.merged(legend, states), ("lanes", legend.resolve("side")))
238
+
239
+ @classmethod
240
+ def for_colorbar(cls, cb: Any) -> "GuideState":
241
+ """A colorbar or size key: attached to its plot, in the lanes, or in its area."""
242
+ from .colorbar import Bar, own_area
243
+ kind, where = ("area", None) if own_area(cb) else cb.placement()
244
+ spot = ("lanes", where) if cb.lanes else (kind, where)
245
+ return cls(Bar(cb, spot), spot)
246
+
247
+ @classmethod
248
+ def for_size_legend(cls, sl: Any) -> "GuideState":
249
+ """A size legend in its own area (``figure.size_legend``)."""
250
+ from .legend import SizeRows
251
+ return cls(SizeRows(sl), ("area", None))
252
+
253
+ @property
254
+ def inside(self) -> str | None:
255
+ """Its anchor inside the axes (``"auto"`` included), else ``None``."""
256
+ kind, where = self.spot
257
+ return where if kind == "inside" else None
258
+
259
+ # -- a legend's place ---------------------------------------------------------------
260
+
261
+ def setting(self) -> str:
262
+ """A legend's place as set: an anchor inside the axes (``"auto"`` included),
263
+ ``"outside_<side>"``, or ``"none"`` (hidden)."""
264
+ c = self.content
265
+ if c.twin or not c.legend.resolve("visible"):
266
+ return "none"
267
+ kind, where = self.spot
268
+ return where if kind == "inside" else "outside_" + where
269
+
270
+ def active(self) -> bool:
271
+ """Whether a legend shows something."""
272
+ if self.spot[0] == "lanes":
273
+ return bool(self.content.entries)
274
+ return bool(self.content.entries) and self.setting() != "none"
275
+
276
+ def position(self) -> str:
277
+ """A legend's place (:meth:`setting`; in the lanes, its side), ``"auto"`` and
278
+ ``"best"`` replaced by the choice once made and ``"outside_right"`` once moved there
279
+ (:meth:`fit`)."""
280
+ if self.spot[0] == "lanes":
281
+ return self.spot[1]
282
+ if self.moved:
283
+ return "outside_right"
284
+ pos = self.setting()
285
+ return self.choice if pos in CHOSEN and self.choice else pos
286
+
287
+ def horizontal(self) -> bool:
288
+ """Whether a legend runs along a horizontal side."""
289
+ if self.spot[0] == "lanes":
290
+ return self.spot[1] in ("top", "bottom")
291
+ o = self.content.legend.resolve("orientation")
292
+ if o == "auto":
293
+ # The setting, not an ``"auto"`` choice: the box was measured before choosing.
294
+ return not self.moved and self.setting() in ("top", "bottom", "outside_top",
295
+ "outside_bottom")
296
+ return o == "horizontal"
297
+
298
+ @property
299
+ def row_major(self) -> bool:
300
+ """A legend's entries in rows (side by side, wrapped), not in columns: horizontal,
301
+ without ``columns`` (in the lanes: on top and bottom, ``columns`` or not)."""
302
+ if self.spot[0] == "lanes":
303
+ return self.horizontal()
304
+ return self.horizontal() and not self.content.legend.is_set("columns")
305
+
306
+ def align(self) -> float:
307
+ """A plot legend outside its axes, along its side: 0 at the start (left, top), 0.5
308
+ centred, 1 at the end; ``align="auto"``: centred when horizontal on top or
309
+ bottom, else at the start."""
310
+ a = self.content.legend.resolve("align")
311
+ if a == "auto":
312
+ return 0.5 if self.position() in ("outside_top", "outside_bottom") \
313
+ and self.horizontal() else 0.0
314
+ return {"start": 0.0, "center": 0.5, "end": 1.0}[align_of(a)]
315
+
316
+ def outside(self) -> str | None:
317
+ """A plot legend's place when outside its axes, else ``None``."""
318
+ if self.spot[0] == "lanes":
319
+ return None
320
+ pos = self.position()
321
+ return pos if self.active() and pos.startswith("outside") else None
322
+
323
+ def fit(self, fonts: Any, w: float, h: float) -> None:
324
+ """Inside axes ``w`` × ``h``: the legend measured to wrap within them (into
325
+ columns, or rows when horizontal); one still larger than them moves outside right
326
+ (``moved``), with a warning when its place was given."""
327
+ c = self.content
328
+ px, py = c.legend.pads()
329
+ room = max(w - 2 * px, 1.0) if self.row_major else max(h - 2 * py, 1.0)
330
+ lw, lh = c.measure(fonts, room)
331
+ if lw <= w - 2 * px + 1e-6 and lh <= h - 2 * py + 1e-6:
332
+ return
333
+ self.moved = True
334
+ if self.setting() not in CHOSEN:
335
+ from .spec import warn
336
+ warn(f"the legend ({len(c.entries)} entries) is larger than its axes inside; "
337
+ f"drawn outside on the right")
338
+
339
+ def choose(self, ctx: Any, outside: bool = True, fonts: Any = None) -> str:
340
+ """A legend's place for ``"auto"`` and ``"best"``, from the occupancy grid
341
+ (:func:`marked`: every series of the plot and its twins marks what it draws:
342
+ points, lines, bars, areas, …; with ``fonts``, the decorations and the colorbars
343
+ inside the axes too):
344
+
345
+ 1. the first candidate (``CANDIDATES``; ``"best"``: ``BEST_CANDIDATES``) whose
346
+ box covers no drawn cell and meets no marker disc, text or inside guide;
347
+ 2. else the free box (one grid cell apart, anywhere inside) nearest to a corner of
348
+ the axes and meeting none of them (``"inside"``, its place kept as fractions of
349
+ the free room);
350
+ 3. else the candidate meeting no marker, text or guide and covering the fewest
351
+ cells, at most ``OUTSIDE_THRESHOLD`` of them (``"best"``: any number of them);
352
+ 4. else ``"outside_right"`` (when ``outside``);
353
+ 5. else the candidate covering the fewest marker cells, then the fewest cells."""
354
+ c = self.content
355
+ occ = marked(c.ps, ctx, fonts)
356
+ w, h = c.size
357
+ px, py = c.legend.pads()
358
+ candidates = BEST_CANDIDATES if self.setting() == "best" else CANDIDATES
359
+ best, best_f = best_anchor(occ, ctx.rect, w, h, px, py, candidates)
360
+ if best_f == 0.0:
361
+ return best
362
+ x0, y0, x1, y1 = ctx.rect
363
+ def corner(p: tuple[float, float]) -> float:
364
+ x, y = p
365
+ return min(x - x0, x1 - x - w) + min(y - y0, y1 - y - h)
366
+ free = next((p for p in sorted(occ.free_boxes(w, h, px), key=corner)
367
+ if not occ.solid_in(*p, w, h)), None)
368
+ if free is not None:
369
+ x, y = free
370
+ room_x, room_y = x1 - x0 - 2 * px - w, y1 - y0 - 2 * py - h
371
+ self.free = ((x - x0 - px) / room_x if room_x > 0 else 0.0,
372
+ (y - y0 - py) / room_y if room_y > 0 else 0.0)
373
+ return "inside"
374
+ cover = [] # (marker cells, cells, order, place)
375
+ for i, pos in enumerate(candidates):
376
+ box = (*anchor_box(ctx.rect, pos, w, h, px, py), w, h)
377
+ cover.append((max(occ.covered(*box, markers=True), float(occ.solid_in(*box))),
378
+ occ.covered(*box), i, pos))
379
+ clean = [(f, i, pos) for m, f, i, pos in cover if m == 0.0 and f <= OUTSIDE_THRESHOLD]
380
+ if clean:
381
+ return min(clean)[2]
382
+ if self.setting() == "best": # any amount of lines and areas, no marker
383
+ unmarked = [(f, i, pos) for m, f, i, pos in cover if m == 0.0]
384
+ if unmarked:
385
+ return min(unmarked)[2]
386
+ if outside:
387
+ return "outside_right"
388
+ return min(cover)[3]
389
+
390
+ def _legend_at(self, ctx: Any, used: dict[str, float] | None = None,
391
+ fonts: Any = None) -> tuple[float, float]:
392
+ """Top-left corner of a plot legend's box: outside, past what the axes and twins
393
+ take on its side (``used``, px); inside, at its position in the axes."""
394
+ c = self.content
395
+ x0, y0, x1, y1 = ctx.rect
396
+ w, h = c.size
397
+ px, py = c.legend.pads()
398
+ pos = self.position()
399
+ used = used or {}
400
+ if pos == "outside_right":
401
+ return x1 + used.get("right", 0.0) + px, along_at(y0, y1, h, self.align())
402
+ if pos == "outside_left":
403
+ return x0 - used.get("left", 0.0) - px - w, along_at(y0, y1, h, self.align())
404
+ if pos == "outside_top":
405
+ return along_at(x0, x1, w, self.align()), y0 - used.get("top", 0.0) - py - h
406
+ if pos == "outside_bottom":
407
+ return along_at(x0, x1, w, self.align()), y1 + used.get("bottom", 0.0) + py
408
+ if pos in CHOSEN:
409
+ pos = self.choose(ctx, outside=False, fonts=fonts)
410
+ if pos == "inside":
411
+ fx, fy = self.free
412
+ return (x0 + px + fx * max(0.0, x1 - x0 - 2 * px - w),
413
+ y0 + py + fy * max(0.0, y1 - y0 - 2 * py - h))
414
+ return anchor_box(ctx.rect, pos, w, h, px, py)
415
+
416
+ def _room(self, pos: str, w: float, h: float) -> float | None:
417
+ """The length an outside plot legend wraps within along its side: the axes height
418
+ on the left and right (entries in columns), the axes width on top and bottom
419
+ (entries in rows), or in a figure of given size the plot's box along that side
420
+ when longer (the legend passes the axes' ends into the box's margins rather than
421
+ widening and taking the axes' room); ``None`` elsewhere."""
422
+ bw, bh = self.content.ps.box_room
423
+ if pos in ("outside_right", "outside_left") and not self.row_major:
424
+ return h if bh is None else max(h, bh)
425
+ if pos in ("outside_top", "outside_bottom") and self.row_major:
426
+ return w if bw is None else max(w, bw)
427
+ return None
428
+
429
+ # -- measure and place, by spot -------------------------------------------------------
430
+
431
+ def measure(self, fonts: Any, w: float, h: float, placed: bool = True) -> Any:
432
+ """The content laid out for its place, ``w`` × ``h`` the room: the axes (inside,
433
+ side), its area, or in the lanes the length of its span along them (``placed``:
434
+ once the areas are placed; before, a legend there does not wrap). Returns:
435
+
436
+ * a legend: its size (inside its axes: wrapped within them, or moved: :meth:`fit`);
437
+ * a colorbar: inside, its box's size; on a side, its band (pad included); in the
438
+ lanes, its lane's thickness (pad included); in its area, its margins;
439
+ * a size legend: its margins."""
440
+ c = self.content
441
+ kind = self.spot[0]
442
+ if c.form == "entries":
443
+ if kind == "lanes":
444
+ return c.measure(fonts, w if placed and self.row_major else None)
445
+ if self.active() and not self.position().startswith("outside"):
446
+ from .spec import set_boxes
447
+ set_boxes(c.ps.plot, axes_width=w, axes_height=h)
448
+ self.fit(fonts, w, h)
449
+ pos = self.position()
450
+ if not self.active() or pos.startswith("outside"):
451
+ c.measure(fonts, self._room(pos, w, h))
452
+ return c.size
453
+ if kind == "area":
454
+ return c.measure_area(fonts, w, h)
455
+ if kind == "side":
456
+ return c.measure_attached(fonts, w, h)
457
+ if kind == "inside":
458
+ return c.inside_size(fonts, w, h)[:2]
459
+ return c.lane_pad() + c.lane_extent(fonts, w)
460
+
461
+ def place(self, fonts: Any, rect: tuple[float, float, float, float], *, ctx: Any = None,
462
+ offsets: dict[str, float] | None = None, occ: Any = None,
463
+ span: tuple[float, float] | None = None, line: float | None = None) -> Any:
464
+ """The content's geometry at its place; ``rect``:
465
+
466
+ * inside and on a side: its plot's axes; a legend chooses ``"auto"`` in ``ctx``
467
+ (their drawing context), a colorbar on the occupancy grid ``occ``; ``offsets``:
468
+ by side, where its band starts past the axes; keys sharing a line: ``span``,
469
+ a key's place along the side, and ``line``, how far the line's middle line is
470
+ from where it starts;
471
+ * in the lanes: a legend's box; a colorbar's near edge across its side, its span
472
+ along it (``(near, s0, near, s1)`` on the left and right, ``(s0, near, s1,
473
+ near)`` on top and bottom);
474
+ * in its area: the area's axes.
475
+
476
+ A legend showing nothing: ``None``."""
477
+ c = self.content
478
+ kind, where = self.spot
479
+ if c.form == "entries":
480
+ if kind == "lanes":
481
+ return c.place_at(fonts, rect[0], rect[1])
482
+ if not self.active() or not c.size[0]:
483
+ return None
484
+ return c.place_at(fonts, *self._legend_at(ctx, offsets, fonts))
485
+ if kind == "area":
486
+ return c.place_area(fonts, rect)
487
+ if kind == "lanes":
488
+ if c.side in ("left", "right"):
489
+ return c.place_lane(fonts, rect[0], rect[1], rect[3])
490
+ return c.place_lane(fonts, rect[1], rect[0], rect[2])
491
+ if kind == "side":
492
+ assert offsets is not None
493
+ return c.place_attached(fonts, rect, offsets[c.side], span, line)
494
+
495
+ def at(w: float, h: float, pad: float) -> tuple[float, float]:
496
+ anchor = where
497
+ if anchor in CHOSEN:
498
+ anchor = best_anchor(occ, rect, w, h, pad, pad, BEST_CANDIDATES
499
+ if anchor == "best" else CANDIDATES)[0]
500
+ x, y = anchor_box(rect, anchor, w, h, pad, pad)
501
+ self.inside_box = (x, y, x + w, y + h)
502
+ return x, y
503
+ return c.place_inside(fonts, rect, at)
@@ -0,0 +1,17 @@
1
+ """Plot types and figures built on :mod:`plot_io.ext`: clustering trees and dendrograms
2
+ (:mod:`.tree`, :mod:`.dendrogram`), QQ plots (:mod:`.qq`), rainclouds (:mod:`.raincloud`),
3
+ volcano, MA and Manhattan plots (:mod:`.volcano`), joint plots and pair grids
4
+ (:mod:`.grids`), clustered heatmaps (:mod:`.clustermap`). Each module imports only
5
+ ``plot_io.ext``, ``plot_io.kernels`` and public names."""
6
+
7
+ from .dendrogram import Dendrogram
8
+ from .qq import QQ
9
+ from .raincloud import Raincloud
10
+ from .tree import Tree, linkage
11
+ from .volcano import Ma, Manhattan, Volcano
12
+ # the figure helpers after every type (pair cells take each type's method)
13
+ from .grids import Joint, Pairs, joint, pairs # noqa: E402
14
+ from .clustermap import ClusterMap, clustermap # noqa: E402
15
+
16
+ __all__ = ["Tree", "linkage", "Dendrogram", "QQ", "Raincloud", "Volcano", "Ma", "Manhattan",
17
+ "joint", "pairs", "Joint", "Pairs", "clustermap", "ClusterMap"]