diffract-core 0.2.1__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.
- diffract/__init__.py +52 -0
- diffract/configs/fast_speed_without_disk.ini +48 -0
- diffract/configs/hybrid.ini +74 -0
- diffract/configs/sqlite.ini +62 -0
- diffract/containers.py +405 -0
- diffract/core/__init__.py +23 -0
- diffract/core/cache/__init__.py +24 -0
- diffract/core/cache/containers.py +93 -0
- diffract/core/cache/interface.py +117 -0
- diffract/core/cache/redis_manager.py +464 -0
- diffract/core/cache/simple_manager.py +478 -0
- diffract/core/compute/__init__.py +46 -0
- diffract/core/compute/config.py +48 -0
- diffract/core/compute/containers.py +81 -0
- diffract/core/compute/decorator.py +125 -0
- diffract/core/compute/exceptions.py +31 -0
- diffract/core/compute/execution/__init__.py +14 -0
- diffract/core/compute/execution/_types.py +70 -0
- diffract/core/compute/execution/aggregation.py +74 -0
- diffract/core/compute/execution/aggregation_runner.py +362 -0
- diffract/core/compute/execution/enums.py +38 -0
- diffract/core/compute/execution/executor.py +125 -0
- diffract/core/compute/execution/parameter_runner.py +125 -0
- diffract/core/compute/execution/restrictions.py +61 -0
- diffract/core/compute/execution/strategy.py +118 -0
- diffract/core/compute/execution/utils.py +112 -0
- diffract/core/compute/extensions/__init__.py +0 -0
- diffract/core/compute/extensions/power_law.py +1051 -0
- diffract/core/compute/extensions/rmt.py +150 -0
- diffract/core/compute/extensions/utils.py +36 -0
- diffract/core/compute/field_signature.py +183 -0
- diffract/core/compute/kernels/__init__.py +48 -0
- diffract/core/compute/kernels/alignment.py +106 -0
- diffract/core/compute/kernels/heavy_tailed.py +394 -0
- diffract/core/compute/kernels/marchenko_pastur.py +99 -0
- diffract/core/compute/kernels/mat_decomposition.py +101 -0
- diffract/core/compute/kernels/mat_properties.py +53 -0
- diffract/core/compute/kernels/model_quality.py +27 -0
- diffract/core/compute/kernels/norms.py +88 -0
- diffract/core/compute/kernels/ranks.py +35 -0
- diffract/core/compute/kernels/tracy_widom.py +36 -0
- diffract/core/compute/metadata.py +67 -0
- diffract/core/compute/registry.py +485 -0
- diffract/core/constants.py +147 -0
- diffract/core/data/__init__.py +32 -0
- diffract/core/data/interface.py +304 -0
- diffract/core/data/metadata/__init__.py +15 -0
- diffract/core/data/metadata/containers.py +60 -0
- diffract/core/data/metadata/interface.py +208 -0
- diffract/core/data/metadata/sqlite_index.py +604 -0
- diffract/core/data/nn/__init__.py +43 -0
- diffract/core/data/nn/aggregates/__init__.py +35 -0
- diffract/core/data/nn/aggregates/metadata.py +96 -0
- diffract/core/data/nn/aggregates/proxy.py +57 -0
- diffract/core/data/nn/aggregates/repository.py +87 -0
- diffract/core/data/nn/aggregates/view.py +307 -0
- diffract/core/data/nn/containers.py +86 -0
- diffract/core/data/nn/extractors/__init__.py +49 -0
- diffract/core/data/nn/extractors/base.py +300 -0
- diffract/core/data/nn/extractors/factory.py +234 -0
- diffract/core/data/nn/extractors/flax.py +112 -0
- diffract/core/data/nn/extractors/handlers/__init__.py +39 -0
- diffract/core/data/nn/extractors/handlers/base.py +110 -0
- diffract/core/data/nn/extractors/handlers/flax_handlers.py +71 -0
- diffract/core/data/nn/extractors/handlers/numpy_handlers.py +63 -0
- diffract/core/data/nn/extractors/handlers/onnx_handlers.py +67 -0
- diffract/core/data/nn/extractors/handlers/tensorflow_handlers.py +82 -0
- diffract/core/data/nn/extractors/handlers/torch_handlers.py +124 -0
- diffract/core/data/nn/extractors/interface.py +56 -0
- diffract/core/data/nn/extractors/numpy.py +94 -0
- diffract/core/data/nn/extractors/onnx.py +108 -0
- diffract/core/data/nn/extractors/tensorflow.py +99 -0
- diffract/core/data/nn/extractors/torch.py +204 -0
- diffract/core/data/nn/params/__init__.py +27 -0
- diffract/core/data/nn/params/interface.py +402 -0
- diffract/core/data/nn/params/metadata.py +110 -0
- diffract/core/data/nn/params/proxy.py +93 -0
- diffract/core/data/nn/params/repository.py +45 -0
- diffract/core/data/nn/params/schema.py +82 -0
- diffract/core/data/nn/params/view.py +393 -0
- diffract/core/data/proxy.py +246 -0
- diffract/core/data/repository.py +227 -0
- diffract/core/data/utils.py +33 -0
- diffract/core/data/view.py +389 -0
- diffract/core/export/__init__.py +27 -0
- diffract/core/export/containers.py +47 -0
- diffract/core/export/exporters.py +191 -0
- diffract/core/export/formatters/__init__.py +23 -0
- diffract/core/export/formatters/base.py +68 -0
- diffract/core/export/formatters/dict_formatter.py +38 -0
- diffract/core/export/formatters/json_formatter.py +58 -0
- diffract/core/export/formatters/list_formatter.py +41 -0
- diffract/core/export/formatters/pandas_formatter.py +86 -0
- diffract/core/export/formatters/polars_formatter.py +86 -0
- diffract/core/export/formatters/registry.py +84 -0
- diffract/core/export/interface.py +105 -0
- diffract/core/parallel/__init__.py +21 -0
- diffract/core/parallel/containers.py +57 -0
- diffract/core/parallel/execution.py +91 -0
- diffract/core/parallel/runtime.py +91 -0
- diffract/core/storage/__init__.py +35 -0
- diffract/core/storage/base_manager.py +330 -0
- diffract/core/storage/containers.py +122 -0
- diffract/core/storage/hdf5_manager.py +856 -0
- diffract/core/storage/hybrid_manager.py +218 -0
- diffract/core/storage/interface.py +222 -0
- diffract/core/storage/metadata.py +89 -0
- diffract/core/storage/ram_manager.py +159 -0
- diffract/core/storage/sqlite_manager.py +1062 -0
- diffract/core/storage/zarr_manager.py +992 -0
- diffract/core/utils/__init__.py +45 -0
- diffract/core/utils/build.py +46 -0
- diffract/core/utils/exceptions.py +11 -0
- diffract/core/utils/hashing.py +90 -0
- diffract/core/utils/imports.py +292 -0
- diffract/core/utils/math.py +15 -0
- diffract/py.typed +0 -0
- diffract/session/__init__.py +24 -0
- diffract/session/errors.py +17 -0
- diffract/session/field_cache.py +216 -0
- diffract/session/namespaces/__init__.py +15 -0
- diffract/session/namespaces/compute/__init__.py +219 -0
- diffract/session/namespaces/models/__init__.py +282 -0
- diffract/session/namespaces/models/parameters/__init__.py +133 -0
- diffract/session/namespaces/models/parameters/meta_patcher.py +167 -0
- diffract/session/namespaces/results/__init__.py +378 -0
- diffract/session/namespaces/results/eraser.py +129 -0
- diffract/session/namespaces/results/injester.py +249 -0
- diffract/session/namespaces/utils/__init__.py +141 -0
- diffract/session/namespaces/utils/merger.py +295 -0
- diffract/session/namespaces/viz/__init__.py +91 -0
- diffract/session/namespaces/viz/_utils.py +14 -0
- diffract/session/namespaces/viz/box.py +207 -0
- diffract/session/namespaces/viz/grid.py +160 -0
- diffract/session/namespaces/viz/heatmap.py +164 -0
- diffract/session/namespaces/viz/scatter.py +202 -0
- diffract/session/namespaces/viz/sparkline.py +270 -0
- diffract/session/namespaces/viz/violin.py +212 -0
- diffract/session/session.py +260 -0
- diffract/session/utils.py +81 -0
- diffract/viz/__init__.py +42 -0
- diffract/viz/data/__init__.py +34 -0
- diffract/viz/data/detection.py +57 -0
- diffract/viz/data/extraction.py +117 -0
- diffract/viz/data/filtering.py +57 -0
- diffract/viz/data/ordering.py +129 -0
- diffract/viz/data/provider.py +99 -0
- diffract/viz/data/types.py +98 -0
- diffract/viz/plots/__init__.py +37 -0
- diffract/viz/plots/base/__init__.py +20 -0
- diffract/viz/plots/base/axis.py +219 -0
- diffract/viz/plots/base/coloraxis.py +133 -0
- diffract/viz/plots/base/configurator.py +18 -0
- diffract/viz/plots/base/jitter.py +368 -0
- diffract/viz/plots/base/line.py +197 -0
- diffract/viz/plots/base/marker.py +278 -0
- diffract/viz/plots/base/overlay.py +14 -0
- diffract/viz/plots/base/plot.py +110 -0
- diffract/viz/plots/base/update.py +50 -0
- diffract/viz/plots/boxplot.py +283 -0
- diffract/viz/plots/cluster.py +374 -0
- diffract/viz/plots/heatmap.py +168 -0
- diffract/viz/plots/scatter.py +233 -0
- diffract/viz/plots/sparkline.py +405 -0
- diffract/viz/plots/subplots/__init__.py +32 -0
- diffract/viz/plots/subplots/coloraxis.py +339 -0
- diffract/viz/plots/subplots/factory.py +713 -0
- diffract/viz/plots/subplots/grid.py +214 -0
- diffract/viz/plots/subplots/layout.py +370 -0
- diffract/viz/plots/subplots/spec.py +39 -0
- diffract/viz/plots/violin.py +298 -0
- diffract/viz/renderer.py +513 -0
- diffract/viz/styling/__init__.py +78 -0
- diffract/viz/styling/palettes/__init__.py +20 -0
- diffract/viz/styling/palettes/bundle.py +16 -0
- diffract/viz/styling/palettes/color.py +83 -0
- diffract/viz/styling/palettes/dashes.py +27 -0
- diffract/viz/styling/palettes/symbols.py +42 -0
- diffract/viz/styling/resolvers/__init__.py +11 -0
- diffract/viz/styling/resolvers/categorical.py +68 -0
- diffract/viz/styling/resolvers/color.py +77 -0
- diffract/viz/styling/resolvers/numeric.py +108 -0
- diffract/viz/styling/sources.py +27 -0
- diffract/viz/styling/theme/__init__.py +29 -0
- diffract/viz/styling/theme/applier.py +114 -0
- diffract/viz/styling/theme/components.py +66 -0
- diffract/viz/styling/theme/presets.py +58 -0
- diffract/viz/styling/theme/theme.py +36 -0
- diffract_core-0.2.1.dist-info/METADATA +530 -0
- diffract_core-0.2.1.dist-info/RECORD +193 -0
- diffract_core-0.2.1.dist-info/WHEEL +4 -0
- diffract_core-0.2.1.dist-info/licenses/LICENSE +201 -0
- diffract_core-0.2.1.dist-info/licenses/NOTICE +2 -0
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from typing import Any, Protocol
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class ColorPalette(Protocol):
|
|
8
|
+
"""Protocol for mapping categorical values to colors."""
|
|
9
|
+
|
|
10
|
+
@property
|
|
11
|
+
def colors(self) -> list[str]:
|
|
12
|
+
"""List of colors in the palette."""
|
|
13
|
+
...
|
|
14
|
+
|
|
15
|
+
def get_color(self, value: Any, all_values: list[Any]) -> str:
|
|
16
|
+
"""Get color for a specific value given all values."""
|
|
17
|
+
...
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass(slots=True)
|
|
21
|
+
class DefaultColorPalette:
|
|
22
|
+
"""Default discrete color palette."""
|
|
23
|
+
|
|
24
|
+
_colors: list[str] = field(
|
|
25
|
+
default_factory=lambda: [
|
|
26
|
+
"navy",
|
|
27
|
+
"crimson",
|
|
28
|
+
"green",
|
|
29
|
+
"chocolate",
|
|
30
|
+
"orange",
|
|
31
|
+
"violet",
|
|
32
|
+
"purple",
|
|
33
|
+
"blue",
|
|
34
|
+
"grey",
|
|
35
|
+
"teal",
|
|
36
|
+
]
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
@property
|
|
40
|
+
def colors(self) -> list[str]:
|
|
41
|
+
"""List of colors in the palette."""
|
|
42
|
+
return self._colors
|
|
43
|
+
|
|
44
|
+
def get_color(self, value: Any, all_values: list[Any]) -> str:
|
|
45
|
+
"""Return the color assigned to a value given all values."""
|
|
46
|
+
try:
|
|
47
|
+
idx = list(all_values).index(value)
|
|
48
|
+
except ValueError:
|
|
49
|
+
idx = 0
|
|
50
|
+
return self._colors[idx % len(self._colors)]
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@dataclass(slots=True)
|
|
54
|
+
class DarkColorPalette:
|
|
55
|
+
"""Color palette optimized for dark themes."""
|
|
56
|
+
|
|
57
|
+
_colors: list[str] = field(
|
|
58
|
+
default_factory=lambda: [
|
|
59
|
+
"#5dade2",
|
|
60
|
+
"#f1948a",
|
|
61
|
+
"#58d68d",
|
|
62
|
+
"#f7dc6f",
|
|
63
|
+
"#bb8fce",
|
|
64
|
+
"#85c1e9",
|
|
65
|
+
"#f8c471",
|
|
66
|
+
"#abebc6",
|
|
67
|
+
"#d7bde2",
|
|
68
|
+
"#aed6f1",
|
|
69
|
+
]
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
@property
|
|
73
|
+
def colors(self) -> list[str]:
|
|
74
|
+
"""List of colors in the palette."""
|
|
75
|
+
return self._colors
|
|
76
|
+
|
|
77
|
+
def get_color(self, value: Any, all_values: list[Any]) -> str:
|
|
78
|
+
"""Return the color assigned to a value given all values."""
|
|
79
|
+
try:
|
|
80
|
+
idx = list(all_values).index(value)
|
|
81
|
+
except ValueError:
|
|
82
|
+
idx = 0
|
|
83
|
+
return self._colors[idx % len(self._colors)]
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from typing import Protocol
|
|
5
|
+
|
|
6
|
+
LINE_DASHES = ["solid", "dot", "dash", "longdash", "dashdot", "longdashdot"]
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class DashPalette(Protocol):
|
|
10
|
+
"""Protocol for line dash pattern palettes."""
|
|
11
|
+
|
|
12
|
+
@property
|
|
13
|
+
def dashes(self) -> list[str]:
|
|
14
|
+
"""List of dash patterns in the palette."""
|
|
15
|
+
...
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass(slots=True)
|
|
19
|
+
class DefaultDashPalette:
|
|
20
|
+
"""Default line dash palette."""
|
|
21
|
+
|
|
22
|
+
_dashes: list[str] = field(default_factory=lambda: LINE_DASHES.copy())
|
|
23
|
+
|
|
24
|
+
@property
|
|
25
|
+
def dashes(self) -> list[str]:
|
|
26
|
+
"""List of dash patterns in the palette."""
|
|
27
|
+
return self._dashes
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from typing import Protocol
|
|
5
|
+
|
|
6
|
+
MARKER_SYMBOLS = [
|
|
7
|
+
"circle",
|
|
8
|
+
"square",
|
|
9
|
+
"diamond",
|
|
10
|
+
"cross",
|
|
11
|
+
"x",
|
|
12
|
+
"triangle-up",
|
|
13
|
+
"triangle-down",
|
|
14
|
+
"triangle-left",
|
|
15
|
+
"triangle-right",
|
|
16
|
+
"pentagon",
|
|
17
|
+
"hexagon",
|
|
18
|
+
"star",
|
|
19
|
+
"hourglass",
|
|
20
|
+
"bowtie",
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class SymbolPalette(Protocol):
|
|
25
|
+
"""Protocol for marker symbol palettes."""
|
|
26
|
+
|
|
27
|
+
@property
|
|
28
|
+
def symbols(self) -> list[str]:
|
|
29
|
+
"""List of symbols in the palette."""
|
|
30
|
+
...
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass(slots=True)
|
|
34
|
+
class DefaultSymbolPalette:
|
|
35
|
+
"""Default marker symbol palette."""
|
|
36
|
+
|
|
37
|
+
_symbols: list[str] = field(default_factory=lambda: MARKER_SYMBOLS.copy())
|
|
38
|
+
|
|
39
|
+
@property
|
|
40
|
+
def symbols(self) -> list[str]:
|
|
41
|
+
"""List of symbols in the palette."""
|
|
42
|
+
return self._symbols
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
from .categorical import CategoricalPropertyResolver
|
|
2
|
+
from .color import ColorResolver, ColorSource, ResolvedColor
|
|
3
|
+
from .numeric import NumericPropertyResolver
|
|
4
|
+
|
|
5
|
+
__all__ = [
|
|
6
|
+
"CategoricalPropertyResolver",
|
|
7
|
+
"ColorResolver",
|
|
8
|
+
"ColorSource",
|
|
9
|
+
"NumericPropertyResolver",
|
|
10
|
+
"ResolvedColor",
|
|
11
|
+
]
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
|
|
5
|
+
from diffract.viz.data import DataType, Entry, FieldRef
|
|
6
|
+
from diffract.viz.data.extraction import get_field_data
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass
|
|
10
|
+
class CategoricalPropertyResolver:
|
|
11
|
+
"""Resolves categorical FieldRef values with optional mapping to candidates."""
|
|
12
|
+
|
|
13
|
+
candidates: list[str] | None = None
|
|
14
|
+
|
|
15
|
+
def resolve(
|
|
16
|
+
self,
|
|
17
|
+
source: FieldRef | str | None,
|
|
18
|
+
entries: dict[str, Entry] | None = None,
|
|
19
|
+
) -> str | list[str] | None:
|
|
20
|
+
"""Resolve a categorical source into a value or list of values.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
source: A literal string, a FieldRef, or None.
|
|
24
|
+
entries: Entries used to resolve a FieldRef, if given.
|
|
25
|
+
|
|
26
|
+
Returns:
|
|
27
|
+
The literal string, the resolved per-entry values, or None.
|
|
28
|
+
"""
|
|
29
|
+
match source:
|
|
30
|
+
case None:
|
|
31
|
+
return None
|
|
32
|
+
case str():
|
|
33
|
+
return source
|
|
34
|
+
case FieldRef():
|
|
35
|
+
return self._resolve_field_ref(source, entries)
|
|
36
|
+
|
|
37
|
+
def _resolve_field_ref(
|
|
38
|
+
self,
|
|
39
|
+
ref: FieldRef,
|
|
40
|
+
entries: dict[str, Entry] | None,
|
|
41
|
+
) -> list[str] | None:
|
|
42
|
+
if entries is None:
|
|
43
|
+
return None
|
|
44
|
+
|
|
45
|
+
values, data_type, _ = get_field_data(entries, ref.field)
|
|
46
|
+
if ref.data_type is not None:
|
|
47
|
+
data_type = ref.data_type
|
|
48
|
+
|
|
49
|
+
if data_type != DataType.CATEGORICAL:
|
|
50
|
+
raise ValueError(
|
|
51
|
+
f"Field '{ref.field}' must be categorical for this property, "
|
|
52
|
+
f"got {data_type.name}"
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
str_values = [str(v) for v in values]
|
|
56
|
+
|
|
57
|
+
if self.candidates is None:
|
|
58
|
+
return str_values
|
|
59
|
+
|
|
60
|
+
unique = list(dict.fromkeys(str_values))
|
|
61
|
+
order_indices = ref.ordering.argsort(unique)
|
|
62
|
+
sorted_unique = [unique[i] for i in order_indices]
|
|
63
|
+
mapping = {
|
|
64
|
+
v: self.candidates[i % len(self.candidates)]
|
|
65
|
+
for i, v in enumerate(sorted_unique)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return [mapping[v] for v in str_values]
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from diffract.viz.data import DataType, Entry, FieldRef
|
|
7
|
+
from diffract.viz.data.extraction import get_field_data
|
|
8
|
+
from diffract.viz.styling.palettes import ColorPalette, DefaultColorPalette
|
|
9
|
+
from diffract.viz.styling.sources import ColorSource
|
|
10
|
+
|
|
11
|
+
__all__ = ["ColorResolver", "ColorSource", "ResolvedColor"]
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass
|
|
15
|
+
class ResolvedColor:
|
|
16
|
+
"""Resolved color result: concrete colors and/or numeric values."""
|
|
17
|
+
|
|
18
|
+
color: str | list[str] | None = None
|
|
19
|
+
values: list[float] | None = None
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass
|
|
23
|
+
class ColorResolver:
|
|
24
|
+
"""Resolves color sources into concrete colors or coloraxis config."""
|
|
25
|
+
|
|
26
|
+
palette: ColorPalette = field(default_factory=DefaultColorPalette)
|
|
27
|
+
|
|
28
|
+
def resolve(
|
|
29
|
+
self,
|
|
30
|
+
source: ColorSource,
|
|
31
|
+
entries: dict[str, Entry] | None = None,
|
|
32
|
+
) -> ResolvedColor:
|
|
33
|
+
"""Resolve a color source into concrete colors."""
|
|
34
|
+
match source:
|
|
35
|
+
case None:
|
|
36
|
+
return ResolvedColor()
|
|
37
|
+
case str():
|
|
38
|
+
return ResolvedColor(color=source)
|
|
39
|
+
case FieldRef():
|
|
40
|
+
return self._resolve_field_ref(source, entries)
|
|
41
|
+
|
|
42
|
+
def _resolve_field_ref(
|
|
43
|
+
self,
|
|
44
|
+
ref: FieldRef,
|
|
45
|
+
entries: dict[str, Entry] | None,
|
|
46
|
+
) -> ResolvedColor:
|
|
47
|
+
if entries is None:
|
|
48
|
+
return ResolvedColor()
|
|
49
|
+
|
|
50
|
+
values, data_type, _ = get_field_data(entries, ref.field)
|
|
51
|
+
if ref.data_type is not None:
|
|
52
|
+
data_type = ref.data_type
|
|
53
|
+
|
|
54
|
+
match data_type:
|
|
55
|
+
case DataType.CATEGORICAL:
|
|
56
|
+
return self._resolve_categorical(values, ref)
|
|
57
|
+
case DataType.NUMERIC:
|
|
58
|
+
return self._resolve_numeric(values)
|
|
59
|
+
|
|
60
|
+
def _resolve_categorical(self, values: list[Any], ref: FieldRef) -> ResolvedColor:
|
|
61
|
+
unique_values = list(dict.fromkeys(values))
|
|
62
|
+
order_indices = ref.ordering.argsort(unique_values)
|
|
63
|
+
sorted_unique = [unique_values[i] for i in order_indices]
|
|
64
|
+
colors = [self.palette.get_color(v, sorted_unique) for v in values]
|
|
65
|
+
return ResolvedColor(color=colors)
|
|
66
|
+
|
|
67
|
+
def _resolve_numeric(self, values: list[Any]) -> ResolvedColor:
|
|
68
|
+
float_values: list[Any] = []
|
|
69
|
+
for v in values:
|
|
70
|
+
if v is None:
|
|
71
|
+
float_values.append(float("nan"))
|
|
72
|
+
else:
|
|
73
|
+
try:
|
|
74
|
+
float_values.append(float(v))
|
|
75
|
+
except (TypeError, ValueError):
|
|
76
|
+
float_values.append(v)
|
|
77
|
+
return ResolvedColor(values=float_values)
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import math
|
|
4
|
+
from collections.abc import Iterable
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
|
|
9
|
+
from diffract.viz.data import DataShape, DataType, Entry, FieldRef
|
|
10
|
+
from diffract.viz.data.extraction import get_field_data
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass
|
|
14
|
+
class NumericPropertyResolver:
|
|
15
|
+
"""Resolves numeric FieldRef values with optional normalization."""
|
|
16
|
+
|
|
17
|
+
output_range: tuple[float, float] | None = None
|
|
18
|
+
|
|
19
|
+
def resolve(
|
|
20
|
+
self,
|
|
21
|
+
source: FieldRef | float | None,
|
|
22
|
+
entries: dict[str, Entry] | None = None,
|
|
23
|
+
) -> float | list[float] | None:
|
|
24
|
+
"""Resolve a numeric source into a value or list of values.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
source: A literal number, a FieldRef, or None.
|
|
28
|
+
entries: Entries used to resolve a FieldRef, if given.
|
|
29
|
+
|
|
30
|
+
Returns:
|
|
31
|
+
The literal number, the resolved per-entry values, or None.
|
|
32
|
+
"""
|
|
33
|
+
match source:
|
|
34
|
+
case None:
|
|
35
|
+
return None
|
|
36
|
+
case int() | float():
|
|
37
|
+
return source
|
|
38
|
+
case FieldRef():
|
|
39
|
+
return self._resolve_field_ref(source, entries)
|
|
40
|
+
|
|
41
|
+
def _resolve_field_ref(
|
|
42
|
+
self,
|
|
43
|
+
ref: FieldRef,
|
|
44
|
+
entries: dict[str, Entry] | None,
|
|
45
|
+
) -> list[float] | None:
|
|
46
|
+
if entries is None:
|
|
47
|
+
return None
|
|
48
|
+
|
|
49
|
+
values, data_type, data_shape = get_field_data(entries, ref.field)
|
|
50
|
+
if ref.data_type is not None:
|
|
51
|
+
data_type = ref.data_type
|
|
52
|
+
|
|
53
|
+
if data_type != DataType.NUMERIC:
|
|
54
|
+
raise ValueError(
|
|
55
|
+
f"Field '{ref.field}' must be numeric for this property, "
|
|
56
|
+
f"got {data_type.name}"
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
if data_shape == DataShape.SCALAR:
|
|
60
|
+
float_values = [float(v) if v is not None else float("nan") for v in values]
|
|
61
|
+
else:
|
|
62
|
+
float_values = [
|
|
63
|
+
v if v is not None else [float("nan")] * len(v) for v in values
|
|
64
|
+
]
|
|
65
|
+
|
|
66
|
+
return self._normalize(float_values)
|
|
67
|
+
|
|
68
|
+
def _normalize(
|
|
69
|
+
self,
|
|
70
|
+
values: list[float | list[float]],
|
|
71
|
+
data_min: float | None = None,
|
|
72
|
+
data_max: float | None = None,
|
|
73
|
+
) -> list[float | list[float]]:
|
|
74
|
+
if self.output_range is None:
|
|
75
|
+
return values
|
|
76
|
+
|
|
77
|
+
if isinstance(values[0], Iterable):
|
|
78
|
+
for v in values:
|
|
79
|
+
valid = [v_item for v_item in v if not math.isnan(v_item)]
|
|
80
|
+
partial_min, partial_max = min(valid), max(valid)
|
|
81
|
+
if data_min is None or partial_min < data_min:
|
|
82
|
+
data_min = partial_min
|
|
83
|
+
if data_max is None or partial_max > data_max:
|
|
84
|
+
data_max = partial_max
|
|
85
|
+
return [self._normalize(v, data_min, data_max) for v in values]
|
|
86
|
+
valid = [v for v in values if not math.isnan(v)]
|
|
87
|
+
if not valid:
|
|
88
|
+
return values
|
|
89
|
+
|
|
90
|
+
if data_min is None:
|
|
91
|
+
data_min = min(valid)
|
|
92
|
+
|
|
93
|
+
if data_max is None:
|
|
94
|
+
data_max = max(valid)
|
|
95
|
+
|
|
96
|
+
if data_min == data_max:
|
|
97
|
+
mid = (self.output_range[0] + self.output_range[1]) / 2
|
|
98
|
+
return [mid if not math.isnan(v) else float("nan") for v in values]
|
|
99
|
+
|
|
100
|
+
out_min, out_max = self.output_range
|
|
101
|
+
scale = (out_max - out_min) / (data_max - data_min)
|
|
102
|
+
|
|
103
|
+
return np.array(
|
|
104
|
+
[
|
|
105
|
+
out_min + (v - data_min) * scale if not math.isnan(v) else float("nan")
|
|
106
|
+
for v in values
|
|
107
|
+
]
|
|
108
|
+
)
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""Typed sources for style properties that accept literals or field names.
|
|
2
|
+
|
|
3
|
+
Each alias annotates ``FieldRef | str | None`` with the kind of plotly
|
|
4
|
+
literal the property accepts. The renderer's config-time coercion pass
|
|
5
|
+
uses the annotation to disambiguate bare strings deterministically: a
|
|
6
|
+
string that is a valid literal of that kind stays a literal, anything
|
|
7
|
+
else becomes a ``FieldRef``. Explicit ``FieldRef`` objects (e.g. the
|
|
8
|
+
refs-style configs) bypass the rule entirely.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from enum import Enum
|
|
12
|
+
from typing import Annotated
|
|
13
|
+
|
|
14
|
+
from diffract.viz.data import FieldRef
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class StyleLiteralKind(Enum):
|
|
18
|
+
"""Kind of plotly literal a style property accepts besides a FieldRef."""
|
|
19
|
+
|
|
20
|
+
COLOR = "color"
|
|
21
|
+
SYMBOL = "symbol"
|
|
22
|
+
DASH = "dash"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
ColorSource = Annotated[FieldRef | str | None, StyleLiteralKind.COLOR]
|
|
26
|
+
SymbolSource = Annotated[FieldRef | str | None, StyleLiteralKind.SYMBOL]
|
|
27
|
+
DashSource = Annotated[FieldRef | str | None, StyleLiteralKind.DASH]
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
from .applier import apply_theme
|
|
2
|
+
from .components import (
|
|
3
|
+
AxesStyle,
|
|
4
|
+
BackgroundStyle,
|
|
5
|
+
ColorbarStyle,
|
|
6
|
+
LayoutStyle,
|
|
7
|
+
LegendStyle,
|
|
8
|
+
TypographyStyle,
|
|
9
|
+
)
|
|
10
|
+
from .presets import DARK_THEME, DEFAULT_THEME, MINIMAL_THEME
|
|
11
|
+
from .theme import Theme
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
"DARK_THEME",
|
|
15
|
+
# Presets
|
|
16
|
+
"DEFAULT_THEME",
|
|
17
|
+
"MINIMAL_THEME",
|
|
18
|
+
"AxesStyle",
|
|
19
|
+
"BackgroundStyle",
|
|
20
|
+
"ColorbarStyle",
|
|
21
|
+
# Components
|
|
22
|
+
"LayoutStyle",
|
|
23
|
+
"LegendStyle",
|
|
24
|
+
# Theme
|
|
25
|
+
"Theme",
|
|
26
|
+
"TypographyStyle",
|
|
27
|
+
# Applier
|
|
28
|
+
"apply_theme",
|
|
29
|
+
]
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
|
|
5
|
+
if TYPE_CHECKING:
|
|
6
|
+
import plotly.graph_objects as go
|
|
7
|
+
|
|
8
|
+
from .theme import Theme
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def apply_theme(fig: go.Figure, theme: Theme) -> go.Figure:
|
|
12
|
+
"""Apply figure-level styling from theme.
|
|
13
|
+
|
|
14
|
+
This applies layout, typography, background, legend, and colorbar styling.
|
|
15
|
+
Does NOT override trace-specific settings or axes configured by configurators.
|
|
16
|
+
|
|
17
|
+
Args:
|
|
18
|
+
fig: Plotly Figure to style.
|
|
19
|
+
theme: Theme to apply.
|
|
20
|
+
|
|
21
|
+
Returns:
|
|
22
|
+
The same figure (mutated in place) for chaining.
|
|
23
|
+
"""
|
|
24
|
+
_apply_layout(fig, theme)
|
|
25
|
+
_apply_background(fig, theme)
|
|
26
|
+
_apply_typography(fig, theme)
|
|
27
|
+
_apply_legend(fig, theme)
|
|
28
|
+
_apply_colorbar_defaults(fig, theme)
|
|
29
|
+
|
|
30
|
+
return fig
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _apply_layout(fig: go.Figure, theme: Theme) -> None:
|
|
34
|
+
"""Apply layout dimensions and margins."""
|
|
35
|
+
if theme.layout.width is not None:
|
|
36
|
+
fig.update_layout(width=theme.layout.width)
|
|
37
|
+
|
|
38
|
+
if theme.layout.height is not None:
|
|
39
|
+
fig.update_layout(height=theme.layout.height)
|
|
40
|
+
|
|
41
|
+
fig.update_layout(margin=theme.layout.margin)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _apply_background(fig: go.Figure, theme: Theme) -> None:
|
|
45
|
+
"""Apply background colors."""
|
|
46
|
+
fig.update_layout(
|
|
47
|
+
plot_bgcolor=theme.background.plot_bgcolor,
|
|
48
|
+
paper_bgcolor=theme.background.paper_bgcolor,
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _apply_typography(fig: go.Figure, theme: Theme) -> None:
|
|
53
|
+
"""Apply font settings."""
|
|
54
|
+
fig.update_layout(
|
|
55
|
+
font=dict(
|
|
56
|
+
family=theme.typography.font_family,
|
|
57
|
+
size=theme.typography.label_font_size,
|
|
58
|
+
),
|
|
59
|
+
title=dict(
|
|
60
|
+
font=dict(
|
|
61
|
+
family=theme.typography.font_family,
|
|
62
|
+
size=theme.typography.title_font_size,
|
|
63
|
+
)
|
|
64
|
+
),
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
# Style annotations with consistent font
|
|
68
|
+
fig.update_annotations(
|
|
69
|
+
font=dict(
|
|
70
|
+
family=theme.typography.font_family,
|
|
71
|
+
size=theme.typography.label_font_size,
|
|
72
|
+
)
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _apply_legend(fig: go.Figure, theme: Theme) -> None:
|
|
77
|
+
"""Apply legend styling."""
|
|
78
|
+
fig.update_layout(
|
|
79
|
+
legend=dict(
|
|
80
|
+
font=dict(
|
|
81
|
+
size=theme.legend.font_size,
|
|
82
|
+
family=theme.typography.font_family,
|
|
83
|
+
),
|
|
84
|
+
bgcolor=theme.legend.bgcolor,
|
|
85
|
+
bordercolor=theme.legend.border_color,
|
|
86
|
+
borderwidth=theme.legend.border_width,
|
|
87
|
+
)
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _apply_colorbar_defaults(fig: go.Figure, theme: Theme) -> None:
|
|
92
|
+
"""Apply colorbar styling to all coloraxis."""
|
|
93
|
+
colorbar_cfg = dict(
|
|
94
|
+
orientation=theme.colorbar.orientation,
|
|
95
|
+
x=theme.colorbar.x,
|
|
96
|
+
y=theme.colorbar.y,
|
|
97
|
+
xanchor=theme.colorbar.xanchor,
|
|
98
|
+
yanchor=theme.colorbar.yanchor,
|
|
99
|
+
thickness=theme.colorbar.thickness,
|
|
100
|
+
len=theme.colorbar.len,
|
|
101
|
+
tickfont=dict(
|
|
102
|
+
size=theme.typography.tick_font_size,
|
|
103
|
+
family=theme.typography.font_family,
|
|
104
|
+
),
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
for attr in dir(fig.layout):
|
|
108
|
+
if attr.startswith("coloraxis"):
|
|
109
|
+
coloraxis = getattr(fig.layout, attr)
|
|
110
|
+
colorbar = getattr(coloraxis, "colorbar", None)
|
|
111
|
+
if colorbar is None:
|
|
112
|
+
coloraxis.update(dict(colorbar=colorbar_cfg))
|
|
113
|
+
else:
|
|
114
|
+
colorbar.update(colorbar_cfg)
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@dataclass
|
|
7
|
+
class LayoutStyle:
|
|
8
|
+
"""Figure layout dimensions and margins."""
|
|
9
|
+
|
|
10
|
+
width: int | None = None
|
|
11
|
+
height: int | None = None
|
|
12
|
+
margin: dict[str, int] = field(
|
|
13
|
+
default_factory=lambda: {"l": 80, "r": 40, "t": 60, "b": 80}
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass
|
|
18
|
+
class TypographyStyle:
|
|
19
|
+
"""Font settings for the figure."""
|
|
20
|
+
|
|
21
|
+
font_family: str = "Times New Roman"
|
|
22
|
+
title_font_size: int = 16
|
|
23
|
+
label_font_size: int = 14
|
|
24
|
+
tick_font_size: int = 12
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass
|
|
28
|
+
class BackgroundStyle:
|
|
29
|
+
"""Background colors for the figure."""
|
|
30
|
+
|
|
31
|
+
plot_bgcolor: str = "white"
|
|
32
|
+
paper_bgcolor: str = "white"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass
|
|
36
|
+
class AxesStyle:
|
|
37
|
+
"""Default axes styling (can be overridden by SupportsAxis fields)."""
|
|
38
|
+
|
|
39
|
+
grid_color: str = "lightgrey"
|
|
40
|
+
line_color: str = "black"
|
|
41
|
+
show_grid: bool = True
|
|
42
|
+
show_line: bool = True
|
|
43
|
+
mirror: bool = True
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@dataclass
|
|
47
|
+
class LegendStyle:
|
|
48
|
+
"""Legend styling."""
|
|
49
|
+
|
|
50
|
+
bgcolor: str = "rgba(255,255,255,0.9)"
|
|
51
|
+
border_color: str = "gray"
|
|
52
|
+
border_width: int = 1
|
|
53
|
+
font_size: int = 12
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@dataclass
|
|
57
|
+
class ColorbarStyle:
|
|
58
|
+
"""Colorbar positioning and appearance."""
|
|
59
|
+
|
|
60
|
+
orientation: str = "h"
|
|
61
|
+
x: float = 0.5
|
|
62
|
+
y: float = -0.15
|
|
63
|
+
xanchor: str = "center"
|
|
64
|
+
yanchor: str = "top"
|
|
65
|
+
thickness: int = 15
|
|
66
|
+
len: float = 0.5
|