Coverage for src / tracekit / jupyter / display.py: 68%
111 statements
« prev ^ index » next coverage.py v7.13.1, created at 2026-01-11 23:04 +0000
« prev ^ index » next coverage.py v7.13.1, created at 2026-01-11 23:04 +0000
1"""Rich display integration for Jupyter notebooks.
3This module provides rich HTML display for TraceKit objects including
4traces, measurements, and spectral data.
6 - HTML tables for measurements
7 - Inline plot rendering
8 - Interactive result display
10Example:
11 In [1]: from tracekit.jupyter.display import display_trace
12 In [2]: display_trace(trace) # Shows rich HTML summary
13"""
15from __future__ import annotations
17from typing import Any
19try:
20 from IPython.display import HTML, SVG, display
22 IPYTHON_AVAILABLE = True
23except ImportError:
24 IPYTHON_AVAILABLE = False
26 class HTML: # type: ignore[no-redef]
27 """Fallback HTML class when IPython not available."""
29 def __init__(self, data: str) -> None:
30 self.data = data
32 class SVG: # type: ignore[no-redef]
33 """Fallback SVG class when IPython not available."""
35 def __init__(self, data: str) -> None:
36 self.data = data
38 def display(*args: Any, **kwargs: Any) -> None: # type: ignore[no-redef,misc]
39 """Fallback display when IPython not available."""
40 for arg in args:
41 print(arg)
44class TraceDisplay:
45 """Rich display wrapper for trace objects.
47 Provides _repr_html_ for Jupyter notebook rendering.
48 """
50 def __init__(self, trace: Any, title: str = "Trace") -> None:
51 """Initialize trace display.
53 Args:
54 trace: WaveformTrace or DigitalTrace object
55 title: Display title
56 """
57 self.trace = trace
58 self.title = title
60 def _repr_html_(self) -> str:
61 """Generate HTML representation for Jupyter."""
62 trace = self.trace
64 # Build info rows
65 rows = []
67 if hasattr(trace, "data"): 67 ↛ 70line 67 didn't jump to line 70 because the condition on line 67 was always true
68 rows.append(("Samples", f"{len(trace.data):,}"))
70 if hasattr(trace, "metadata"): 70 ↛ 89line 70 didn't jump to line 89 because the condition on line 70 was always true
71 meta = trace.metadata
72 if hasattr(meta, "sample_rate") and meta.sample_rate: 72 ↛ 82line 72 didn't jump to line 82 because the condition on line 72 was always true
73 rate = meta.sample_rate
74 if rate >= 1e9: 74 ↛ 75line 74 didn't jump to line 75 because the condition on line 74 was never true
75 rate_str = f"{rate / 1e9:.3f} GSa/s"
76 elif rate >= 1e6: 76 ↛ 79line 76 didn't jump to line 79 because the condition on line 76 was always true
77 rate_str = f"{rate / 1e6:.3f} MSa/s"
78 else:
79 rate_str = f"{rate / 1e3:.3f} kSa/s"
80 rows.append(("Sample Rate", rate_str))
82 if hasattr(meta, "channel_name") and meta.channel_name:
83 rows.append(("Channel", meta.channel_name))
85 if hasattr(meta, "source_file") and meta.source_file: 85 ↛ 86line 85 didn't jump to line 86 because the condition on line 85 was never true
86 rows.append(("Source", meta.source_file))
88 # Calculate duration if possible
89 if hasattr(trace, "data") and hasattr(trace, "metadata"): 89 ↛ 103line 89 didn't jump to line 103 because the condition on line 89 was always true
90 if hasattr(trace.metadata, "sample_rate") and trace.metadata.sample_rate: 90 ↛ 103line 90 didn't jump to line 103 because the condition on line 90 was always true
91 duration = len(trace.data) / trace.metadata.sample_rate
92 if duration >= 1: 92 ↛ 93line 92 didn't jump to line 93 because the condition on line 92 was never true
93 dur_str = f"{duration:.3f} s"
94 elif duration >= 1e-3:
95 dur_str = f"{duration * 1e3:.3f} ms"
96 elif duration >= 1e-6: 96 ↛ 99line 96 didn't jump to line 99 because the condition on line 96 was always true
97 dur_str = f"{duration * 1e6:.3f} us"
98 else:
99 dur_str = f"{duration * 1e9:.3f} ns"
100 rows.append(("Duration", dur_str))
102 # Data statistics
103 if hasattr(trace, "data"): 103 ↛ 113line 103 didn't jump to line 113 because the condition on line 103 was always true
104 import numpy as np
106 data = trace.data
107 rows.append(("Min", f"{np.min(data):.4g}"))
108 rows.append(("Max", f"{np.max(data):.4g}"))
109 rows.append(("Mean", f"{np.mean(data):.4g}"))
110 rows.append(("Std Dev", f"{np.std(data):.4g}"))
112 # Build HTML table
113 html = f"""
114<div style="border: 1px solid #ccc; border-radius: 4px; padding: 10px; max-width: 400px;">
115 <h4 style="margin: 0 0 10px 0; color: #333;">{self.title}</h4>
116 <table style="width: 100%; border-collapse: collapse;">
117"""
118 for label, value in rows:
119 html += f"""
120 <tr>
121 <td style="padding: 4px 8px; border-bottom: 1px solid #eee; font-weight: bold; color: #666;">{label}</td>
122 <td style="padding: 4px 8px; border-bottom: 1px solid #eee;">{value}</td>
123 </tr>
124"""
125 html += """
126 </table>
127</div>
128"""
129 return html
132class MeasurementDisplay:
133 """Rich display wrapper for measurement results.
135 Provides _repr_html_ for Jupyter notebook rendering.
136 """
138 def __init__(self, measurements: dict[str, Any], title: str = "Measurements") -> None:
139 """Initialize measurement display.
141 Args:
142 measurements: Dictionary of measurement name -> value
143 title: Display title
144 """
145 self.measurements = measurements
146 self.title = title
148 def _format_value(self, value: Any) -> str:
149 """Format a measurement value with appropriate units."""
150 if isinstance(value, float):
151 # Determine scale and units for common measurements
152 abs_val = abs(value)
153 if abs_val == 0: 153 ↛ 154line 153 didn't jump to line 154 because the condition on line 153 was never true
154 return "0"
155 elif abs_val >= 1e9: 155 ↛ 156line 155 didn't jump to line 156 because the condition on line 155 was never true
156 return f"{value / 1e9:.3f} G"
157 elif abs_val >= 1e6:
158 return f"{value / 1e6:.3f} M"
159 elif abs_val >= 1e3: 159 ↛ 160line 159 didn't jump to line 160 because the condition on line 159 was never true
160 return f"{value / 1e3:.3f} k"
161 elif abs_val >= 1:
162 return f"{value:.4f}"
163 elif abs_val >= 1e-3: 163 ↛ 164line 163 didn't jump to line 164 because the condition on line 163 was never true
164 return f"{value * 1e3:.3f} m"
165 elif abs_val >= 1e-6: 165 ↛ 166line 165 didn't jump to line 166 because the condition on line 165 was never true
166 return f"{value * 1e6:.3f} u"
167 elif abs_val >= 1e-9: 167 ↛ 169line 167 didn't jump to line 169 because the condition on line 167 was always true
168 return f"{value * 1e9:.3f} n"
169 elif abs_val >= 1e-12:
170 return f"{value * 1e12:.3f} p"
171 else:
172 return f"{value:.3e}"
173 else:
174 return str(value)
176 def _repr_html_(self) -> str:
177 """Generate HTML representation for Jupyter."""
178 html = f"""
179<div style="border: 1px solid #ccc; border-radius: 4px; padding: 10px; max-width: 500px;">
180 <h4 style="margin: 0 0 10px 0; color: #333;">{self.title}</h4>
181 <table style="width: 100%; border-collapse: collapse;">
182 <tr style="background-color: #f5f5f5;">
183 <th style="padding: 8px; text-align: left; border-bottom: 2px solid #ddd;">Measurement</th>
184 <th style="padding: 8px; text-align: right; border-bottom: 2px solid #ddd;">Value</th>
185 </tr>
186"""
187 for name, value in self.measurements.items():
188 formatted = self._format_value(value)
189 html += f"""
190 <tr>
191 <td style="padding: 6px 8px; border-bottom: 1px solid #eee;">{name}</td>
192 <td style="padding: 6px 8px; border-bottom: 1px solid #eee; text-align: right; font-family: monospace;">{formatted}</td>
193 </tr>
194"""
195 html += """
196 </table>
197</div>
198"""
199 return html
202def display_trace(trace: Any, title: str = "Trace") -> None:
203 """Display a trace with rich HTML formatting.
205 Args:
206 trace: WaveformTrace or DigitalTrace object
207 title: Display title
208 """
209 wrapper = TraceDisplay(trace, title)
210 if IPYTHON_AVAILABLE: 210 ↛ 211line 210 didn't jump to line 211 because the condition on line 210 was never true
211 display(HTML(wrapper._repr_html_())) # type: ignore[no-untyped-call]
212 else:
213 print(wrapper._repr_html_())
216def display_measurements(measurements: dict[str, Any], title: str = "Measurements") -> None:
217 """Display measurements with rich HTML formatting.
219 Args:
220 measurements: Dictionary of measurement name -> value
221 title: Display title
222 """
223 wrapper = MeasurementDisplay(measurements, title)
224 if IPYTHON_AVAILABLE: 224 ↛ 225line 224 didn't jump to line 225 because the condition on line 224 was never true
225 display(HTML(wrapper._repr_html_())) # type: ignore[no-untyped-call]
226 else:
227 for name, value in measurements.items():
228 print(f"{name}: {value}")
231def display_spectrum(
232 frequencies: Any,
233 magnitudes: Any,
234 title: str = "Spectrum",
235 log_scale: bool = True,
236 figsize: tuple[int, int] = (10, 4),
237) -> None:
238 """Display a spectrum plot inline in Jupyter.
240 Args:
241 frequencies: Frequency array (Hz)
242 magnitudes: Magnitude array (dB or linear)
243 title: Plot title
244 log_scale: Use log scale for x-axis
245 figsize: Figure size tuple
246 """
247 import matplotlib.pyplot as plt
248 import numpy as np
250 _fig, ax = plt.subplots(figsize=figsize)
252 if log_scale and np.min(frequencies[frequencies > 0]) > 0:
253 ax.semilogx(frequencies, magnitudes)
254 else:
255 ax.plot(frequencies, magnitudes)
257 ax.set_xlabel("Frequency (Hz)")
258 ax.set_ylabel("Magnitude (dB)")
259 ax.set_title(title)
260 ax.grid(True, alpha=0.3)
262 plt.tight_layout()
264 if IPYTHON_AVAILABLE:
265 # Display inline
266 plt.show()
267 else:
268 plt.show()