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
fluxplot/surface3d.py
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
"""Named categorical and continuous 3D surface fields with the 2D surface color law."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
from collections.abc import Mapping
|
|
4
|
+
from copy import deepcopy
|
|
5
|
+
import warnings
|
|
6
|
+
import numpy as np
|
|
7
|
+
from matplotlib.colors import to_hex, to_rgba
|
|
8
|
+
from matplotlib.ticker import MaxNLocator
|
|
9
|
+
from ._fieldmap import _normalise_missing, infer_kind, categorical_colors, category_name, continuous_mapping
|
|
10
|
+
from .surface import _face_labels, _face_values
|
|
11
|
+
from .mesh3d import (load_mesh, _states, _prepare_part, _reference, _publish, _named_meshes,
|
|
12
|
+
_state_for_part, _state_vertices, _check_max_faces, _check_alpha, _with_alpha,
|
|
13
|
+
_warn_unknown_palette)
|
|
14
|
+
from .scene3d import Scene3D, MeshPart, _clean_float
|
|
15
|
+
from .ids import series_id, slugify
|
|
16
|
+
from .descriptors import Mark
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _face_rows_subset(subset, faces):
|
|
20
|
+
"""True when every triangle of ``subset`` is also a triangle of ``faces``."""
|
|
21
|
+
def rows(a):
|
|
22
|
+
a = np.ascontiguousarray(a, dtype=np.int64)
|
|
23
|
+
return a.view(np.dtype((np.void, a.dtype.itemsize * a.shape[1]))).ravel()
|
|
24
|
+
return bool(np.isin(rows(subset), rows(faces)).all())
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def surface3d(scene,values,*,series,surfaces,kind='auto',categories=None,palette=None,cmap=None,
|
|
28
|
+
color_range=None,percentile=None,missing_below=None,missing_values=(),missing_color='#D8D8D8',
|
|
29
|
+
colorbar=False,cbar_label=None,cbar_ticks=None,legend=None,legend_missing=False,label=None,
|
|
30
|
+
alpha=None,states=None,sequence=False,share_topology_with=None,morph_group=None,max_faces=None):
|
|
31
|
+
"""Add a per-vertex value map on a mesh: a categorical atlas or a continuous field.
|
|
32
|
+
|
|
33
|
+
The value-to-colour law is the one :func:`fp.surface <fluxplot.surface>` uses (same
|
|
34
|
+
arguments and missing-data rules). The raw values are saved too, so Flux can change
|
|
35
|
+
the colormap and limits without rerunning Python.
|
|
36
|
+
|
|
37
|
+
Parameters
|
|
38
|
+
----------
|
|
39
|
+
scene
|
|
40
|
+
The :func:`fp.scene3d <fluxplot.scene3d>` to add to.
|
|
41
|
+
values
|
|
42
|
+
One value per vertex: an array in mesh order (concatenated when ``surfaces`` is a
|
|
43
|
+
mapping) or ``{name: array}`` matching ``surfaces``.
|
|
44
|
+
series
|
|
45
|
+
Part-id prefix: label maps give ``<series>.<category>``, continuous maps
|
|
46
|
+
``<series>.field``; faces touching missing data form ``<series>.missing``.
|
|
47
|
+
surfaces
|
|
48
|
+
One mesh (any :func:`mesh3d` input) or ``{name: mesh}``, e.g. both hemispheres.
|
|
49
|
+
A mapping is drawn as one combined mesh; for separately hideable hemispheres,
|
|
50
|
+
call ``surface3d`` once per hemisphere with its own series and a shared
|
|
51
|
+
``color_range``.
|
|
52
|
+
kind
|
|
53
|
+
``"label"`` (``"categorical"``), ``"continuous"``, or ``"auto"`` (label when the
|
|
54
|
+
data are integral and have at most 32 distinct values).
|
|
55
|
+
categories
|
|
56
|
+
``{code: name}`` for label maps. Unnamed codes become ``category-<code>``
|
|
57
|
+
(negative codes ``category-m<abs>``).
|
|
58
|
+
palette
|
|
59
|
+
``{name_or_code: color}`` for label maps; fixed, never remapped. Codes need
|
|
60
|
+
``categories``.
|
|
61
|
+
cmap, color_range, percentile
|
|
62
|
+
Continuous styling. ``cmap`` is a Colormap or a name: matplotlib's, or fluxplot's
|
|
63
|
+
collections (``"emerald"``, ``"crameri.batlow"``). ``percentile=(2, 98)`` clips
|
|
64
|
+
to those percentiles; an explicit ``color_range=(lo, hi)`` wins. Pass the same
|
|
65
|
+
``color_range`` to several calls (e.g. two hemispheres) for identical colours.
|
|
66
|
+
missing_below, missing_values, missing_color
|
|
67
|
+
Values below ``missing_below`` or in ``missing_values`` are missing, like NaN
|
|
68
|
+
and infinities; zero stays a real value. Missing faces use ``missing_color``.
|
|
69
|
+
colorbar, cbar_label, cbar_ticks
|
|
70
|
+
Add a colorbar for a continuous map, with an optional title and fixed ticks.
|
|
71
|
+
legend, legend_missing
|
|
72
|
+
Add the category parts to the scene legend (default: on for label maps). A
|
|
73
|
+
continuous field has no legend swatch; its key is the colorbar.
|
|
74
|
+
``legend_missing=True`` adds the missing-data part.
|
|
75
|
+
label
|
|
76
|
+
For a continuous map, the field's display label. For a label map it names the
|
|
77
|
+
whole series (as in ``fp.surface``); categories keep their names.
|
|
78
|
+
alpha
|
|
79
|
+
Opacity in [0, 1] for every part of this call.
|
|
80
|
+
states, sequence, share_topology_with, morph_group, max_faces
|
|
81
|
+
As in :func:`mesh3d`. States are other shapes of the same mesh (bare N×3
|
|
82
|
+
vertex arrays are fine), e.g. ``states={"inflated": inflated}``.
|
|
83
|
+
|
|
84
|
+
Returns
|
|
85
|
+
-------
|
|
86
|
+
list of MeshPart
|
|
87
|
+
The new parts, in order.
|
|
88
|
+
|
|
89
|
+
Examples
|
|
90
|
+
--------
|
|
91
|
+
>>> cx = fp.scene3d(figsize=(3, 3), units="mm")
|
|
92
|
+
>>> fp.surface3d(cx, thickness, series="thickness", surfaces=pial, kind="continuous",
|
|
93
|
+
... cmap="emerald", percentile=(2, 98), colorbar=True,
|
|
94
|
+
... cbar_label="Thickness (mm)", states={"inflated": inflated})
|
|
95
|
+
"""
|
|
96
|
+
if not isinstance(scene,Scene3D): raise TypeError('first argument must be fp.scene3d()')
|
|
97
|
+
max_faces=_check_max_faces(max_faces)
|
|
98
|
+
alpha=_check_alpha(alpha)
|
|
99
|
+
meshes,named=_named_meshes(surfaces)
|
|
100
|
+
if not meshes: raise ValueError('surfaces mapping is empty')
|
|
101
|
+
shapes=_states(states,sequence)
|
|
102
|
+
verts=[]; faces=[]; vals=[]; state_parts={k:[] for k in shapes}; offset=0
|
|
103
|
+
flat=None if isinstance(values,Mapping) else np.asarray(values,dtype=float)
|
|
104
|
+
for name,spec in meshes.items():
|
|
105
|
+
v,f=load_mesh(spec); n=len(v)
|
|
106
|
+
if isinstance(values,Mapping):
|
|
107
|
+
if name not in values: raise ValueError(f'values has no array for surface {name!r}')
|
|
108
|
+
val=np.asarray(values[name],dtype=float)
|
|
109
|
+
else:
|
|
110
|
+
val=flat[offset:offset+n]
|
|
111
|
+
if val.shape!=(n,): raise ValueError(f'{name}: exactly one value per vertex is required ({len(val)} values for {n} vertices)')
|
|
112
|
+
vals.append(_normalise_missing(val,missing_below,missing_values))
|
|
113
|
+
verts.append(v); faces.append(f+offset)
|
|
114
|
+
for state,target in shapes.items():
|
|
115
|
+
shape=_state_for_part(target,state,name,named)
|
|
116
|
+
state_parts[state].append(_state_vertices(shape,v,f,state,f'{series}.{name}' if named else str(series)))
|
|
117
|
+
offset+=n
|
|
118
|
+
if flat is not None and flat.shape!=(offset,): raise ValueError(f'values length must equal the total vertex count ({flat.size} values for {offset} vertices)')
|
|
119
|
+
v=np.concatenate(verts); f=np.concatenate(faces); data=np.concatenate(vals)
|
|
120
|
+
finite=data[np.isfinite(data)]; kind=infer_kind(finite,kind)
|
|
121
|
+
if not finite.size: warnings.warn('surface3d(): every vertex is missing; drawing no-data',stacklevel=2)
|
|
122
|
+
# Make all scientific non-finites missing before face assignment and serialization.
|
|
123
|
+
data[~np.isfinite(data)]=np.nan
|
|
124
|
+
fv=_face_labels(data,f) if kind=='label' else _face_values(data,f)
|
|
125
|
+
missing=~np.isfinite(fv)
|
|
126
|
+
groups={}
|
|
127
|
+
field_spec=None; rgba=None
|
|
128
|
+
if kind=='label':
|
|
129
|
+
for code in np.unique(fv[~missing]):
|
|
130
|
+
name=str(category_name(code,categories))
|
|
131
|
+
if name=='missing': raise ValueError("category name 'missing' is reserved for no-data")
|
|
132
|
+
groups.setdefault(name,np.zeros(len(f),dtype=bool))
|
|
133
|
+
groups[name]|=fv==code
|
|
134
|
+
known=[*groups,*(categories or {}),*(categories or {}).values()]
|
|
135
|
+
_warn_unknown_palette(palette,list(dict.fromkeys(known)),
|
|
136
|
+
hint='; palette codes need categories={code: name}, or key it by part name')
|
|
137
|
+
resolved=categorical_colors(groups,palette,categories)
|
|
138
|
+
else:
|
|
139
|
+
if palette:
|
|
140
|
+
warnings.warn('palette= applies to label maps and is ignored for a continuous field; use cmap=',stacklevel=2)
|
|
141
|
+
cmap_obj,norm=continuous_mapping(finite,cmap,color_range,percentile)
|
|
142
|
+
rgba=np.asarray(cmap_obj(norm(data)))
|
|
143
|
+
rgba[~np.isfinite(data)]=to_rgba(missing_color)
|
|
144
|
+
# Dense stops preserve the used mapping as portable data; no cmap package in Flux.
|
|
145
|
+
points=np.linspace(0,1,256)
|
|
146
|
+
field_spec={'cmap':{'name':cmap_obj.name,'stops':[[float(x),to_hex(cmap_obj(x),keep_alpha=False)] for x in points]},
|
|
147
|
+
'range':[float(norm.vmin),float(norm.vmax)],'missingColor':to_hex(missing_color)}
|
|
148
|
+
if percentile is not None: field_spec['rule']={'percentile':list(percentile)}
|
|
149
|
+
if cbar_label is not None: field_spec['label']=str(cbar_label)
|
|
150
|
+
if cbar_ticks is not None:
|
|
151
|
+
ticks=[float(x) for x in cbar_ticks]
|
|
152
|
+
else:
|
|
153
|
+
ticks=[_clean_float(x) for x in MaxNLocator(nbins=5).tick_values(norm.vmin,norm.vmax)]
|
|
154
|
+
field_spec['ticks']=[x for x in ticks if norm.vmin<=x<=norm.vmax]
|
|
155
|
+
groups['field']=~missing; resolved={'field':'#FFFFFF'}
|
|
156
|
+
if missing.any(): groups['missing']=missing; resolved['missing']=to_hex(missing_color)
|
|
157
|
+
if max_faces is not None and max_faces<sum(bool(mask.any()) for mask in groups.values()):
|
|
158
|
+
raise ValueError('max_faces must allow at least one triangle per semantic part')
|
|
159
|
+
allstates={k:np.concatenate(a) for k,a in state_parts.items()}
|
|
160
|
+
alloc=deepcopy(scene._alloc); parts=[]; marks=[]; legend_ids=[]
|
|
161
|
+
from ._mesh_reduce import face_budgets
|
|
162
|
+
nonempty=[(name,mask) for name,mask in sorted(groups.items()) if mask.any()]
|
|
163
|
+
budgets=face_budgets([mask.sum() for name,mask in nonempty],max_faces)
|
|
164
|
+
for (name,mask),budget in zip(nonempty,budgets):
|
|
165
|
+
pid=alloc.take(series_id(series,slugify(name)))
|
|
166
|
+
ref=_reference(share_topology_with,pid,single=len(groups)==1)
|
|
167
|
+
if (isinstance(ref,MeshPart) and ref.source_count==len(v) and not np.array_equal(ref.source_faces,f[mask])
|
|
168
|
+
and _face_rows_subset(ref.source_faces,f)):
|
|
169
|
+
what='category labels' if kind=='label' and name!='missing' else 'missing-data vertices'
|
|
170
|
+
raise ValueError(f'{pid}: same mesh as its reference {ref.id}, but the {what} differ, so this part '
|
|
171
|
+
'covers different faces; morph partners need identical labels and missing data')
|
|
172
|
+
# States and reference must use this exact face subset so category boundaries persist.
|
|
173
|
+
part=_prepare_part(pid,(v,f[mask]),_with_alpha(resolved[name],alpha),allstates,reference=ref,max_faces=budget,
|
|
174
|
+
values=data if kind=='continuous' or name=='missing' else None,
|
|
175
|
+
colors=rgba if kind=='continuous' and name=='field' else None)
|
|
176
|
+
if kind=='continuous' and name=='field':
|
|
177
|
+
part.colors=np.asarray(cmap_obj(norm(part.values)))
|
|
178
|
+
part.colors[~np.isfinite(part.values)]=to_rgba(missing_color)
|
|
179
|
+
partkind='missing' if name=='missing' else ('field' if kind=='continuous' else 'mesh')
|
|
180
|
+
# A continuous field is the call's one data part, so label= names it; category
|
|
181
|
+
# parts keep their names and label= names the series (as in 2D surface).
|
|
182
|
+
part_label=str(label) if partkind=='field' and label is not None else str(name)
|
|
183
|
+
spec={'id':pid,'role':'surface-field' if partkind=='field' else 'surface-region','kind':partkind,
|
|
184
|
+
'node':pid,'series':str(series),'label':part_label,'color':part.color}
|
|
185
|
+
if partkind=='field': spec['field']=field_spec
|
|
186
|
+
parts.append(part); marks.append(Mark(role=spec['role'],series=str(series),name=name,kind='surface',label=label,gid=pid,data={'scene3d':spec}))
|
|
187
|
+
if partkind=='mesh' or (partkind=='missing' and legend_missing): legend_ids.append(pid)
|
|
188
|
+
field_ids=[m.gid for m in marks if m.data['scene3d']['kind']=='field']
|
|
189
|
+
if colorbar and field_ids:
|
|
190
|
+
pid=alloc.take(series_id(series,'colorbar')); field_id=field_ids[0]
|
|
191
|
+
marks.append(Mark(role='colorbar',series=str(series),name='colorbar',gid=pid,data={'scene3d':{'id':pid,'role':'colorbar','kind':'furniture','field':field_id}}))
|
|
192
|
+
if legend and kind=='continuous' and not legend_missing:
|
|
193
|
+
warnings.warn('a continuous field has no legend swatch; use colorbar=True for its key '
|
|
194
|
+
'(legend_missing=True still lists the missing-data part)',stacklevel=2)
|
|
195
|
+
result=_publish(scene,parts,marks,alloc,sequence=sequence,morph_group=morph_group)
|
|
196
|
+
if legend if legend is not None else kind=='label': scene._legend_entries.extend(legend_ids)
|
|
197
|
+
return result
|