oscura 0.8.0__py3-none-any.whl → 0.11.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.
Files changed (161) hide show
  1. oscura/__init__.py +19 -19
  2. oscura/__main__.py +4 -0
  3. oscura/analyzers/__init__.py +2 -0
  4. oscura/analyzers/digital/extraction.py +2 -3
  5. oscura/analyzers/digital/quality.py +1 -1
  6. oscura/analyzers/digital/timing.py +1 -1
  7. oscura/analyzers/ml/signal_classifier.py +6 -0
  8. oscura/analyzers/patterns/__init__.py +66 -0
  9. oscura/analyzers/power/basic.py +3 -3
  10. oscura/analyzers/power/soa.py +1 -1
  11. oscura/analyzers/power/switching.py +3 -3
  12. oscura/analyzers/signal_classification.py +529 -0
  13. oscura/analyzers/signal_integrity/sparams.py +3 -3
  14. oscura/analyzers/statistics/basic.py +10 -7
  15. oscura/analyzers/validation.py +1 -1
  16. oscura/analyzers/waveform/measurements.py +200 -156
  17. oscura/analyzers/waveform/measurements_with_uncertainty.py +91 -35
  18. oscura/analyzers/waveform/spectral.py +182 -84
  19. oscura/api/dsl/commands.py +15 -6
  20. oscura/api/server/templates/base.html +137 -146
  21. oscura/api/server/templates/export.html +84 -110
  22. oscura/api/server/templates/home.html +248 -267
  23. oscura/api/server/templates/protocols.html +44 -48
  24. oscura/api/server/templates/reports.html +27 -35
  25. oscura/api/server/templates/session_detail.html +68 -78
  26. oscura/api/server/templates/sessions.html +62 -72
  27. oscura/api/server/templates/waveforms.html +54 -64
  28. oscura/automotive/__init__.py +1 -1
  29. oscura/automotive/can/session.py +1 -1
  30. oscura/automotive/dbc/generator.py +638 -23
  31. oscura/automotive/dtc/data.json +17 -102
  32. oscura/automotive/flexray/fibex.py +9 -1
  33. oscura/automotive/uds/decoder.py +99 -6
  34. oscura/cli/analyze.py +8 -2
  35. oscura/cli/batch.py +36 -5
  36. oscura/cli/characterize.py +18 -4
  37. oscura/cli/export.py +47 -5
  38. oscura/cli/main.py +2 -0
  39. oscura/cli/onboarding/wizard.py +10 -6
  40. oscura/cli/pipeline.py +585 -0
  41. oscura/cli/visualize.py +6 -4
  42. oscura/convenience.py +400 -32
  43. oscura/core/measurement_result.py +286 -0
  44. oscura/core/progress.py +1 -1
  45. oscura/core/schemas/device_mapping.json +2 -8
  46. oscura/core/schemas/packet_format.json +4 -24
  47. oscura/core/schemas/protocol_definition.json +2 -12
  48. oscura/core/types.py +232 -239
  49. oscura/correlation/multi_protocol.py +1 -1
  50. oscura/export/legacy/__init__.py +11 -0
  51. oscura/export/legacy/wav.py +75 -0
  52. oscura/exporters/__init__.py +19 -0
  53. oscura/exporters/wireshark.py +809 -0
  54. oscura/hardware/acquisition/file.py +5 -19
  55. oscura/hardware/acquisition/saleae.py +10 -10
  56. oscura/hardware/acquisition/socketcan.py +4 -6
  57. oscura/hardware/acquisition/synthetic.py +1 -5
  58. oscura/hardware/acquisition/visa.py +6 -6
  59. oscura/hardware/security/side_channel_detector.py +5 -508
  60. oscura/inference/message_format.py +686 -1
  61. oscura/jupyter/display.py +2 -2
  62. oscura/jupyter/magic.py +3 -3
  63. oscura/loaders/__init__.py +17 -12
  64. oscura/loaders/binary.py +1 -1
  65. oscura/loaders/chipwhisperer.py +1 -2
  66. oscura/loaders/configurable.py +1 -1
  67. oscura/loaders/csv_loader.py +2 -2
  68. oscura/loaders/hdf5_loader.py +1 -1
  69. oscura/loaders/lazy.py +6 -1
  70. oscura/loaders/mmap_loader.py +0 -1
  71. oscura/loaders/numpy_loader.py +8 -7
  72. oscura/loaders/preprocessing.py +3 -5
  73. oscura/loaders/rigol.py +21 -7
  74. oscura/loaders/sigrok.py +2 -5
  75. oscura/loaders/tdms.py +3 -2
  76. oscura/loaders/tektronix.py +38 -32
  77. oscura/loaders/tss.py +20 -27
  78. oscura/loaders/validation.py +17 -10
  79. oscura/loaders/vcd.py +13 -8
  80. oscura/loaders/wav.py +1 -6
  81. oscura/pipeline/__init__.py +76 -0
  82. oscura/pipeline/handlers/__init__.py +165 -0
  83. oscura/pipeline/handlers/analyzers.py +1045 -0
  84. oscura/pipeline/handlers/decoders.py +899 -0
  85. oscura/pipeline/handlers/exporters.py +1103 -0
  86. oscura/pipeline/handlers/filters.py +891 -0
  87. oscura/pipeline/handlers/loaders.py +640 -0
  88. oscura/pipeline/handlers/transforms.py +768 -0
  89. oscura/reporting/formatting/measurements.py +55 -14
  90. oscura/reporting/templates/enhanced/protocol_re.html +504 -503
  91. oscura/sessions/legacy.py +49 -1
  92. oscura/side_channel/__init__.py +38 -57
  93. oscura/utils/builders/signal_builder.py +5 -5
  94. oscura/utils/comparison/compare.py +7 -9
  95. oscura/utils/comparison/golden.py +1 -1
  96. oscura/utils/filtering/convenience.py +2 -2
  97. oscura/utils/math/arithmetic.py +38 -62
  98. oscura/utils/math/interpolation.py +20 -20
  99. oscura/utils/pipeline/__init__.py +4 -17
  100. oscura/utils/progressive.py +1 -4
  101. oscura/utils/triggering/edge.py +1 -1
  102. oscura/utils/triggering/pattern.py +2 -2
  103. oscura/utils/triggering/pulse.py +2 -2
  104. oscura/utils/triggering/window.py +3 -3
  105. oscura/validation/hil_testing.py +11 -11
  106. oscura/visualization/__init__.py +46 -284
  107. oscura/visualization/batch.py +72 -433
  108. oscura/visualization/plot.py +542 -53
  109. oscura/visualization/styles.py +184 -318
  110. oscura/workflows/batch/advanced.py +1 -1
  111. oscura/workflows/batch/aggregate.py +12 -9
  112. oscura/workflows/complete_re.py +251 -23
  113. oscura/workflows/digital.py +27 -4
  114. oscura/workflows/multi_trace.py +136 -17
  115. oscura/workflows/waveform.py +11 -6
  116. oscura-0.11.0.dist-info/METADATA +460 -0
  117. {oscura-0.8.0.dist-info → oscura-0.11.0.dist-info}/RECORD +120 -145
  118. oscura/side_channel/dpa.py +0 -1025
  119. oscura/utils/optimization/__init__.py +0 -19
  120. oscura/utils/optimization/parallel.py +0 -443
  121. oscura/utils/optimization/search.py +0 -532
  122. oscura/utils/pipeline/base.py +0 -338
  123. oscura/utils/pipeline/composition.py +0 -248
  124. oscura/utils/pipeline/parallel.py +0 -449
  125. oscura/utils/pipeline/pipeline.py +0 -375
  126. oscura/utils/search/__init__.py +0 -16
  127. oscura/utils/search/anomaly.py +0 -424
  128. oscura/utils/search/context.py +0 -294
  129. oscura/utils/search/pattern.py +0 -288
  130. oscura/utils/storage/__init__.py +0 -61
  131. oscura/utils/storage/database.py +0 -1166
  132. oscura/visualization/accessibility.py +0 -526
  133. oscura/visualization/annotations.py +0 -371
  134. oscura/visualization/axis_scaling.py +0 -305
  135. oscura/visualization/colors.py +0 -451
  136. oscura/visualization/digital.py +0 -436
  137. oscura/visualization/eye.py +0 -571
  138. oscura/visualization/histogram.py +0 -281
  139. oscura/visualization/interactive.py +0 -1035
  140. oscura/visualization/jitter.py +0 -1042
  141. oscura/visualization/keyboard.py +0 -394
  142. oscura/visualization/layout.py +0 -400
  143. oscura/visualization/optimization.py +0 -1079
  144. oscura/visualization/palettes.py +0 -446
  145. oscura/visualization/power.py +0 -508
  146. oscura/visualization/power_extended.py +0 -955
  147. oscura/visualization/presets.py +0 -469
  148. oscura/visualization/protocols.py +0 -1246
  149. oscura/visualization/render.py +0 -223
  150. oscura/visualization/rendering.py +0 -444
  151. oscura/visualization/reverse_engineering.py +0 -838
  152. oscura/visualization/signal_integrity.py +0 -989
  153. oscura/visualization/specialized.py +0 -643
  154. oscura/visualization/spectral.py +0 -1226
  155. oscura/visualization/thumbnails.py +0 -340
  156. oscura/visualization/time_axis.py +0 -351
  157. oscura/visualization/waveform.py +0 -454
  158. oscura-0.8.0.dist-info/METADATA +0 -661
  159. {oscura-0.8.0.dist-info → oscura-0.11.0.dist-info}/WHEEL +0 -0
  160. {oscura-0.8.0.dist-info → oscura-0.11.0.dist-info}/entry_points.txt +0 -0
  161. {oscura-0.8.0.dist-info → oscura-0.11.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
- ]