oscura 0.8.0__py3-none-any.whl → 0.10.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- oscura/__init__.py +19 -19
- oscura/analyzers/__init__.py +2 -0
- oscura/analyzers/digital/extraction.py +2 -3
- oscura/analyzers/digital/quality.py +1 -1
- oscura/analyzers/digital/timing.py +1 -1
- oscura/analyzers/patterns/__init__.py +66 -0
- oscura/analyzers/power/basic.py +3 -3
- oscura/analyzers/power/soa.py +1 -1
- oscura/analyzers/power/switching.py +3 -3
- oscura/analyzers/signal_classification.py +529 -0
- oscura/analyzers/signal_integrity/sparams.py +3 -3
- oscura/analyzers/statistics/basic.py +10 -7
- oscura/analyzers/validation.py +1 -1
- oscura/analyzers/waveform/measurements.py +200 -156
- oscura/analyzers/waveform/measurements_with_uncertainty.py +91 -35
- oscura/analyzers/waveform/spectral.py +164 -73
- oscura/api/dsl/commands.py +15 -6
- oscura/api/server/templates/base.html +137 -146
- oscura/api/server/templates/export.html +84 -110
- oscura/api/server/templates/home.html +248 -267
- oscura/api/server/templates/protocols.html +44 -48
- oscura/api/server/templates/reports.html +27 -35
- oscura/api/server/templates/session_detail.html +68 -78
- oscura/api/server/templates/sessions.html +62 -72
- oscura/api/server/templates/waveforms.html +54 -64
- oscura/automotive/__init__.py +1 -1
- oscura/automotive/can/session.py +1 -1
- oscura/automotive/dbc/generator.py +638 -23
- oscura/automotive/uds/decoder.py +99 -6
- oscura/cli/analyze.py +8 -2
- oscura/cli/batch.py +36 -5
- oscura/cli/characterize.py +18 -4
- oscura/cli/export.py +47 -5
- oscura/cli/main.py +2 -0
- oscura/cli/onboarding/wizard.py +10 -6
- oscura/cli/pipeline.py +585 -0
- oscura/cli/visualize.py +6 -4
- oscura/convenience.py +400 -32
- oscura/core/measurement_result.py +286 -0
- oscura/core/progress.py +1 -1
- oscura/core/types.py +232 -239
- oscura/correlation/multi_protocol.py +1 -1
- oscura/export/legacy/__init__.py +11 -0
- oscura/export/legacy/wav.py +75 -0
- oscura/exporters/__init__.py +19 -0
- oscura/exporters/wireshark.py +809 -0
- oscura/hardware/acquisition/file.py +5 -19
- oscura/hardware/acquisition/saleae.py +10 -10
- oscura/hardware/acquisition/socketcan.py +4 -6
- oscura/hardware/acquisition/synthetic.py +1 -5
- oscura/hardware/acquisition/visa.py +6 -6
- oscura/hardware/security/side_channel_detector.py +5 -508
- oscura/inference/message_format.py +686 -1
- oscura/jupyter/display.py +2 -2
- oscura/jupyter/magic.py +3 -3
- oscura/loaders/__init__.py +17 -12
- oscura/loaders/binary.py +1 -1
- oscura/loaders/chipwhisperer.py +1 -2
- oscura/loaders/configurable.py +1 -1
- oscura/loaders/csv_loader.py +2 -2
- oscura/loaders/hdf5_loader.py +1 -1
- oscura/loaders/lazy.py +6 -1
- oscura/loaders/mmap_loader.py +0 -1
- oscura/loaders/numpy_loader.py +8 -7
- oscura/loaders/preprocessing.py +3 -5
- oscura/loaders/rigol.py +21 -7
- oscura/loaders/sigrok.py +2 -5
- oscura/loaders/tdms.py +3 -2
- oscura/loaders/tektronix.py +38 -32
- oscura/loaders/tss.py +20 -27
- oscura/loaders/vcd.py +13 -8
- oscura/loaders/wav.py +1 -6
- oscura/pipeline/__init__.py +76 -0
- oscura/pipeline/handlers/__init__.py +165 -0
- oscura/pipeline/handlers/analyzers.py +1045 -0
- oscura/pipeline/handlers/decoders.py +899 -0
- oscura/pipeline/handlers/exporters.py +1103 -0
- oscura/pipeline/handlers/filters.py +891 -0
- oscura/pipeline/handlers/loaders.py +640 -0
- oscura/pipeline/handlers/transforms.py +768 -0
- oscura/reporting/formatting/measurements.py +55 -14
- oscura/reporting/templates/enhanced/protocol_re.html +504 -503
- oscura/side_channel/__init__.py +38 -57
- oscura/utils/builders/signal_builder.py +5 -5
- oscura/utils/comparison/compare.py +7 -9
- oscura/utils/comparison/golden.py +1 -1
- oscura/utils/filtering/convenience.py +2 -2
- oscura/utils/math/arithmetic.py +38 -62
- oscura/utils/math/interpolation.py +20 -20
- oscura/utils/pipeline/__init__.py +4 -17
- oscura/utils/progressive.py +1 -4
- oscura/utils/triggering/edge.py +1 -1
- oscura/utils/triggering/pattern.py +2 -2
- oscura/utils/triggering/pulse.py +2 -2
- oscura/utils/triggering/window.py +3 -3
- oscura/validation/hil_testing.py +11 -11
- oscura/visualization/__init__.py +46 -284
- oscura/visualization/batch.py +72 -433
- oscura/visualization/plot.py +542 -53
- oscura/visualization/styles.py +184 -318
- oscura/workflows/batch/advanced.py +1 -1
- oscura/workflows/batch/aggregate.py +7 -8
- oscura/workflows/complete_re.py +251 -23
- oscura/workflows/digital.py +27 -4
- oscura/workflows/multi_trace.py +136 -17
- oscura/workflows/waveform.py +11 -6
- {oscura-0.8.0.dist-info → oscura-0.10.0.dist-info}/METADATA +59 -79
- {oscura-0.8.0.dist-info → oscura-0.10.0.dist-info}/RECORD +111 -136
- oscura/side_channel/dpa.py +0 -1025
- oscura/utils/optimization/__init__.py +0 -19
- oscura/utils/optimization/parallel.py +0 -443
- oscura/utils/optimization/search.py +0 -532
- oscura/utils/pipeline/base.py +0 -338
- oscura/utils/pipeline/composition.py +0 -248
- oscura/utils/pipeline/parallel.py +0 -449
- oscura/utils/pipeline/pipeline.py +0 -375
- oscura/utils/search/__init__.py +0 -16
- oscura/utils/search/anomaly.py +0 -424
- oscura/utils/search/context.py +0 -294
- oscura/utils/search/pattern.py +0 -288
- oscura/utils/storage/__init__.py +0 -61
- oscura/utils/storage/database.py +0 -1166
- oscura/visualization/accessibility.py +0 -526
- oscura/visualization/annotations.py +0 -371
- oscura/visualization/axis_scaling.py +0 -305
- oscura/visualization/colors.py +0 -451
- oscura/visualization/digital.py +0 -436
- oscura/visualization/eye.py +0 -571
- oscura/visualization/histogram.py +0 -281
- oscura/visualization/interactive.py +0 -1035
- oscura/visualization/jitter.py +0 -1042
- oscura/visualization/keyboard.py +0 -394
- oscura/visualization/layout.py +0 -400
- oscura/visualization/optimization.py +0 -1079
- oscura/visualization/palettes.py +0 -446
- oscura/visualization/power.py +0 -508
- oscura/visualization/power_extended.py +0 -955
- oscura/visualization/presets.py +0 -469
- oscura/visualization/protocols.py +0 -1246
- oscura/visualization/render.py +0 -223
- oscura/visualization/rendering.py +0 -444
- oscura/visualization/reverse_engineering.py +0 -838
- oscura/visualization/signal_integrity.py +0 -989
- oscura/visualization/specialized.py +0 -643
- oscura/visualization/spectral.py +0 -1226
- oscura/visualization/thumbnails.py +0 -340
- oscura/visualization/time_axis.py +0 -351
- oscura/visualization/waveform.py +0 -454
- {oscura-0.8.0.dist-info → oscura-0.10.0.dist-info}/WHEEL +0 -0
- {oscura-0.8.0.dist-info → oscura-0.10.0.dist-info}/entry_points.txt +0 -0
- {oscura-0.8.0.dist-info → oscura-0.10.0.dist-info}/licenses/LICENSE +0 -0
oscura/visualization/layout.py
DELETED
|
@@ -1,400 +0,0 @@
|
|
|
1
|
-
"""Visualization layout functions for multi-channel plots and annotation placement.
|
|
2
|
-
|
|
3
|
-
This module provides intelligent layout algorithms for stacking multiple
|
|
4
|
-
channels and optimizing annotation placement with collision avoidance.
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
Example:
|
|
8
|
-
>>> from oscura.visualization.layout import layout_stacked_channels
|
|
9
|
-
>>> layout = layout_stacked_channels(n_channels=4, figsize=(10, 8))
|
|
10
|
-
>>> print(f"Channel heights: {layout['heights']}")
|
|
11
|
-
|
|
12
|
-
References:
|
|
13
|
-
- Force-directed graph layout (Fruchterman-Reingold)
|
|
14
|
-
- Constrained layout solver for equal spacing
|
|
15
|
-
"""
|
|
16
|
-
|
|
17
|
-
from __future__ import annotations
|
|
18
|
-
|
|
19
|
-
from dataclasses import dataclass
|
|
20
|
-
from typing import TYPE_CHECKING
|
|
21
|
-
|
|
22
|
-
import numpy as np
|
|
23
|
-
|
|
24
|
-
from oscura.utils.geometry import generate_leader_line
|
|
25
|
-
|
|
26
|
-
if TYPE_CHECKING:
|
|
27
|
-
from numpy.typing import NDArray
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
@dataclass
|
|
31
|
-
class ChannelLayout:
|
|
32
|
-
"""Layout specification for stacked channels.
|
|
33
|
-
|
|
34
|
-
Attributes:
|
|
35
|
-
n_channels: Number of channels to stack
|
|
36
|
-
heights: Array of subplot heights (normalized 0-1)
|
|
37
|
-
gaps: Array of gap sizes between channels (normalized 0-1)
|
|
38
|
-
y_positions: Array of Y positions for each channel (normalized 0-1)
|
|
39
|
-
shared_x: Whether channels share X-axis
|
|
40
|
-
figsize: Figure size (width, height) in inches
|
|
41
|
-
"""
|
|
42
|
-
|
|
43
|
-
n_channels: int
|
|
44
|
-
heights: NDArray[np.float64]
|
|
45
|
-
gaps: NDArray[np.float64]
|
|
46
|
-
y_positions: NDArray[np.float64]
|
|
47
|
-
shared_x: bool
|
|
48
|
-
figsize: tuple[float, float]
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
@dataclass
|
|
52
|
-
class Annotation:
|
|
53
|
-
"""Annotation specification with position and bounding box.
|
|
54
|
-
|
|
55
|
-
Attributes:
|
|
56
|
-
text: Annotation text
|
|
57
|
-
x: X coordinate in data units
|
|
58
|
-
y: Y coordinate in data units
|
|
59
|
-
bbox_width: Bounding box width in display units
|
|
60
|
-
bbox_height: Bounding box height in display units
|
|
61
|
-
priority: Priority for placement (0-1, higher is more important)
|
|
62
|
-
anchor: Preferred anchor position ("top", "bottom", "left", "right", "auto")
|
|
63
|
-
"""
|
|
64
|
-
|
|
65
|
-
text: str
|
|
66
|
-
x: float
|
|
67
|
-
y: float
|
|
68
|
-
bbox_width: float = 50.0
|
|
69
|
-
bbox_height: float = 20.0
|
|
70
|
-
priority: float = 0.5
|
|
71
|
-
anchor: str = "auto"
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
@dataclass
|
|
75
|
-
class PlacedAnnotation:
|
|
76
|
-
"""Annotation with optimized placement.
|
|
77
|
-
|
|
78
|
-
Attributes:
|
|
79
|
-
annotation: Original annotation
|
|
80
|
-
display_x: Optimized X position in display units
|
|
81
|
-
display_y: Optimized Y position in display units
|
|
82
|
-
needs_leader: Whether a leader line is needed
|
|
83
|
-
leader_points: Points for leader line (if needed)
|
|
84
|
-
"""
|
|
85
|
-
|
|
86
|
-
annotation: Annotation
|
|
87
|
-
display_x: float
|
|
88
|
-
display_y: float
|
|
89
|
-
needs_leader: bool
|
|
90
|
-
leader_points: list[tuple[float, float]] | None = None
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
def layout_stacked_channels(
|
|
94
|
-
n_channels: int,
|
|
95
|
-
*,
|
|
96
|
-
figsize: tuple[float, float] = (10, 8),
|
|
97
|
-
gap_ratio: float = 0.1,
|
|
98
|
-
shared_x: bool = True,
|
|
99
|
-
) -> ChannelLayout:
|
|
100
|
-
"""Calculate equal vertical spacing for stacked multi-channel plots.
|
|
101
|
-
|
|
102
|
-
Implements constrained layout solver for equal spacing with configurable
|
|
103
|
-
gaps between channels, ensuring proper vertical alignment.
|
|
104
|
-
|
|
105
|
-
Args:
|
|
106
|
-
n_channels: Number of channels to stack.
|
|
107
|
-
figsize: Figure size (width, height) in inches.
|
|
108
|
-
gap_ratio: Ratio of gap to channel height (default 0.1 = 10%).
|
|
109
|
-
shared_x: Whether channels share X-axis (affects bottom margin).
|
|
110
|
-
|
|
111
|
-
Returns:
|
|
112
|
-
ChannelLayout with heights, gaps, and positions.
|
|
113
|
-
|
|
114
|
-
Raises:
|
|
115
|
-
ValueError: If n_channels < 1 or gap_ratio invalid.
|
|
116
|
-
|
|
117
|
-
Example:
|
|
118
|
-
>>> layout = layout_stacked_channels(n_channels=3, gap_ratio=0.1)
|
|
119
|
-
>>> print(f"Channel 0 position: {layout.y_positions[0]:.3f}")
|
|
120
|
-
|
|
121
|
-
References:
|
|
122
|
-
VIS-015: Multi-Channel Stack Optimization
|
|
123
|
-
"""
|
|
124
|
-
if n_channels < 1:
|
|
125
|
-
raise ValueError("n_channels must be >= 1")
|
|
126
|
-
|
|
127
|
-
if gap_ratio < 0 or gap_ratio > 1:
|
|
128
|
-
raise ValueError(f"gap_ratio must be in [0, 1], got {gap_ratio}")
|
|
129
|
-
|
|
130
|
-
# Total available height (normalized to 1.0)
|
|
131
|
-
# Reserve space for margins
|
|
132
|
-
top_margin = 0.05
|
|
133
|
-
bottom_margin = 0.1 if shared_x else 0.05
|
|
134
|
-
available_height = 1.0 - top_margin - bottom_margin
|
|
135
|
-
|
|
136
|
-
# Calculate channel height with gaps
|
|
137
|
-
# Total height = n_channels * h + (n_channels - 1) * gap
|
|
138
|
-
# where gap = gap_ratio * h
|
|
139
|
-
# Solving: available_height = n_channels * h + (n_channels - 1) * gap_ratio * h
|
|
140
|
-
# = h * (n_channels + (n_channels - 1) * gap_ratio)
|
|
141
|
-
denominator = n_channels + (n_channels - 1) * gap_ratio
|
|
142
|
-
channel_height = available_height / denominator
|
|
143
|
-
gap_height = channel_height * gap_ratio
|
|
144
|
-
|
|
145
|
-
# Calculate heights and gaps arrays
|
|
146
|
-
heights = np.full(n_channels, channel_height, dtype=np.float64)
|
|
147
|
-
gaps = np.full(n_channels - 1, gap_height, dtype=np.float64) if n_channels > 1 else np.array([])
|
|
148
|
-
|
|
149
|
-
# Calculate Y positions (from bottom)
|
|
150
|
-
y_positions = np.zeros(n_channels, dtype=np.float64)
|
|
151
|
-
current_y = bottom_margin
|
|
152
|
-
|
|
153
|
-
for i in range(n_channels):
|
|
154
|
-
# Channels are indexed from bottom to top
|
|
155
|
-
y_positions[i] = current_y
|
|
156
|
-
current_y += channel_height
|
|
157
|
-
if i < n_channels - 1:
|
|
158
|
-
current_y += gap_height
|
|
159
|
-
|
|
160
|
-
return ChannelLayout(
|
|
161
|
-
n_channels=n_channels,
|
|
162
|
-
heights=heights,
|
|
163
|
-
gaps=gaps,
|
|
164
|
-
y_positions=y_positions,
|
|
165
|
-
shared_x=shared_x,
|
|
166
|
-
figsize=figsize,
|
|
167
|
-
)
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
def _initialize_placed_annotations(
|
|
171
|
-
annotations: list[Annotation],
|
|
172
|
-
) -> list[PlacedAnnotation]:
|
|
173
|
-
"""Initialize placed annotations at anchor points.
|
|
174
|
-
|
|
175
|
-
Args:
|
|
176
|
-
annotations: List of annotations to place.
|
|
177
|
-
|
|
178
|
-
Returns:
|
|
179
|
-
List of PlacedAnnotation initially at anchor points.
|
|
180
|
-
"""
|
|
181
|
-
placed = []
|
|
182
|
-
for annot in annotations:
|
|
183
|
-
placed.append(
|
|
184
|
-
PlacedAnnotation(
|
|
185
|
-
annotation=annot,
|
|
186
|
-
display_x=annot.x,
|
|
187
|
-
display_y=annot.y,
|
|
188
|
-
needs_leader=False,
|
|
189
|
-
leader_points=None,
|
|
190
|
-
)
|
|
191
|
-
)
|
|
192
|
-
return placed
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
def _calculate_repulsive_force(
|
|
196
|
-
placed_i: PlacedAnnotation,
|
|
197
|
-
placed_j: PlacedAnnotation,
|
|
198
|
-
min_spacing: float,
|
|
199
|
-
repulsion_strength: float,
|
|
200
|
-
) -> tuple[float, float]:
|
|
201
|
-
"""Calculate repulsive force between two annotations.
|
|
202
|
-
|
|
203
|
-
Args:
|
|
204
|
-
placed_i: First annotation.
|
|
205
|
-
placed_j: Second annotation.
|
|
206
|
-
min_spacing: Minimum spacing in pixels.
|
|
207
|
-
repulsion_strength: Repulsive force strength.
|
|
208
|
-
|
|
209
|
-
Returns:
|
|
210
|
-
Tuple of (fx, fy) force components.
|
|
211
|
-
"""
|
|
212
|
-
# Check for bounding box overlap
|
|
213
|
-
dx = placed_j.display_x - placed_i.display_x
|
|
214
|
-
dy = placed_j.display_y - placed_i.display_y
|
|
215
|
-
|
|
216
|
-
# Bounding box sizes
|
|
217
|
-
w1 = placed_i.annotation.bbox_width
|
|
218
|
-
h1 = placed_i.annotation.bbox_height
|
|
219
|
-
w2 = placed_j.annotation.bbox_width
|
|
220
|
-
h2 = placed_j.annotation.bbox_height
|
|
221
|
-
|
|
222
|
-
# Minimum separation (sum of half-widths + spacing)
|
|
223
|
-
min_dx = (w1 + w2) / 2 + min_spacing
|
|
224
|
-
min_dy = (h1 + h2) / 2 + min_spacing
|
|
225
|
-
|
|
226
|
-
# Check if overlapping
|
|
227
|
-
if abs(dx) < min_dx and abs(dy) < min_dy:
|
|
228
|
-
# Calculate repulsive force
|
|
229
|
-
distance = np.sqrt(dx**2 + dy**2)
|
|
230
|
-
if distance < 1e-6:
|
|
231
|
-
# Avoid division by zero
|
|
232
|
-
distance = 1e-6
|
|
233
|
-
dx = np.random.randn() * 0.1
|
|
234
|
-
dy = np.random.randn() * 0.1
|
|
235
|
-
|
|
236
|
-
# Repulsion inversely proportional to distance
|
|
237
|
-
force = repulsion_strength / distance
|
|
238
|
-
|
|
239
|
-
# Return force in direction away from overlap
|
|
240
|
-
return -force * dx / distance, -force * dy / distance
|
|
241
|
-
|
|
242
|
-
return 0.0, 0.0
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
def _apply_force_iteration(
|
|
246
|
-
placed: list[PlacedAnnotation],
|
|
247
|
-
display_width: float,
|
|
248
|
-
display_height: float,
|
|
249
|
-
min_spacing: float,
|
|
250
|
-
repulsion_strength: float,
|
|
251
|
-
) -> bool:
|
|
252
|
-
"""Apply one iteration of force-directed layout.
|
|
253
|
-
|
|
254
|
-
Args:
|
|
255
|
-
placed: List of placed annotations to update.
|
|
256
|
-
display_width: Display width for clamping.
|
|
257
|
-
display_height: Display height for clamping.
|
|
258
|
-
min_spacing: Minimum spacing in pixels.
|
|
259
|
-
repulsion_strength: Repulsive force strength.
|
|
260
|
-
|
|
261
|
-
Returns:
|
|
262
|
-
True if any annotation moved significantly.
|
|
263
|
-
"""
|
|
264
|
-
moved = False
|
|
265
|
-
|
|
266
|
-
for i in range(len(placed)):
|
|
267
|
-
fx = 0.0
|
|
268
|
-
fy = 0.0
|
|
269
|
-
|
|
270
|
-
# Calculate forces from all other annotations
|
|
271
|
-
for j in range(len(placed)):
|
|
272
|
-
if i != j:
|
|
273
|
-
force_x, force_y = _calculate_repulsive_force(
|
|
274
|
-
placed[i], placed[j], min_spacing, repulsion_strength
|
|
275
|
-
)
|
|
276
|
-
fx += force_x
|
|
277
|
-
fy += force_y
|
|
278
|
-
|
|
279
|
-
# Apply forces with damping (priority affects inertia)
|
|
280
|
-
damping = 0.5
|
|
281
|
-
priority_factor = 1.0 - placed[i].annotation.priority
|
|
282
|
-
step_size = damping * priority_factor
|
|
283
|
-
|
|
284
|
-
new_x = placed[i].display_x + fx * step_size
|
|
285
|
-
new_y = placed[i].display_y + fy * step_size
|
|
286
|
-
|
|
287
|
-
# Clamp to display bounds
|
|
288
|
-
new_x = np.clip(new_x, 0, display_width)
|
|
289
|
-
new_y = np.clip(new_y, 0, display_height)
|
|
290
|
-
|
|
291
|
-
# Update if moved significantly
|
|
292
|
-
if abs(new_x - placed[i].display_x) > 0.1 or abs(new_y - placed[i].display_y) > 0.1:
|
|
293
|
-
placed[i] = PlacedAnnotation(
|
|
294
|
-
annotation=placed[i].annotation,
|
|
295
|
-
display_x=new_x,
|
|
296
|
-
display_y=new_y,
|
|
297
|
-
needs_leader=False,
|
|
298
|
-
leader_points=None,
|
|
299
|
-
)
|
|
300
|
-
moved = True
|
|
301
|
-
|
|
302
|
-
return moved
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
def _add_leader_lines(placed: list[PlacedAnnotation], leader_threshold: float = 20.0) -> None:
|
|
306
|
-
"""Add leader lines to annotations displaced from anchor points.
|
|
307
|
-
|
|
308
|
-
Args:
|
|
309
|
-
placed: List of placed annotations to update in-place.
|
|
310
|
-
leader_threshold: Displacement threshold for leader line in pixels.
|
|
311
|
-
"""
|
|
312
|
-
for i, p in enumerate(placed):
|
|
313
|
-
anchor_x = p.annotation.x
|
|
314
|
-
anchor_y = p.annotation.y
|
|
315
|
-
|
|
316
|
-
displacement = np.sqrt((p.display_x - anchor_x) ** 2 + (p.display_y - anchor_y) ** 2)
|
|
317
|
-
|
|
318
|
-
if displacement > leader_threshold:
|
|
319
|
-
# Generate simple orthogonal leader line
|
|
320
|
-
leader_points = generate_leader_line(
|
|
321
|
-
(anchor_x, anchor_y),
|
|
322
|
-
(p.display_x, p.display_y),
|
|
323
|
-
)
|
|
324
|
-
|
|
325
|
-
placed[i] = PlacedAnnotation(
|
|
326
|
-
annotation=p.annotation,
|
|
327
|
-
display_x=p.display_x,
|
|
328
|
-
display_y=p.display_y,
|
|
329
|
-
needs_leader=True,
|
|
330
|
-
leader_points=leader_points,
|
|
331
|
-
)
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
def optimize_annotation_placement(
|
|
335
|
-
annotations: list[Annotation],
|
|
336
|
-
*,
|
|
337
|
-
display_width: float = 800.0,
|
|
338
|
-
display_height: float = 600.0,
|
|
339
|
-
max_iterations: int = 100,
|
|
340
|
-
repulsion_strength: float = 10.0,
|
|
341
|
-
min_spacing: float = 5.0,
|
|
342
|
-
) -> list[PlacedAnnotation]:
|
|
343
|
-
"""Optimize annotation placement with collision avoidance.
|
|
344
|
-
|
|
345
|
-
Uses force-directed layout algorithm to separate overlapping labels
|
|
346
|
-
with repulsive forces. Generates leader lines when labels must be
|
|
347
|
-
displaced from anchor points.
|
|
348
|
-
|
|
349
|
-
Args:
|
|
350
|
-
annotations: List of annotations to place.
|
|
351
|
-
display_width: Display area width in pixels.
|
|
352
|
-
display_height: Display area height in pixels.
|
|
353
|
-
max_iterations: Maximum iterations for force-directed layout.
|
|
354
|
-
repulsion_strength: Strength of repulsive force between overlapping labels.
|
|
355
|
-
min_spacing: Minimum spacing between annotations in pixels.
|
|
356
|
-
|
|
357
|
-
Returns:
|
|
358
|
-
List of PlacedAnnotation with optimized positions.
|
|
359
|
-
|
|
360
|
-
Raises:
|
|
361
|
-
ValueError: If annotations list is empty.
|
|
362
|
-
|
|
363
|
-
Example:
|
|
364
|
-
>>> annots = [Annotation("Peak", 0.5, 1.0, priority=0.9)]
|
|
365
|
-
>>> placed = optimize_annotation_placement(annots)
|
|
366
|
-
>>> print(f"Needs leader: {placed[0].needs_leader}")
|
|
367
|
-
|
|
368
|
-
References:
|
|
369
|
-
VIS-016: Annotation Placement Intelligence
|
|
370
|
-
Force-directed graph layout (Fruchterman-Reingold)
|
|
371
|
-
"""
|
|
372
|
-
if len(annotations) == 0:
|
|
373
|
-
raise ValueError("annotations list cannot be empty")
|
|
374
|
-
|
|
375
|
-
# Data preparation - initialize at anchor points
|
|
376
|
-
placed = _initialize_placed_annotations(annotations)
|
|
377
|
-
|
|
378
|
-
# Force-directed layout iterations
|
|
379
|
-
for _iteration in range(max_iterations):
|
|
380
|
-
moved = _apply_force_iteration(
|
|
381
|
-
placed, display_width, display_height, min_spacing, repulsion_strength
|
|
382
|
-
)
|
|
383
|
-
|
|
384
|
-
# Converged if nothing moved
|
|
385
|
-
if not moved:
|
|
386
|
-
break
|
|
387
|
-
|
|
388
|
-
# Annotation - add leader lines for displaced annotations
|
|
389
|
-
_add_leader_lines(placed, leader_threshold=20.0)
|
|
390
|
-
|
|
391
|
-
return placed
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
__all__ = [
|
|
395
|
-
"Annotation",
|
|
396
|
-
"ChannelLayout",
|
|
397
|
-
"PlacedAnnotation",
|
|
398
|
-
"layout_stacked_channels",
|
|
399
|
-
"optimize_annotation_placement",
|
|
400
|
-
]
|