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
fluxplot/__init__.py ADDED
@@ -0,0 +1,115 @@
1
+ """FluxPlot — matplotlib, but every meaningful thing has a name.
2
+
3
+ A thin, additive semantic-tagging layer over matplotlib. Plot in real matplotlib (using the
4
+ ``fp.*`` convenience helpers or by tagging raw artists), then :func:`save` emits a semantic SVG +
5
+ ``*.fluxplot.json`` manifest + ``*.recipe.json``.
6
+
7
+ See ``Flux_SemanticSVG_Spec.md`` for the conceptual spec.
8
+ """
9
+
10
+ from . import (
11
+ colorcheck, # noqa: E402,F401 (accessibility lint — colorcheck.check_palette, simulate, contrast, …)
12
+ colors, # noqa: E402,F401 (canonical palette/colormaps — colors.green400, colors.maps.emerald, …)
13
+ colorscale, # noqa: E402,F401 (the portable colour-scale law — colorscale.apply(record, values))
14
+ stats, # noqa: E402,F401 (tests behind the plots — stats.welch_hedges(a, b), …)
15
+ style, # noqa: E402,F401 (house plotting style — fx.use_light(), fx.FLEXOKI, …)
16
+ )
17
+ from .api import ( # noqa: E402,F401
18
+ annotation,
19
+ area,
20
+ band,
21
+ bar,
22
+ barh,
23
+ box,
24
+ errorbar,
25
+ hist,
26
+ legend,
27
+ line,
28
+ reference_line,
29
+ save,
30
+ scatter,
31
+ secondary_axis,
32
+ significance_bracket,
33
+ stem,
34
+ step,
35
+ tag,
36
+ tag_points,
37
+ tag_seaborn,
38
+ violin,
39
+ )
40
+ from .recipe import (
41
+ params, # noqa: E402,F401 (overridable tunables for rerun-plot/Regenerate)
42
+ )
43
+ from .surface import ( # noqa: E402,F401
44
+ surface,
45
+ )
46
+ from .signature_fluxplots import ( # noqa: E402,F401 (preset plot types unique to Flux)
47
+ fluxbox,
48
+ glowbar,
49
+ hexmatrix,
50
+ )
51
+ from .style import ( # noqa: E402,F401 (re-exported for convenience)
52
+ use_dark,
53
+ use_light,
54
+ use_paper,
55
+ )
56
+ from .scene3d import Scene3D, scene3d, SCENE3D_SPEC_VERSION
57
+ from .mesh3d import mesh3d, can_morph
58
+ from .surface3d import surface3d
59
+ from .fields import heatmap, contour, contourf, colorbar, color_scale
60
+ from .brackets import brackets
61
+ from .images import image, scalebar
62
+ from .fits import regression, kde
63
+ from .panels import panel
64
+ from .version import SPEC_VERSION, __version__ # noqa: F401
65
+
66
+ __all__ = [
67
+ "__version__",
68
+ "Scene3D", "scene3d", "mesh3d", "surface3d", "can_morph", "SCENE3D_SPEC_VERSION",
69
+ "surface",
70
+ "glowbar",
71
+ "fluxbox",
72
+ "hexmatrix",
73
+ "panel",
74
+ "heatmap",
75
+ "contour",
76
+ "contourf",
77
+ "colorbar",
78
+ "color_scale",
79
+ "brackets",
80
+ "image",
81
+ "scalebar",
82
+ "regression",
83
+ "kde",
84
+ "SPEC_VERSION",
85
+ "colors",
86
+ "colorcheck",
87
+ "colorscale",
88
+ "stats",
89
+ "style",
90
+ "use_light",
91
+ "use_dark",
92
+ "use_paper",
93
+ "params",
94
+ "legend",
95
+ "line",
96
+ "scatter",
97
+ "secondary_axis",
98
+ "step",
99
+ "stem",
100
+ "bar",
101
+ "barh",
102
+ "errorbar",
103
+ "area",
104
+ "band",
105
+ "box",
106
+ "violin",
107
+ "hist",
108
+ "tag",
109
+ "tag_points",
110
+ "tag_seaborn",
111
+ "significance_bracket",
112
+ "reference_line",
113
+ "annotation",
114
+ "save",
115
+ ]
fluxplot/_fieldmap.py ADDED
@@ -0,0 +1,97 @@
1
+ """One value-to-color law shared by 2D surface and Scene3D; no geometry or rendering."""
2
+ from __future__ import annotations
3
+ import numpy as np
4
+ from matplotlib.colors import Normalize, to_hex, to_rgba
5
+
6
+
7
+ def _normalise_missing(values, missing_below=None, missing_values=()):
8
+ """Normalize explicit sentinel codes; zero remains a real observation."""
9
+ v=np.asarray(values,dtype=float).copy()
10
+ if missing_below is not None: v[v<missing_below]=np.nan
11
+ for mv in missing_values: v[v==mv]=np.nan
12
+ return v
13
+
14
+
15
+ def _resolve_range(finite,color_range,percentile):
16
+ if color_range is not None: return float(color_range[0]),float(color_range[1])
17
+ if percentile is not None: return float(np.percentile(finite,percentile[0])),float(np.percentile(finite,percentile[1]))
18
+ return float(np.min(finite)),float(np.max(finite))
19
+
20
+
21
+ def category_name(code, categories=None):
22
+ """Name of one integer label code: ``categories[code]``, else ``category-<code>``.
23
+
24
+ Negative codes are spelled ``category-m<abs>`` (``-1`` -> ``category-m1``). A plain
25
+ ``category--1`` would slugify to ``category-1`` and collide with code ``+1``, so the
26
+ id of one category would shift depending on whether the other is present.
27
+ """
28
+ code = int(code)
29
+ if categories and code in categories:
30
+ return categories[code]
31
+ return f'category-{code}' if code >= 0 else f'category-m{-code}'
32
+
33
+
34
+ def categorical_colors(names,palette=None,categories=None):
35
+ """Resolve the fixed category-name/code palette in stable sorted-name order.
36
+
37
+ A category the palette does not name takes its colour from the registry
38
+ (``fp.colors.categories``): pinned, or the next free slot of the active cycle, remembered —
39
+ so ``'b'`` is the same colour whether or not ``'a'`` is present, in every figure.
40
+ """
41
+ from .colors import categories as _registry
42
+ resolved={}
43
+ for name in sorted(names):
44
+ color=None
45
+ if palette:
46
+ color=palette.get(name)
47
+ if color is None and categories:
48
+ for code,nm in categories.items():
49
+ if nm==name and code in palette:
50
+ color=palette[code]; break
51
+ if color is None: color=_registry.get(name)
52
+ rgba=to_rgba(color)
53
+ resolved[name]=to_hex(rgba, keep_alpha=rgba[3] < 1) # an opaque colour stays #rrggbb
54
+ return resolved
55
+
56
+
57
+ def continuous_mapping(finite,cmap=None,color_range=None,percentile=None):
58
+ """Return the shared matplotlib colormap and Normalize, including all-missing fallback."""
59
+ if finite.size==0 and color_range is None: lo,hi=0.,1.
60
+ else: lo,hi=_resolve_range(finite,color_range,percentile)
61
+ if not np.isfinite([lo,hi]).all() or lo>hi: raise ValueError('color_range must be finite and nondecreasing')
62
+ return resolve_colormap(cmap),Normalize(vmin=lo,vmax=hi)
63
+
64
+
65
+ def resolve_colormap(cmap=None):
66
+ """A Colormap from ``None`` (the style default), a Colormap, or a name.
67
+
68
+ Names resolve through matplotlib first (so existing names keep their exact maps),
69
+ then through fluxplot's shipped collections: ``'emerald'``, ``'crameri.batlow'``,
70
+ ``'batlow_r'``, or a map added with ``fp.colors.maps.register``.
71
+ """
72
+ import matplotlib as mpl
73
+ if cmap is None:
74
+ return mpl.colormaps[mpl.rcParams['image.cmap']]
75
+ if not isinstance(cmap, str):
76
+ return cmap
77
+ try:
78
+ return mpl.colormaps[cmap]
79
+ except KeyError:
80
+ pass
81
+ from .colors import maps
82
+ try:
83
+ return maps.get(cmap)
84
+ except KeyError:
85
+ raise ValueError(f"unknown colormap {cmap!r}: use a matplotlib name ('viridis') or a "
86
+ "fluxplot map ('emerald', 'crameri.batlow'); list them with "
87
+ "fp.colors.maps.collections()") from None
88
+
89
+
90
+ def infer_kind(finite,kind):
91
+ if kind=='categorical': kind='label'
92
+ if kind=='auto':
93
+ if not finite.size: raise ValueError("every vertex is missing and kind='auto' cannot infer label vs continuous; pass kind explicitly")
94
+ uniq=np.unique(finite)
95
+ kind='label' if uniq.size<=32 and np.allclose(uniq,np.round(uniq)) else 'continuous'
96
+ if kind not in ('label','continuous'): raise ValueError("kind must be 'label', 'continuous', or 'auto'")
97
+ return kind
@@ -0,0 +1,54 @@
1
+ """Optional collapse replay; named-part boundaries never share a simplification pass."""
2
+ from __future__ import annotations
3
+ import warnings
4
+ import numpy as np
5
+
6
+
7
+ def face_budgets(counts, total):
8
+ """Apportion one triangle per part, then remaining capacity by largest remainder."""
9
+ counts=[int(n) for n in counts]
10
+ if total is None: return [None]*len(counts)
11
+ if total<len(counts): raise ValueError('max_faces must allow at least one triangle per part')
12
+ if any(n<1 for n in counts): raise ValueError('mesh parts must contain triangles')
13
+ target=min(int(total),sum(counts)); remaining=target-len(counts)
14
+ capacity=sum(n-1 for n in counts)
15
+ if not capacity: return counts
16
+ portions=[divmod(remaining*(n-1),capacity) for n in counts]
17
+ result=[1+q for q,r in portions]
18
+ for i in sorted(range(len(counts)),key=lambda i:(-portions[i][1],i))[:target-sum(result)]: result[i]+=1
19
+ return result
20
+
21
+
22
+ def reduce_part(part, *, max_faces=None, collapses=None):
23
+ try: import fast_simplification as fs
24
+ except ImportError as exc:
25
+ raise ImportError('max_faces needs the optional fluxplot[mesh] extra (fast-simplification); omit max_faces to save full resolution') from exc
26
+ vertices=part.vertices.copy(); faces=part.faces.copy()
27
+ if collapses is None:
28
+ _,_,collapses=fs.simplify(vertices,faces,target_count=int(max_faces),return_collapses=True)
29
+ collapses=np.asarray(collapses,dtype=np.int32)
30
+ out, outfaces, mapping=fs.replay_simplification(vertices.astype(np.float32),faces,collapses)
31
+ mapping=np.asarray(mapping,dtype=np.int64)
32
+ if len(outfaces)==0: raise ValueError(f'{part.id}: max_faces removed every triangle; choose a larger budget')
33
+ if max_faces is not None and len(outfaces)>max_faces:
34
+ warnings.warn(f'{part.id}: simplifier retained {len(outfaces)} faces above requested max_faces={max_faces}',stacklevel=3)
35
+ # Some backends leave unused output vertices. Compact by index only, in the
36
+ # same order for all states and fields; this bounds stored vertices by 3F.
37
+ used=np.unique(outfaces)
38
+ for name,target in list(part.states.items()):
39
+ sv,sf,_=fs.replay_simplification(target.astype(np.float32),faces,collapses)
40
+ if not np.array_equal(outfaces,sf): raise ValueError(f'state {name}: collapse replay changed topology')
41
+ part.states[name]=np.asarray(sv,dtype=float)[used]
42
+ def remap(array):
43
+ a=np.asarray(array,dtype=float); scalar=a.ndim==1
44
+ if scalar: a=a[:,None]
45
+ valid=np.isfinite(a); good=(mapping>=0)&(mapping<len(out))
46
+ sums=np.zeros((len(out),a.shape[1])); count=np.zeros_like(sums)
47
+ np.add.at(sums,mapping[good],np.where(valid[good],a[good],0))
48
+ np.add.at(count,mapping[good],valid[good].astype(float))
49
+ result=np.divide(sums,count,out=np.full_like(sums,np.nan),where=count>0)
50
+ return result[:,0] if scalar else result
51
+ if part.values is not None: part.values=remap(part.values)[used]
52
+ if part.colors is not None: part.colors=remap(part.colors)[used]
53
+ part.vertices=np.asarray(out,dtype=float)[used]; part.faces=np.searchsorted(used,outfaces).astype(np.int64)
54
+ part.collapses=collapses.copy()
@@ -0,0 +1,95 @@
1
+ """Conservative, scene-specific face caps without running a simplifier."""
2
+ from __future__ import annotations
3
+
4
+ from copy import deepcopy
5
+ import json
6
+ import struct
7
+
8
+ WARN_BYTES = 50 * 1024**2
9
+ WARN_TRIANGLES = 2_000_000
10
+
11
+
12
+ def _json_bytes(value, *, upper):
13
+ # Counts/offsets are bounded by GLB's uint32 size; finite float32 bounds and
14
+ # Python float weights need at most 25 characters. Reserve 34, including
15
+ # quotes, so simple original bounds cannot underestimate collapsed bounds.
16
+ def numbers(item):
17
+ if isinstance(item, dict): return {k: numbers(v) for k, v in item.items()}
18
+ if isinstance(item, list): return [numbers(v) for v in item]
19
+ if isinstance(item, (int, float)) and not isinstance(item, bool):
20
+ return '0' * 32 if upper else 0
21
+ return item
22
+ size = len(json.dumps(numbers(value), sort_keys=True, separators=(',', ':'), allow_nan=False).encode())
23
+ return (size + 3) // 4 * 4
24
+
25
+
26
+ def face_cap_recommendation(scene, glb, *, max_bytes=None, max_triangles=None):
27
+ """Return an actionable per-helper-call cap, or explain why none is safe.
28
+
29
+ Each part is bounded independently by min(original faces, cap), even when
30
+ a call apportions its cap among several parts. The byte bound uses at most
31
+ three referenced vertices per face, every attribute/state, uint32 indices,
32
+ and the actual JSON structure/strings with worst-case numeric widths.
33
+ This is a sufficient bound, not a prediction of backend simplification.
34
+ """
35
+ max_bytes = WARN_BYTES if max_bytes is None else max_bytes
36
+ max_triangles = WARN_TRIANGLES if max_triangles is None else max_triangles
37
+ # A scene may already have been simplified. The hint applies to rerunning
38
+ # the authored calls, so use their original face counts, not a prior quota.
39
+ counts = [len(part.source_faces) if part.source_faces is not None else len(part.faces)
40
+ for part in scene.parts]
41
+ total = sum(counts)
42
+ limit = f'{max_bytes / 1024**2:g} MiB / {max_triangles:,} triangles'
43
+ if len(counts) > max_triangles:
44
+ return f'No max_faces cap can fit {limit} while preserving all {len(counts):,} parts; split the scene or remove parts'
45
+ if total == len(counts) and len(glb) > max_bytes:
46
+ return f'No max_faces cap can fit {limit}: all {len(counts):,} parts already contain one triangle; use fewer parts or shape states, or split the scene'
47
+
48
+ size = struct.unpack_from('<I', glb, 12)[0]
49
+ document = json.loads(glb[20:20 + size])
50
+ # Even ignoring every buffer and shortening every number, these names,
51
+ # target lists and structural keys cannot be removed by a face cap.
52
+ required = deepcopy(document)
53
+ optional_accessors, optional_views = set(), set()
54
+ for mesh in required['meshes']:
55
+ for primitive in mesh['primitives']:
56
+ index = primitive['attributes'].pop('_VALID', None)
57
+ if index is not None:
58
+ optional_accessors.add(index)
59
+ optional_views.add(required['accessors'][index]['bufferView'])
60
+ required['accessors'] = [a for i, a in enumerate(required['accessors']) if i not in optional_accessors]
61
+ required['bufferViews'] = [v for i, v in enumerate(required['bufferViews']) if i not in optional_views]
62
+ for material in required['materials']: material.pop('alphaMode', None)
63
+ if 28 + _json_bytes(required, upper=False) > max_bytes:
64
+ return f'No max_faces cap can fit {limit}: fixed GLB part/state metadata alone exceeds the byte limit; use fewer parts or shape states, or split the scene'
65
+
66
+ # Averaging valid values cannot add a channel normally, but reserve _VALID
67
+ # even for presently all-valid data so the bound does not rely on that.
68
+ for part, mesh in zip(scene.parts, document['meshes']):
69
+ attrs = mesh['primitives'][0]['attributes']
70
+ if part.values is not None and '_VALID' not in attrs:
71
+ attrs['_VALID'] = len(document['accessors'])
72
+ document['accessors'].append({'bufferView': len(document['bufferViews']), 'componentType': 5121, 'count': 1, 'type': 'SCALAR'})
73
+ document['bufferViews'].append({'buffer': 0, 'byteOffset': 0, 'byteLength': 4, 'target': 34962, 'byteStride': 4})
74
+ for material in document['materials']: material['alphaMode'] = 'BLEND'
75
+ overhead = 28 + _json_bytes(document, upper=True)
76
+ costs = [24 + 24 * len(p.states) + (8 if p.values is not None else 0) + (4 if p.colors is not None else 0) for p in scene.parts]
77
+
78
+ def fits(cap):
79
+ faces = [min(count, cap) for count in counts]
80
+ # Compaction after reduction guarantees at most three vertices per face.
81
+ # Do not cap this by today's vertex count: a user may rerun an already
82
+ # simplified scene from its original meshes with the recommended cap.
83
+ binary = sum(3 * count * cost + 12 * count for count, cost in zip(faces, costs))
84
+ return sum(faces) <= max_triangles and overhead + binary <= max_bytes
85
+
86
+ low = max(1, scene._max_call_parts)
87
+ if not fits(low):
88
+ return f'No conservative max_faces cap can be recommended for {limit} while preserving every call\'s parts and shape states; use fewer parts or states, or split the scene'
89
+ high = max(counts)
90
+ while low < high:
91
+ middle = (low + high + 1) // 2
92
+ if fits(middle): low = middle
93
+ else: high = middle - 1
94
+ return (f'use max_faces={low} on each mesh3d/surface3d call to fit {limit} '
95
+ '(conservative bound if the simplifier reaches each requested face cap; retained-face warnings still apply)')
@@ -0,0 +1,23 @@
1
+ three 0.186.1
2
+
3
+ The MIT License
4
+
5
+ Copyright © 2010-2026 three.js authors
6
+
7
+ Permission is hereby granted, free of charge, to any person obtaining a copy
8
+ of this software and associated documentation files (the "Software"), to deal
9
+ in the Software without restriction, including without limitation the rights
10
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11
+ copies of the Software, and to permit persons to whom the Software is
12
+ furnished to do so, subject to the following conditions:
13
+
14
+ The above copyright notice and this permission notice shall be included in
15
+ all copies or substantial portions of the Software.
16
+
17
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
23
+ THE SOFTWARE.