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,129 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Callable, Sequence
4
+ from dataclasses import dataclass
5
+ from enum import Enum, auto
6
+ from typing import Any
7
+
8
+ import numpy as np
9
+ from numpy.typing import NDArray
10
+
11
+
12
+ class OrderMode(Enum):
13
+ """How to order categorical values."""
14
+
15
+ AS_IS = auto()
16
+ LEXICOGRAPHIC = auto()
17
+ NUMERIC = auto()
18
+ MAP = auto()
19
+ CUSTOM = auto()
20
+
21
+ @classmethod
22
+ def from_string(cls, name: str) -> OrderMode:
23
+ """Return the OrderMode matching the given name (case-insensitive)."""
24
+ match name.lower():
25
+ case "as_is":
26
+ return cls.AS_IS
27
+ case "lexicographic":
28
+ return cls.LEXICOGRAPHIC
29
+ case "numeric":
30
+ return cls.NUMERIC
31
+ case "map":
32
+ return cls.MAP
33
+ case "custom":
34
+ return cls.CUSTOM
35
+ case _:
36
+ raise ValueError(f"Unknown order mode: {name!r}")
37
+
38
+
39
+ @dataclass(kw_only=True)
40
+ class Ordering:
41
+ """Defines how to order values.
42
+
43
+ Returns indices (like np.argsort) so the same ordering
44
+ can be applied to multiple related arrays.
45
+ """
46
+
47
+ mode: OrderMode = OrderMode.AS_IS
48
+ descending: bool = False
49
+
50
+ # For MAP mode: function to extract comparison key
51
+ key: Callable[[Any], Any] | None = None
52
+
53
+ # For CUSTOM mode: explicit order of values
54
+ custom_order: Sequence[Any] | None = None
55
+
56
+ def argsort(self, values: Sequence[Any]) -> NDArray[np.intp]:
57
+ """Return indices that would sort unique values.
58
+
59
+ Args:
60
+ values: Sequence of values to order.
61
+
62
+ Returns:
63
+ Array of indices into unique values that produces the desired order.
64
+ Use np.unique(..., return_inverse=True) to map back to original data.
65
+ """
66
+ unique = list(dict.fromkeys(values)) # preserve first occurrence order
67
+
68
+ if self.mode == OrderMode.AS_IS:
69
+ indices = np.arange(len(unique))
70
+
71
+ elif self.mode == OrderMode.LEXICOGRAPHIC:
72
+ indices = np.argsort([str(v) for v in unique])
73
+
74
+ elif self.mode == OrderMode.NUMERIC:
75
+ indices = np.argsort(
76
+ [float(v) if v is not None else float("-inf") for v in unique]
77
+ )
78
+
79
+ elif self.mode == OrderMode.MAP:
80
+ if self.key is None:
81
+ indices = np.arange(len(unique))
82
+ else:
83
+ indices = np.argsort([self.key(v) for v in unique])
84
+
85
+ elif True:
86
+ if self.custom_order is None:
87
+ indices = np.arange(len(unique))
88
+ else:
89
+ order_map = {v: i for i, v in enumerate(self.custom_order)}
90
+ max_idx = len(order_map)
91
+ # Values not in custom_order go to the end
92
+ priorities = [order_map.get(v, max_idx) for v in unique]
93
+ indices = np.argsort(priorities)
94
+
95
+ else:
96
+ indices = np.arange(len(unique))
97
+
98
+ if self.descending:
99
+ indices = indices[::-1].copy()
100
+
101
+ return indices
102
+
103
+
104
+ # Convenience constructors
105
+
106
+
107
+ def as_is() -> Ordering:
108
+ """Keep original order from data."""
109
+ return Ordering(mode=OrderMode.AS_IS)
110
+
111
+
112
+ def lexicographic(descending: bool = False) -> Ordering:
113
+ """Sort alphabetically by string representation."""
114
+ return Ordering(mode=OrderMode.LEXICOGRAPHIC, descending=descending)
115
+
116
+
117
+ def numeric(descending: bool = False) -> Ordering:
118
+ """Sort by numeric value."""
119
+ return Ordering(mode=OrderMode.NUMERIC, descending=descending)
120
+
121
+
122
+ def by_key(key: Callable[[Any], Any], descending: bool = False) -> Ordering:
123
+ """Sort by applying key function to each value."""
124
+ return Ordering(mode=OrderMode.MAP, key=key, descending=descending)
125
+
126
+
127
+ def custom(order: Sequence[Any], descending: bool = False) -> Ordering:
128
+ """Sort by explicit order. Values not in order go to the end."""
129
+ return Ordering(mode=OrderMode.CUSTOM, custom_order=order, descending=descending)
@@ -0,0 +1,99 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING, Any
4
+
5
+ from .extraction import get_field_data
6
+ from .filtering import apply_filter
7
+ from .types import DataShape, DataType, Entry
8
+
9
+ if TYPE_CHECKING:
10
+ from diffract.session import Session
11
+
12
+
13
+ class DataProvider:
14
+ """Data provider for visualization.
15
+
16
+ Fetches data from session and provides typed field access.
17
+ Always fetches aggregates merged with entries.
18
+ """
19
+
20
+ def __init__(self, session: Session) -> None:
21
+ self._session = session
22
+ self._entries: dict[str, Entry] = {}
23
+ self._cached_fields: set[str] = set()
24
+
25
+ def fetch(
26
+ self,
27
+ fields: list[str],
28
+ *,
29
+ value_filter: dict[str, tuple[str, Any]] | None = None,
30
+ ) -> dict[str, Entry]:
31
+ """Fetch data from session with aggregates merged.
32
+
33
+ Uses unified export API that handles aggregate merge internally.
34
+ Always expands contextual fields into entries.
35
+
36
+ Args:
37
+ fields: Computed field names to fetch.
38
+ value_filter: Optional filter {field: (op, threshold)}.
39
+
40
+ Returns:
41
+ Entries dict: {uid: Entry}
42
+ """
43
+ fields_to_fetch = set(fields)
44
+ if value_filter:
45
+ fields_to_fetch.update(value_filter.keys())
46
+
47
+ missing_fields = fields_to_fetch - self._cached_fields
48
+ if missing_fields:
49
+ update = self._session.results.export(
50
+ *sorted(missing_fields),
51
+ sources="all",
52
+ export_format="dict",
53
+ expand_contextual=True,
54
+ )
55
+ self._merge_entries(update)
56
+ self._cached_fields.update(missing_fields)
57
+
58
+ if value_filter:
59
+ return apply_filter(self._entries, value_filter)
60
+
61
+ return self._entries
62
+
63
+ def get_field_data(self, field: str) -> tuple[list[Any], DataType, DataShape]:
64
+ """Fetch a field and return its values, data type, and data shape.
65
+
66
+ Args:
67
+ field: Computed field name to fetch.
68
+
69
+ Returns:
70
+ Tuple of (values, data type, data shape).
71
+ """
72
+ return get_field_data(self.fetch(fields=[field]), field)
73
+
74
+ def _merge_entries(self, update: dict[str, Any]) -> None:
75
+ for uid, item in update.items():
76
+ fields = item.get("fields")
77
+ metadata = item.get("metadata")
78
+
79
+ if not isinstance(fields, dict):
80
+ raise TypeError(
81
+ f"Expected 'fields' mapping for entry '{uid}', "
82
+ f"got {type(fields).__name__}"
83
+ )
84
+ if not isinstance(metadata, dict):
85
+ raise TypeError(
86
+ f"Expected 'metadata' mapping for entry '{uid}', "
87
+ f"got {type(metadata).__name__}"
88
+ )
89
+
90
+ merged_fields = dict(fields)
91
+ merged_fields.update(metadata)
92
+
93
+ existing = self._entries.get(uid)
94
+ if existing is not None:
95
+ existing_fields = existing.get("fields", {})
96
+ if isinstance(existing_fields, dict):
97
+ merged_fields = dict(existing_fields) | merged_fields
98
+
99
+ self._entries[uid] = Entry(fields=merged_fields)
@@ -0,0 +1,98 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, field
4
+ from enum import Enum, auto
5
+ from typing import TYPE_CHECKING, Any, TypedDict
6
+
7
+ from .ordering import as_is
8
+
9
+ if TYPE_CHECKING:
10
+ from .ordering import Ordering
11
+
12
+
13
+ class Entry(TypedDict, total=False):
14
+ """Single data entry from session results."""
15
+
16
+ fields: dict[str, Any]
17
+
18
+
19
+ class DataType(Enum):
20
+ """Whether a field's values are numeric or categorical."""
21
+
22
+ NUMERIC = auto()
23
+ CATEGORICAL = auto()
24
+
25
+ @classmethod
26
+ def from_string(cls, name: str) -> DataType:
27
+ """Return the DataType matching the given name (case-insensitive)."""
28
+ match name.lower():
29
+ case "numeric":
30
+ return DataType.NUMERIC
31
+ case "categorical":
32
+ return DataType.CATEGORICAL
33
+ case _:
34
+ raise ValueError(f"Unknown data type: {name!r}")
35
+
36
+
37
+ class DataShape(Enum):
38
+ """Whether a field holds a scalar or a vector per entry."""
39
+
40
+ SCALAR = auto()
41
+ VECTOR = auto()
42
+
43
+ @classmethod
44
+ def from_string(cls, name: str) -> DataShape:
45
+ """Return the DataShape matching the given name (case-insensitive)."""
46
+ match name.lower():
47
+ case "scalar":
48
+ return DataShape.SCALAR
49
+ case "vector":
50
+ return DataShape.VECTOR
51
+ case _:
52
+ raise ValueError(f"Unknown data shape: {name!r}")
53
+
54
+
55
+ @dataclass
56
+ class FieldRef:
57
+ """Reference to a data field with optional ordering.
58
+
59
+ Attributes:
60
+ field: Name of the field to reference.
61
+ ordering: How to order values of this field. Defaults to AS_IS (no reordering).
62
+ """
63
+
64
+ field: str
65
+ data_type: DataType | None = field(default=None)
66
+ data_shape: DataShape | None = field(default=None)
67
+ ordering: Ordering = field(default_factory=as_is)
68
+
69
+
70
+ @dataclass
71
+ class FieldMeta:
72
+ """Metadata about a field's type and shape."""
73
+
74
+ data_type: DataType
75
+ data_shape: DataShape
76
+
77
+
78
+ @dataclass
79
+ class EntryContext:
80
+ """Entry fields plus extracted model and parameter context."""
81
+
82
+ fields: dict[str, Any]
83
+ model_id: Any
84
+ parameter_name: Any
85
+
86
+ @classmethod
87
+ def from_entry(cls, entry: Entry) -> EntryContext:
88
+ """Create EntryContext from a session entry."""
89
+ entry_fields = entry.get("fields", {})
90
+ if not isinstance(entry_fields, dict):
91
+ raise TypeError(
92
+ f"Entry 'fields' must be a mapping, got {type(entry_fields).__name__}"
93
+ )
94
+ return cls(
95
+ fields=entry_fields,
96
+ model_id=entry_fields.get("model_id"),
97
+ parameter_name=entry_fields.get("name"),
98
+ )
@@ -0,0 +1,37 @@
1
+ """Built-in plots for `diffract.viz`.
2
+
3
+ This module mirrors (approximately) the public surface of `diffract.viz.plots`
4
+ for the refactored plotting stack.
5
+ """
6
+
7
+ try:
8
+ import plotly
9
+ except ImportError as e:
10
+ _msg = (
11
+ "diffract.viz requires the viz extra. "
12
+ 'Install it with: pip install "diffract-core[viz]"'
13
+ )
14
+ raise ImportError(_msg) from e
15
+
16
+ from .base import UpdateFigure
17
+ from .boxplot import BoxPlot
18
+ from .cluster import ClusterBarChart
19
+ from .heatmap import HeatmapPlot
20
+ from .scatter import ScatterPlot
21
+ from .sparkline import SparklinePlot
22
+ from .subplots import GridPlot, SubplotSpec
23
+ from .violin import ViolinPlot
24
+
25
+ Sparkline = SparklinePlot
26
+
27
+ __all__ = [
28
+ "BoxPlot",
29
+ "ClusterBarChart",
30
+ "GridPlot",
31
+ "HeatmapPlot",
32
+ "ScatterPlot",
33
+ "Sparkline",
34
+ "SparklinePlot",
35
+ "SubplotSpec",
36
+ "ViolinPlot",
37
+ ]
@@ -0,0 +1,20 @@
1
+ """Base plot components and mixins."""
2
+
3
+ from .axis import AxisType, SupportsAxis
4
+ from .coloraxis import SupportsColoraxis
5
+ from .configurator import Configurator
6
+ from .jitter import SupportsJitter, density_scaled_jitter
7
+ from .marker import SupportsMarker
8
+ from .plot import Plot
9
+ from .update import UpdateFigure
10
+
11
+ __all__ = [
12
+ "AxisType",
13
+ "Configurator",
14
+ "Plot",
15
+ "SupportsAxis",
16
+ "SupportsColoraxis",
17
+ "SupportsJitter",
18
+ "SupportsMarker",
19
+ "density_scaled_jitter",
20
+ ]
@@ -0,0 +1,219 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import field, make_dataclass
4
+ from enum import Flag, auto
5
+ from typing import Any, Literal
6
+
7
+ import plotly.graph_objects as go
8
+
9
+ from .configurator import Configurator
10
+
11
+
12
+ class AxisType(Flag):
13
+ """Bit flags describing which axis modes an axis supports."""
14
+
15
+ NUMERIC = auto()
16
+ CATEGORICAL = auto()
17
+
18
+
19
+ AxisMode = Literal["numeric", "categorical"]
20
+
21
+
22
+ def SupportsAxis(axis_name: str, axis_type: AxisType) -> type[Configurator]: # noqa: N802 factory returns a class
23
+ """Factory that creates axis configurator mixins."""
24
+ has_numeric = AxisType.NUMERIC in axis_type
25
+ has_categorical = AxisType.CATEGORICAL in axis_type
26
+ has_both = has_numeric and has_categorical
27
+
28
+ fields = _common_fields(axis_name)
29
+
30
+ if has_numeric:
31
+ fields += _numeric_fields(axis_name)
32
+ if has_categorical:
33
+ fields += _categorical_fields(axis_name)
34
+ if has_both:
35
+ fields += _mode_fields(axis_name)
36
+
37
+ def configure(self: Any, fig: go.Figure) -> None:
38
+ kwargs: dict[str, Any] = {}
39
+
40
+ _configure_common(self, axis_name, kwargs)
41
+
42
+ if has_both:
43
+ mode = getattr(self, f"{axis_name}_axis_mode")
44
+ if mode is None:
45
+ mode = getattr(self, f"_{axis_name}_resolved_mode", "categorical")
46
+ if mode == "numeric":
47
+ _configure_numeric(self, axis_name, kwargs)
48
+ else:
49
+ _configure_categorical(self, axis_name, kwargs)
50
+ elif has_numeric:
51
+ _configure_numeric(self, axis_name, kwargs)
52
+ elif has_categorical:
53
+ _configure_categorical(self, axis_name, kwargs)
54
+
55
+ fig.update_layout(**{f"{axis_name}axis": kwargs})
56
+
57
+ return make_dataclass(
58
+ f"SupportsAxis_{axis_name}",
59
+ fields=fields,
60
+ bases=(Configurator,),
61
+ namespace={
62
+ "axis_name": axis_name,
63
+ "axis_type": axis_type,
64
+ "configure": configure,
65
+ },
66
+ kw_only=True,
67
+ )
68
+
69
+
70
+ # --- Field definitions ---
71
+
72
+
73
+ def _common_fields(axis_name: str) -> list[tuple[str, type, Any]]:
74
+ return [
75
+ (f"{axis_name}_title", str | None, field(default=None)),
76
+ (f"{axis_name}_showticklabels", bool, field(default=True)),
77
+ (f"{axis_name}_tickangle", int | None, field(default=None)),
78
+ (f"{axis_name}_tickfont_size", int | None, field(default=None)),
79
+ (f"{axis_name}_tickfont_family", str | None, field(default=None)),
80
+ (f"{axis_name}_tickfont_color", str | None, field(default=None)),
81
+ (f"{axis_name}_showgrid", bool, field(default=True)),
82
+ (f"{axis_name}_gridcolor", str | None, field(default=None)),
83
+ (f"{axis_name}_showline", bool, field(default=True)),
84
+ (f"{axis_name}_linecolor", str | None, field(default=None)),
85
+ ]
86
+
87
+
88
+ def _numeric_fields(axis_name: str) -> list[tuple[str, type, Any]]:
89
+ return [
90
+ (f"{axis_name}_range", tuple[float, float] | None, field(default=None)),
91
+ (f"{axis_name}_dtick", float | str | None, field(default=None)),
92
+ (f"{axis_name}_tick0", float | None, field(default=None)),
93
+ (f"{axis_name}_tickformat", str | None, field(default=None)),
94
+ (f"{axis_name}_tickmode", str | None, field(default=None)),
95
+ (f"{axis_name}_tickvals", list[float] | None, field(default=None)),
96
+ (f"{axis_name}_ticktext", list[str] | None, field(default=None)),
97
+ (f"{axis_name}_zeroline", bool, field(default=False)),
98
+ (f"{axis_name}_zerolinecolor", str | None, field(default=None)),
99
+ ]
100
+
101
+
102
+ def _categorical_fields(axis_name: str) -> list[tuple[str, type, Any]]:
103
+ return [
104
+ (f"{axis_name}_categoryorder", str | None, field(default=None)),
105
+ (f"{axis_name}_categoryarray", list[str] | None, field(default=None)),
106
+ ]
107
+
108
+
109
+ def _mode_fields(axis_name: str) -> list[tuple[str, type, Any]]:
110
+ # None means "infer from the data"; the plot stores the outcome in
111
+ # _{axis}_resolved_mode before configuration runs.
112
+ return [
113
+ (f"{axis_name}_axis_mode", AxisMode | None, field(default=None)),
114
+ ]
115
+
116
+
117
+ # --- Configure helpers ---
118
+
119
+
120
+ def _configure_common(self: Any, axis_name: str, kwargs: dict[str, Any]) -> None:
121
+ def _get(suffix: str) -> Any:
122
+ return getattr(self, f"{axis_name}_{suffix}")
123
+
124
+ # Get theme axes style for defaults
125
+ theme = getattr(self, "_theme", None)
126
+ axes_style = theme.axes if theme else None
127
+
128
+ title = _get("title")
129
+ if title is not None:
130
+ kwargs["title"] = title
131
+
132
+ kwargs["showticklabels"] = _get("showticklabels")
133
+
134
+ # showgrid: explicit value or theme default
135
+ show_grid = _get("showgrid")
136
+ if show_grid is not None:
137
+ kwargs["showgrid"] = show_grid
138
+ elif axes_style:
139
+ kwargs["showgrid"] = axes_style.show_grid
140
+
141
+ # showline: explicit value or theme default
142
+ show_line = _get("showline")
143
+ if show_line is not None:
144
+ kwargs["showline"] = show_line
145
+ elif axes_style:
146
+ kwargs["showline"] = axes_style.show_line
147
+
148
+ # mirror: from theme if available
149
+ if axes_style:
150
+ kwargs["mirror"] = axes_style.mirror
151
+
152
+ tickangle = _get("tickangle")
153
+ if tickangle is not None:
154
+ kwargs["tickangle"] = tickangle
155
+
156
+ # gridcolor: explicit value or theme default
157
+ gridcolor = _get("gridcolor")
158
+ if gridcolor is not None:
159
+ kwargs["gridcolor"] = gridcolor
160
+ elif axes_style:
161
+ kwargs["gridcolor"] = axes_style.grid_color
162
+
163
+ # linecolor: explicit value or theme default
164
+ linecolor = _get("linecolor")
165
+ if linecolor is not None:
166
+ kwargs["linecolor"] = linecolor
167
+ elif axes_style:
168
+ kwargs["linecolor"] = axes_style.line_color
169
+
170
+ tickfont: dict[str, Any] = {}
171
+ if (size := _get("tickfont_size")) is not None:
172
+ tickfont["size"] = size
173
+ if (family := _get("tickfont_family")) is not None:
174
+ tickfont["family"] = family
175
+ if (color := _get("tickfont_color")) is not None:
176
+ tickfont["color"] = color
177
+ if tickfont:
178
+ kwargs["tickfont"] = tickfont
179
+
180
+
181
+ def _configure_numeric(self: Any, axis_name: str, kwargs: dict[str, Any]) -> None:
182
+ def _get(suffix: str) -> Any:
183
+ return getattr(self, f"{axis_name}_{suffix}")
184
+
185
+ if (rng := _get("range")) is not None:
186
+ kwargs["range"] = rng
187
+
188
+ if (dtick := _get("dtick")) is not None:
189
+ kwargs["dtick"] = dtick
190
+
191
+ if (tick0 := _get("tick0")) is not None:
192
+ kwargs["tick0"] = tick0
193
+
194
+ if (tickmode := _get("tickmode")) is not None:
195
+ kwargs["tickmode"] = tickmode
196
+
197
+ if (tickvals := _get("tickvals")) is not None:
198
+ kwargs["tickvals"] = tickvals
199
+
200
+ if (ticktext := _get("ticktext")) is not None:
201
+ kwargs["ticktext"] = ticktext
202
+
203
+ if (fmt := _get("tickformat")) is not None:
204
+ kwargs["tickformat"] = fmt
205
+
206
+ kwargs["zeroline"] = _get("zeroline")
207
+ if (zcolor := _get("zerolinecolor")) is not None:
208
+ kwargs["zerolinecolor"] = zcolor
209
+
210
+
211
+ def _configure_categorical(self: Any, axis_name: str, kwargs: dict[str, Any]) -> None:
212
+ def _get(suffix: str) -> Any:
213
+ return getattr(self, f"{axis_name}_{suffix}")
214
+
215
+ if (order := _get("categoryorder")) is not None:
216
+ kwargs["categoryorder"] = order
217
+
218
+ if (arr := _get("categoryarray")) is not None:
219
+ kwargs["categoryarray"] = arr