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