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.
Files changed (65) hide show
  1. fluxplot/__init__.py +115 -0
  2. fluxplot/_fieldmap.py +97 -0
  3. fluxplot/_mesh_reduce.py +54 -0
  4. fluxplot/_scene3d_size.py +95 -0
  5. fluxplot/_viewer/THIRD-PARTY.txt +23 -0
  6. fluxplot/_viewer/flux-model3d-viewer.min.js +4221 -0
  7. fluxplot/_viewer/stamp.json +4 -0
  8. fluxplot/api.py +1196 -0
  9. fluxplot/autotag.py +164 -0
  10. fluxplot/base.mplstyle +0 -0
  11. fluxplot/brackets.py +242 -0
  12. fluxplot/canonical_json.py +23 -0
  13. fluxplot/capture.py +150 -0
  14. fluxplot/colorcheck.py +285 -0
  15. fluxplot/colors.py +727 -0
  16. fluxplot/colorscale.py +477 -0
  17. fluxplot/data.py +178 -0
  18. fluxplot/definitions/colormaps.json +1639 -0
  19. fluxplot/definitions/flexoki.tokens.json +2571 -0
  20. fluxplot/definitions/palettes.json +2547 -0
  21. fluxplot/descriptors.py +87 -0
  22. fluxplot/fields.py +611 -0
  23. fluxplot/fits.py +240 -0
  24. fluxplot/glb.py +84 -0
  25. fluxplot/ids.py +173 -0
  26. fluxplot/images.py +362 -0
  27. fluxplot/integrity.py +27 -0
  28. fluxplot/manifest.py +788 -0
  29. fluxplot/mesh3d.py +376 -0
  30. fluxplot/panels.py +284 -0
  31. fluxplot/postprocess.py +638 -0
  32. fluxplot/presets.py +66 -0
  33. fluxplot/provenance.py +177 -0
  34. fluxplot/raster.py +295 -0
  35. fluxplot/recipe.py +178 -0
  36. fluxplot/render.py +66 -0
  37. fluxplot/roles.py +147 -0
  38. fluxplot/scene3d.py +386 -0
  39. fluxplot/scene3d_manifest.py +112 -0
  40. fluxplot/scene3d_viewer.py +633 -0
  41. fluxplot/schemas/.gitkeep +0 -0
  42. fluxplot/schemas/manifest.schema.json +2479 -0
  43. fluxplot/schemas/recipe.schema.json +179 -0
  44. fluxplot/schemas/scene3d.schema.json +461 -0
  45. fluxplot/seaborn_adapters.py +323 -0
  46. fluxplot/signature_fluxplots/__init__.py +18 -0
  47. fluxplot/signature_fluxplots/_colour.py +412 -0
  48. fluxplot/signature_fluxplots/fluxbox.py +433 -0
  49. fluxplot/signature_fluxplots/glowbar.py +769 -0
  50. fluxplot/signature_fluxplots/hexmatrix.py +927 -0
  51. fluxplot/stats/__init__.py +63 -0
  52. fluxplot/stats/_common.py +196 -0
  53. fluxplot/stats/multi_group.py +443 -0
  54. fluxplot/stats/paired.py +209 -0
  55. fluxplot/stats/two_group.py +149 -0
  56. fluxplot/style.py +469 -0
  57. fluxplot/surface.py +487 -0
  58. fluxplot/surface3d.py +197 -0
  59. fluxplot/tagger.py +561 -0
  60. fluxplot/version.py +19 -0
  61. fluxplot-0.1.0.dist-info/METADATA +1199 -0
  62. fluxplot-0.1.0.dist-info/RECORD +65 -0
  63. fluxplot-0.1.0.dist-info/WHEEL +4 -0
  64. fluxplot-0.1.0.dist-info/licenses/LICENSE +21 -0
  65. fluxplot-0.1.0.dist-info/licenses/THIRD_PARTY_NOTICES.md +472 -0
@@ -0,0 +1,323 @@
1
+ """Exact identity for seaborn's categorical plots (:func:`fluxplot.tag_seaborn`).
2
+
3
+ seaborn draws a categorical plot as anonymous artists in a fixed order: one ``BoxPlotContainer``
4
+ per hue level, one violin body per (category, hue) pair, one strip / swarm collection per
5
+ category with the hue levels mixed inside it, one mean line plus its error lines per hue level.
6
+ Given the plot kind — and, for the row-wise splits, the frame it was drawn from — those artists
7
+ can be named exactly: the hue levels in seaborn's own ``categorical_order``, the categories in
8
+ tick order, and a mixed collection's points by the frame rows they came from
9
+ (``Mark.data["point_subset"]``). Nothing is inferred from colour or geometry.
10
+ """
11
+ from __future__ import annotations
12
+
13
+ import warnings
14
+ from typing import Any, Dict, List, Optional
15
+
16
+ import numpy as np
17
+
18
+ from .descriptors import Mark
19
+
20
+ CATEGORICAL_KINDS = ("boxplot", "violinplot", "stripplot", "swarmplot", "pointplot")
21
+
22
+
23
+ def categorical_order(values) -> list:
24
+ """seaborn's category order: a Categorical's categories, else sorted numbers, else the order
25
+ of appearance (``seaborn._core.rules.categorical_order`` when seaborn is importable)."""
26
+ try:
27
+ from seaborn._core.rules import categorical_order as _order
28
+ import pandas as pd
29
+ return list(_order(pd.Series(values)))
30
+ except Exception:
31
+ pass
32
+ seen = list(dict.fromkeys(v for v in list(values) if not _missing(v)))
33
+ try:
34
+ if all(isinstance(v, (int, float, np.number)) for v in seen):
35
+ return sorted(seen)
36
+ except TypeError:
37
+ pass
38
+ return seen
39
+
40
+
41
+ def _missing(v) -> bool:
42
+ if v is None:
43
+ return True
44
+ try:
45
+ return bool(np.isnan(v))
46
+ except (TypeError, ValueError):
47
+ return False
48
+
49
+
50
+ def _column(data, key) -> list:
51
+ if key is None:
52
+ return []
53
+ try:
54
+ col = data[key]
55
+ except Exception as exc:
56
+ raise KeyError(f"tag_seaborn: {key!r} is not a column of data") from exc
57
+ for attr in ("to_list", "tolist"):
58
+ if hasattr(col, attr):
59
+ return list(getattr(col, attr)())
60
+ return list(col)
61
+
62
+
63
+ def _is_numeric(values) -> bool:
64
+ try:
65
+ return all(isinstance(v, (int, float, np.number)) and not isinstance(v, bool) for v in values if not _missing(v))
66
+ except TypeError:
67
+ return False
68
+
69
+
70
+ class Frame:
71
+ """What the frame says about a categorical plot: the categorical variable and its order, the
72
+ hue levels and, per row, the (category, hue) it belongs to."""
73
+
74
+ def __init__(self, data, x, y, hue, order=None, hue_order=None, orient=None):
75
+ xs, ys = _column(data, x), _column(data, y)
76
+ if orient is None:
77
+ if x is not None and y is not None:
78
+ orient = "h" if _is_numeric(xs) and not _is_numeric(ys) else "v"
79
+ else:
80
+ orient = "v" if x is not None else "h"
81
+ self.orient = orient
82
+ self.cat_var, self.val_var = (x, y) if orient == "v" else (y, x)
83
+ cats = _column(data, self.cat_var)
84
+ vals = _column(data, self.val_var)
85
+ self.hue_var = hue
86
+ hues = _column(data, hue) if hue is not None else None
87
+ self.categories = list(order) if order is not None else categorical_order(cats)
88
+ self.hues = (list(hue_order) if hue_order is not None else categorical_order(hues)) if hue is not None else None
89
+ n = len(cats) if cats else len(vals)
90
+ # rows seaborn keeps, in frame order: the value, the category and the hue all present
91
+ self.rows: List[int] = []
92
+ self.cat_of: Dict[int, Any] = {}
93
+ self.hue_of: Dict[int, Any] = {}
94
+ for i in range(n):
95
+ c = cats[i] if cats else None
96
+ v = vals[i] if vals else None
97
+ h = hues[i] if hues is not None else None
98
+ if (cats and _missing(c)) or (vals and _missing(v)) or (hues is not None and _missing(h)):
99
+ continue
100
+ if cats and c not in self.categories:
101
+ continue
102
+ if hues is not None and h not in self.hues:
103
+ continue
104
+ self.rows.append(i)
105
+ self.cat_of[i] = c
106
+ self.hue_of[i] = h
107
+
108
+ def rows_in(self, category) -> List[int]:
109
+ return [i for i in self.rows if self.cat_of.get(i) == category]
110
+
111
+ @property
112
+ def n_hues(self) -> int:
113
+ return len(self.hues) if self.hues else 1
114
+
115
+
116
+ def _names(values) -> List[str]:
117
+ return [str(v) for v in values]
118
+
119
+
120
+ def _untagged(artists, already):
121
+ return [a for a in artists if id(a) not in already and a.get_visible()]
122
+
123
+
124
+ def tag_categorical(ax, plot: str, *, frame: Optional[Frame], categories, hues, reg, already, tagged, pin) -> None:
125
+ """Tag one of seaborn's categorical plot kinds. ``categories`` / ``hues`` are the levels in
126
+ seaborn's order (from the frame, or from the tick labels / legend when there is no frame)."""
127
+ cats = _names(categories)
128
+ hue_names = _names(hues) if hues else None
129
+ if plot == "boxplot":
130
+ _tag_boxes(ax, cats, hue_names, reg, already, tagged, pin)
131
+ elif plot == "violinplot":
132
+ _tag_violins(ax, cats, hue_names, reg, already, tagged, pin)
133
+ elif plot in ("stripplot", "swarmplot"):
134
+ _tag_strips(ax, cats, hue_names, frame, categories, reg, already, tagged, pin)
135
+ elif plot == "pointplot":
136
+ _tag_points(ax, cats, hue_names, reg, already, tagged, pin)
137
+
138
+
139
+ def _record(tagged, name, role):
140
+ tagged.setdefault(str(name), []).append(role)
141
+
142
+
143
+ # ---------------------------------------------------------------------------------------------
144
+ # box plots: one BoxPlotContainer per hue level, k boxes each
145
+ # ---------------------------------------------------------------------------------------------
146
+ def _tag_boxes(ax, cats, hues, reg, already, tagged, pin):
147
+ containers = [c for c in getattr(ax, "containers", []) if type(c).__name__ == "BoxPlotContainer"]
148
+ containers = [c for c in containers if any(id(b) not in already for b in c.boxes)]
149
+ if not containers:
150
+ return
151
+ if hues is not None and len(containers) != len(hues):
152
+ warnings.warn(f"tag_seaborn: {len(containers)} box containers for {len(hues)} hue levels; boxes left as extras", stacklevel=3)
153
+ return
154
+ if hues is None and len(containers) != 1:
155
+ warnings.warn("tag_seaborn: several box containers without hue=; boxes left as extras", stacklevel=3)
156
+ return
157
+ for j, cont in enumerate(containers):
158
+ boxes = list(cont.boxes)
159
+ if len(boxes) != len(cats):
160
+ warnings.warn(f"tag_seaborn: {len(boxes)} boxes for {len(cats)} categories; boxes left as extras", stacklevel=3)
161
+ return
162
+ for i, cat in enumerate(cats):
163
+ series = hues[j] if hues is not None else cat
164
+ name = cat if hues is not None else None
165
+ pin(series, boxes[i], lambda a, c: a.set_facecolor(c))
166
+ reg.add(Mark(role="box", series=series, name=name, kind="box", label=series, artists=[boxes[i]]))
167
+ _record(tagged, series, "box")
168
+ parts = (("whisker", list(cont.whiskers)[2 * i:2 * i + 2]), ("cap", list(cont.caps)[2 * i:2 * i + 2]),
169
+ ("median", list(cont.medians)[i:i + 1]), ("flier", list(cont.fliers)[i:i + 1]),
170
+ ("mean", list(getattr(cont, "means", []))[i:i + 1]))
171
+ for role, arts in parts:
172
+ arts = [a for a in arts if a is not None]
173
+ if not arts:
174
+ continue
175
+ part_name = f"{cat}-{role}" if hues is not None else None
176
+ reg.add(Mark(role=role, series=series, name=part_name, kind="box", artists=arts))
177
+ _record(tagged, series, role)
178
+
179
+
180
+ # ---------------------------------------------------------------------------------------------
181
+ # violins: one body (PolyCollection) per (category, hue), category-major; inner lines follow
182
+ # ---------------------------------------------------------------------------------------------
183
+ def _tag_violins(ax, cats, hues, reg, already, tagged, pin):
184
+ from matplotlib.collections import PolyCollection
185
+ bodies = _untagged([c for c in ax.collections if isinstance(c, PolyCollection)], already)
186
+ n_h = len(hues) if hues else 1
187
+ expected = len(cats) * n_h
188
+ if len(bodies) != expected:
189
+ warnings.warn(f"tag_seaborn: {len(bodies)} violin bodies for {len(cats)} categories × {n_h} hue levels; left as extras", stacklevel=3)
190
+ return
191
+ lines = _untagged([ln for ln in ax.lines if len(ln.get_xdata())], already)
192
+ per = len(lines) // expected if expected and len(lines) % expected == 0 else 0
193
+ # seaborn iterates the categorical variable outermost, then hue: (a, p), (a, q), (b, p), …
194
+ for k, body in enumerate(bodies):
195
+ cat, hue = cats[k // n_h], (hues[k % n_h] if hues else None)
196
+ series = hue if hues is not None else cat
197
+ name = cat if hues is not None else None
198
+ pin(series, body, lambda a, c: a.set_facecolor(c))
199
+ reg.add(Mark(role="violin", series=series, name=name, kind="violin", label=series, artists=[body]))
200
+ _record(tagged, series, "violin")
201
+ if per:
202
+ inner = lines[k * per:(k + 1) * per]
203
+ reg.add(Mark(role="segment", series=series, name=f"{cat}-inner" if hues is not None else "inner",
204
+ kind="violin", artists=inner))
205
+ _record(tagged, series, "segment")
206
+
207
+
208
+ # ---------------------------------------------------------------------------------------------
209
+ # strips / swarms: one PathCollection per category (hue levels mixed inside, split by rows) or
210
+ # per (category, hue) when dodged
211
+ # ---------------------------------------------------------------------------------------------
212
+ def _tag_strips(ax, cats, hues, frame, categories, reg, already, tagged, pin):
213
+ from matplotlib.collections import PathCollection, PolyCollection
214
+ colls = _untagged([c for c in ax.collections if isinstance(c, PathCollection) and not isinstance(c, PolyCollection)], already)
215
+ n_h = len(hues) if hues else 1
216
+ if len(colls) == len(cats) * n_h and n_h > 1:
217
+ # dodged: one collection per (category, hue), category-major
218
+ for k, coll in enumerate(colls):
219
+ cat, hue = cats[k // n_h], hues[k % n_h]
220
+ _point_mark(reg, coll, series=hue, name=cat, tagged=tagged, pin=pin)
221
+ return
222
+ if len(colls) != len(cats):
223
+ warnings.warn(f"tag_seaborn: {len(colls)} point collections for {len(cats)} categories; left as extras", stacklevel=3)
224
+ return
225
+ for i, (cat, coll) in enumerate(zip(cats, colls)):
226
+ if hues is None:
227
+ _point_mark(reg, coll, series=cat, name=None, tagged=tagged, pin=pin)
228
+ continue
229
+ if frame is None:
230
+ warnings.warn("tag_seaborn: hue levels inside one collection need data= to split by row; tagging per category", stacklevel=3)
231
+ _point_mark(reg, coll, series=cat, name=None, tagged=tagged, pin=pin)
232
+ continue
233
+ rows = frame.rows_in(categories[i])
234
+ off = coll.get_offsets()
235
+ if len(rows) != len(off):
236
+ warnings.warn(f"tag_seaborn: category {cat!r} draws {len(off)} points but the frame has {len(rows)} rows; tagging per category", stacklevel=3)
237
+ _point_mark(reg, coll, series=cat, name=None, tagged=tagged, pin=pin)
238
+ continue
239
+ x, y = [float(v) for v in off[:, 0]], [float(v) for v in off[:, 1]]
240
+ for hue, level in zip(hues, frame.hues):
241
+ subset = [k for k, r in enumerate(rows) if frame.hue_of[r] == level]
242
+ if not subset:
243
+ continue
244
+ reg.add(Mark(role="point", series=hue, name=cat, kind="scatter", x=x, y=y, artists=[coll], indexed=True,
245
+ data={"point_subset": subset}))
246
+ _record(tagged, hue, "point")
247
+
248
+
249
+ def _point_mark(reg, coll, *, series, name, tagged, pin):
250
+ pin(series, coll, lambda a, c: a.set_facecolor(c))
251
+ off = coll.get_offsets()
252
+ x, y = [float(v) for v in off[:, 0]], [float(v) for v in off[:, 1]]
253
+ reg.add(Mark(role="point", series=series, name=name, kind="scatter", x=x, y=y, artists=[coll], indexed=True, live_data=True))
254
+ _record(tagged, series, "point")
255
+
256
+
257
+ def split_scatter(reg, coll, frame: Frame, tagged) -> bool:
258
+ """``scatterplot(hue=)``: one collection for every hue level. Register one point mark per
259
+ level owning the rows of that level (``point_subset``); the collection stays one artist."""
260
+ off = coll.get_offsets()
261
+ if len(frame.rows) != len(off):
262
+ warnings.warn(f"tag_seaborn: the scatter draws {len(off)} points but the frame keeps {len(frame.rows)} rows; not split by hue", stacklevel=3)
263
+ return False
264
+ x, y = [float(v) for v in off[:, 0]], [float(v) for v in off[:, 1]]
265
+ for level in frame.hues:
266
+ subset = [k for k, r in enumerate(frame.rows) if frame.hue_of[r] == level]
267
+ if not subset:
268
+ continue
269
+ reg.add(Mark(role="point", series=str(level), kind="scatter", x=x, y=y, artists=[coll], indexed=True,
270
+ data={"point_subset": subset}))
271
+ _record(tagged, level, "point")
272
+ return True
273
+
274
+
275
+ # ---------------------------------------------------------------------------------------------
276
+ # point plots: per hue level a mean line (with markers) followed by its error lines
277
+ # ---------------------------------------------------------------------------------------------
278
+ def _tag_points(ax, cats, hues, reg, already, tagged, pin):
279
+ from matplotlib.lines import Line2D
280
+ lines = _untagged([ln for ln in ax.lines if len(ln.get_xdata())], already)
281
+ n_h = len(hues) if hues else 1
282
+ if not lines or len(lines) % n_h:
283
+ warnings.warn(f"tag_seaborn: {len(lines)} lines for {n_h} hue levels in a pointplot; left as extras", stacklevel=3)
284
+ return
285
+ per = len(lines) // n_h
286
+ for j in range(n_h):
287
+ chunk = lines[j * per:(j + 1) * per]
288
+ mean = chunk[0]
289
+ series = hues[j] if hues else (ax.get_ylabel() or "means")
290
+ pin(series, mean, lambda a, c: (a.set_color(c), a.set_markerfacecolor(c), a.set_markeredgecolor(c)))
291
+ has_line = str(mean.get_linestyle()).lower() not in ("none", "", " ")
292
+ has_marker = str(mean.get_marker()).lower() not in ("none", "", " ")
293
+ if has_marker:
294
+ pts = Line2D([], [])
295
+ pts.update_from(mean)
296
+ pts.set_zorder(mean.get_zorder())
297
+ pts.set_data(mean.get_xdata(orig=False), mean.get_ydata(orig=False))
298
+ pts.set_linestyle("none")
299
+ pts.set_label("_nolegend_")
300
+ ax.add_line(pts)
301
+ reg.add(Mark(role="point", series=series, kind="line", live_data=True, x=None, y=None, artists=[pts], indexed=True))
302
+ _record(tagged, series, "point")
303
+ if has_line:
304
+ mean.set_marker("none")
305
+ else:
306
+ mean.set_visible(False)
307
+ if has_line:
308
+ gx, gy = mean.get_data()
309
+ reg.add(Mark(role="line", series=series, kind="line", x=[float(v) for v in gx], y=[float(v) for v in gy],
310
+ live_data=True, label=series, artists=[mean]))
311
+ _record(tagged, series, "line")
312
+ errs = chunk[1:]
313
+ if errs:
314
+ for ln in errs:
315
+ pin(series, ln, lambda a, c: a.set_color(c))
316
+ reg.add(Mark(role="errorbar", series=series, kind="errorbar", artists=errs))
317
+ _record(tagged, series, "errorbar")
318
+
319
+
320
+ def tick_categories(ax, orient: str = "v") -> List[str]:
321
+ """The categories seaborn wrote as tick labels (when no frame was given)."""
322
+ axis = ax.xaxis if orient == "v" else ax.yaxis
323
+ return [t.get_text() for t in axis.get_ticklabels() if t.get_text()]
@@ -0,0 +1,18 @@
1
+ """Signature fluxplots — preset plot types that are unique to Flux.
2
+
3
+ Each one is a complete, opinionated plot built from ordinary matplotlib artists and tagged part by
4
+ part, so it saves, validates and round-trips through Flux like any hand-built FluxPlot. They take a
5
+ DataFrame plus column names, seaborn-style, and are re-exported at the top level (``fp.glowbar``).
6
+
7
+ * :func:`glowbar` — individual points beside a glowing interval bar with a mean line and a median
8
+ notch; fixed per-unit lanes and colours, optional paired connectors.
9
+ * :func:`fluxbox` — the glowbar with a box plot for its summary: a slim translucent box (Q1–Q3)
10
+ with a solid median line, a mean notch and capless whiskers.
11
+ * :func:`hexmatrix` — hexagonal binning (counts, densities, reductions of a third variable) or a 2D
12
+ array on a hex lattice; every hexagon a named part, with marginals, colour key and log axes.
13
+ """
14
+ from .fluxbox import FluxboxResult, fluxbox
15
+ from .hexmatrix import HexMatrixResult, hexmatrix
16
+ from .glowbar import GlowbarResult, even_shades, glowbar, interleaved_order
17
+
18
+ __all__ = ["glowbar", "GlowbarResult", "fluxbox", "FluxboxResult", "hexmatrix", "HexMatrixResult", "even_shades", "interleaved_order"]