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/presets/box.py ADDED
@@ -0,0 +1,400 @@
1
+ """Box plots (stat ``box``): bodies (``Rects``), whiskers, caps and medians
2
+ (``Segments``), outliers and means (``Points``), dodged per category."""
3
+
4
+ import array
5
+ from typing import Any
6
+
7
+ from .._methods import BoxMixin
8
+ from .. import columns, groups
9
+ from ..color.values import color
10
+ from ..layers import stats
11
+ from ..layers.base import Series, common
12
+ from ..layers.core import Table
13
+ from ..layers.geoms import Marks
14
+ from ..layers.grammar import Lay, Spec
15
+ from ..layers.paint import MarkerStyle, base_colors, role
16
+ from ..layers.positions import Dodge, placed
17
+ from ..markers import check_marker, resolve
18
+ from ..settings import Setting, boolean, either, literal, number, one_of, pair, table
19
+ from ..transform import settings_both as coord_settings
20
+ from ..validators import length, typed
21
+ from ..values import Values, category_paint
22
+ from ._util import fill_colors, memo
23
+
24
+ __all__ = ["Box"]
25
+
26
+
27
+ def _outlier_paint(name: str) -> Any:
28
+ """``marker_color``/``marker_edge_color``: colors only (outliers are computed, so there
29
+ are no values per point to map)."""
30
+ def check(v: Any) -> Any:
31
+ from ..values import is_computed, paint
32
+ if name == "marker_edge_color" and v == "auto":
33
+ return v
34
+ out = paint(v, name)
35
+ if isinstance(out, Values) or is_computed(out):
36
+ raise ValueError("a color, or colors per outlier: outliers are computed, so "
37
+ "numbers or a density are not mapped here")
38
+ return out
39
+ return check
40
+
41
+
42
+ def _whiskers(v: Any) -> Any:
43
+ if v == "range":
44
+ return "range"
45
+ if isinstance(v, (tuple, list)):
46
+ lo, hi = pair(number(0, 100))(v)
47
+ if not lo < hi:
48
+ raise ValueError("whisker percentiles must increase")
49
+ return (lo, hi)
50
+ return number(lo=0)(v)
51
+
52
+
53
+ @typed("ColorLike")
54
+ def _line_color(v: Any) -> Any:
55
+ if v in ("series", "auto"):
56
+ return v
57
+ return color(v)
58
+
59
+
60
+ class Box(BoxMixin, Series):
61
+ """Box plots of ``values``: a dict ``{category: values}``, a list of arrays
62
+ (categories ``0, 1, …`` or ``categories=``), or one array.
63
+
64
+ ``values`` may also be a data frame (categories by column) or a 2-D buffer;
65
+ ``positions`` places the boxes on a numeric axis instead. Quartiles interpolate
66
+ linearly (numpy's default; with ``weights``, Hmisc's ``wtd.quantile`` on the cumulative
67
+ weights); whiskers reach the furthest data within ``whiskers`` × IQR, or the range, or
68
+ percentiles; points beyond are outliers. Box series of a plot share each category's
69
+ slot side by side, in call order, among those having that category. ``offset`` moves
70
+ the boxes within their slot (``"-20%"``: a fifth of the slot toward lower positions), as
71
+ on strip and swarm series. ``box.stats``: ``{category: BoxStats(count, mean, q1,
72
+ median, q3, low, high, outliers)}``.
73
+
74
+ Example::
75
+
76
+ rng = random.Random(1)
77
+ p = plt.Plot(width=240, height=160)
78
+ p.box({k: [rng.gauss(m, 1) for _ in range(50)] for k, m in (("A", 0), ("B", 1), ("C", 0.5))})
79
+ """
80
+
81
+ SETTINGS = table(
82
+ **coord_settings(),
83
+ color=Setting(category_paint,
84
+ doc="Box fill: a color, one per box (list or dict by category), or "
85
+ "numbers per box (list or dict) mapped through `color_map`; "
86
+ "default: the plot's next color."),
87
+ **common("color_map"),
88
+ **groups.Stroke.embed("edge_",
89
+ only=("color", "line_width"),
90
+ checks={"color": _line_color},
91
+ docs={"color": "Box outline, whiskers, caps and median: a "
92
+ "color, `\"series\"` (each box's own color) or "
93
+ "`\"auto\"` (a grey 0.6 × as light as the "
94
+ "series' lightest color, as seaborn).",
95
+ "line_width": "Outline width."}),
96
+ **groups.Stroke.embed("whisker_",
97
+ only=("color", "line_width", "line_style"),
98
+ checks={"color": _line_color},
99
+ docs={"color": "Whisker and cap color; default `edge_color`.",
100
+ "line_width": "Whisker and cap width; default "
101
+ "`edge_width`.",
102
+ "line_style": "Whisker line style: `solid`, `dashed`, "
103
+ "`dotted`, `dashdot` or a dash list (caps "
104
+ "stay solid)."}),
105
+ **groups.Stroke.embed("median_",
106
+ only=("color", "line_width"),
107
+ checks={"color": _line_color},
108
+ docs={"color": "Median line color; default `edge_color`.",
109
+ "line_width": "Median line width."}),
110
+ max_width=Setting(length(positive=True, data=True, percent="slot"),
111
+ doc="Largest width of the series' part of a slot (as `width`)."),
112
+ width=Setting(length(positive=True, data=True, percent="slot"),
113
+ doc="Box width: `%` of the slot, a length (bare numbers: px), `d` "
114
+ "data units, or a duration on time axes."),
115
+ offset=Setting(length(data=True, percent="slot"),
116
+ doc="Shift within the slot, toward higher positions: `%` of the slot, "
117
+ "a length (bare numbers: px) or `d` data units."),
118
+ cap_length=Setting(length(lo=0, percent="box_width"),
119
+ doc="Whisker cap width (`%` of the box width)."),
120
+ whiskers=Setting(_whiskers,
121
+ doc="Whisker reach: a multiple of the IQR, `\"range\"`, or "
122
+ "percentiles `(lo, hi)`."),
123
+ weights=Setting(columns.weights,
124
+ doc="A weight per value, counted as repeated values (a dict or a list "
125
+ "per category, as the values; long data: a weight per row)."),
126
+ outliers=Setting(boolean, doc="Draw points beyond the whiskers."),
127
+ saturation=Setting(number(0, 1),
128
+ doc="Saturation of the box fills (1: the colors as given; seaborn "
129
+ "uses 0.75)."),
130
+ **groups.FillStyle.embed(docs={"fill": "Fill: `True` (the box's color), a color, or "
131
+ "`False`.",
132
+ "fill_opacity": "Opacity of the fill, times `opacity` "
133
+ "(lines keep `opacity`)."}),
134
+ mean=Setting(boolean, doc="Draw the mean as a marker."),
135
+ mean_marker=Setting(check_marker, doc="Marker of the mean."),
136
+ mean_color=Setting(color, doc="Fill of the mean marker (outline: "
137
+ "`edge_color`)."),
138
+ orientation=Setting(one_of("vertical", "horizontal"),
139
+ doc="`vertical` (categories on x) or `horizontal`."),
140
+ dodge=Setting(boolean, doc="Box and violin series sharing a category share its "
141
+ "slot side by side; `False` overlays them."),
142
+ gap=Setting(length(lo=0, percent="slot_part"), doc="Fraction of each box's part of a shared slot left "
143
+ "empty."),
144
+ categories=Setting(either(literal(None), lambda v: list(v)),
145
+ doc="Category names of a list of arrays."),
146
+ **groups.Marker.embed("marker_",
147
+ checks={"color": _outlier_paint("marker_color"),
148
+ "edge_color": _outlier_paint("marker_edge_color")},
149
+ docs={"color": "Outlier fill: a color, or colors per outlier.",
150
+ "edge_color": "Outlier outline: as the fill, or `\"auto\"` "
151
+ "(white around filled markers; hollow ones "
152
+ "outlined in their color)."}),
153
+ **common("name", "opacity", "z", "rasterize", "simplify", "font_size", "font_family",
154
+ "text_color"),
155
+ )
156
+
157
+ DATA = {"values": "categories", "positions": "slots"}
158
+ ITEMS = "boxes"
159
+
160
+ def __init__(self, values: Any, positions: Any = None, plot: Any = None,
161
+ **settings: Any) -> None:
162
+ super().__init__(plot=plot, **settings)
163
+
164
+ @property
165
+ def cats(self) -> list[Any]:
166
+ """The category of each box."""
167
+ return self.cols.categories
168
+
169
+ @property
170
+ def stats(self) -> dict[Any, Any]:
171
+ """``{category: BoxStats}``: count, mean, q1, median, q3, low, high, outliers."""
172
+ return stats.run(self, "box")
173
+
174
+ # -- paint -----------------------------------------------------------------------
175
+
176
+ def _base_colors(self, ctx: Any) -> list[Any]:
177
+ return base_colors(self, ctx, self.cols.categories, "boxes", items=True,
178
+ saturation=False)
179
+
180
+ def _fills(self, ctx: Any) -> list[Any]:
181
+ return fill_colors(self, ctx, self._base_colors(ctx))
182
+
183
+ def _lines(self, ctx: Any, setting: str, fallback: list[Any] | None = None) -> list[Any]:
184
+ """Per-box colors of a line setting (`edge_color`, `whisker_color`, `median_color`)."""
185
+ v = self.resolve(setting)
186
+ n = len(self.cols.categories)
187
+ if v is None:
188
+ return list(fallback) if fallback is not None else [None] * n
189
+ if v == "series":
190
+ return self._base_colors(ctx)
191
+ if v == "auto":
192
+ import colorsys
193
+ from ..color.values import rgba_of
194
+ rgbas = [rgba_of(c) for c in self._base_colors(ctx) if c is not None]
195
+ light = [colorsys.rgb_to_hls(r[0], r[1], r[2])[1] for r in rgbas if r is not None]
196
+ g = 0.6 * min(light) if light else 0.25
197
+ return [(g, g, g)] * n
198
+ return [ctx.color(v)] * n
199
+
200
+ SPEC = Spec(
201
+ [Lay("rects", ("box.bodies", {"table": "#body"}), table="body", name="bodies",
202
+ stat="box", position="dodge", occupy_aes={"y0": "low", "y1": "high"},
203
+ each=True),
204
+ Lay("segments", ("box.lines", {"table": "#whisk"}), table="whisk", stat="box",
205
+ key=False, occupy=False),
206
+ Lay("points", ("box.outliers", {"table": "#outs"}), table="outs", stat="box",
207
+ key=False, when="outliers"),
208
+ Lay("points", ("box.mean", {"table": "#means"}), table="means", stat="box",
209
+ key=False, occupy=False, when="mean")],
210
+ tables={"body": lambda s, b: _body(s, b), "whisk": lambda s, b: _whisker_table(s, b),
211
+ "outs": lambda s, b: _outliers(s, b), "means": lambda s, b: _means(s, b)},
212
+ key={"fill": "symbol", "stroke": "symbol", "marker": "@bodies"},
213
+ zero=("y",), titles={"values": "y", "categories": "x"}, slots="positions",
214
+ color_sources="items", finish=lambda s, b: _finish(s, b))
215
+
216
+
217
+ def _colors(s: Box, ctx: Any) -> tuple[list[Any], list[Any], list[Any], list[Any]]:
218
+ """Fills, outline, whisker and median colors of each box."""
219
+ def get() -> Any:
220
+ lcs = s._lines(ctx, "edge_color")
221
+ wcs = s._lines(ctx, "whisker_color", lcs)
222
+ mcs = [m if m is not None else lc
223
+ for m, lc in zip(s._lines(ctx, "median_color", lcs), lcs)]
224
+ return s._fills(ctx), lcs, wcs, mcs
225
+ return memo(ctx, (id(s), "colors"), get)
226
+
227
+
228
+ def _geo(ctx: Any, body: Any) -> list[tuple[float, float]]:
229
+ """``(centre, width)`` of each drawn box, in slot space."""
230
+ sl = placed(body.dodge, ctx).geo
231
+ return [sl[i] for i in body.drawn]
232
+
233
+
234
+ def _cap_half(s: Box, ctx: Any, body: Any, w: float) -> float:
235
+ """Half a cap in slot units (``cap_length`` is px + `%` of the box width)."""
236
+ c0, c1 = memo(ctx, (id(s), "cap"), lambda: s.resolve_linear("cap_length"))
237
+ pl = placed(body.dodge, ctx)
238
+ px = abs(pl.px(w) - pl.px(0.0))
239
+ return (c0 + px * c1) / 2 / (abs(pl.map.a) or 1.0)
240
+
241
+
242
+ def _body(s: Box, b: Any) -> Table:
243
+ """The boxes (quartiles, whisker ends) in slots shared with the plot's dodged boxes
244
+ and violins; ``drawn``: the categories with values, ``sts`` their stats."""
245
+ bs = s.stats
246
+ cats = s.cols.categories
247
+ drawn = [i for i, k in enumerate(cats) if bs[k].count]
248
+ d = Dodge(s, "box", b.flip, list(cats), s.cols["positions"], None)
249
+ sts = [bs[cats[i]] for i in drawn]
250
+ t: Any = Table(y0=[st.q1 for st in sts], y1=[st.q3 for st in sts],
251
+ low=[st.low for st in sts], high=[st.high for st in sts])
252
+ t.dodge, t.drawn, t.sts = d, drawn, sts
253
+ t.lazy["x0"] = lambda ctx: placed(d, ctx).back([c - w / 2 for c, w in _geo(ctx, t)])
254
+ t.lazy["x1"] = lambda ctx: placed(d, ctx).back([c + w / 2 for c, w in _geo(ctx, t)])
255
+ return t
256
+
257
+
258
+ def _whisker_table(s: Box, b: Any) -> Table:
259
+ """Whiskers (with caps) and medians, per box in drawing order (``order``: ``(box,
260
+ kind)``)."""
261
+ body = b.tables["body"]
262
+ t: Any = Table()
263
+ order = []
264
+ b0, b1 = [], []
265
+ for j, st in enumerate(body.sts):
266
+ for q, end in ((st.q1, st.low), (st.q3, st.high)):
267
+ order.append((j, "whisker"))
268
+ b0.append(q)
269
+ b1.append(end)
270
+ order.append((j, "cap"))
271
+ b0.append(end)
272
+ b1.append(end)
273
+ order.append((j, "median"))
274
+ b0.append(st.median)
275
+ b1.append(st.median)
276
+ t["y0"], t["y1"] = b0, b1
277
+ t.order, t.body = order, body
278
+
279
+ def ends(ctx: Any, side: int) -> list[float]:
280
+ geo = _geo(ctx, body)
281
+ out = []
282
+ for j, kind in order:
283
+ c, w = geo[j]
284
+ if kind == "whisker":
285
+ out.append(c)
286
+ elif kind == "cap":
287
+ out.append(c + side * _cap_half(s, ctx, body, w))
288
+ else:
289
+ out.append(c + side * w / 2)
290
+ return placed(body.dodge, ctx).back(out)
291
+
292
+ t.lazy["x0"] = lambda ctx: ends(ctx, -1)
293
+ t.lazy["x1"] = lambda ctx: ends(ctx, 1)
294
+ return t
295
+
296
+
297
+ def _outliers(s: Box, b: Any) -> Table | None:
298
+ body = b.tables["body"]
299
+ if not sum(len(st.outliers) for st in body.sts):
300
+ return None
301
+ values = array.array("d")
302
+ for st in body.sts:
303
+ values.extend(st.outliers)
304
+ t: Any = Table(y=values)
305
+ t.body = body
306
+
307
+ def centres(ctx: Any) -> Any:
308
+ out = array.array("d")
309
+ for (c, _), st in zip(_geo(ctx, body), body.sts):
310
+ out.extend([c] * len(st.outliers))
311
+ return placed(body.dodge, ctx).back(out)
312
+ t.lazy["x"] = centres
313
+ return t
314
+
315
+
316
+ def _means(s: Box, b: Any) -> Table | None:
317
+ body = b.tables["body"]
318
+ if not body.sts:
319
+ return None
320
+ t: Any = Table(y=[st.mean for st in body.sts])
321
+ t.body = body
322
+ t.lazy["x"] = lambda ctx: placed(body.dodge, ctx).back([c for c, _ in _geo(ctx, body)])
323
+ return t
324
+
325
+
326
+ @role("box.bodies")
327
+ def _bodies_paint(s: Box, ctx: Any, glyph: bool, table: Any = None) -> dict[str, Any]:
328
+ fills, lcs, _, _ = _colors(s, ctx)
329
+ lw = s.resolve("edge_width")
330
+ if glyph:
331
+ return {"fill": fills[0] if fills else None,
332
+ "stroke": lcs[0] if lcs else None, "line_width": lw}
333
+ return {"fill": [fills[i] for i in table.drawn],
334
+ "stroke": [lcs[i] for i in table.drawn] if lw else None,
335
+ "line_width": lw if lw else None}
336
+
337
+
338
+ @role("box.lines")
339
+ def _lines_paint(s: Box, ctx: Any, glyph: bool, table: Any = None) -> dict[str, Any]:
340
+ _, _, wcs, mcs = _colors(s, ctx)
341
+ body = table.body
342
+ ww, mw = s.resolve("whisker_width"), s.resolve("median_width")
343
+ wstyle = s.resolve("whisker_style")
344
+ geo = _geo(ctx, body)
345
+ cols: list[Any] = []
346
+ widths: list[Any] = []
347
+ dashes: list[Any] = []
348
+ for j, kind in table.order:
349
+ i = body.drawn[j]
350
+ if kind == "median":
351
+ cols.append(mcs[i])
352
+ widths.append(mw)
353
+ dashes.append(None)
354
+ else:
355
+ # a box without caps (none drawn when they have no length)
356
+ no_cap = kind == "cap" and not _cap_half(s, ctx, body, geo[j][1])
357
+ cols.append(None if no_cap else wcs[i])
358
+ widths.append(ww)
359
+ dashes.append(wstyle if kind == "whisker" and wstyle != "solid" else None)
360
+ return {"color": cols, "width": widths, "dash": dashes, "cap": "butt"}
361
+
362
+
363
+ @role("box.outliers")
364
+ def _outliers_paint(s: Box, ctx: Any, glyph: bool, table: Any = None) -> MarkerStyle:
365
+ _, lcs, _, _ = _colors(s, ctx)
366
+ body = table.body
367
+ edges = [] # the edge color of its box
368
+ for j, st in enumerate(body.sts):
369
+ edges.extend([lcs[body.drawn[j]]] * len(st.outliers))
370
+ style = MarkerStyle(s, ctx, "marker_", None, len(table["y"]), glyph)
371
+ if style.fill is None and style.stroke is None:
372
+ if edges and len(set(edges)) > 1 and None not in edges:
373
+ style.stroke = edges
374
+ else:
375
+ style.stroke = lcs[0] if s.cols.categories else None
376
+ style.line_width = s.resolve("marker_edge_width")
377
+ style.simplify = s.resolve("simplify")
378
+ return style
379
+
380
+
381
+ @role("box.mean")
382
+ def _mean_paint(s: Box, ctx: Any, glyph: bool, table: Any = None) -> Marks:
383
+ _, lcs, _, _ = _colors(s, ctx)
384
+ body = table.body
385
+ edges = [lcs[i] or "black" for i in body.drawn]
386
+ n_out = sum(len(st.outliers) for st in body.sts) if s.resolve("outliers") else 0
387
+ style = MarkerStyle(s, ctx, "marker_", None, n_out, False)
388
+ return Marks(resolve(s.resolve("mean_marker"))[0],
389
+ style.size if isinstance(style.size, float) else 4.0,
390
+ ctx.color(s.resolve("mean_color")),
391
+ edges[0] if len(set(edges)) == 1 else edges,
392
+ s.resolve("marker_edge_width"))
393
+
394
+
395
+ def _finish(s: Box, b: Any) -> None:
396
+ """Dates on the category axis; exact limits at numeric positions."""
397
+ cat = b.role("x")
398
+ b.tables["body"].dodge.time = cat in b.call.times
399
+ if s.cols["positions"] is not None:
400
+ b.call.exact = {cat}
@@ -0,0 +1,177 @@
1
+ """Points per category (``Points``): jittered (position ``jitter``: strip) or side by side
2
+ without overlaps (position ``swarm``), dodged per category."""
3
+
4
+ import array
5
+ from typing import Any
6
+
7
+ from .._methods import StripMixin, SwarmMixin
8
+ from .. import groups
9
+ from ..layers.base import Series, common
10
+ from ..layers.core import Table
11
+ from ..layers.grammar import Lay, Spec, flipped
12
+ from ..layers.paint import (MarkerStyle, base_color, density_points, density_stats,
13
+ order_settings)
14
+ from ..layers.positions import Dodge, jitter, placed, swarm_offsets
15
+ from ..settings import Setting, boolean, either, literal, number, one_of, table
16
+ from ..transform import settings_both as coord_settings
17
+ from ..validators import length
18
+ from .scatter import _radius
19
+
20
+ __all__ = ["Strip", "Swarm"]
21
+
22
+
23
+ class _CatPoints(Series):
24
+ SETTINGS: dict[str, Setting] = {}
25
+ DATA = {"values": "categories", "positions": "slots"}
26
+ POSITION = "jitter"
27
+
28
+ def __init__(self, values: Any, positions: Any = None, plot: Any = None,
29
+ **settings: Any) -> None:
30
+ super().__init__(plot=plot, **settings)
31
+
32
+ def _flat(self) -> tuple[list[int], array.array]:
33
+ cat_index, vals = [], array.array("d")
34
+ for k, d in enumerate(self.cols.values):
35
+ cat_index.extend([k] * len(d))
36
+ vals.extend(d)
37
+ return cat_index, vals
38
+
39
+ def offsets(self, ctx: Any, d: Dodge, widths: list[float], t: Table) -> array.array:
40
+ raise NotImplementedError
41
+
42
+ @property
43
+ def stats(self) -> dict[str, Any]:
44
+ """Derived data: ``{"density": values}`` when the series maps a density."""
45
+ return density_stats(self)
46
+
47
+ def density_points(self) -> tuple[Any, Any]:
48
+ idx, vals = self._flat()
49
+ pos = self.cols["positions"] or [float(i) for i in range(len(self.cols.categories))]
50
+ cat = array.array("d", (pos[k] for k in idx))
51
+ if flipped(self):
52
+ return density_points(self, vals, cat)
53
+ return density_points(self, cat, vals)
54
+
55
+ SPEC = Spec(
56
+ [Lay("points", ("marker", {"order": True,
57
+ "n": lambda s: sum(map(len, s.cols.values))}),
58
+ table="points", name="points", position=lambda s: s.POSITION,
59
+ occupy_aes={"x": "x_center"}, radius=lambda s: _radius(s.resolve("size")))],
60
+ tables={"points": lambda s, b: _points(s, b)},
61
+ key={"fill": "marker", "stroke": "marker", "marker": "@points"},
62
+ titles={"values": "y", "categories": "x"}, slots="positions", color_sources=True,
63
+ size_sources=True, finish=lambda s, b: _finish(s, b))
64
+
65
+ def _prepare(self, t: Table, n: int) -> None:
66
+ """Position columns drawn before the axes are placed."""
67
+
68
+
69
+ def _points(s: _CatPoints, b: Any) -> Table:
70
+ """The points by category, placed in slots shared with the plot's dodged points."""
71
+ d = Dodge(s, "points", b.flip, list(s.cols.categories), s.cols["positions"], None)
72
+ idx, vals = s._flat()
73
+ t = Table(y=vals)
74
+ t.dodge = d
75
+ s._prepare(t, len(vals))
76
+
77
+ def x(ctx: Any) -> Any:
78
+ pl = placed(d, ctx)
79
+ sl = pl.geo
80
+ offs = s.offsets(ctx, d, [w for _, w in sl], t)
81
+ return pl.back(array.array("d", (sl[k][0] + o for k, o in zip(idx, offs))))
82
+
83
+ def centres(ctx: Any) -> Any:
84
+ pl = placed(d, ctx)
85
+ return pl.back([pl.geo[k][0] for k in idx])
86
+
87
+ t.lazy["x"] = x
88
+ t.lazy["x_center"] = centres
89
+ return t
90
+
91
+
92
+ def _finish(s: _CatPoints, b: Any) -> None:
93
+ b.tables["points"].dodge.time = b.role("x") in b.call.times
94
+
95
+
96
+ def _settings(extra: dict[str, Setting]) -> dict[str, Setting]:
97
+ return table(
98
+ **coord_settings(),
99
+ **groups.Marker.embed(),
100
+ **common("color_map", "size_map", "size_key"),
101
+ orientation=Setting(one_of("vertical", "horizontal"),
102
+ doc="`vertical` (categories on x) or `horizontal`."),
103
+ max_width=Setting(length(positive=True, data=True, percent="slot"),
104
+ doc="Largest width of the series' part of a slot (as `width`); "
105
+ "default none."),
106
+ width=Setting(length(positive=True, data=True, percent="slot"),
107
+ doc="Width the points spread over: `%` of the slot, a length (bare "
108
+ "numbers: px), `d` data units, or a duration on time axes."),
109
+ offset=Setting(length(data=True, percent="slot"),
110
+ doc="Shift within the slot, toward higher positions: `%` of the slot, "
111
+ "a length (bare numbers: px) or `d` data units."),
112
+ dodge=Setting(boolean, doc="Point series sharing a category share its slot "
113
+ "side by side; `False` overlays them."),
114
+ gap=Setting(length(lo=0, percent="slot_part"), doc="Fraction of each part of a shared slot left "
115
+ "empty."),
116
+ categories=Setting(either(literal(None), lambda v: list(v)),
117
+ doc="Category names of a list of arrays."),
118
+ **extra,
119
+ **order_settings(),
120
+ **common("name", "z", "rasterize", "simplify", "font_size", "font_family",
121
+ "text_color"),
122
+ )
123
+
124
+
125
+ class Strip(StripMixin, _CatPoints):
126
+ """Points of each category's values, spread across the slot by random jitter (from a
127
+ fixed seed, so output is reproducible).
128
+
129
+ ``values`` as :class:`Box` takes them. Jitter is uniform, ± ``jitter`` (``%`` of the
130
+ slot width), from ``seed``. Marker settings as :class:`Scatter`'s. ``offset`` moves the
131
+ points within their slot (``%`` of the slot, toward higher positions).
132
+
133
+ Example::
134
+
135
+ rng = random.Random(1)
136
+ p = plt.Plot(width=240, height=160)
137
+ p.strip({k: [rng.gauss(m, 1) for _ in range(30)] for k, m in (("A", 0), ("B", 1), ("C", 0.5))})
138
+ """
139
+
140
+ SETTINGS = _settings(dict(
141
+ jitter=Setting(length(lo=0, percent="slot"),
142
+ doc="Half-width of the jitter (`%` of the slot width)."),
143
+ seed=Setting(number(integer=True), doc="Seed of the jitter."),
144
+ ))
145
+
146
+ def _prepare(self, t: Table, n: int) -> None:
147
+ t["jitter_u"] = jitter(n, self.resolve("seed"))
148
+
149
+ def offsets(self, ctx: Any, d: Dodge, widths: list[float], t: Table) -> array.array:
150
+ scale = abs((ctx.ymap if d.flip else ctx.xmap).a) or None
151
+ ja, jb = self.resolve_linear("jitter", scale=scale, unit="data")
152
+ idx, _ = self._flat()
153
+ return array.array("d", (r * (ja + jb * widths[k]) for k, r in zip(idx, t["jitter_u"])))
154
+
155
+
156
+ class Swarm(SwarmMixin, _CatPoints):
157
+ """Points of each category's values, side by side so that they do not overlap
158
+ (points that do not fit go to the slot's edge, with a warning; above 100,000 points,
159
+ jittered across the width as a strip, with a warning).
160
+
161
+ ``values`` as :class:`Box` takes them. The settings are :class:`Strip`'s but ``jitter``
162
+ and ``seed``.
163
+
164
+ Example::
165
+
166
+ rng = random.Random(1)
167
+ p = plt.Plot(width=240, height=160)
168
+ p.swarm({k: [rng.gauss(m, 1) for _ in range(30)] for k, m in (("A", 0), ("B", 1), ("C", 0.5))})
169
+ """
170
+
171
+ SETTINGS = _settings({})
172
+ POSITION = "swarm"
173
+
174
+ def offsets(self, ctx: Any, d: Dodge, widths: list[float], t: Table) -> array.array:
175
+ style = MarkerStyle(self, ctx, "", base_color(self, ctx),
176
+ sum(len(v) for v in self.cols.values))
177
+ return swarm_offsets(ctx, d, self.cols.values, style.size, widths)