fluxplot 0.1.0__py3-none-any.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.
- fluxplot/__init__.py +115 -0
- fluxplot/_fieldmap.py +97 -0
- fluxplot/_mesh_reduce.py +54 -0
- fluxplot/_scene3d_size.py +95 -0
- fluxplot/_viewer/THIRD-PARTY.txt +23 -0
- fluxplot/_viewer/flux-model3d-viewer.min.js +4221 -0
- fluxplot/_viewer/stamp.json +4 -0
- fluxplot/api.py +1196 -0
- fluxplot/autotag.py +164 -0
- fluxplot/base.mplstyle +0 -0
- fluxplot/brackets.py +242 -0
- fluxplot/canonical_json.py +23 -0
- fluxplot/capture.py +150 -0
- fluxplot/colorcheck.py +285 -0
- fluxplot/colors.py +727 -0
- fluxplot/colorscale.py +477 -0
- fluxplot/data.py +178 -0
- fluxplot/definitions/colormaps.json +1639 -0
- fluxplot/definitions/flexoki.tokens.json +2571 -0
- fluxplot/definitions/palettes.json +2547 -0
- fluxplot/descriptors.py +87 -0
- fluxplot/fields.py +611 -0
- fluxplot/fits.py +240 -0
- fluxplot/glb.py +84 -0
- fluxplot/ids.py +173 -0
- fluxplot/images.py +362 -0
- fluxplot/integrity.py +27 -0
- fluxplot/manifest.py +788 -0
- fluxplot/mesh3d.py +376 -0
- fluxplot/panels.py +284 -0
- fluxplot/postprocess.py +638 -0
- fluxplot/presets.py +66 -0
- fluxplot/provenance.py +177 -0
- fluxplot/raster.py +295 -0
- fluxplot/recipe.py +178 -0
- fluxplot/render.py +66 -0
- fluxplot/roles.py +147 -0
- fluxplot/scene3d.py +386 -0
- fluxplot/scene3d_manifest.py +112 -0
- fluxplot/scene3d_viewer.py +633 -0
- fluxplot/schemas/.gitkeep +0 -0
- fluxplot/schemas/manifest.schema.json +2479 -0
- fluxplot/schemas/recipe.schema.json +179 -0
- fluxplot/schemas/scene3d.schema.json +461 -0
- fluxplot/seaborn_adapters.py +323 -0
- fluxplot/signature_fluxplots/__init__.py +18 -0
- fluxplot/signature_fluxplots/_colour.py +412 -0
- fluxplot/signature_fluxplots/fluxbox.py +433 -0
- fluxplot/signature_fluxplots/glowbar.py +769 -0
- fluxplot/signature_fluxplots/hexmatrix.py +927 -0
- fluxplot/stats/__init__.py +63 -0
- fluxplot/stats/_common.py +196 -0
- fluxplot/stats/multi_group.py +443 -0
- fluxplot/stats/paired.py +209 -0
- fluxplot/stats/two_group.py +149 -0
- fluxplot/style.py +469 -0
- fluxplot/surface.py +487 -0
- fluxplot/surface3d.py +197 -0
- fluxplot/tagger.py +561 -0
- fluxplot/version.py +19 -0
- fluxplot-0.1.0.dist-info/METADATA +1199 -0
- fluxplot-0.1.0.dist-info/RECORD +65 -0
- fluxplot-0.1.0.dist-info/WHEEL +4 -0
- fluxplot-0.1.0.dist-info/licenses/LICENSE +21 -0
- fluxplot-0.1.0.dist-info/licenses/THIRD_PARTY_NOTICES.md +472 -0
|
@@ -0,0 +1,433 @@
|
|
|
1
|
+
"""``fp.fluxbox`` — the box plot, drawn the FluxPlot way.
|
|
2
|
+
|
|
3
|
+
A fluxbox is a glowbar whose summary mark is a box plot. Every observation is a dot and, beside each
|
|
4
|
+
group, sits a slim box; everything else — the fixed per-unit lanes and colours, the paired
|
|
5
|
+
connectors, the names — is the glowbar's own machinery, so a fluxbox and a glowbar of the same table
|
|
6
|
+
agree dot for dot and can be swapped for one another. Four statistics are read straight off the box:
|
|
7
|
+
|
|
8
|
+
* the **interquartile range** (Q1–Q3) — the box, a half-strength wash of the group colour;
|
|
9
|
+
* the **median** — a solid line across the box in the group's own hue, deepened (on a dark
|
|
10
|
+
background: lifted) just as far as it takes to stand clearly off the box, so it reads for any
|
|
11
|
+
palette and any theme. That colour is the box plot's one *ink*: the whiskers (and fliers) share it;
|
|
12
|
+
* the **mean** — a V-notch cut into both edges of the box (the glowbar's median notch). A mean that
|
|
13
|
+
falls outside the box (a strongly skewed group) keeps its mark: the same two V's, drawn solid in
|
|
14
|
+
the ink, pointing in at the whisker;
|
|
15
|
+
* the **whiskers** — plain capless lines in the median's colour, reaching the most extreme
|
|
16
|
+
observations within ``whis`` × IQR of the box (Tukey's rule — exactly ``plt.boxplot``'s
|
|
17
|
+
whiskers), the full range, or a pair of percentiles.
|
|
18
|
+
|
|
19
|
+
Observations beyond the whiskers are not drawn a second time as fliers — the individual points
|
|
20
|
+
already show them. With ``show_individual_points=False`` they are (``show_fliers="auto"``).
|
|
21
|
+
|
|
22
|
+
Everything drawn is a named part, so a fluxbox round-trips through Flux like any other FluxPlot:
|
|
23
|
+
|
|
24
|
+
======================= =============================================== ===========
|
|
25
|
+
part default id role
|
|
26
|
+
======================= =============================================== ===========
|
|
27
|
+
box (Q1–Q3) ``<category>.box`` ``box``
|
|
28
|
+
whiskers ``<category>.whiskers`` ``whisker``
|
|
29
|
+
whisker caps (opt-in) ``<category>.caps`` ``cap``
|
|
30
|
+
median line ``<category>.median`` ``median``
|
|
31
|
+
mean notch ``<category>.mean`` ``mean``
|
|
32
|
+
fliers (points hidden) ``<category>.fliers`` ``flier``
|
|
33
|
+
a unit's point(s) ``<unit>.points``, ``<unit>.point.<k>`` ``point``
|
|
34
|
+
a unit's connector ``<unit>.line`` ``line``
|
|
35
|
+
points (no ``units``) ``<category>.points``, ``<category>.point.<k>`` ``point``
|
|
36
|
+
======================= =============================================== ===========
|
|
37
|
+
|
|
38
|
+
Each category series carries a ``fluxbox`` payload in the manifest with the exact statistics drawn
|
|
39
|
+
(n, mean, median, sd, sem, q1, q3, iqr, the whisker rule and ends, the outliers, the group colour),
|
|
40
|
+
and each unit series carries its identity, exactly as for the glowbar.
|
|
41
|
+
|
|
42
|
+
Example
|
|
43
|
+
-------
|
|
44
|
+
>>> import fluxplot as fp, matplotlib.pyplot as plt
|
|
45
|
+
>>> fig, ax = plt.subplots(figsize=(1.6, 1.8))
|
|
46
|
+
>>> fb = fp.fluxbox(df, x="condition", y="APP/GAPDH", units="subject", ax=ax)
|
|
47
|
+
>>> fb.stats["SD"]["whiskerHigh"]
|
|
48
|
+
3.47
|
|
49
|
+
>>> fp.save(fig, "plots/app_gapdh.svg")
|
|
50
|
+
"""
|
|
51
|
+
from __future__ import annotations
|
|
52
|
+
|
|
53
|
+
from dataclasses import dataclass, field
|
|
54
|
+
from numbers import Real
|
|
55
|
+
from typing import Any, Optional, Sequence, Union
|
|
56
|
+
|
|
57
|
+
import numpy as np
|
|
58
|
+
|
|
59
|
+
from .. import tagger as _tagger
|
|
60
|
+
from ..descriptors import Mark
|
|
61
|
+
from ._colour import median_ink as _median_ink
|
|
62
|
+
from .glowbar import (
|
|
63
|
+
_SIDES,
|
|
64
|
+
_cut_colour,
|
|
65
|
+
_draw_units,
|
|
66
|
+
_finish,
|
|
67
|
+
_frame,
|
|
68
|
+
_hex,
|
|
69
|
+
_notch_marker,
|
|
70
|
+
_palette_spec_json,
|
|
71
|
+
_plain,
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
__all__ = ["fluxbox", "FluxboxResult"]
|
|
75
|
+
|
|
76
|
+
# ---------------------------------------------------------------------------
|
|
77
|
+
# statistics
|
|
78
|
+
# ---------------------------------------------------------------------------
|
|
79
|
+
def _whisker_rule(whis):
|
|
80
|
+
"""Validate ``whis`` into its manifest form: a factor of the IQR, ``"range"`` or ``[lo, hi]``."""
|
|
81
|
+
def number(v):
|
|
82
|
+
return isinstance(v, Real) and not isinstance(v, bool)
|
|
83
|
+
|
|
84
|
+
if isinstance(whis, str) and whis == "range" or (number(whis) and np.isinf(whis) and whis > 0):
|
|
85
|
+
return "range"
|
|
86
|
+
if number(whis) and whis >= 0:
|
|
87
|
+
return float(whis)
|
|
88
|
+
if isinstance(whis, (list, tuple, np.ndarray)) and len(whis) == 2 and all(map(number, whis)):
|
|
89
|
+
lo, hi = float(whis[0]), float(whis[1])
|
|
90
|
+
if 0.0 <= lo <= hi <= 100.0:
|
|
91
|
+
return [lo, hi]
|
|
92
|
+
raise ValueError("fluxbox: whis must be a non-negative factor of the IQR, 'range', or a "
|
|
93
|
+
f"(low, high) pair of percentiles; got {whis!r}")
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _box_stats(vals, rule):
|
|
97
|
+
"""One group's box-plot statistics. Quartiles and whiskers follow ``plt.boxplot`` exactly."""
|
|
98
|
+
v = vals[np.isfinite(vals)]
|
|
99
|
+
n = int(v.size)
|
|
100
|
+
if n == 0:
|
|
101
|
+
return None
|
|
102
|
+
mean, median = float(v.mean()), float(np.median(v))
|
|
103
|
+
sd = float(v.std(ddof=1)) if n > 1 else float("nan")
|
|
104
|
+
sem = sd / float(np.sqrt(n)) if n > 1 else float("nan")
|
|
105
|
+
q1, q3 = (float(q) for q in np.percentile(v, [25, 75]))
|
|
106
|
+
if rule == "range":
|
|
107
|
+
reach_lo, reach_hi = -np.inf, np.inf
|
|
108
|
+
elif isinstance(rule, list):
|
|
109
|
+
reach_lo, reach_hi = np.percentile(v, rule)
|
|
110
|
+
else:
|
|
111
|
+
reach_lo, reach_hi = q1 - rule * (q3 - q1), q3 + rule * (q3 - q1)
|
|
112
|
+
# a whisker ends ON an observation — the most extreme one within reach — and never inside the box
|
|
113
|
+
below, above = v[v >= reach_lo], v[v <= reach_hi]
|
|
114
|
+
w_lo = float(below.min()) if below.size and below.min() <= q1 else q1
|
|
115
|
+
w_hi = float(above.max()) if above.size and above.max() >= q3 else q3
|
|
116
|
+
outliers = sorted(float(o) for o in v[(v < w_lo) | (v > w_hi)])
|
|
117
|
+
return {"n": n, "mean": mean, "median": median, "sd": sd, "sem": sem, "q1": q1, "q3": q3,
|
|
118
|
+
"iqr": q3 - q1, "whis": rule, "whiskerLow": w_lo, "whiskerHigh": w_hi,
|
|
119
|
+
"outliers": outliers}
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _manifest_value(v):
|
|
123
|
+
return [_plain(e) for e in v] if isinstance(v, list) else _plain(v)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
@dataclass
|
|
127
|
+
class FluxboxResult:
|
|
128
|
+
"""What :func:`fluxbox` drew — the axes plus everything needed to reuse or annotate it."""
|
|
129
|
+
|
|
130
|
+
ax: Any
|
|
131
|
+
#: category values in plotted (left → right) order; category ``i`` sits at x = ``i``
|
|
132
|
+
categories: list
|
|
133
|
+
#: category → the statistics drawn (``n, mean, median, sd, sem, q1, q3, iqr, whis, whiskerLow,
|
|
134
|
+
#: whiskerHigh, outliers, x``)
|
|
135
|
+
stats: dict
|
|
136
|
+
#: category → its group colour (the box; the median / whisker ink is derived from it)
|
|
137
|
+
group_colors: dict
|
|
138
|
+
#: unit (or row index without ``units``) → {category: point colour}
|
|
139
|
+
point_colors: dict
|
|
140
|
+
#: category → series name, and unit → series name (the roots of every part id)
|
|
141
|
+
series: dict
|
|
142
|
+
unit_series: dict
|
|
143
|
+
#: part → matplotlib artists (``box``, ``whiskers``, ``caps``, ``median``, ``mean``, ``fliers``,
|
|
144
|
+
#: ``points``, ``lines``)
|
|
145
|
+
artists: dict = field(default_factory=dict)
|
|
146
|
+
|
|
147
|
+
@property
|
|
148
|
+
def positions(self) -> dict:
|
|
149
|
+
"""Category name → x (category ``i`` sits at ``i``), as :func:`fluxplot.brackets` wants it."""
|
|
150
|
+
return {str(c): float(i) for i, c in enumerate(self.categories)}
|
|
151
|
+
|
|
152
|
+
def brackets(self, rows, **kw) -> list:
|
|
153
|
+
"""Draw ``fp.stats`` post-hoc rows as stacked significance brackets over these
|
|
154
|
+
categories: :func:`fluxplot.brackets` with this plot's ``positions``."""
|
|
155
|
+
from ..brackets import brackets as _brackets
|
|
156
|
+
return _brackets(self.ax, rows, positions=kw.pop("positions", self.positions), **kw)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
# ---------------------------------------------------------------------------
|
|
160
|
+
# the plot
|
|
161
|
+
# ---------------------------------------------------------------------------
|
|
162
|
+
def fluxbox(
|
|
163
|
+
data=None,
|
|
164
|
+
*,
|
|
165
|
+
x,
|
|
166
|
+
y,
|
|
167
|
+
units=None,
|
|
168
|
+
order: Optional[Sequence] = None,
|
|
169
|
+
unit_order: Optional[Sequence] = None,
|
|
170
|
+
ax=None,
|
|
171
|
+
# the box
|
|
172
|
+
whis: Union[float, str, Sequence[float]] = 1.5,
|
|
173
|
+
show_mean: bool = True,
|
|
174
|
+
show_median: bool = True,
|
|
175
|
+
show_whiskers: bool = True,
|
|
176
|
+
show_caps: bool = False,
|
|
177
|
+
show_fliers: Union[bool, str] = "auto",
|
|
178
|
+
box_width: float = 7.0,
|
|
179
|
+
box_alpha: float = 0.5,
|
|
180
|
+
box_offset: float = 0.42,
|
|
181
|
+
box_side: str = "outer",
|
|
182
|
+
median_line_width: float = 1.4,
|
|
183
|
+
median_color=None,
|
|
184
|
+
median_contrast: float = 30.0,
|
|
185
|
+
mean_notch_depth: float = 1.5,
|
|
186
|
+
mean_notch_height: float = 2.6,
|
|
187
|
+
whisker_width: float = 1.0,
|
|
188
|
+
whisker_color=None,
|
|
189
|
+
cap_size: float = 4.0,
|
|
190
|
+
cap_width: float = 0.8,
|
|
191
|
+
cap_color=None,
|
|
192
|
+
flier_size: float = 8.0,
|
|
193
|
+
cut_color=None,
|
|
194
|
+
# the individual points
|
|
195
|
+
show_individual_points: bool = True,
|
|
196
|
+
point_size: float = 18.0,
|
|
197
|
+
jitter: float = 0.14,
|
|
198
|
+
point_edge="rim",
|
|
199
|
+
point_edge_width: float = 0.5,
|
|
200
|
+
point_fill_alpha: float = 1.0,
|
|
201
|
+
# colour
|
|
202
|
+
palette=None,
|
|
203
|
+
group_color_position: Optional[float] = None,
|
|
204
|
+
group_color=None,
|
|
205
|
+
point_colors="auto",
|
|
206
|
+
shade_range: tuple = (88.0, 22.0),
|
|
207
|
+
interleave_shades: bool = True,
|
|
208
|
+
# paired / repeated measures
|
|
209
|
+
connect_identical_points_across_x_values: bool = False,
|
|
210
|
+
connect_line_width: float = 0.6,
|
|
211
|
+
connect_color=None,
|
|
212
|
+
connect_alpha: float = 0.8,
|
|
213
|
+
# identity (the sidecar names)
|
|
214
|
+
series=None,
|
|
215
|
+
unit_series=None,
|
|
216
|
+
label_axes: bool = True,
|
|
217
|
+
zorder: float = 2.0,
|
|
218
|
+
) -> FluxboxResult:
|
|
219
|
+
"""Draw a fluxbox: the individual points plus a slim box plot with a median line and a mean notch.
|
|
220
|
+
|
|
221
|
+
Parameters
|
|
222
|
+
----------
|
|
223
|
+
data
|
|
224
|
+
A pandas or polars DataFrame, a dict of columns, or ``None`` when ``x``/``y``/``units`` are
|
|
225
|
+
passed as arrays.
|
|
226
|
+
x, y
|
|
227
|
+
Column names (or array-likes): ``x`` is categorical (one box per value), ``y`` numeric. Rows
|
|
228
|
+
whose ``x`` or ``y`` is missing are not drawn.
|
|
229
|
+
units
|
|
230
|
+
Optional identity column (subject, animal, cell…). Each unit gets a fixed lane and colour
|
|
231
|
+
inside every category, derived from the table order (``unit_order``), never from the values
|
|
232
|
+
— so separate plots of different measures from the same table agree, even when a unit is
|
|
233
|
+
missing a value in one of them. Units are also what
|
|
234
|
+
``connect_identical_points_across_x_values`` joins.
|
|
235
|
+
order, unit_order
|
|
236
|
+
Category order (left → right) and unit order (first lane → last). Default: first appearance
|
|
237
|
+
in the data (numeric categories are sorted). Categories missing from ``order`` are dropped.
|
|
238
|
+
ax
|
|
239
|
+
Target axes (default: the current axes).
|
|
240
|
+
|
|
241
|
+
whis
|
|
242
|
+
How far the whiskers reach: a factor of the IQR (default ``1.5`` — Tukey's rule, as
|
|
243
|
+
``plt.boxplot``), ``"range"`` (the minimum and maximum), or a ``(low, high)`` pair of
|
|
244
|
+
percentiles. Each whisker ends on the most extreme observation within that reach.
|
|
245
|
+
show_mean, show_median, show_whiskers, show_caps
|
|
246
|
+
Toggle the mean notch, the median line, the whiskers and whisker caps (off by default).
|
|
247
|
+
show_fliers
|
|
248
|
+
Mark the observations beyond the whiskers on the box's own axis: ``"auto"`` (only when the
|
|
249
|
+
individual points are hidden, since otherwise they are already drawn — default), ``True``
|
|
250
|
+
or ``False``.
|
|
251
|
+
box_width, box_alpha
|
|
252
|
+
Box width in points (the median line spans exactly this width), and the opacity of its fill
|
|
253
|
+
(default ``0.5``: a wash of the group colour that the solid median line stands out from).
|
|
254
|
+
box_offset, box_side
|
|
255
|
+
Distance (category units) of the box from its category's centre, and its side: ``"outer"``
|
|
256
|
+
(the first category's box to the left, every other to the right, so the boxes frame the
|
|
257
|
+
comparison — default), ``"left"`` or ``"right"``.
|
|
258
|
+
median_line_width, median_color, median_contrast
|
|
259
|
+
Stroke of the median line (points) and its colour. By default it is the group colour,
|
|
260
|
+
deepened — or, where the box renders darker than the group colour (a dark background),
|
|
261
|
+
lifted — only as far as it takes to differ from the box by ``median_contrast`` units of
|
|
262
|
+
perceived lightness (0–100 scale); give ``median_color`` to set it outright. Whichever it is,
|
|
263
|
+
it is also the colour of the whiskers, fliers and a solid (out-of-box) mean mark.
|
|
264
|
+
mean_notch_depth, mean_notch_height
|
|
265
|
+
How far each V-cut of the mean notch reaches into the box, and its height, in points. The
|
|
266
|
+
notch is drawn above the median, so it stays visible when the mean meets the median.
|
|
267
|
+
whisker_width, whisker_color
|
|
268
|
+
Whisker stroke (points) and colour (default: the median's colour).
|
|
269
|
+
cap_size, cap_width, cap_color
|
|
270
|
+
Whisker cap length and stroke (points), and colour (default: the whiskers') — when
|
|
271
|
+
``show_caps=True``. A whisker of zero length (no observation beyond the box) gets no cap.
|
|
272
|
+
flier_size
|
|
273
|
+
Marker area (points²) of the fliers.
|
|
274
|
+
cut_color
|
|
275
|
+
Colour of the mean notch "cut" (default: the axes background); also the ground the median's
|
|
276
|
+
contrast is judged against.
|
|
277
|
+
|
|
278
|
+
show_individual_points
|
|
279
|
+
Draw the individual observations (default ``True``).
|
|
280
|
+
point_size, jitter
|
|
281
|
+
Marker area (points²), and the half-width of the lane spread (category units). Lanes are
|
|
282
|
+
evenly spaced, never random.
|
|
283
|
+
point_edge, point_edge_width
|
|
284
|
+
``"rim"`` (a deeper shade of each point's own colour, which keeps pale points crisp —
|
|
285
|
+
default), ``"none"`` or any colour; and the edge width in points.
|
|
286
|
+
point_fill_alpha
|
|
287
|
+
Opacity of the points' fill only (default ``1.0``). The rim / edge keeps full opacity, so
|
|
288
|
+
translucent points stay crisply outlined where they overlap.
|
|
289
|
+
|
|
290
|
+
palette
|
|
291
|
+
Per-category colour source, given once for every category, as a list (cycled in category
|
|
292
|
+
order) or as a ``{category: spec}`` mapping — any colormap or palette in fluxplot's colour
|
|
293
|
+
library, a matplotlib colormap, a list of colours, or a single colour; exactly as for
|
|
294
|
+
:func:`fluxplot.glowbar`. Default: the ColorBrewer maps ``YlGnBu``, ``YlOrRd``, ``RdPu``,
|
|
295
|
+
``BuGn``, ``Purples``, ``YlOrBr``.
|
|
296
|
+
group_color_position
|
|
297
|
+
Pin the group colour to a point of an ordered (sequential) source's light → dark ramp
|
|
298
|
+
(0 = palest, 1 = darkest). Default ``None``: 0.75 along a ColorBrewer ramp, else the
|
|
299
|
+
source's most chromatic mid-lightness colour (or a neutral ink for multi-hue sources).
|
|
300
|
+
group_color
|
|
301
|
+
Override the group colour (the box, and the base of the median / whisker ink) outright: one
|
|
302
|
+
colour, or a ``{category: colour}`` mapping. Point colours still come from ``palette``.
|
|
303
|
+
point_colors
|
|
304
|
+
``"shades"`` (each unit its own shade of its category's map), ``"group"`` (every point in its
|
|
305
|
+
group colour), ``"auto"`` (shades with ``units``, else group — default), one colour for all
|
|
306
|
+
points, or a ``{unit: colour}`` mapping.
|
|
307
|
+
shade_range, interleave_shades
|
|
308
|
+
Lightness of the palest and darkest shade (0 = black, 100 = white), spaced in equal
|
|
309
|
+
perceptual steps; and whether shades are dealt across lanes so neighbours always contrast
|
|
310
|
+
(default ``True``) or run pale → dark in lane order.
|
|
311
|
+
|
|
312
|
+
connect_identical_points_across_x_values
|
|
313
|
+
Join each unit's points across the x categories with a line (needs ``units``) — for paired
|
|
314
|
+
or repeated-measures designs. A unit missing from a category breaks its line there instead
|
|
315
|
+
of bridging the gap; several rows of one unit in one category are joined through their mean.
|
|
316
|
+
connect_line_width, connect_color, connect_alpha
|
|
317
|
+
Connector stroke (points), colour (a colour, or ``"unit"`` for each unit's own point colour)
|
|
318
|
+
and opacity. The default is a quiet neutral — the active theme's grid colour, else Flexoki
|
|
319
|
+
base-300 — so the lines never compete with the points.
|
|
320
|
+
|
|
321
|
+
series, unit_series
|
|
322
|
+
Override the series names — the roots of every part id — per category / per unit, as a
|
|
323
|
+
mapping or a callable. Defaults: the category and unit values (``"SD"`` → ``sd.box``,
|
|
324
|
+
``"B6_8"`` → ``b6-8.points``).
|
|
325
|
+
label_axes
|
|
326
|
+
Put the category names on the x ticks and the ``y`` column name on the y axis, and set the x
|
|
327
|
+
limits (default ``True``).
|
|
328
|
+
zorder
|
|
329
|
+
Base z-order: connectors sit just below it, the points at it, the box above.
|
|
330
|
+
|
|
331
|
+
Returns
|
|
332
|
+
-------
|
|
333
|
+
FluxboxResult
|
|
334
|
+
The axes, category order, statistics, colours, series names and artists.
|
|
335
|
+
"""
|
|
336
|
+
import matplotlib.pyplot as plt
|
|
337
|
+
|
|
338
|
+
if ax is None:
|
|
339
|
+
ax = plt.gca()
|
|
340
|
+
if box_side not in _SIDES:
|
|
341
|
+
raise ValueError(f"fluxbox: box_side must be 'outer', 'left' or 'right'; got {box_side!r}")
|
|
342
|
+
if show_fliers not in (True, False, "auto"):
|
|
343
|
+
raise ValueError(f"fluxbox: show_fliers must be True, False or 'auto'; got {show_fliers!r}")
|
|
344
|
+
if not 0.0 <= box_alpha <= 1.0:
|
|
345
|
+
raise ValueError(f"fluxbox: box_alpha must be in [0, 1]; got {box_alpha!r}")
|
|
346
|
+
rule = _whisker_rule(whis)
|
|
347
|
+
cut = _cut_colour(ax, cut_color) # the ground every ink is judged against
|
|
348
|
+
fr = _frame("fluxbox", data, x, y, units, order, unit_order, jitter=jitter, palette=palette,
|
|
349
|
+
group_color=group_color, group_color_position=group_color_position,
|
|
350
|
+
point_colors=point_colors, shade_range=shade_range,
|
|
351
|
+
interleave_shades=interleave_shades, series=series, unit_series=unit_series,
|
|
352
|
+
connect=connect_identical_points_across_x_values, point_fill_alpha=point_fill_alpha,
|
|
353
|
+
ground=cut)
|
|
354
|
+
reg = _tagger.registry_for(ax.figure)
|
|
355
|
+
artists = {"box": [], "whiskers": [], "caps": [], "median": [], "mean": [], "fliers": [],
|
|
356
|
+
"points": [], "lines": []}
|
|
357
|
+
artists["lines"], artists["points"] = _draw_units(
|
|
358
|
+
fr, ax, reg, connect=connect_identical_points_across_x_values,
|
|
359
|
+
connect_line_width=connect_line_width, connect_color=connect_color,
|
|
360
|
+
connect_alpha=connect_alpha, show_points=show_individual_points, point_size=point_size,
|
|
361
|
+
point_edge=point_edge, point_edge_width=point_edge_width,
|
|
362
|
+
point_fill_alpha=point_fill_alpha, zorder=zorder, ground=cut)
|
|
363
|
+
fliers_on = not show_individual_points if show_fliers == "auto" else show_fliers
|
|
364
|
+
|
|
365
|
+
# ---- the box ---------------------------------------------------------------------------------------
|
|
366
|
+
notch, notch_ms = _notch_marker(box_width, mean_notch_depth, mean_notch_height)
|
|
367
|
+
stats = {}
|
|
368
|
+
for k, c in enumerate(fr.cats):
|
|
369
|
+
st = _box_stats(fr.ys[fr.rows_in[c]], rule)
|
|
370
|
+
if st is None:
|
|
371
|
+
continue
|
|
372
|
+
s, col = fr.series_of[c], fr.group_colors[c]
|
|
373
|
+
bx = fr.summary_x(k, box_side, box_offset)
|
|
374
|
+
st["x"] = bx
|
|
375
|
+
stats[c] = dict(st, groupColor=col)
|
|
376
|
+
q1, q3, w_lo, w_hi = st["q1"], st["q3"], st["whiskerLow"], st["whiskerHigh"]
|
|
377
|
+
# one ink for every solid part (median, whiskers, caps, fliers); the box alone is the wash
|
|
378
|
+
ink = median_color if median_color is not None else \
|
|
379
|
+
_median_ink(col, box_alpha, cut, median_contrast)
|
|
380
|
+
whisker_ink = whisker_color if whisker_color is not None else ink
|
|
381
|
+
# a whisker (and its cap) only where an observation lies beyond the box on that side
|
|
382
|
+
ends = [(w, q) for w, q in ((w_lo, q1), (w_hi, q3)) if w != q]
|
|
383
|
+
|
|
384
|
+
if show_whiskers and ends:
|
|
385
|
+
wx, wy = [], []
|
|
386
|
+
for w, q in ends:
|
|
387
|
+
wx += [bx, bx, np.nan]
|
|
388
|
+
wy += [q, w, np.nan]
|
|
389
|
+
(wh,) = ax.plot(wx[:-1], wy[:-1], color=whisker_ink, lw=whisker_width, solid_capstyle="butt", zorder=zorder + 0.5)
|
|
390
|
+
reg.add(Mark(role="whisker", series=s, name="whiskers", kind="fluxbox", artists=[wh]))
|
|
391
|
+
artists["whiskers"].append(wh)
|
|
392
|
+
if show_caps and ends:
|
|
393
|
+
(caps,) = ax.plot([bx] * len(ends), [w for w, _ in ends], ls="none", marker="_",
|
|
394
|
+
ms=cap_size, mew=cap_width,
|
|
395
|
+
color=cap_color if cap_color is not None else whisker_ink,
|
|
396
|
+
zorder=zorder + 0.5)
|
|
397
|
+
reg.add(Mark(role="cap", series=s, name="caps", kind="fluxbox", artists=[caps]))
|
|
398
|
+
artists["caps"].append(caps)
|
|
399
|
+
if fliers_on and st["outliers"]:
|
|
400
|
+
fl = ax.scatter([bx] * len(st["outliers"]), st["outliers"], s=flier_size, color=[ink],
|
|
401
|
+
linewidths=0, zorder=zorder + 0.5)
|
|
402
|
+
reg.add(Mark(role="flier", series=s, name="fliers", kind="fluxbox", artists=[fl]))
|
|
403
|
+
artists["fliers"].append(fl)
|
|
404
|
+
|
|
405
|
+
# the box: a butt-capped stroke exactly box_width points wide, spanning Q1–Q3 in data units;
|
|
406
|
+
# the exact statistics drawn ride on it → the manifest
|
|
407
|
+
(body,) = ax.plot([bx, bx], [q1, q3], color=col, alpha=box_alpha, lw=box_width,
|
|
408
|
+
solid_capstyle="butt", zorder=zorder + 0.6)
|
|
409
|
+
payload = {"fluxbox": {"part": "summary", "category": _plain(c), "units": fr.units_name,
|
|
410
|
+
"groupColor": _hex(col), "palette": _palette_spec_json(fr.palette_used.get(c)),
|
|
411
|
+
**{key: _manifest_value(v) for key, v in st.items()}}}
|
|
412
|
+
reg.add(Mark(role="box", series=s, kind="fluxbox", artists=[body], data=payload))
|
|
413
|
+
artists["box"].append(body)
|
|
414
|
+
|
|
415
|
+
if show_median:
|
|
416
|
+
# marker '_' spans exactly ms points: the median line is as wide as the box
|
|
417
|
+
(med,) = ax.plot([bx], [st["median"]], ls="none", marker="_", ms=box_width,
|
|
418
|
+
mew=median_line_width, color=ink, zorder=zorder + 1.5)
|
|
419
|
+
reg.add(Mark(role="median", series=s, kind="fluxbox", artists=[med]))
|
|
420
|
+
artists["median"].append(med)
|
|
421
|
+
if show_mean:
|
|
422
|
+
# inside the box the notch is cut out of it; outside (or on a box of no height) there is
|
|
423
|
+
# nothing to cut, so the same V's are drawn solid in the ink, pointing in at the whisker
|
|
424
|
+
cut_in = q1 < q3 and q1 <= st["mean"] <= q3
|
|
425
|
+
(mean_mk,) = ax.plot([bx], [st["mean"]], ls="none", marker=notch, ms=notch_ms,
|
|
426
|
+
mfc=cut if cut_in else ink, mec="none", mew=0, zorder=zorder + 2)
|
|
427
|
+
reg.add(Mark(role="mean", series=s, kind="fluxbox", artists=[mean_mk]))
|
|
428
|
+
artists["mean"].append(mean_mk)
|
|
429
|
+
|
|
430
|
+
_finish(fr, ax, label_axes)
|
|
431
|
+
return FluxboxResult(ax=ax, categories=fr.cats, stats=stats, group_colors=fr.group_colors,
|
|
432
|
+
point_colors=fr.point_colors_by_unit(), series=fr.series_of,
|
|
433
|
+
unit_series=fr.unit_series_of, artists=artists)
|