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.
- plot_io/__init__.py +73 -0
- plot_io/_methods.py +1691 -0
- plot_io/_native/__init__.py +199 -0
- plot_io/_native/__main__.py +197 -0
- plot_io/_native/_api.py +457 -0
- plot_io/_native/bin/windows-x86_64.dll +0 -0
- plot_io/_native/kernels.py +335 -0
- plot_io/_native/manifest.json +49 -0
- plot_io/axis.py +1347 -0
- plot_io/boxes.py +1156 -0
- plot_io/canvas.py +389 -0
- plot_io/color/__init__.py +3 -0
- plot_io/color/data.py +1646 -0
- plot_io/color/maps.py +922 -0
- plot_io/color/values.py +295 -0
- plot_io/color/variants.py +1246 -0
- plot_io/colorbar.py +1426 -0
- plot_io/columns.py +614 -0
- plot_io/decorations.py +920 -0
- plot_io/defaults.py +809 -0
- plot_io/density.py +113 -0
- plot_io/ext.py +68 -0
- plot_io/facets.py +240 -0
- plot_io/figure.py +788 -0
- plot_io/formatters.py +97 -0
- plot_io/genome/__init__.py +17 -0
- plot_io/genome/adapters.py +356 -0
- plot_io/genome/combine.py +123 -0
- plot_io/genome/contacts.py +508 -0
- plot_io/genome/intervals.py +750 -0
- plot_io/genome/lanes.py +191 -0
- plot_io/genome/layout.py +184 -0
- plot_io/genome/links.py +364 -0
- plot_io/genome/pack.py +27 -0
- plot_io/genome/readers.py +145 -0
- plot_io/genome/regions.py +228 -0
- plot_io/genome/scale.py +349 -0
- plot_io/genome/series.py +344 -0
- plot_io/genome/signal.py +621 -0
- plot_io/genome/sites.py +782 -0
- plot_io/genome/sources.py +459 -0
- plot_io/genome/testing.py +161 -0
- plot_io/geometry.py +289 -0
- plot_io/groups.py +308 -0
- plot_io/guides.py +503 -0
- plot_io/helpers/__init__.py +17 -0
- plot_io/helpers/clustermap.py +202 -0
- plot_io/helpers/dendrogram.py +172 -0
- plot_io/helpers/grids.py +304 -0
- plot_io/helpers/qq.py +334 -0
- plot_io/helpers/raincloud.py +249 -0
- plot_io/helpers/tree.py +214 -0
- plot_io/helpers/volcano.py +505 -0
- plot_io/hints.py +125 -0
- plot_io/kernels.py +740 -0
- plot_io/labels.py +566 -0
- plot_io/layers/__init__.py +100 -0
- plot_io/layers/base.py +630 -0
- plot_io/layers/context.py +87 -0
- plot_io/layers/core.py +446 -0
- plot_io/layers/geoms.py +1362 -0
- plot_io/layers/grammar.py +586 -0
- plot_io/layers/paint.py +614 -0
- plot_io/layers/pipeline.py +170 -0
- plot_io/layers/positions.py +431 -0
- plot_io/layers/stats.py +1088 -0
- plot_io/layout.py +1381 -0
- plot_io/legend.py +605 -0
- plot_io/levels.py +509 -0
- plot_io/limits.py +124 -0
- plot_io/lines.py +42 -0
- plot_io/mapped.py +522 -0
- plot_io/markers.py +110 -0
- plot_io/markup.py +128 -0
- plot_io/numbers.py +354 -0
- plot_io/occupancy.py +295 -0
- plot_io/plot.py +597 -0
- plot_io/presets/__init__.py +51 -0
- plot_io/presets/_util.py +153 -0
- plot_io/presets/area.py +145 -0
- plot_io/presets/bar.py +261 -0
- plot_io/presets/box.py +400 -0
- plot_io/presets/catpoints.py +177 -0
- plot_io/presets/contour.py +353 -0
- plot_io/presets/curves.py +30 -0
- plot_io/presets/dots.py +355 -0
- plot_io/presets/ecdf.py +97 -0
- plot_io/presets/errorbar.py +106 -0
- plot_io/presets/errors.py +296 -0
- plot_io/presets/fill.py +74 -0
- plot_io/presets/heatmap.py +322 -0
- plot_io/presets/hexbin.py +198 -0
- plot_io/presets/hist.py +391 -0
- plot_io/presets/hist2d.py +87 -0
- plot_io/presets/kde.py +162 -0
- plot_io/presets/kde2d.py +147 -0
- plot_io/presets/line.py +143 -0
- plot_io/presets/pie.py +502 -0
- plot_io/presets/regression.py +115 -0
- plot_io/presets/scatter.py +194 -0
- plot_io/presets/stem.py +100 -0
- plot_io/presets/violin.py +323 -0
- plot_io/py.typed +0 -0
- plot_io/registry.py +82 -0
- plot_io/render.py +480 -0
- plot_io/rows.py +491 -0
- plot_io/scales.py +103 -0
- plot_io/seams.py +138 -0
- plot_io/settings.py +807 -0
- plot_io/shapes.py +66 -0
- plot_io/sharing.py +232 -0
- plot_io/sizemaps.py +97 -0
- plot_io/spec.py +240 -0
- plot_io/states.py +216 -0
- plot_io/styles/__init__.py +307 -0
- plot_io/styles/library.py +397 -0
- plot_io/tables.py +138 -0
- plot_io/text.py +412 -0
- plot_io/ticks.py +400 -0
- plot_io/times.py +287 -0
- plot_io/titles.py +225 -0
- plot_io/transform.py +114 -0
- plot_io/types.py +13604 -0
- plot_io/units.py +768 -0
- plot_io/validators.py +1029 -0
- plot_io/values.py +190 -0
- plot_io-0.12.0.dist-info/METADATA +215 -0
- plot_io-0.12.0.dist-info/RECORD +130 -0
- plot_io-0.12.0.dist-info/WHEEL +4 -0
- 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)
|