oscura 0.7.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/eye/__init__.py +5 -1
- oscura/analyzers/eye/generation.py +501 -0
- oscura/analyzers/jitter/__init__.py +6 -6
- oscura/analyzers/jitter/timing.py +419 -0
- oscura/analyzers/patterns/__init__.py +94 -0
- oscura/analyzers/patterns/reverse_engineering.py +991 -0
- oscura/analyzers/power/__init__.py +35 -12
- 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/__init__.py +4 -0
- oscura/analyzers/statistics/basic.py +152 -0
- oscura/analyzers/statistics/correlation.py +47 -6
- oscura/analyzers/validation.py +1 -1
- oscura/analyzers/waveform/__init__.py +2 -0
- oscura/analyzers/waveform/measurements.py +329 -163
- oscura/analyzers/waveform/measurements_with_uncertainty.py +91 -35
- oscura/analyzers/waveform/spectral.py +498 -54
- 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/dtc/data.json +102 -17
- 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/config/loader.py +0 -1
- oscura/core/measurement_result.py +286 -0
- oscura/core/progress.py +1 -1
- oscura/core/schemas/device_mapping.json +8 -2
- oscura/core/schemas/packet_format.json +24 -4
- oscura/core/schemas/protocol_definition.json +12 -2
- oscura/core/types.py +300 -199
- 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/__init__.py +88 -1
- oscura/reporting/automation.py +348 -0
- oscura/reporting/citations.py +374 -0
- oscura/reporting/core.py +54 -0
- oscura/reporting/formatting/__init__.py +11 -0
- oscura/reporting/formatting/measurements.py +320 -0
- oscura/reporting/html.py +57 -0
- oscura/reporting/interpretation.py +431 -0
- oscura/reporting/summary.py +329 -0
- oscura/reporting/templates/enhanced/protocol_re.html +504 -503
- oscura/reporting/visualization.py +542 -0
- 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 +47 -284
- oscura/visualization/batch.py +160 -0
- oscura/visualization/plot.py +542 -53
- oscura/visualization/styles.py +184 -318
- oscura/workflows/__init__.py +2 -0
- 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 +788 -0
- {oscura-0.7.0.dist-info → oscura-0.10.0.dist-info}/METADATA +59 -79
- {oscura-0.7.0.dist-info → oscura-0.10.0.dist-info}/RECORD +135 -149
- 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.7.0.dist-info → oscura-0.10.0.dist-info}/WHEEL +0 -0
- {oscura-0.7.0.dist-info → oscura-0.10.0.dist-info}/entry_points.txt +0 -0
- {oscura-0.7.0.dist-info → oscura-0.10.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,526 +0,0 @@
|
|
|
1
|
-
"""Accessibility utilities for Oscura visualizations.
|
|
2
|
-
|
|
3
|
-
This module provides accessibility features for visualizations including
|
|
4
|
-
colorblind-safe palettes, alt-text generation, and keyboard navigation support.
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
Example:
|
|
8
|
-
>>> from oscura.visualization.accessibility import (
|
|
9
|
-
... get_colorblind_palette,
|
|
10
|
-
... generate_alt_text,
|
|
11
|
-
... KeyboardHandler
|
|
12
|
-
... )
|
|
13
|
-
>>> palette = get_colorblind_palette("viridis")
|
|
14
|
-
>>> alt_text = generate_alt_text(trace, "Time-domain waveform")
|
|
15
|
-
|
|
16
|
-
References:
|
|
17
|
-
- Colorblind-safe palette design (Brettel 1997)
|
|
18
|
-
- WCAG 2.1 accessibility guidelines
|
|
19
|
-
- WAI-ARIA best practices
|
|
20
|
-
"""
|
|
21
|
-
|
|
22
|
-
from __future__ import annotations
|
|
23
|
-
|
|
24
|
-
from typing import TYPE_CHECKING, Any, Literal
|
|
25
|
-
|
|
26
|
-
import matplotlib.pyplot as plt
|
|
27
|
-
import numpy as np
|
|
28
|
-
|
|
29
|
-
if TYPE_CHECKING:
|
|
30
|
-
from collections.abc import Callable
|
|
31
|
-
|
|
32
|
-
from matplotlib.axes import Axes
|
|
33
|
-
from matplotlib.figure import Figure
|
|
34
|
-
from numpy.typing import NDArray
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
# Line style patterns for multi-line plots (ACC-001)
|
|
38
|
-
LINE_STYLES = ["solid", "dashed", "dotted", "dashdot"]
|
|
39
|
-
|
|
40
|
-
# Pass/fail symbols (ACC-001)
|
|
41
|
-
PASS_SYMBOL = "✓"
|
|
42
|
-
FAIL_SYMBOL = "✗"
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
def get_colorblind_palette(
|
|
46
|
-
name: Literal["viridis", "cividis", "plasma", "inferno", "magma"] = "viridis",
|
|
47
|
-
) -> str:
|
|
48
|
-
"""Get colorblind-safe colormap name.
|
|
49
|
-
|
|
50
|
-
: All visualizations use colorblind-safe palettes by default.
|
|
51
|
-
Returns matplotlib colormap names that are perceptually uniform and colorblind-safe.
|
|
52
|
-
|
|
53
|
-
Args:
|
|
54
|
-
name: Colormap name. Options:
|
|
55
|
-
- "viridis": Default, excellent for sequential data
|
|
56
|
-
- "cividis": Optimized for colorblind users
|
|
57
|
-
- "plasma": High contrast sequential
|
|
58
|
-
- "inferno": Warm sequential
|
|
59
|
-
- "magma": Dark to bright sequential
|
|
60
|
-
|
|
61
|
-
Returns:
|
|
62
|
-
Matplotlib colormap name string
|
|
63
|
-
|
|
64
|
-
Raises:
|
|
65
|
-
ValueError: If colormap name is not recognized
|
|
66
|
-
|
|
67
|
-
Example:
|
|
68
|
-
>>> import matplotlib.pyplot as plt
|
|
69
|
-
>>> from oscura.visualization.accessibility import get_colorblind_palette
|
|
70
|
-
>>> cmap = get_colorblind_palette("viridis")
|
|
71
|
-
>>> plt.plot([1, 2, 3], [1, 4, 2], color=plt.get_cmap(cmap)(0.5))
|
|
72
|
-
|
|
73
|
-
References:
|
|
74
|
-
ACC-001: Colorblind-Safe Visualization Palette
|
|
75
|
-
Matplotlib perceptually uniform colormaps
|
|
76
|
-
"""
|
|
77
|
-
valid_names = ["viridis", "cividis", "plasma", "inferno", "magma"]
|
|
78
|
-
if name not in valid_names:
|
|
79
|
-
raise ValueError(f"Unknown colormap: {name}. Valid options: {', '.join(valid_names)}")
|
|
80
|
-
return name
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
def get_multi_line_styles(n_lines: int) -> list[tuple[tuple[float, float, float, float], str]]:
|
|
84
|
-
"""Get distinct line styles and colors for multi-line plots.
|
|
85
|
-
|
|
86
|
-
: Multi-line plots use distinct line styles in addition to colors.
|
|
87
|
-
Combines colorblind-safe colors with varied line styles for maximum distinguishability.
|
|
88
|
-
|
|
89
|
-
Args:
|
|
90
|
-
n_lines: Number of lines to style
|
|
91
|
-
|
|
92
|
-
Returns:
|
|
93
|
-
List of (color, linestyle) tuples where color is RGBA tuple
|
|
94
|
-
|
|
95
|
-
Example:
|
|
96
|
-
>>> from oscura.visualization.accessibility import get_multi_line_styles
|
|
97
|
-
>>> import matplotlib.pyplot as plt
|
|
98
|
-
>>> styles = get_multi_line_styles(4)
|
|
99
|
-
>>> for i, (color, ls) in enumerate(styles):
|
|
100
|
-
... plt.plot([1, 2, 3], [i, i+1, i+2], color=color, linestyle=ls)
|
|
101
|
-
|
|
102
|
-
References:
|
|
103
|
-
ACC-001: Colorblind-Safe Visualization Palette
|
|
104
|
-
"""
|
|
105
|
-
# Use viridis colormap for colorblind-safe colors
|
|
106
|
-
cmap = plt.get_cmap("viridis")
|
|
107
|
-
colors = [cmap(i / max(n_lines - 1, 1)) for i in range(n_lines)]
|
|
108
|
-
|
|
109
|
-
# Cycle through line styles
|
|
110
|
-
styles: list[tuple[tuple[float, float, float, float], str]] = []
|
|
111
|
-
for i in range(n_lines):
|
|
112
|
-
linestyle = LINE_STYLES[i % len(LINE_STYLES)]
|
|
113
|
-
# Colors from colormap are RGBA tuples
|
|
114
|
-
rgba_color = tuple(colors[i])
|
|
115
|
-
styles.append((rgba_color, linestyle)) # type: ignore[arg-type]
|
|
116
|
-
|
|
117
|
-
return styles
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
def format_pass_fail(
|
|
121
|
-
passed: bool,
|
|
122
|
-
*,
|
|
123
|
-
use_color: bool = True,
|
|
124
|
-
use_symbols: bool = True,
|
|
125
|
-
) -> str:
|
|
126
|
-
"""Format pass/fail status with symbols and optional colors.
|
|
127
|
-
|
|
128
|
-
: Pass/fail uses symbols (✓/✗) not just red/green.
|
|
129
|
-
Ensures accessibility by using symbols in addition to or instead of colors.
|
|
130
|
-
|
|
131
|
-
Args:
|
|
132
|
-
passed: Whether the test passed
|
|
133
|
-
use_color: Include ANSI color codes (default: True)
|
|
134
|
-
use_symbols: Include checkmark/cross symbols (default: True)
|
|
135
|
-
|
|
136
|
-
Returns:
|
|
137
|
-
Formatted string with symbol and/or color
|
|
138
|
-
|
|
139
|
-
Example:
|
|
140
|
-
>>> from oscura.visualization.accessibility import format_pass_fail
|
|
141
|
-
>>> print(format_pass_fail(True))
|
|
142
|
-
✓ PASS
|
|
143
|
-
>>> print(format_pass_fail(False))
|
|
144
|
-
✗ FAIL
|
|
145
|
-
|
|
146
|
-
References:
|
|
147
|
-
ACC-001: Colorblind-Safe Visualization Palette
|
|
148
|
-
"""
|
|
149
|
-
if passed:
|
|
150
|
-
symbol = PASS_SYMBOL if use_symbols else ""
|
|
151
|
-
text = "PASS"
|
|
152
|
-
color_code = "\033[92m" if use_color else "" # Green
|
|
153
|
-
else:
|
|
154
|
-
symbol = FAIL_SYMBOL if use_symbols else ""
|
|
155
|
-
text = "FAIL"
|
|
156
|
-
color_code = "\033[91m" if use_color else "" # Red
|
|
157
|
-
|
|
158
|
-
reset_code = "\033[0m" if use_color else ""
|
|
159
|
-
|
|
160
|
-
if use_symbols:
|
|
161
|
-
return f"{color_code}{symbol} {text}{reset_code}"
|
|
162
|
-
else:
|
|
163
|
-
return f"{color_code}{text}{reset_code}"
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
def generate_alt_text(
|
|
167
|
-
data: NDArray[np.float64] | dict[str, Any],
|
|
168
|
-
plot_type: str,
|
|
169
|
-
*,
|
|
170
|
-
title: str | None = None,
|
|
171
|
-
x_label: str = "Time",
|
|
172
|
-
y_label: str = "Amplitude",
|
|
173
|
-
sample_rate: float | None = None,
|
|
174
|
-
) -> str:
|
|
175
|
-
"""Generate descriptive alt-text for a plot.
|
|
176
|
-
|
|
177
|
-
: Every plot has alt_text property describing content.
|
|
178
|
-
Provides text-based summary for screen readers and accessibility tools.
|
|
179
|
-
|
|
180
|
-
Args:
|
|
181
|
-
data: Signal data array or statistics dictionary
|
|
182
|
-
plot_type: Type of plot ("waveform", "spectrum", "histogram", "eye_diagram")
|
|
183
|
-
title: Plot title (optional)
|
|
184
|
-
x_label: X-axis label
|
|
185
|
-
y_label: Y-axis label
|
|
186
|
-
sample_rate: Sample rate in Hz (for time calculations)
|
|
187
|
-
|
|
188
|
-
Returns:
|
|
189
|
-
Descriptive alt-text string
|
|
190
|
-
|
|
191
|
-
Example:
|
|
192
|
-
>>> import numpy as np
|
|
193
|
-
>>> from oscura.visualization.accessibility import generate_alt_text
|
|
194
|
-
>>> signal = np.sin(2 * np.pi * 1e3 * np.linspace(0, 1e-3, 1000))
|
|
195
|
-
>>> alt_text = generate_alt_text(signal, "waveform", title="1 kHz sine wave")
|
|
196
|
-
>>> print(alt_text)
|
|
197
|
-
1 kHz sine wave. Waveform plot showing Time vs Amplitude...
|
|
198
|
-
|
|
199
|
-
References:
|
|
200
|
-
ACC-002: Text Alternatives for Visualizations
|
|
201
|
-
WCAG 2.1 Section 1.1.1 (Non-text Content)
|
|
202
|
-
"""
|
|
203
|
-
parts = []
|
|
204
|
-
|
|
205
|
-
# Add title if provided
|
|
206
|
-
if title:
|
|
207
|
-
parts.append(f"{title}.")
|
|
208
|
-
|
|
209
|
-
# Describe plot type
|
|
210
|
-
parts.append(f"{plot_type.replace('_', ' ').title()} plot showing {x_label} vs {y_label}.")
|
|
211
|
-
|
|
212
|
-
# Add data statistics
|
|
213
|
-
if isinstance(data, dict):
|
|
214
|
-
# Already have statistics
|
|
215
|
-
stats = data
|
|
216
|
-
else:
|
|
217
|
-
# Calculate statistics from array
|
|
218
|
-
stats = {
|
|
219
|
-
"min": float(np.min(data)),
|
|
220
|
-
"max": float(np.max(data)),
|
|
221
|
-
"mean": float(np.mean(data)),
|
|
222
|
-
"std": float(np.std(data)),
|
|
223
|
-
"n_samples": len(data),
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
# Format statistics
|
|
227
|
-
if "n_samples" in stats:
|
|
228
|
-
parts.append(f"Contains {stats['n_samples']} samples.")
|
|
229
|
-
|
|
230
|
-
if "min" in stats and "max" in stats:
|
|
231
|
-
parts.append(f"Range: {stats['min']:.3g} to {stats['max']:.3g} {y_label}.")
|
|
232
|
-
|
|
233
|
-
if "mean" in stats:
|
|
234
|
-
parts.append(f"Mean: {stats['mean']:.3g}.")
|
|
235
|
-
|
|
236
|
-
if "std" in stats:
|
|
237
|
-
parts.append(f"Standard deviation: {stats['std']:.3g}.")
|
|
238
|
-
|
|
239
|
-
# Add duration if sample rate provided
|
|
240
|
-
if sample_rate is not None and "n_samples" in stats:
|
|
241
|
-
duration_s = stats["n_samples"] / sample_rate
|
|
242
|
-
if duration_s < 1e-6:
|
|
243
|
-
duration_str = f"{duration_s * 1e9:.2f} ns"
|
|
244
|
-
elif duration_s < 1e-3:
|
|
245
|
-
duration_str = f"{duration_s * 1e6:.2f} µs"
|
|
246
|
-
elif duration_s < 1:
|
|
247
|
-
duration_str = f"{duration_s * 1e3:.2f} ms"
|
|
248
|
-
else:
|
|
249
|
-
duration_str = f"{duration_s:.2f} s"
|
|
250
|
-
parts.append(f"Duration: {duration_str}.")
|
|
251
|
-
|
|
252
|
-
return " ".join(parts)
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
def add_plot_aria_attributes(
|
|
256
|
-
fig: Figure,
|
|
257
|
-
alt_text: str,
|
|
258
|
-
*,
|
|
259
|
-
role: str = "img",
|
|
260
|
-
label: str | None = None,
|
|
261
|
-
) -> None:
|
|
262
|
-
"""Add ARIA attributes to matplotlib figure for accessibility.
|
|
263
|
-
|
|
264
|
-
: HTML reports include aria-describedby for plots.
|
|
265
|
-
Adds WAI-ARIA attributes to figure metadata for screen reader support.
|
|
266
|
-
|
|
267
|
-
Args:
|
|
268
|
-
fig: Matplotlib figure object
|
|
269
|
-
alt_text: Alternative text description
|
|
270
|
-
role: ARIA role (default: "img")
|
|
271
|
-
label: ARIA label (optional)
|
|
272
|
-
|
|
273
|
-
Example:
|
|
274
|
-
>>> import matplotlib.pyplot as plt
|
|
275
|
-
>>> from oscura.visualization.accessibility import (
|
|
276
|
-
... add_plot_aria_attributes,
|
|
277
|
-
... generate_alt_text
|
|
278
|
-
... )
|
|
279
|
-
>>> fig, ax = plt.subplots()
|
|
280
|
-
>>> ax.plot([1, 2, 3], [1, 4, 2])
|
|
281
|
-
>>> alt_text = generate_alt_text([1, 4, 2], "waveform")
|
|
282
|
-
>>> add_plot_aria_attributes(fig, alt_text)
|
|
283
|
-
|
|
284
|
-
References:
|
|
285
|
-
ACC-002: Text Alternatives for Visualizations
|
|
286
|
-
WAI-ARIA 1.2 specification
|
|
287
|
-
"""
|
|
288
|
-
# Store as figure metadata
|
|
289
|
-
if not hasattr(fig, "_oscura_accessibility"):
|
|
290
|
-
fig._oscura_accessibility = {} # type: ignore[attr-defined]
|
|
291
|
-
|
|
292
|
-
fig._oscura_accessibility["alt_text"] = alt_text # type: ignore[attr-defined]
|
|
293
|
-
fig._oscura_accessibility["aria_role"] = role # type: ignore[attr-defined]
|
|
294
|
-
|
|
295
|
-
if label:
|
|
296
|
-
fig._oscura_accessibility["aria_label"] = label # type: ignore[attr-defined]
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
class KeyboardHandler:
|
|
300
|
-
"""Keyboard navigation handler for interactive plots.
|
|
301
|
-
|
|
302
|
-
: Interactive visualizations are fully keyboard-navigable.
|
|
303
|
-
Provides standard keyboard controls for plot interaction.
|
|
304
|
-
|
|
305
|
-
Keyboard shortcuts:
|
|
306
|
-
- Tab: Navigate between plot elements
|
|
307
|
-
- Arrow keys: Move cursors/markers
|
|
308
|
-
- Enter: Select/activate element
|
|
309
|
-
- Escape: Close modals/menus
|
|
310
|
-
- +/-: Zoom in/out
|
|
311
|
-
- Home/End: Jump to start/end
|
|
312
|
-
- Space: Toggle play/pause (for animations)
|
|
313
|
-
|
|
314
|
-
Args:
|
|
315
|
-
fig: Matplotlib figure to attach handlers to
|
|
316
|
-
axes: Axes object for cursor/marker operations
|
|
317
|
-
|
|
318
|
-
Example:
|
|
319
|
-
>>> import matplotlib.pyplot as plt
|
|
320
|
-
>>> from oscura.visualization.accessibility import KeyboardHandler
|
|
321
|
-
>>> fig, ax = plt.subplots()
|
|
322
|
-
>>> ax.plot([1, 2, 3], [1, 4, 2])
|
|
323
|
-
>>> handler = KeyboardHandler(fig, ax)
|
|
324
|
-
>>> handler.enable()
|
|
325
|
-
>>> plt.show()
|
|
326
|
-
|
|
327
|
-
References:
|
|
328
|
-
ACC-003: Keyboard Navigation for Interactive Plots
|
|
329
|
-
WAI-ARIA Authoring Practices 1.2
|
|
330
|
-
"""
|
|
331
|
-
|
|
332
|
-
def __init__(self, fig: Figure, axes: Axes) -> None:
|
|
333
|
-
"""Initialize keyboard handler.
|
|
334
|
-
|
|
335
|
-
Args:
|
|
336
|
-
fig: Matplotlib figure
|
|
337
|
-
axes: Axes for operations
|
|
338
|
-
"""
|
|
339
|
-
self.fig = fig
|
|
340
|
-
self.axes = axes
|
|
341
|
-
self.cursor_position: float = 0.0
|
|
342
|
-
self.cursor_line: Any = None
|
|
343
|
-
self.enabled: bool = False
|
|
344
|
-
self._connection_id: int | None = None
|
|
345
|
-
|
|
346
|
-
# Callback registry
|
|
347
|
-
self.on_cursor_move: Callable[[float], None] | None = None
|
|
348
|
-
self.on_select: Callable[[], None] | None = None
|
|
349
|
-
self.on_escape: Callable[[], None] | None = None
|
|
350
|
-
|
|
351
|
-
def enable(self) -> None:
|
|
352
|
-
"""Enable keyboard navigation.
|
|
353
|
-
|
|
354
|
-
Connects keyboard event handlers to the figure.
|
|
355
|
-
|
|
356
|
-
Example:
|
|
357
|
-
>>> handler = KeyboardHandler(fig, ax)
|
|
358
|
-
>>> handler.enable()
|
|
359
|
-
|
|
360
|
-
References:
|
|
361
|
-
ACC-003: Keyboard Navigation for Interactive Plots
|
|
362
|
-
"""
|
|
363
|
-
if not self.enabled:
|
|
364
|
-
self._connection_id = self.fig.canvas.mpl_connect("key_press_event", self._on_key_press)
|
|
365
|
-
self.enabled = True
|
|
366
|
-
|
|
367
|
-
def disable(self) -> None:
|
|
368
|
-
"""Disable keyboard navigation.
|
|
369
|
-
|
|
370
|
-
Disconnects keyboard event handlers.
|
|
371
|
-
|
|
372
|
-
Example:
|
|
373
|
-
>>> handler.disable()
|
|
374
|
-
|
|
375
|
-
References:
|
|
376
|
-
ACC-003: Keyboard Navigation for Interactive Plots
|
|
377
|
-
"""
|
|
378
|
-
if self.enabled and self._connection_id is not None:
|
|
379
|
-
self.fig.canvas.mpl_disconnect(self._connection_id)
|
|
380
|
-
self._connection_id = None
|
|
381
|
-
self.enabled = False
|
|
382
|
-
|
|
383
|
-
def _on_key_press(self, event: Any) -> None:
|
|
384
|
-
"""Handle keyboard events.
|
|
385
|
-
|
|
386
|
-
Args:
|
|
387
|
-
event: Matplotlib key press event
|
|
388
|
-
|
|
389
|
-
References:
|
|
390
|
-
ACC-003: Keyboard Navigation for Interactive Plots
|
|
391
|
-
"""
|
|
392
|
-
if event.key is None:
|
|
393
|
-
return
|
|
394
|
-
|
|
395
|
-
# Arrow keys: move cursor
|
|
396
|
-
if event.key in ("left", "right"):
|
|
397
|
-
self._move_cursor(event.key)
|
|
398
|
-
|
|
399
|
-
# Enter: select/activate
|
|
400
|
-
elif event.key == "enter":
|
|
401
|
-
if self.on_select:
|
|
402
|
-
self.on_select()
|
|
403
|
-
|
|
404
|
-
# Escape: close/cancel
|
|
405
|
-
elif event.key == "escape":
|
|
406
|
-
if self.on_escape:
|
|
407
|
-
self.on_escape()
|
|
408
|
-
|
|
409
|
-
# +/-: zoom
|
|
410
|
-
elif event.key in ("+", "="):
|
|
411
|
-
self._zoom(1.2)
|
|
412
|
-
elif event.key in ("-", "_"):
|
|
413
|
-
self._zoom(0.8)
|
|
414
|
-
|
|
415
|
-
# Home/End: jump to edges
|
|
416
|
-
elif event.key == "home":
|
|
417
|
-
self._jump_to_start()
|
|
418
|
-
elif event.key == "end":
|
|
419
|
-
self._jump_to_end()
|
|
420
|
-
|
|
421
|
-
def _move_cursor(self, direction: str) -> None:
|
|
422
|
-
"""Move cursor left or right.
|
|
423
|
-
|
|
424
|
-
Args:
|
|
425
|
-
direction: "left" or "right"
|
|
426
|
-
|
|
427
|
-
References:
|
|
428
|
-
ACC-003: Keyboard Navigation for Interactive Plots
|
|
429
|
-
"""
|
|
430
|
-
xlim = self.axes.get_xlim()
|
|
431
|
-
step = (xlim[1] - xlim[0]) * 0.01 # 1% of range
|
|
432
|
-
|
|
433
|
-
if direction == "left":
|
|
434
|
-
self.cursor_position = max(xlim[0], self.cursor_position - step)
|
|
435
|
-
else:
|
|
436
|
-
self.cursor_position = min(xlim[1], self.cursor_position + step)
|
|
437
|
-
|
|
438
|
-
self._update_cursor()
|
|
439
|
-
|
|
440
|
-
if self.on_cursor_move:
|
|
441
|
-
self.on_cursor_move(self.cursor_position)
|
|
442
|
-
|
|
443
|
-
def _update_cursor(self) -> None:
|
|
444
|
-
"""Update cursor line on plot.
|
|
445
|
-
|
|
446
|
-
References:
|
|
447
|
-
ACC-003: Keyboard Navigation for Interactive Plots
|
|
448
|
-
"""
|
|
449
|
-
ylim = self.axes.get_ylim()
|
|
450
|
-
|
|
451
|
-
if self.cursor_line is None:
|
|
452
|
-
# Create cursor line
|
|
453
|
-
(self.cursor_line,) = self.axes.plot(
|
|
454
|
-
[self.cursor_position, self.cursor_position],
|
|
455
|
-
ylim,
|
|
456
|
-
"r--",
|
|
457
|
-
linewidth=2,
|
|
458
|
-
label="Cursor",
|
|
459
|
-
)
|
|
460
|
-
else:
|
|
461
|
-
# Update existing cursor
|
|
462
|
-
self.cursor_line.set_xdata([self.cursor_position, self.cursor_position])
|
|
463
|
-
|
|
464
|
-
self.fig.canvas.draw_idle()
|
|
465
|
-
|
|
466
|
-
def _zoom(self, factor: float) -> None:
|
|
467
|
-
"""Zoom in or out.
|
|
468
|
-
|
|
469
|
-
Args:
|
|
470
|
-
factor: Zoom factor (>1 = zoom in, <1 = zoom out)
|
|
471
|
-
|
|
472
|
-
References:
|
|
473
|
-
ACC-003: Keyboard Navigation for Interactive Plots
|
|
474
|
-
"""
|
|
475
|
-
xlim = self.axes.get_xlim()
|
|
476
|
-
ylim = self.axes.get_ylim()
|
|
477
|
-
|
|
478
|
-
x_center = (xlim[0] + xlim[1]) / 2
|
|
479
|
-
y_center = (ylim[0] + ylim[1]) / 2
|
|
480
|
-
|
|
481
|
-
x_range = (xlim[1] - xlim[0]) / factor
|
|
482
|
-
y_range = (ylim[1] - ylim[0]) / factor
|
|
483
|
-
|
|
484
|
-
self.axes.set_xlim(x_center - x_range / 2, x_center + x_range / 2)
|
|
485
|
-
self.axes.set_ylim(y_center - y_range / 2, y_center + y_range / 2)
|
|
486
|
-
|
|
487
|
-
self.fig.canvas.draw_idle()
|
|
488
|
-
|
|
489
|
-
def _jump_to_start(self) -> None:
|
|
490
|
-
"""Jump cursor to start of plot.
|
|
491
|
-
|
|
492
|
-
References:
|
|
493
|
-
ACC-003: Keyboard Navigation for Interactive Plots
|
|
494
|
-
"""
|
|
495
|
-
xlim = self.axes.get_xlim()
|
|
496
|
-
self.cursor_position = xlim[0]
|
|
497
|
-
self._update_cursor()
|
|
498
|
-
|
|
499
|
-
if self.on_cursor_move:
|
|
500
|
-
self.on_cursor_move(self.cursor_position)
|
|
501
|
-
|
|
502
|
-
def _jump_to_end(self) -> None:
|
|
503
|
-
"""Jump cursor to end of plot.
|
|
504
|
-
|
|
505
|
-
References:
|
|
506
|
-
ACC-003: Keyboard Navigation for Interactive Plots
|
|
507
|
-
"""
|
|
508
|
-
xlim = self.axes.get_xlim()
|
|
509
|
-
self.cursor_position = xlim[1]
|
|
510
|
-
self._update_cursor()
|
|
511
|
-
|
|
512
|
-
if self.on_cursor_move:
|
|
513
|
-
self.on_cursor_move(self.cursor_position)
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
__all__ = [
|
|
517
|
-
"FAIL_SYMBOL",
|
|
518
|
-
"LINE_STYLES",
|
|
519
|
-
"PASS_SYMBOL",
|
|
520
|
-
"KeyboardHandler",
|
|
521
|
-
"add_plot_aria_attributes",
|
|
522
|
-
"format_pass_fail",
|
|
523
|
-
"generate_alt_text",
|
|
524
|
-
"get_colorblind_palette",
|
|
525
|
-
"get_multi_line_styles",
|
|
526
|
-
]
|