plot-io 0.12.0__py3-none-win_amd64.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-x86_64.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/presets/pie.py ADDED
@@ -0,0 +1,502 @@
1
+ """Pie and donut charts, on a plot without axes: wedges (``Polygons``) and labels
2
+ (``Text``)."""
3
+
4
+ import array
5
+ import math
6
+ import warnings
7
+ from typing import Any
8
+
9
+ import vec_io
10
+
11
+ from ..canvas import Canvas
12
+ from .._methods import PieMixin
13
+ from .. import groups
14
+ from ..color.values import ink_for
15
+ from ..numbers import NumberStyle, check_format, format_numbers
16
+ from ..color.values import color
17
+ from ..settings import Setting, boolean, either, literal, number, one_of, table
18
+ from ..units import resolve_raw
19
+ from ..validators import Custom, angle, length
20
+ from ..color.values import with_alpha
21
+ from ..text import Font, font_of, measure
22
+ from ..layers.base import Series, common
23
+ from ..layers.core import Call, Layer, Table
24
+ from ..layers.geoms import GEOMS
25
+ from ..layers.grammar import entry
26
+ from ..layers.paint import Paint, auto_colors, role
27
+ from ..labels import column
28
+ from ._util import given_colors, map_or_color, placed_text
29
+
30
+ __all__ = ["Pie"]
31
+
32
+
33
+ _OFFSET = length(lo=0, percent="radius")
34
+
35
+
36
+ def _explode(v: Any) -> Any:
37
+ if isinstance(v, (list, tuple)):
38
+ return [_OFFSET(x) for x in v]
39
+ return _OFFSET(v)
40
+
41
+
42
+ _EXPLODE = Custom(_explode, like=_OFFSET, inner=_OFFSET)
43
+
44
+
45
+ def _outline(w: dict[str, Any], cx: float, cy: float, r: float, hole: float, end_seam: bool,
46
+ start_seam: bool) -> Any:
47
+ """The outline of an unexploded wedge without its seamed radial edges: open paths."""
48
+ a0, a1 = math.radians(w["start"]), math.radians(w["end"])
49
+ point = (lambda a, rr: (cx + rr * math.cos(a), cy - rr * math.sin(a)))
50
+ inner = hole * r
51
+ # edges in order around the wedge: (start point, append to the path, removed)
52
+ edges: list[tuple[tuple[float, float], Any, bool]] = [
53
+ (point(a0, inner), lambda p: p.line_to(*point(a0, r)), start_seam),
54
+ (point(a0, r), lambda p: _arc(p, cx, cy, r, a0, a1), False),
55
+ (point(a1, r), lambda p: p.line_to(*point(a1, inner)), end_seam)]
56
+ if hole:
57
+ edges.append((point(a1, inner), lambda p: _arc(p, cx, cy, inner, a1, a0), False))
58
+ first = next(i for i, e in enumerate(edges) if e[2])
59
+ edges = edges[first + 1:] + edges[:first + 1] # starts after a removed edge
60
+ p = vec_io.Path()
61
+ open_ = False
62
+ for start, add, removed in edges:
63
+ if removed:
64
+ open_ = False
65
+ continue
66
+ if not open_:
67
+ p.move_to(*start)
68
+ open_ = True
69
+ add(p)
70
+ return p
71
+
72
+
73
+ def _arc(path: Any, cx: float, cy: float, r: float, a0: float, a1: float) -> None:
74
+ """Append an arc of radius ``r`` from angle ``a0`` to ``a1`` (radians, counterclockwise
75
+ on the page: y up) as cubic Béziers of at most 90°."""
76
+ n = max(1, math.ceil(abs(a1 - a0) / (math.pi / 2) - 1e-9))
77
+ step = (a1 - a0) / n
78
+ k = 4.0 / 3.0 * math.tan(step / 4.0)
79
+ for i in range(n):
80
+ t0, t1 = a0 + i * step, a0 + (i + 1) * step
81
+ x0, y0 = cx + r * math.cos(t0), cy - r * math.sin(t0)
82
+ x3, y3 = cx + r * math.cos(t1), cy - r * math.sin(t1)
83
+ x1, y1 = x0 - k * r * math.sin(t0), y0 - k * r * math.cos(t0)
84
+ x2, y2 = x3 + k * r * math.sin(t1), y3 + k * r * math.cos(t1)
85
+ path.curve_to(x1, y1, x2, y2, x3, y3)
86
+
87
+
88
+ class Pie(PieMixin, Series):
89
+ """A pie (or donut, with ``hole``) of ``values``: a sequence, or a dict
90
+ ``{label: value}``; the plot is drawn without axes.
91
+
92
+ Wedges start at ``start``; the plot is drawn without axes (``Plot.axes=False``, aspect
93
+ 1). ``hole`` makes a donut (``"40%"`` of the radius), ``explode`` moves wedges out
94
+ (``%`` of the radius, one value or one per wedge). Names go outside (beside their
95
+ wedges; a crowded side's in a column beside the pie with elbow leaders, the smallest
96
+ wedges' left out with a warning when the column is taller than the plot), inside, or
97
+ into the legend (``name_position="none"``, the legend then outside right); ``color_map``
98
+ colors them (a map's colors for the wedges); ``value_labels`` writes percents or values inside, or outside with
99
+ a leader line when a wedge is too thin.
100
+
101
+ Example::
102
+
103
+ p = plt.Plot(width=240, height=160)
104
+ p.pie({"apples": 5, "pears": 3, "plums": 2, "figs": 1}, value_labels="percent")
105
+ """
106
+
107
+ SETTINGS = table(
108
+ names=Setting(lambda v: [str(x) for x in v],
109
+ doc="Wedge names (default: the dict's keys)."),
110
+ color_map=Setting(map_or_color,
111
+ doc="Wedge colors: a map (a list of colors or a qualitative map: "
112
+ "taken in turn; another kind: evenly spaced), one color, a "
113
+ "kind (that kind's map) or `\"auto\"` (the plot's next "
114
+ "colors)."),
115
+ start=Setting(angle(), doc="Angle where the first wedge starts (degrees, 0: "
116
+ "3 o'clock, 90: 12 o'clock)."),
117
+ counterclockwise=Setting(boolean, doc="Direction of the wedges."),
118
+ hole=Setting(length(lo=0, percent="radius"), doc="Radius of the hole (`%` of the "
119
+ "radius): a donut."),
120
+ explode=Setting(_EXPLODE, doc="Offset of the wedges from the centre (`%` of the "
121
+ "radius): one value or one per wedge."),
122
+ sort=Setting(boolean, doc="Sort the wedges by decreasing value."),
123
+ name_position=Setting(one_of("outside", "inside", "none"),
124
+ doc="Where the names go: `outside`, `inside` or `none` (in "
125
+ "the legend instead)."),
126
+ value_labels=Setting(either(literal(False), one_of("percent", "value")),
127
+ doc="Write each wedge's `percent` or `value` inside it (outside, "
128
+ "with a leader line, when it does not fit)."),
129
+ label_spacing=Setting(length(positive=True, percent="em"),
130
+ doc="Smallest distance between outside labels on a side."),
131
+ value_label_format=Setting(check_format,
132
+ doc="Number format of the value labels (percentages: of "
133
+ "100)."),
134
+ **groups.Stroke.embed("edge_",
135
+ only=("color", "line_width"),
136
+ checks={"color": groups.series_color},
137
+ docs={"color": "Wedge outline color: a color, `\"series\"` "
138
+ "(each wedge's own color) or `none`.",
139
+ "line_width": "Wedge outline width."}),
140
+ opacity=Setting(number(0, 1), doc="Opacity."),
141
+ **groups.FillStyle.embed(docs={"fill": "Wedge fill: `True` (each wedge's color), a "
142
+ "color, or `False`.",
143
+ "fill_opacity": "Opacity of the fills, times "
144
+ "`opacity` (outlines keep `opacity`)."}),
145
+ seam_color=Setting(color,
146
+ doc="The edges neighbouring wedges share, in this color; `none`: "
147
+ "both outlines as drawn."),
148
+ **common("name", "z", "rasterize", "simplify", "font_size", "font_family", "text_color"),
149
+ **groups.NumberFormat.embed(),
150
+ )
151
+ AXES = False
152
+
153
+ DATA = {"values": "wedges"}
154
+
155
+ def __init__(self, values: Any, plot: Any = None, **settings: Any) -> None:
156
+ if any(not (v >= 0) for v in self.vals if math.isfinite(v)):
157
+ raise ValueError("pie values must be non-negative")
158
+ super().__init__(plot=plot, **settings)
159
+ if not self.plot.is_set("axes"):
160
+ self.plot.set(axes=False)
161
+ if not self.plot.legend.is_placed():
162
+ self.plot.legend.set(side="right")
163
+
164
+ @property
165
+ def vals(self) -> Any:
166
+ """The value of each wedge."""
167
+ return self.cols["values"]
168
+
169
+ # -- geometry ---------------------------------------------------------------------
170
+
171
+ def wedge_names(self) -> list[Any]:
172
+ given = self.resolve("names")
173
+ if given is not None:
174
+ if len(given) != len(self.vals):
175
+ raise ValueError(f"{len(given)} names for {len(self.vals)} wedges")
176
+ return given
177
+ return list(self.cols.categories)
178
+
179
+ def order(self) -> list[int]:
180
+ idx = [i for i in range(len(self.vals)) if math.isfinite(self.vals[i]) and self.vals[i] > 0]
181
+ if self.resolve("sort"):
182
+ idx.sort(key=lambda i: -self.vals[i])
183
+ return idx
184
+
185
+ @property
186
+ def stats(self) -> list[dict[str, Any]]:
187
+ """Per wedge (in drawing order): ``index``, ``value``, ``fraction``, ``start`` and
188
+ ``end`` angles (degrees, counterclockwise from 3 o'clock)."""
189
+ order = self.order()
190
+ total = math.fsum(self.vals[i] for i in order)
191
+ sign = 1.0 if self.resolve("counterclockwise") else -1.0
192
+ a = self.resolve("start")
193
+ if not isinstance(a, (int, float)):
194
+ raise ValueError(f"Pie: start is not an angle: {self.get('start')!r}")
195
+ out = []
196
+ for i in order:
197
+ frac = self.vals[i] / total if total else 0.0
198
+ b = a + sign * 360.0 * frac
199
+ out.append({"index": i, "value": self.vals[i], "fraction": frac, "start": a, "end": b})
200
+ a = b
201
+ return out
202
+
203
+ def _fraction(self, name: str, raw: Any, r: float | None) -> float:
204
+ """A length of setting ``name`` as a fraction of the radius ``r`` (px; ``None``:
205
+ unknown, lengths count as 0)."""
206
+ a = resolve_raw(self, name, _OFFSET, raw, of=0.0)
207
+ b = resolve_raw(self, name, _OFFSET, raw, of=1.0)
208
+ if a is None or b is None:
209
+ raise ValueError(f"Pie setting {name!r}: {raw!r} gives no length")
210
+ return b - a + (a / r if r else 0.0)
211
+
212
+ def _explode_of(self, i: int, r: float | None = None) -> float:
213
+ e = self._raw("explode")
214
+ if e is None:
215
+ return 0.0
216
+ if isinstance(e, list):
217
+ if len(e) != len(self.vals):
218
+ raise ValueError(f"{len(e)} explode values for {len(self.vals)} wedges")
219
+ e = e[i]
220
+ return self._fraction("explode", e, r)
221
+
222
+ def _hole(self, r: float) -> float:
223
+ """``hole`` as a fraction of the radius ``r``."""
224
+ return self._fraction("hole", self._raw("hole"), r)
225
+
226
+ def _reach(self) -> float:
227
+ n = len(self.vals) if self._raw("explode") is not None else 0
228
+ ex = max((self._explode_of(i) for i in range(n)), default=0.0)
229
+ out = self.resolve("name_position") == "outside" or self.resolve("value_labels")
230
+ return 1.0 + ex + (0.45 if out else 0.05)
231
+
232
+ def wedge_colors(self, ctx: Any) -> list[Any]:
233
+ """Colors in value order (one per value)."""
234
+ n = len(self.vals)
235
+ given = given_colors(self, n)
236
+ if given is not None:
237
+ given = given if isinstance(given, list) else [given]
238
+ cols = [ctx.color(given[i % len(given)]) for i in range(n)]
239
+ else:
240
+ cols = auto_colors(self, ctx)
241
+ cols = (cols * (n // max(len(cols), 1) + 1))[:n]
242
+ a = self._opacity()
243
+ return [with_alpha(c, a) for c in cols]
244
+
245
+ def _opacity(self) -> float:
246
+ a = self.resolve("opacity")
247
+ if not isinstance(a, (int, float)):
248
+ raise ValueError(f"Pie: opacity is not a number: {self.get('opacity')!r}")
249
+ return a
250
+
251
+ def wedge_fills(self, ctx: Any) -> list[Any]:
252
+ """Fills in value order: the colors at ``fill_opacity``."""
253
+ fc, f = groups.fill_parts(self)
254
+ cols = self.wedge_colors(ctx)
255
+ if fc is not None:
256
+ cols = [with_alpha(ctx.color(fc), self._opacity())] * len(cols)
257
+ return [with_alpha(c, (c[3] if isinstance(c, tuple) and len(c) > 3 else 1.0) * f)
258
+ for c in cols]
259
+
260
+ def wedge_edges(self, ctx: Any) -> list[Any]:
261
+ """Outline colors in value order (``None``: no outline)."""
262
+ v = self.resolve("edge_color")
263
+ if v == "series":
264
+ return self.wedge_colors(ctx)
265
+ return [ctx.color(v)] * len(self.vals)
266
+
267
+ # -- the preset --------------------------------------------------------------------
268
+
269
+ def _center(self, ctx: Any) -> tuple[float, float, float]:
270
+ cx, cy = ctx.xmap(0.0), ctx.ymap(0.0)
271
+ r = abs(ctx.xmap(1.0) - cx)
272
+ return cx, cy, r
273
+
274
+ def _wedges(self, ctx: Any) -> dict[str, Any]:
275
+ """The wedges in page units: paths, fills, outline colors, and with seams the
276
+ outlines (without the seamed radial edges) and the seams."""
277
+ cx, cy, r = self._center(ctx)
278
+ hole = self._hole(r)
279
+ cols, edges = self.wedge_fills(ctx), self.wedge_edges(ctx)
280
+ paths, fills, strokes = [], [], []
281
+ st = self.stats
282
+ for w in st:
283
+ i = w["index"]
284
+ a0, a1 = math.radians(w["start"]), math.radians(w["end"])
285
+ mid = (a0 + a1) / 2
286
+ ex = self._explode_of(i, r) * r
287
+ ox, oy = cx + ex * math.cos(mid), cy - ex * math.sin(mid)
288
+ p = vec_io.Path()
289
+ if w["fraction"] >= 1.0 - 1e-12:
290
+ p.move_to(ox + r, oy)
291
+ _arc(p, ox, oy, r, 0.0, 2 * math.pi)
292
+ p.close()
293
+ if hole:
294
+ p.move_to(ox + hole * r, oy)
295
+ _arc(p, ox, oy, hole * r, 2 * math.pi, 0.0)
296
+ p.close()
297
+ else:
298
+ p.move_to(ox + r * math.cos(a0), oy - r * math.sin(a0))
299
+ _arc(p, ox, oy, r, a0, a1)
300
+ if hole:
301
+ p.line_to(ox + hole * r * math.cos(a1), oy - hole * r * math.sin(a1))
302
+ _arc(p, ox, oy, hole * r, a1, a0)
303
+ else:
304
+ p.line_to(ox, oy)
305
+ p.close()
306
+ paths.append(p)
307
+ fills.append(cols[i])
308
+ strokes.append(edges[i])
309
+ lw = self.resolve("edge_width")
310
+ seam = ctx.color(self.resolve("seam_color"))
311
+ seamed = set() # wedges k whose end touches wedge k + 1
312
+ if seam is not None and lw and len(st) > 1:
313
+ for k, w in enumerate(st):
314
+ nxt = st[(k + 1) % len(st)]
315
+ if not (self._explode_of(w["index"], r) or self._explode_of(nxt["index"], r)):
316
+ seamed.add(k) # exploded wedges do not touch
317
+ out: dict[str, Any] = {"paths": paths, "fill": fills, "stroke": strokes,
318
+ "line_width": lw, "simplify": self.resolve("simplify")}
319
+ if not seamed:
320
+ return out
321
+ n = len(st)
322
+ outlines = []
323
+ for k, w in enumerate(st):
324
+ end, start = k in seamed, (k - 1) % n in seamed
325
+ whole = not (end or start) or w["fraction"] >= 1.0 - 1e-12
326
+ outlines.append(paths[k] if whole else _outline(w, cx, cy, r, hole, end, start))
327
+ segs: list[tuple[float, ...]] = []
328
+ for k in sorted(seamed):
329
+ a = math.radians(st[k]["end"])
330
+ c, s_ = math.cos(a), -math.sin(a)
331
+ segs.append((cx + hole * r * c, cy + hole * r * s_, cx + r * c, cy + r * s_, lw))
332
+ out.update(outlines=outlines, seams=segs, seam=seam)
333
+ return out
334
+
335
+ def layers(self) -> Call:
336
+ call = Call(self)
337
+ key = id(self)
338
+
339
+ t = Table()
340
+ t.lazy["page_paths"] = lambda ctx: _wedges(self, ctx)["paths"]
341
+
342
+ call.layers.append(Layer(GEOMS["polygons"], t, paint=Paint(self, "pie.wedges"),
343
+ stat="identity", key=False))
344
+
345
+ def font(ctx: Any) -> Any:
346
+ return font_of(self, self.resolve("font_size"),
347
+ color=ctx.color(self.resolve("text_color")))
348
+
349
+ def labels(ctx: Any) -> list[Any]:
350
+ cx, cy, r = self._center(ctx)
351
+ f = font(ctx)
352
+ inside, placed = self._labels(ctx.canvas, cx, cy, r, f, self.wedge_fills(ctx))
353
+ return inside + self._outside_labels(placed, f, cx, r, ctx.rect[1], ctx.rect[3],
354
+ True)
355
+
356
+ call.layers.append(placed_text(self, key, labels, font))
357
+ if self.resolve("name_position") == "none":
358
+ names = self.wedge_names()
359
+ if names:
360
+ for k, w in enumerate(self.stats):
361
+ key_layer = Layer(GEOMS["rects"], Table(),
362
+ paint=Paint(self, "pie.key", index=k))
363
+ call.entries.append(entry(self, names[w["index"]], [key_layer],
364
+ fill="symbol", stroke="symbol",
365
+ marker=key_layer.paint))
366
+ call.axes = False
367
+ call.color_count = 0 if given_colors(self, len(self.vals)) is not None \
368
+ else len(self.vals)
369
+ r = self._reach()
370
+ call.extra = {"x": array.array("d", [-r, r]), "y": array.array("d", [-r, r])}
371
+ call.exact = {"x", "y"}
372
+ call.overhang = self.overhang
373
+ return call
374
+
375
+ def overhang(self, fonts: Any, w: float, h: float) -> tuple[float, float]:
376
+ """How far (px) the outside labels reach past the left and right of axes ``w`` ×
377
+ ``h``."""
378
+ r = min(w, h) / (2 * self._reach())
379
+ font = font_of(self, self.resolve("font_size"))
380
+ left = right = 0.0
381
+ placed = self._labels(fonts, w / 2, h / 2, r, font, None)[1]
382
+ for text, tx, _, _, ha, *_ in self._outside_labels(placed, font, w / 2, r, 0.0, h):
383
+ tw = measure(fonts, text, font)[0]
384
+ if ha == "right":
385
+ left = max(left, tw - tx)
386
+ else:
387
+ right = max(right, tx + tw - w)
388
+ return left, right
389
+
390
+ def _labels(self, canvas: Canvas, cx: float, cy: float, r: float, font: Font,
391
+ cols: list[Any] | None) -> tuple[list[Any], list[list[Any]]]:
392
+ """The labels inside the wedges (``cols``: the wedge fills; ``None``: measure only)
393
+ as ``(text, x, y, color, ha, va)``, and the outside ones, for
394
+ :meth:`_outside_labels`."""
395
+ hole = self._hole(r)
396
+ names = self.wedge_names()
397
+ pos = self.resolve("name_position")
398
+ vl = self.resolve("value_labels")
399
+ stats = self.stats
400
+ texts: list[Any] = [None] * len(stats)
401
+ placed: list[list[Any]] = []
402
+ inside: list[Any] = []
403
+ if vl:
404
+ vals = [100.0 * w["fraction"] if vl == "percent" else w["value"] for w in stats]
405
+ style = NumberStyle.of(self)
406
+ fmt = self.resolve("value_label_format")
407
+ if vl == "percent" and fmt == "auto":
408
+ style.suffix = style.suffix or "%"
409
+ if style.precision is None:
410
+ style.precision = 2
411
+ texts = format_numbers(vals, fmt, style)
412
+ for k, w in enumerate(stats):
413
+ i = w["index"]
414
+ a0, a1 = math.radians(w["start"]), math.radians(w["end"])
415
+ mid = (a0 + a1) / 2
416
+ ex = self._explode_of(i, r) * r
417
+ ox, oy = cx + ex * math.cos(mid), cy - ex * math.sin(mid)
418
+ ux, uy = math.cos(mid), -math.sin(mid)
419
+ side = "left" if ux >= 0 else "right"
420
+ outside_texts = []
421
+ if names and pos == "outside":
422
+ outside_texts.append(names[i])
423
+ if names and pos == "inside" and cols is not None:
424
+ inside.append((names[i], ox + ux * r * (0.5 + hole / 2),
425
+ oy + uy * r * (0.5 + hole / 2), ink_for(cols[i]), "center",
426
+ "bottom" if vl else "center"))
427
+ if texts[k] is not None:
428
+ rin = r * (0.5 + hole / 2) if hole else r * 0.62
429
+ tw, ta, td = measure(canvas, texts[k], font)
430
+ arc_len = abs(a1 - a0) * rin
431
+ fits = arc_len >= 1.2 * max(tw, ta + td) and (1 - hole) * r >= 1.1 * (ta + td)
432
+ if fits:
433
+ if cols is not None:
434
+ inside.append((texts[k], ox + ux * rin, oy + uy * rin,
435
+ ink_for(cols[i]), "center",
436
+ "top" if names and pos == "inside" else "center"))
437
+ else:
438
+ outside_texts.append(texts[k])
439
+ if outside_texts:
440
+ text = " ".join(str(t) for t in outside_texts) if all(
441
+ isinstance(t, str) for t in outside_texts) else outside_texts[0]
442
+ lead = not (names and pos == "outside") or len(outside_texts) > 1
443
+ placed.append([oy + uy * r * 1.12, ox + ux * r * 1.12, side, text, lead,
444
+ (ox + ux * r, oy + uy * r), w["value"]])
445
+ return inside, placed
446
+
447
+ def _outside_labels(self, placed: list[list[Any]], font: Font, cx: float, r: float,
448
+ lo: float, hi: float, warn: bool = False) -> list[Any]:
449
+ """The outside labels of each side: beside their wedges when they keep
450
+ ``label_spacing`` apart within ``lo``–``hi`` (page y), else in a column beside the
451
+ pie with elbow leaders (:func:`~plot_io.labels.column`), the smallest wedges'
452
+ left out (with a warning: ``warn``) when the column is taller than ``lo``–``hi``."""
453
+ gap = self.resolve("label_spacing")
454
+ n = len(self.vals) if self._raw("explode") is not None else 0
455
+ ex = max((self._explode_of(i, r) for i in range(n)), default=0.0)
456
+ out: list[Any] = []
457
+ dropped = 0
458
+ for side in ("left", "right"):
459
+ items = sorted((p for p in placed if p[2] == side), key=lambda p: p[0])
460
+ ys = [p[0] for p in items]
461
+ if all(b - a >= gap - 1e-9 for a, b in zip(ys, ys[1:])) and all(
462
+ lo + gap / 2 <= y <= hi - gap / 2 for y in ys):
463
+ for ty, tx, _, text, lead, (ax, ay), _ in items:
464
+ leader = (ax, ay, tx - (3 if side == "left" else -3), ty) if lead else None
465
+ out.append((text, tx, ty, font.color, side, "center", 0.0, None, leader))
466
+ continue
467
+ x = cx + (1 if side == "left" else -1) * r * (1.12 + ex)
468
+ got = column([p[5] for p in items], ys, [gap] * len(items), x,
469
+ max(0.06 * r, 4.0), lo, hi, [p[6] for p in items])
470
+ for p, g in zip(items, got):
471
+ if g is None:
472
+ dropped += 1
473
+ continue
474
+ out.append((p[3], x, g[0], font.color, side, "center", 0.0, None, g[1]))
475
+ if dropped and warn:
476
+ warnings.warn(f"pie: {dropped} outside labels do not fit beside the pie; the "
477
+ "smallest wedges' are left out", UserWarning, stacklevel=2)
478
+ return out
479
+
480
+
481
+ def _wedges(s: Pie, ctx: Any) -> dict[str, Any]:
482
+ from ._util import memo
483
+ return memo(ctx, (id(s), "wedges"), lambda: s._wedges(ctx))
484
+
485
+
486
+ @role("pie.wedges")
487
+ def _wedges_paint(s: Pie, ctx: Any, glyph: bool) -> dict[str, Any]:
488
+ p = dict(_wedges(s, ctx))
489
+ p.pop("paths")
490
+ return p
491
+
492
+
493
+ @role("pie.key")
494
+ def _key_paint(s: Pie, ctx: Any, glyph: bool, index: int = 0) -> dict[str, Any]:
495
+ """The legend rectangle of wedge ``index``."""
496
+ st = s.stats
497
+ if index >= len(st):
498
+ return {"fill": None, "stroke": None, "line_width": None}
499
+ k = st[index]["index"]
500
+ edge = s.wedge_edges(ctx)[k] if s.resolve("edge_width") else None
501
+ return {"fill": s.wedge_fills(ctx)[k], "stroke": edge,
502
+ "line_width": s.resolve("edge_width") if edge is not None else None}
@@ -0,0 +1,115 @@
1
+ """Regression (stat ``regression``): a fitted curve (``Path``, polynomial or LOWESS) with a
2
+ confidence band (``Band``) and the points (``Points``)."""
3
+
4
+ from typing import Any
5
+
6
+ from .._methods import RegressionMixin
7
+ from .. import columns, groups
8
+ from ..color.values import color
9
+ from ..layers import stats
10
+ from ..layers.base import Series, column, common
11
+ from ..layers.core import Table
12
+ from ..layers.grammar import Lay, Spec
13
+ from ..layers.paint import xy_density_points
14
+ from ..settings import Setting, boolean, either, literal, number, one_of, pair, table
15
+ from ..transform import settings_both as coord_settings
16
+
17
+ __all__ = ["Regression", "t_quantile"]
18
+
19
+ t_quantile = stats.t_quantile
20
+
21
+
22
+ class Regression(RegressionMixin, Series):
23
+ """A regression of ``y`` on ``x``: a polynomial of ``order`` (least squares) with a
24
+ confidence band of the mean, or a LOWESS curve; ``scatter`` also draws the points
25
+ (numbers per point in ``marker_color`` map through the series' color axis).
26
+
27
+ The band is Student's t. ``lowess=True``: no band; above 10,000 points it fits at
28
+ points 1 % of the x range apart and interpolates between them (as statsmodels'
29
+ ``delta``). ``logx`` fits against log x; ``range`` sets the x range of the polynomial
30
+ curve (default the data's), drawn in ``points`` points. With fewer distinct x values
31
+ than ``order`` + 1 the order is lowered (one distinct x: no fit), with a warning. The
32
+ points take marker settings with the ``marker_`` prefix. With ``weights``, weighted
33
+ least squares, the band with the weight total minus the coefficients as degrees of
34
+ freedom (as the repeated rows); ``lowess=True`` takes no weights.
35
+ ``regression.stats``: ``coefficients`` (lowest power first), ``r2``, ``se``, and the
36
+ curve.
37
+
38
+ Example::
39
+
40
+ rng = random.Random(1)
41
+ x = [rng.uniform(0, 10) for _ in range(60)]
42
+ p = plt.Plot(width=240, height=160)
43
+ p.regression(x, [0.5 * v + rng.gauss(0, 1) for v in x])
44
+ """
45
+
46
+ SETTINGS = table(
47
+ **coord_settings(),
48
+ order=Setting(number(lo=1, hi=5, integer=True), doc="Polynomial order (1–5)."),
49
+ lowess=Setting(boolean, doc="Fit a LOWESS curve instead (no band)."),
50
+ frac=Setting(number(0, 1), doc="Fraction of the points in each LOWESS fit."),
51
+ weights=Setting(lambda v: columns.numbers(v, "weights"),
52
+ doc="A weight per point, counted as repeated points (weighted least "
53
+ "squares; not with `lowess`)."),
54
+ ci=Setting(either(literal(None), number(0, 100)),
55
+ doc="Confidence level of the band (%); `None`: no band."),
56
+ logx=Setting(boolean, doc="Fit y against log(x) (x > 0)."),
57
+ scatter=Setting(boolean, doc="Draw the points too."),
58
+ range=Setting(either(literal(None), pair(number())),
59
+ doc="x range of the fitted polynomial (and band); default the "
60
+ "data range."),
61
+ points=Setting(number(lo=2, hi=65536, integer=True), doc="Points of the fitted curve."),
62
+ color=Setting(color, doc="Line, band and point color; default the plot's next color."),
63
+ **groups.Stroke.embed(only=("line_width", "line_style"),
64
+ inherited=("line_width",),
65
+ docs={"line_width": "Line width; default 1.4 × the plot's.",
66
+ "line_style": "Line style or dash list."}),
67
+ band_opacity=Setting(number(0, 1), doc="Opacity of the band."),
68
+ **groups.Marker.embed("marker_"),
69
+ **common("color_map"),
70
+ orientation=Setting(one_of("vertical", "horizontal"),
71
+ doc="`vertical` (y on x) or `horizontal` (the fit drawn with x "
72
+ "along the y axis)."),
73
+ **common("name", "opacity", "z", "rasterize", "simplify", "font_size", "font_family",
74
+ "text_color"),
75
+ )
76
+
77
+ DATA = {"x": "numbers", "y": "numbers"}
78
+ x = column("x", "x of each point (categories at their index among their own).")
79
+ y = column("y", "y of each point (as x).")
80
+
81
+ def __init__(self, x: Any, y: Any, plot: Any = None, **settings: Any) -> None:
82
+ super().__init__(plot=plot, **settings)
83
+
84
+ @property
85
+ def stats(self) -> dict[str, Any]:
86
+ """``coefficients`` (lowest power first, in x or log x), ``r2``, ``se`` (residual
87
+ standard error), and the curve: ``x``, ``y``, ``low``, ``high``."""
88
+ return stats.run(self, "regression")
89
+
90
+ def density_points(self) -> tuple[Any, Any]:
91
+ """The points' positions on the plot's axes (a density in ``marker_color``)."""
92
+ return xy_density_points(self, self.x, self.y)
93
+
94
+ SPEC = Spec(
95
+ [Lay("points", ("marker", {"prefix": "marker_", "color": "faded", "when": "scatter",
96
+ "n": lambda s: len(s.x)}),
97
+ key=False, occupy=lambda s: bool(s.resolve("scatter"))),
98
+ Lay("band", ("band", {"color": "faded", "opacity": "band_opacity"}), table="band",
99
+ stat="regression", glyph="band"),
100
+ # the curve stays within the data's and the band's extent (it does not widen the
101
+ # axes)
102
+ Lay("path", ("stroke", {"color": "faded", "dash": True}), table="curve",
103
+ aes={"x": "curve_x", "y": "curve_y"}, stat="regression", bounds=False)],
104
+ tables={"band": lambda s, b: _band(s.stats), "curve": lambda s, b: _curve(s.stats)},
105
+ key={"stroke": "faded"}, color_sources="scatter")
106
+
107
+
108
+ def _band(st: dict[str, Any]) -> Table | None:
109
+ if not len(st["x"]) or st["low"] is None:
110
+ return None
111
+ return Table(x=st["x"], y1=st["low"], y2=st["high"])
112
+
113
+
114
+ def _curve(st: dict[str, Any]) -> Table | None:
115
+ return Table(curve_x=st["x"], curve_y=st["y"]) if len(st["x"]) else None