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,133 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import field, make_dataclass
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
import plotly.graph_objects as go
|
|
7
|
+
|
|
8
|
+
from .configurator import Configurator
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def SupportsColoraxis(prefix: str) -> type[Configurator]: # noqa: N802 factory returns a class
|
|
12
|
+
"""Factory that creates coloraxis configurator mixins."""
|
|
13
|
+
fields = _coloraxis_fields(prefix)
|
|
14
|
+
|
|
15
|
+
def configure(self: Any, fig: go.Figure) -> None:
|
|
16
|
+
_configure_coloraxis(self, fig, prefix)
|
|
17
|
+
|
|
18
|
+
def resolve_coloraxis(self: Any, fig: go.Figure) -> str:
|
|
19
|
+
return _resolve_coloraxis(self, fig, prefix)
|
|
20
|
+
|
|
21
|
+
return make_dataclass(
|
|
22
|
+
f"SupportsColoraxis_{prefix}",
|
|
23
|
+
fields=fields,
|
|
24
|
+
bases=(Configurator,),
|
|
25
|
+
namespace={
|
|
26
|
+
"prefix": prefix,
|
|
27
|
+
"configure": configure,
|
|
28
|
+
"resolve_coloraxis": resolve_coloraxis,
|
|
29
|
+
},
|
|
30
|
+
kw_only=True,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
# --- Field definitions ---
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _coloraxis_fields(prefix: str) -> list[tuple[str, type, Any]]:
|
|
38
|
+
return [
|
|
39
|
+
(f"{prefix}_coloraxis_id", int | None, field(default=None)),
|
|
40
|
+
(f"{prefix}_colorscale", str, field(default="Viridis")),
|
|
41
|
+
(f"{prefix}_showscale", bool, field(default=True)),
|
|
42
|
+
(f"{prefix}_cmin", float | None, field(default=None)),
|
|
43
|
+
(f"{prefix}_cmax", float | None, field(default=None)),
|
|
44
|
+
(f"{prefix}_colorbar_title", str | None, field(default=None)),
|
|
45
|
+
(f"{prefix}_coloraxis_override", bool, field(default=False)),
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
# --- Configure helper ---
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _configure_coloraxis(self: Any, fig: go.Figure, prefix: str) -> None:
|
|
53
|
+
def _get(suffix: str) -> Any:
|
|
54
|
+
return getattr(self, f"{prefix}_{suffix}")
|
|
55
|
+
|
|
56
|
+
coloraxis_name = _resolve_coloraxis(self, fig, prefix)
|
|
57
|
+
|
|
58
|
+
if _coloraxis_exists(fig, coloraxis_name) and not _get("coloraxis_override"):
|
|
59
|
+
return
|
|
60
|
+
|
|
61
|
+
config: dict[str, Any] = {
|
|
62
|
+
"colorscale": _get("colorscale"),
|
|
63
|
+
"showscale": _get("showscale"),
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
cmin = _get("cmin")
|
|
67
|
+
if cmin is not None:
|
|
68
|
+
config["cmin"] = cmin
|
|
69
|
+
|
|
70
|
+
cmax = _get("cmax")
|
|
71
|
+
if cmax is not None:
|
|
72
|
+
config["cmax"] = cmax
|
|
73
|
+
|
|
74
|
+
colorbar_title = _get("colorbar_title")
|
|
75
|
+
if colorbar_title is not None:
|
|
76
|
+
config["colorbar"] = {"title": colorbar_title}
|
|
77
|
+
|
|
78
|
+
fig.update_layout(**{coloraxis_name: config})
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _resolve_coloraxis(self: Any, fig: go.Figure, prefix: str) -> str:
|
|
82
|
+
"""Resolve coloraxis id and return the coloraxis name (e.g. 'coloraxis2')."""
|
|
83
|
+
cache_attr = f"__{prefix}_resolved_coloraxis"
|
|
84
|
+
|
|
85
|
+
cached = getattr(self, cache_attr, None)
|
|
86
|
+
if cached is not None:
|
|
87
|
+
return cached
|
|
88
|
+
|
|
89
|
+
coloraxis_id = getattr(self, f"{prefix}_coloraxis_id")
|
|
90
|
+
|
|
91
|
+
if coloraxis_id is not None:
|
|
92
|
+
result = _coloraxis_name(coloraxis_id)
|
|
93
|
+
else:
|
|
94
|
+
result = _next_coloraxis_name(fig)
|
|
95
|
+
|
|
96
|
+
object.__setattr__(self, cache_attr, result)
|
|
97
|
+
return result
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
# --- Helpers ---
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _coloraxis_name(coloraxis_id: int) -> str:
|
|
104
|
+
"""Convert coloraxis id to Plotly coloraxis name."""
|
|
105
|
+
if coloraxis_id == 1:
|
|
106
|
+
return "coloraxis"
|
|
107
|
+
return f"coloraxis{coloraxis_id}"
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _coloraxis_exists(fig: go.Figure, coloraxis_name: str) -> bool:
|
|
111
|
+
"""Check if coloraxis already exists in figure layout."""
|
|
112
|
+
return getattr(fig.layout, coloraxis_name, None) is not None
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _next_coloraxis_name(fig: go.Figure) -> str:
|
|
116
|
+
"""Find next available coloraxis name in figure."""
|
|
117
|
+
layout = fig.layout
|
|
118
|
+
existing_ids: list[int] = []
|
|
119
|
+
|
|
120
|
+
for key in dir(layout):
|
|
121
|
+
if key == "coloraxis":
|
|
122
|
+
if getattr(layout, key, None) is not None:
|
|
123
|
+
existing_ids.append(1)
|
|
124
|
+
elif (
|
|
125
|
+
key.startswith("coloraxis")
|
|
126
|
+
and key[9:].isdigit()
|
|
127
|
+
and getattr(layout, key, None) is not None
|
|
128
|
+
):
|
|
129
|
+
existing_ids.append(int(key[9:]))
|
|
130
|
+
|
|
131
|
+
next_id = max(existing_ids, default=0) + 1
|
|
132
|
+
|
|
133
|
+
return _coloraxis_name(next_id)
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from abc import ABC, abstractmethod
|
|
4
|
+
|
|
5
|
+
import plotly.graph_objects as go
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Configurator(ABC):
|
|
9
|
+
"""A mixin that can configure a Plotly figure.
|
|
10
|
+
|
|
11
|
+
In `viz`, configurators are intended to be used as mixins on a plot
|
|
12
|
+
class and invoked in MRO order by `Plot._configure_by_mro(...)`.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
@abstractmethod
|
|
16
|
+
def configure(self, fig: go.Figure) -> None:
|
|
17
|
+
"""Apply configuration to the given figure."""
|
|
18
|
+
raise NotImplementedError
|
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
"""Jitter overlay mixin for categorical plots."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import TYPE_CHECKING, Any
|
|
7
|
+
|
|
8
|
+
import numpy as np
|
|
9
|
+
import plotly.graph_objects as go
|
|
10
|
+
|
|
11
|
+
from diffract.viz.data import Entry
|
|
12
|
+
from diffract.viz.styling import (
|
|
13
|
+
ColorResolver,
|
|
14
|
+
ColorSource,
|
|
15
|
+
DefaultColorPalette,
|
|
16
|
+
ResolvedColor,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
from .coloraxis import SupportsColoraxis
|
|
20
|
+
from .overlay import Overlay
|
|
21
|
+
|
|
22
|
+
if TYPE_CHECKING:
|
|
23
|
+
from diffract.viz.styling import Theme
|
|
24
|
+
|
|
25
|
+
_JITTER_TRACE_ID_SUFFIX = "_jitter"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def density_scaled_jitter(
|
|
29
|
+
y: np.ndarray,
|
|
30
|
+
jitter: np.ndarray,
|
|
31
|
+
*,
|
|
32
|
+
n_bins: int = 20,
|
|
33
|
+
) -> np.ndarray:
|
|
34
|
+
"""Scale jitter by local density to reduce overlap in dense regions.
|
|
35
|
+
|
|
36
|
+
Points in denser regions get larger jitter spread, while isolated points
|
|
37
|
+
stay closer to the center.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
y: Y-values for the points.
|
|
41
|
+
jitter: Base jitter values (uniform random).
|
|
42
|
+
n_bins: Number of bins for density estimation.
|
|
43
|
+
|
|
44
|
+
Returns:
|
|
45
|
+
Scaled jitter values.
|
|
46
|
+
"""
|
|
47
|
+
if y.size == 0:
|
|
48
|
+
return jitter
|
|
49
|
+
|
|
50
|
+
y_min, y_max = np.nanmin(y), np.nanmax(y)
|
|
51
|
+
if y_min == y_max:
|
|
52
|
+
return jitter
|
|
53
|
+
|
|
54
|
+
bins = np.linspace(y_min, y_max, n_bins + 1)
|
|
55
|
+
counts, _ = np.histogram(y, bins=bins)
|
|
56
|
+
max_count = counts.max() if counts.max() > 0 else 1
|
|
57
|
+
|
|
58
|
+
bin_indices = np.clip(
|
|
59
|
+
np.digitize(y, bins) - 1,
|
|
60
|
+
0,
|
|
61
|
+
n_bins - 1,
|
|
62
|
+
)
|
|
63
|
+
scale = counts[bin_indices] / max_count
|
|
64
|
+
|
|
65
|
+
return jitter * scale
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@dataclass(slots=False, kw_only=True)
|
|
69
|
+
class SupportsJitter(Overlay, SupportsColoraxis("jitter")):
|
|
70
|
+
"""Mixin that adds jitter scatter overlay to categorical plots.
|
|
71
|
+
|
|
72
|
+
Jitter displays individual data points with horizontal spread,
|
|
73
|
+
useful for showing distribution density alongside box/violin plots.
|
|
74
|
+
|
|
75
|
+
Creates ``go.Scatter`` traces with their own ``trace_id``
|
|
76
|
+
(``{parent_trace_id}_jitter``). Visual properties are applied
|
|
77
|
+
in :meth:`configure`:
|
|
78
|
+
|
|
79
|
+
- **Color**: from ``jitter_color`` if set, otherwise falls back
|
|
80
|
+
to the parent trace's ``marker_color`` / ``marker_color_values``.
|
|
81
|
+
- **Size, opacity, symbol**: inherited from the parent trace's
|
|
82
|
+
``marker_size``, ``marker_opacity``, ``marker_symbol``.
|
|
83
|
+
|
|
84
|
+
Requires ``_traces_data`` to contain for each trace:
|
|
85
|
+
- ``y_values``: list of y values for points
|
|
86
|
+
- ``category_idx`` *or* ``jitter_x_center``: numeric x center
|
|
87
|
+
|
|
88
|
+
Optional per-trace key:
|
|
89
|
+
- ``jitter_xaxis``: Plotly x-axis reference for overlay points
|
|
90
|
+
(for example ``"x2"`` for a hidden linear axis overlaid on a
|
|
91
|
+
categorical ``x`` axis).
|
|
92
|
+
"""
|
|
93
|
+
|
|
94
|
+
jitter_enabled: bool = False
|
|
95
|
+
jitter_width: float = 0.12
|
|
96
|
+
jitter_offset: float = -0.35
|
|
97
|
+
jitter_seed: int = 42
|
|
98
|
+
jitter_density_scale: bool = True
|
|
99
|
+
jitter_color: ColorSource = None
|
|
100
|
+
|
|
101
|
+
# --- Overlay: add geometry ------------------------------------------------
|
|
102
|
+
|
|
103
|
+
def add_overlay_traces(self, fig: go.Figure) -> None:
|
|
104
|
+
"""Add jitter scatter traces if enabled."""
|
|
105
|
+
if not self.jitter_enabled:
|
|
106
|
+
return
|
|
107
|
+
|
|
108
|
+
if self._traces_data is None:
|
|
109
|
+
return
|
|
110
|
+
|
|
111
|
+
self._configure_jitter_overlay_xaxes(fig)
|
|
112
|
+
|
|
113
|
+
for trace_id, trace_data in self._traces_data.items():
|
|
114
|
+
self._add_jitter_trace(fig, trace_id, trace_data)
|
|
115
|
+
|
|
116
|
+
def _add_jitter_trace(
|
|
117
|
+
self,
|
|
118
|
+
fig: go.Figure,
|
|
119
|
+
trace_id: str,
|
|
120
|
+
trace_data: dict[str, Any],
|
|
121
|
+
) -> None:
|
|
122
|
+
"""Add a single jitter scatter trace for a category."""
|
|
123
|
+
y_values = trace_data.get("y_values", [])
|
|
124
|
+
if len(y_values) == 0:
|
|
125
|
+
return
|
|
126
|
+
|
|
127
|
+
x_center = _coerce_jitter_center(trace_data)
|
|
128
|
+
if x_center is None:
|
|
129
|
+
return
|
|
130
|
+
|
|
131
|
+
y_arr = np.asarray(y_values, dtype=np.float64)
|
|
132
|
+
n_points = len(y_arr)
|
|
133
|
+
|
|
134
|
+
# Generate jittered x positions
|
|
135
|
+
rng = np.random.default_rng(self.jitter_seed)
|
|
136
|
+
jitter = rng.uniform(-self.jitter_width, self.jitter_width, size=n_points)
|
|
137
|
+
|
|
138
|
+
if self.jitter_density_scale:
|
|
139
|
+
jitter = density_scaled_jitter(y_arr, jitter)
|
|
140
|
+
|
|
141
|
+
x_jittered = (
|
|
142
|
+
np.full(n_points, x_center, dtype=np.float64) + self.jitter_offset + jitter
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
jitter_trace_id = f"{trace_id}{_JITTER_TRACE_ID_SUFFIX}"
|
|
146
|
+
scatter_kwargs: dict[str, Any] = {
|
|
147
|
+
"x": x_jittered,
|
|
148
|
+
"y": y_arr,
|
|
149
|
+
"mode": "markers",
|
|
150
|
+
"showlegend": False,
|
|
151
|
+
"meta": {"trace_id": jitter_trace_id},
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
jitter_xaxis = trace_data.get("jitter_xaxis")
|
|
155
|
+
if isinstance(jitter_xaxis, str) and jitter_xaxis:
|
|
156
|
+
scatter_kwargs["xaxis"] = jitter_xaxis
|
|
157
|
+
|
|
158
|
+
fig.add_trace(go.Scatter(**scatter_kwargs))
|
|
159
|
+
|
|
160
|
+
def _configure_jitter_overlay_xaxes(self, fig: go.Figure) -> None:
|
|
161
|
+
"""Ensure overlay x-axes exist for jitter traces that need them."""
|
|
162
|
+
if self._traces_data is None:
|
|
163
|
+
return
|
|
164
|
+
|
|
165
|
+
centers_by_axis: dict[str, list[float]] = {}
|
|
166
|
+
for trace_data in self._traces_data.values():
|
|
167
|
+
jitter_xaxis = trace_data.get("jitter_xaxis")
|
|
168
|
+
if not isinstance(jitter_xaxis, str) or jitter_xaxis == "x":
|
|
169
|
+
continue
|
|
170
|
+
|
|
171
|
+
center = _coerce_jitter_center(trace_data)
|
|
172
|
+
if center is None:
|
|
173
|
+
continue
|
|
174
|
+
|
|
175
|
+
centers_by_axis.setdefault(jitter_xaxis, []).append(center)
|
|
176
|
+
|
|
177
|
+
for axis_ref, centers in centers_by_axis.items():
|
|
178
|
+
self._configure_single_overlay_xaxis(
|
|
179
|
+
fig, axis_ref=axis_ref, centers=centers
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
def _configure_single_overlay_xaxis(
|
|
183
|
+
self,
|
|
184
|
+
fig: go.Figure,
|
|
185
|
+
*,
|
|
186
|
+
axis_ref: str,
|
|
187
|
+
centers: list[float],
|
|
188
|
+
) -> None:
|
|
189
|
+
"""Configure one hidden linear axis overlaid on the primary x-axis."""
|
|
190
|
+
if not axis_ref.startswith("x") or not centers:
|
|
191
|
+
return
|
|
192
|
+
|
|
193
|
+
suffix = axis_ref[1:]
|
|
194
|
+
axis_layout_key = "xaxis" if not suffix else f"xaxis{suffix}"
|
|
195
|
+
|
|
196
|
+
# Keep jitter points near category centers while allowing configured offsets.
|
|
197
|
+
half_span = max(0.5, abs(self.jitter_offset) + self.jitter_width + 0.05)
|
|
198
|
+
axis_range = [min(centers) - half_span, max(centers) + 0.5 * half_span]
|
|
199
|
+
|
|
200
|
+
fig.update_layout(
|
|
201
|
+
**{
|
|
202
|
+
axis_layout_key: {
|
|
203
|
+
"overlaying": "x",
|
|
204
|
+
"showgrid": False,
|
|
205
|
+
"zeroline": False,
|
|
206
|
+
"showline": False,
|
|
207
|
+
"showticklabels": False,
|
|
208
|
+
"ticks": "",
|
|
209
|
+
"range": axis_range,
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
# --- Configurator: apply visuals ------------------------------------------
|
|
215
|
+
|
|
216
|
+
def configure(self, fig: go.Figure) -> None:
|
|
217
|
+
"""Apply marker styling to jitter traces.
|
|
218
|
+
|
|
219
|
+
Color is taken from ``jitter_color`` / ``jitter_color_values``
|
|
220
|
+
in trace data if present, otherwise falls back to
|
|
221
|
+
``marker_color`` / ``marker_color_values``. Size, opacity
|
|
222
|
+
and symbol are always inherited from the parent marker.
|
|
223
|
+
"""
|
|
224
|
+
super().configure(fig) # SupportsColoraxis("jitter") layout
|
|
225
|
+
|
|
226
|
+
if not self.jitter_enabled or self._traces_data is None:
|
|
227
|
+
return
|
|
228
|
+
|
|
229
|
+
coloraxis = self.resolve_coloraxis(fig)
|
|
230
|
+
|
|
231
|
+
for trace_id, trace_data in self._traces_data.items():
|
|
232
|
+
jitter_trace_id = f"{trace_id}{_JITTER_TRACE_ID_SUFFIX}"
|
|
233
|
+
kwargs = _build_jitter_marker_kwargs(trace_data, coloraxis)
|
|
234
|
+
|
|
235
|
+
if kwargs:
|
|
236
|
+
fig.update_traces(
|
|
237
|
+
marker=kwargs,
|
|
238
|
+
selector=lambda t, jtid=jitter_trace_id: (
|
|
239
|
+
t.meta and t.meta.get("trace_id") == jtid
|
|
240
|
+
),
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
# --- Resolution helpers (called by concrete plots in _build_traces_data) --
|
|
244
|
+
|
|
245
|
+
def _resolve_jitter_color(
|
|
246
|
+
self,
|
|
247
|
+
entries: dict[str, Entry] | None,
|
|
248
|
+
theme: Theme | None = None,
|
|
249
|
+
) -> ResolvedColor | None:
|
|
250
|
+
"""Resolve ``jitter_color`` source.
|
|
251
|
+
|
|
252
|
+
Returns ``None`` when ``jitter_color`` is not set, signalling
|
|
253
|
+
that :meth:`configure` should fall back to the parent's
|
|
254
|
+
``marker_color``.
|
|
255
|
+
"""
|
|
256
|
+
if self.jitter_color is None:
|
|
257
|
+
return None
|
|
258
|
+
color_palette = theme.palettes.color if theme else DefaultColorPalette()
|
|
259
|
+
resolver = ColorResolver(color_palette)
|
|
260
|
+
return resolver.resolve(self.jitter_color, entries)
|
|
261
|
+
|
|
262
|
+
def _add_jitter_color_to_trace(
|
|
263
|
+
self,
|
|
264
|
+
trace_data: dict[str, Any],
|
|
265
|
+
resolved_color: ResolvedColor | None,
|
|
266
|
+
) -> None:
|
|
267
|
+
"""Add resolved jitter color to trace data.
|
|
268
|
+
|
|
269
|
+
Vector values (numpy arrays) are flattened to match the
|
|
270
|
+
observation-level expansion performed by ``_group_by_category``.
|
|
271
|
+
"""
|
|
272
|
+
if resolved_color is None:
|
|
273
|
+
return
|
|
274
|
+
if resolved_color.values is not None:
|
|
275
|
+
trace_data["jitter_color_values"] = _flatten_color_values(
|
|
276
|
+
resolved_color.values,
|
|
277
|
+
)
|
|
278
|
+
elif resolved_color.color is not None:
|
|
279
|
+
trace_data["jitter_color"] = resolved_color.color
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
# --- Private helpers ----------------------------------------------------------
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def _build_jitter_marker_kwargs(
|
|
286
|
+
trace_data: dict[str, Any],
|
|
287
|
+
coloraxis: str | None,
|
|
288
|
+
) -> dict[str, Any]:
|
|
289
|
+
"""Build marker kwargs for a jitter trace.
|
|
290
|
+
|
|
291
|
+
Reads jitter-specific color keys first, falling back to the parent
|
|
292
|
+
trace's marker color when no jitter color is present.
|
|
293
|
+
"""
|
|
294
|
+
kwargs: dict[str, Any] = {}
|
|
295
|
+
|
|
296
|
+
# Inherit size, opacity, symbol from parent marker
|
|
297
|
+
size = trace_data.get("marker_size")
|
|
298
|
+
if size is not None:
|
|
299
|
+
kwargs["size"] = size
|
|
300
|
+
|
|
301
|
+
opacity = trace_data.get("marker_opacity")
|
|
302
|
+
if opacity is not None:
|
|
303
|
+
kwargs["opacity"] = opacity
|
|
304
|
+
|
|
305
|
+
symbol = trace_data.get("marker_symbol")
|
|
306
|
+
if symbol is not None:
|
|
307
|
+
kwargs["symbol"] = symbol
|
|
308
|
+
|
|
309
|
+
# Color: jitter-specific keys take priority over marker fallback
|
|
310
|
+
jitter_color = trace_data.get("jitter_color")
|
|
311
|
+
jitter_color_values = trace_data.get("jitter_color_values")
|
|
312
|
+
|
|
313
|
+
if jitter_color_values is not None:
|
|
314
|
+
kwargs["color"] = jitter_color_values
|
|
315
|
+
if coloraxis is not None:
|
|
316
|
+
kwargs["coloraxis"] = coloraxis
|
|
317
|
+
elif jitter_color is not None:
|
|
318
|
+
kwargs["color"] = jitter_color
|
|
319
|
+
else:
|
|
320
|
+
# Fallback to parent marker color.
|
|
321
|
+
# Flatten potential arrays (vector fields stored by SupportsMarker).
|
|
322
|
+
marker_color = trace_data.get("marker_color")
|
|
323
|
+
marker_color_values = trace_data.get("marker_color_values")
|
|
324
|
+
|
|
325
|
+
if marker_color_values is not None:
|
|
326
|
+
kwargs["color"] = _flatten_color_values(marker_color_values)
|
|
327
|
+
if coloraxis is not None:
|
|
328
|
+
kwargs["coloraxis"] = coloraxis
|
|
329
|
+
elif marker_color is not None:
|
|
330
|
+
kwargs["color"] = marker_color
|
|
331
|
+
|
|
332
|
+
return kwargs
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
def _flatten_color_values(values: list[Any]) -> list[float]:
|
|
336
|
+
"""Flatten per-entry color values, expanding numpy arrays.
|
|
337
|
+
|
|
338
|
+
Scalar values are converted to ``float``. Array values are
|
|
339
|
+
``ravel()``-ed and their elements appended individually, matching
|
|
340
|
+
the observation-level expansion performed by ``_group_by_category``.
|
|
341
|
+
|
|
342
|
+
If all values are already scalar, the output is equivalent to a
|
|
343
|
+
plain ``[float(v) for v in values]``.
|
|
344
|
+
"""
|
|
345
|
+
flat: list[float] = []
|
|
346
|
+
for v in values:
|
|
347
|
+
if isinstance(v, np.ndarray):
|
|
348
|
+
flat.extend(v.ravel().tolist())
|
|
349
|
+
elif v is None:
|
|
350
|
+
flat.append(float("nan"))
|
|
351
|
+
else:
|
|
352
|
+
flat.append(float(v))
|
|
353
|
+
return flat
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
def _coerce_jitter_center(trace_data: dict[str, Any]) -> float | None:
|
|
357
|
+
"""Extract a numeric x-center for jitter from trace data."""
|
|
358
|
+
x_center = trace_data.get("jitter_x_center")
|
|
359
|
+
if x_center is None:
|
|
360
|
+
x_center = trace_data.get("category_idx")
|
|
361
|
+
|
|
362
|
+
if x_center is None:
|
|
363
|
+
return None
|
|
364
|
+
|
|
365
|
+
try:
|
|
366
|
+
return float(x_center)
|
|
367
|
+
except (TypeError, ValueError):
|
|
368
|
+
return None
|