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.
Files changed (193) hide show
  1. diffract/__init__.py +52 -0
  2. diffract/configs/fast_speed_without_disk.ini +48 -0
  3. diffract/configs/hybrid.ini +74 -0
  4. diffract/configs/sqlite.ini +62 -0
  5. diffract/containers.py +405 -0
  6. diffract/core/__init__.py +23 -0
  7. diffract/core/cache/__init__.py +24 -0
  8. diffract/core/cache/containers.py +93 -0
  9. diffract/core/cache/interface.py +117 -0
  10. diffract/core/cache/redis_manager.py +464 -0
  11. diffract/core/cache/simple_manager.py +478 -0
  12. diffract/core/compute/__init__.py +46 -0
  13. diffract/core/compute/config.py +48 -0
  14. diffract/core/compute/containers.py +81 -0
  15. diffract/core/compute/decorator.py +125 -0
  16. diffract/core/compute/exceptions.py +31 -0
  17. diffract/core/compute/execution/__init__.py +14 -0
  18. diffract/core/compute/execution/_types.py +70 -0
  19. diffract/core/compute/execution/aggregation.py +74 -0
  20. diffract/core/compute/execution/aggregation_runner.py +362 -0
  21. diffract/core/compute/execution/enums.py +38 -0
  22. diffract/core/compute/execution/executor.py +125 -0
  23. diffract/core/compute/execution/parameter_runner.py +125 -0
  24. diffract/core/compute/execution/restrictions.py +61 -0
  25. diffract/core/compute/execution/strategy.py +118 -0
  26. diffract/core/compute/execution/utils.py +112 -0
  27. diffract/core/compute/extensions/__init__.py +0 -0
  28. diffract/core/compute/extensions/power_law.py +1051 -0
  29. diffract/core/compute/extensions/rmt.py +150 -0
  30. diffract/core/compute/extensions/utils.py +36 -0
  31. diffract/core/compute/field_signature.py +183 -0
  32. diffract/core/compute/kernels/__init__.py +48 -0
  33. diffract/core/compute/kernels/alignment.py +106 -0
  34. diffract/core/compute/kernels/heavy_tailed.py +394 -0
  35. diffract/core/compute/kernels/marchenko_pastur.py +99 -0
  36. diffract/core/compute/kernels/mat_decomposition.py +101 -0
  37. diffract/core/compute/kernels/mat_properties.py +53 -0
  38. diffract/core/compute/kernels/model_quality.py +27 -0
  39. diffract/core/compute/kernels/norms.py +88 -0
  40. diffract/core/compute/kernels/ranks.py +35 -0
  41. diffract/core/compute/kernels/tracy_widom.py +36 -0
  42. diffract/core/compute/metadata.py +67 -0
  43. diffract/core/compute/registry.py +485 -0
  44. diffract/core/constants.py +147 -0
  45. diffract/core/data/__init__.py +32 -0
  46. diffract/core/data/interface.py +304 -0
  47. diffract/core/data/metadata/__init__.py +15 -0
  48. diffract/core/data/metadata/containers.py +60 -0
  49. diffract/core/data/metadata/interface.py +208 -0
  50. diffract/core/data/metadata/sqlite_index.py +604 -0
  51. diffract/core/data/nn/__init__.py +43 -0
  52. diffract/core/data/nn/aggregates/__init__.py +35 -0
  53. diffract/core/data/nn/aggregates/metadata.py +96 -0
  54. diffract/core/data/nn/aggregates/proxy.py +57 -0
  55. diffract/core/data/nn/aggregates/repository.py +87 -0
  56. diffract/core/data/nn/aggregates/view.py +307 -0
  57. diffract/core/data/nn/containers.py +86 -0
  58. diffract/core/data/nn/extractors/__init__.py +49 -0
  59. diffract/core/data/nn/extractors/base.py +300 -0
  60. diffract/core/data/nn/extractors/factory.py +234 -0
  61. diffract/core/data/nn/extractors/flax.py +112 -0
  62. diffract/core/data/nn/extractors/handlers/__init__.py +39 -0
  63. diffract/core/data/nn/extractors/handlers/base.py +110 -0
  64. diffract/core/data/nn/extractors/handlers/flax_handlers.py +71 -0
  65. diffract/core/data/nn/extractors/handlers/numpy_handlers.py +63 -0
  66. diffract/core/data/nn/extractors/handlers/onnx_handlers.py +67 -0
  67. diffract/core/data/nn/extractors/handlers/tensorflow_handlers.py +82 -0
  68. diffract/core/data/nn/extractors/handlers/torch_handlers.py +124 -0
  69. diffract/core/data/nn/extractors/interface.py +56 -0
  70. diffract/core/data/nn/extractors/numpy.py +94 -0
  71. diffract/core/data/nn/extractors/onnx.py +108 -0
  72. diffract/core/data/nn/extractors/tensorflow.py +99 -0
  73. diffract/core/data/nn/extractors/torch.py +204 -0
  74. diffract/core/data/nn/params/__init__.py +27 -0
  75. diffract/core/data/nn/params/interface.py +402 -0
  76. diffract/core/data/nn/params/metadata.py +110 -0
  77. diffract/core/data/nn/params/proxy.py +93 -0
  78. diffract/core/data/nn/params/repository.py +45 -0
  79. diffract/core/data/nn/params/schema.py +82 -0
  80. diffract/core/data/nn/params/view.py +393 -0
  81. diffract/core/data/proxy.py +246 -0
  82. diffract/core/data/repository.py +227 -0
  83. diffract/core/data/utils.py +33 -0
  84. diffract/core/data/view.py +389 -0
  85. diffract/core/export/__init__.py +27 -0
  86. diffract/core/export/containers.py +47 -0
  87. diffract/core/export/exporters.py +191 -0
  88. diffract/core/export/formatters/__init__.py +23 -0
  89. diffract/core/export/formatters/base.py +68 -0
  90. diffract/core/export/formatters/dict_formatter.py +38 -0
  91. diffract/core/export/formatters/json_formatter.py +58 -0
  92. diffract/core/export/formatters/list_formatter.py +41 -0
  93. diffract/core/export/formatters/pandas_formatter.py +86 -0
  94. diffract/core/export/formatters/polars_formatter.py +86 -0
  95. diffract/core/export/formatters/registry.py +84 -0
  96. diffract/core/export/interface.py +105 -0
  97. diffract/core/parallel/__init__.py +21 -0
  98. diffract/core/parallel/containers.py +57 -0
  99. diffract/core/parallel/execution.py +91 -0
  100. diffract/core/parallel/runtime.py +91 -0
  101. diffract/core/storage/__init__.py +35 -0
  102. diffract/core/storage/base_manager.py +330 -0
  103. diffract/core/storage/containers.py +122 -0
  104. diffract/core/storage/hdf5_manager.py +856 -0
  105. diffract/core/storage/hybrid_manager.py +218 -0
  106. diffract/core/storage/interface.py +222 -0
  107. diffract/core/storage/metadata.py +89 -0
  108. diffract/core/storage/ram_manager.py +159 -0
  109. diffract/core/storage/sqlite_manager.py +1062 -0
  110. diffract/core/storage/zarr_manager.py +992 -0
  111. diffract/core/utils/__init__.py +45 -0
  112. diffract/core/utils/build.py +46 -0
  113. diffract/core/utils/exceptions.py +11 -0
  114. diffract/core/utils/hashing.py +90 -0
  115. diffract/core/utils/imports.py +292 -0
  116. diffract/core/utils/math.py +15 -0
  117. diffract/py.typed +0 -0
  118. diffract/session/__init__.py +24 -0
  119. diffract/session/errors.py +17 -0
  120. diffract/session/field_cache.py +216 -0
  121. diffract/session/namespaces/__init__.py +15 -0
  122. diffract/session/namespaces/compute/__init__.py +219 -0
  123. diffract/session/namespaces/models/__init__.py +282 -0
  124. diffract/session/namespaces/models/parameters/__init__.py +133 -0
  125. diffract/session/namespaces/models/parameters/meta_patcher.py +167 -0
  126. diffract/session/namespaces/results/__init__.py +378 -0
  127. diffract/session/namespaces/results/eraser.py +129 -0
  128. diffract/session/namespaces/results/injester.py +249 -0
  129. diffract/session/namespaces/utils/__init__.py +141 -0
  130. diffract/session/namespaces/utils/merger.py +295 -0
  131. diffract/session/namespaces/viz/__init__.py +91 -0
  132. diffract/session/namespaces/viz/_utils.py +14 -0
  133. diffract/session/namespaces/viz/box.py +207 -0
  134. diffract/session/namespaces/viz/grid.py +160 -0
  135. diffract/session/namespaces/viz/heatmap.py +164 -0
  136. diffract/session/namespaces/viz/scatter.py +202 -0
  137. diffract/session/namespaces/viz/sparkline.py +270 -0
  138. diffract/session/namespaces/viz/violin.py +212 -0
  139. diffract/session/session.py +260 -0
  140. diffract/session/utils.py +81 -0
  141. diffract/viz/__init__.py +42 -0
  142. diffract/viz/data/__init__.py +34 -0
  143. diffract/viz/data/detection.py +57 -0
  144. diffract/viz/data/extraction.py +117 -0
  145. diffract/viz/data/filtering.py +57 -0
  146. diffract/viz/data/ordering.py +129 -0
  147. diffract/viz/data/provider.py +99 -0
  148. diffract/viz/data/types.py +98 -0
  149. diffract/viz/plots/__init__.py +37 -0
  150. diffract/viz/plots/base/__init__.py +20 -0
  151. diffract/viz/plots/base/axis.py +219 -0
  152. diffract/viz/plots/base/coloraxis.py +133 -0
  153. diffract/viz/plots/base/configurator.py +18 -0
  154. diffract/viz/plots/base/jitter.py +368 -0
  155. diffract/viz/plots/base/line.py +197 -0
  156. diffract/viz/plots/base/marker.py +278 -0
  157. diffract/viz/plots/base/overlay.py +14 -0
  158. diffract/viz/plots/base/plot.py +110 -0
  159. diffract/viz/plots/base/update.py +50 -0
  160. diffract/viz/plots/boxplot.py +283 -0
  161. diffract/viz/plots/cluster.py +374 -0
  162. diffract/viz/plots/heatmap.py +168 -0
  163. diffract/viz/plots/scatter.py +233 -0
  164. diffract/viz/plots/sparkline.py +405 -0
  165. diffract/viz/plots/subplots/__init__.py +32 -0
  166. diffract/viz/plots/subplots/coloraxis.py +339 -0
  167. diffract/viz/plots/subplots/factory.py +713 -0
  168. diffract/viz/plots/subplots/grid.py +214 -0
  169. diffract/viz/plots/subplots/layout.py +370 -0
  170. diffract/viz/plots/subplots/spec.py +39 -0
  171. diffract/viz/plots/violin.py +298 -0
  172. diffract/viz/renderer.py +513 -0
  173. diffract/viz/styling/__init__.py +78 -0
  174. diffract/viz/styling/palettes/__init__.py +20 -0
  175. diffract/viz/styling/palettes/bundle.py +16 -0
  176. diffract/viz/styling/palettes/color.py +83 -0
  177. diffract/viz/styling/palettes/dashes.py +27 -0
  178. diffract/viz/styling/palettes/symbols.py +42 -0
  179. diffract/viz/styling/resolvers/__init__.py +11 -0
  180. diffract/viz/styling/resolvers/categorical.py +68 -0
  181. diffract/viz/styling/resolvers/color.py +77 -0
  182. diffract/viz/styling/resolvers/numeric.py +108 -0
  183. diffract/viz/styling/sources.py +27 -0
  184. diffract/viz/styling/theme/__init__.py +29 -0
  185. diffract/viz/styling/theme/applier.py +114 -0
  186. diffract/viz/styling/theme/components.py +66 -0
  187. diffract/viz/styling/theme/presets.py +58 -0
  188. diffract/viz/styling/theme/theme.py +36 -0
  189. diffract_core-0.2.1.dist-info/METADATA +530 -0
  190. diffract_core-0.2.1.dist-info/RECORD +193 -0
  191. diffract_core-0.2.1.dist-info/WHEEL +4 -0
  192. diffract_core-0.2.1.dist-info/licenses/LICENSE +201 -0
  193. 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