Coverage for src / tracekit / reporting / tables.py: 95%
182 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"""Table generation and formatting for TraceKit reports.
3This module provides utilities for creating and formatting measurement
4summary tables with professional appearance.
7Example:
8 >>> from tracekit.reporting.tables import create_measurement_table
9 >>> table = create_measurement_table(measurements, format="markdown")
10"""
12from __future__ import annotations
14from typing import TYPE_CHECKING, Any, Literal
16import numpy as np
18from tracekit.reporting.formatting import NumberFormatter
20if TYPE_CHECKING:
21 from numpy.typing import NDArray
24def create_measurement_table(
25 measurements: dict[str, Any],
26 *,
27 format: Literal["dict", "markdown", "html", "csv"] = "dict",
28 show_spec: bool = True,
29 show_margin: bool = True,
30 show_status: bool = True,
31 sort_by: str | None = None,
32) -> dict[str, Any] | str:
33 """Create formatted measurement summary table.
35 Args:
36 measurements: Dictionary of measurement name -> measurement data.
37 format: Output format (dict, markdown, html, csv).
38 show_spec: Include specification column.
39 show_margin: Include margin column.
40 show_status: Include pass/fail status column.
41 sort_by: Column to sort by (None for original order).
43 Returns:
44 Formatted table as dictionary or string depending on format.
46 Raises:
47 ValueError: If format is unknown.
49 Example:
50 >>> measurements = {
51 ... "rise_time": {"value": 2.3e-9, "spec": 5e-9, "unit": "s"},
52 ... "fall_time": {"value": 1.8e-9, "spec": 5e-9, "unit": "s"},
53 ... }
54 >>> table = create_measurement_table(measurements, format="markdown")
56 References:
57 REPORT-004, REPORT-006
58 """
59 # Build table headers
60 headers = ["Parameter", "Value"]
61 if show_spec:
62 headers.append("Specification")
63 if show_margin: 63 ↛ 65line 63 didn't jump to line 65 because the condition on line 63 was always true
64 headers.append("Margin")
65 if show_status: 65 ↛ 69line 65 didn't jump to line 69 because the condition on line 65 was always true
66 headers.append("Status")
68 # Build table rows
69 rows = []
70 formatter = NumberFormatter(sig_figs=3)
72 for name, meas in measurements.items():
73 row = [name]
75 # Value
76 value = meas.get("value")
77 unit = meas.get("unit", "")
78 if value is not None:
79 row.append(formatter.format(value, unit))
80 else:
81 row.append("N/A")
83 # Specification
84 if show_spec:
85 spec = meas.get("spec")
86 spec_type = meas.get("spec_type", "max")
87 if spec is not None:
88 prefix = "<" if spec_type == "max" else ">" if spec_type == "min" else "="
89 row.append(f"{prefix}{formatter.format(spec, unit)}")
90 else:
91 row.append("-")
93 # Margin
94 if show_margin: 94 ↛ 107line 94 didn't jump to line 107 because the condition on line 94 was always true
95 spec = meas.get("spec")
96 if value is not None and spec is not None and spec != 0:
97 spec_type = meas.get("spec_type", "max")
98 if spec_type == "max":
99 margin = (spec - value) / spec * 100
100 else:
101 margin = (value - spec) / spec * 100
102 row.append(f"{margin:.1f}%")
103 else:
104 row.append("-")
106 # Status
107 if show_status: 107 ↛ 114line 107 didn't jump to line 114 because the condition on line 107 was always true
108 passed = meas.get("passed", True)
109 if value is None:
110 row.append("N/A")
111 else:
112 row.append("✓ PASS" if passed else "✗ FAIL")
114 rows.append(row)
116 # Sort if requested
117 if sort_by and sort_by in headers:
118 col_idx = headers.index(sort_by)
119 rows.sort(key=lambda r: r[col_idx])
121 # Format output
122 if format == "dict":
123 return {"type": "table", "headers": headers, "data": rows}
124 elif format == "markdown":
125 return _format_markdown_table(headers, rows)
126 elif format == "html":
127 return _format_html_table(headers, rows)
128 elif format == "csv":
129 return _format_csv_table(headers, rows)
130 else:
131 raise ValueError(f"Unknown format: {format}")
134def create_comparison_table(
135 baseline: dict[str, Any],
136 current: dict[str, Any],
137 *,
138 format: Literal["dict", "markdown", "html"] = "dict",
139 show_delta: bool = True,
140 show_percent_change: bool = True,
141) -> dict[str, Any] | str:
142 """Create comparison table between baseline and current measurements.
144 Args:
145 baseline: Baseline measurements.
146 current: Current measurements.
147 format: Output format.
148 show_delta: Show absolute difference.
149 show_percent_change: Show percentage change.
151 Returns:
152 Formatted comparison table.
154 Raises:
155 ValueError: If format is unknown.
157 References:
158 REPORT-008 (Comparison Reports)
159 """
160 headers = ["Parameter", "Baseline", "Current"]
161 if show_delta: 161 ↛ 163line 161 didn't jump to line 163 because the condition on line 161 was always true
162 headers.append("Delta")
163 if show_percent_change: 163 ↛ 166line 163 didn't jump to line 166 because the condition on line 163 was always true
164 headers.append("% Change")
166 rows = []
167 formatter = NumberFormatter(sig_figs=3)
169 # Get all parameter names
170 all_params = set(baseline.keys()) | set(current.keys())
172 for name in sorted(all_params):
173 row = [name]
175 base_meas = baseline.get(name, {})
176 curr_meas = current.get(name, {})
178 base_val = base_meas.get("value")
179 curr_val = curr_meas.get("value")
180 unit = base_meas.get("unit", curr_meas.get("unit", ""))
182 # Baseline value
183 if base_val is not None:
184 row.append(formatter.format(base_val, unit))
185 else:
186 row.append("-")
188 # Current value
189 if curr_val is not None:
190 row.append(formatter.format(curr_val, unit))
191 else:
192 row.append("-")
194 # Delta
195 if show_delta: 195 ↛ 203line 195 didn't jump to line 203 because the condition on line 195 was always true
196 if base_val is not None and curr_val is not None:
197 delta = curr_val - base_val
198 row.append(formatter.format(delta, unit))
199 else:
200 row.append("-")
202 # Percent change
203 if show_percent_change: 203 ↛ 210line 203 didn't jump to line 210 because the condition on line 203 was always true
204 if base_val is not None and curr_val is not None and base_val != 0:
205 pct_change = (curr_val - base_val) / base_val * 100
206 row.append(f"{pct_change:+.1f}%")
207 else:
208 row.append("-")
210 rows.append(row)
212 if format == "dict":
213 return {"type": "table", "headers": headers, "data": rows}
214 elif format == "markdown":
215 return _format_markdown_table(headers, rows)
216 elif format == "html": 216 ↛ 219line 216 didn't jump to line 219 because the condition on line 216 was always true
217 return _format_html_table(headers, rows)
218 else:
219 raise ValueError(f"Unknown format: {format}")
222def create_statistics_table(
223 data: dict[str, NDArray[np.float64]],
224 *,
225 format: Literal["dict", "markdown", "html"] = "dict",
226 statistics: list[str] | None = None,
227) -> dict[str, Any] | str:
228 """Create statistics summary table.
230 Args:
231 data: Dictionary of parameter name -> data array.
232 format: Output format.
233 statistics: List of statistics to include (mean, std, min, max, median).
235 Returns:
236 Formatted statistics table.
238 Raises:
239 ValueError: If format is unknown.
241 References:
242 REPORT-004
243 """
244 if statistics is None:
245 statistics = ["mean", "std", "min", "max", "median"]
247 headers = ["Parameter"] + [stat.capitalize() for stat in statistics]
248 rows = []
250 for name, values in data.items():
251 row = [name]
253 for stat in statistics:
254 if stat == "mean":
255 row.append(f"{np.mean(values):.3g}")
256 elif stat == "std":
257 row.append(f"{np.std(values):.3g}")
258 elif stat == "min":
259 row.append(f"{np.min(values):.3g}")
260 elif stat == "max":
261 row.append(f"{np.max(values):.3g}")
262 elif stat == "median":
263 row.append(f"{np.median(values):.3g}")
264 else:
265 row.append("-")
267 rows.append(row)
269 if format == "dict":
270 return {"type": "table", "headers": headers, "data": rows}
271 elif format == "markdown":
272 return _format_markdown_table(headers, rows)
273 elif format == "html": 273 ↛ 276line 273 didn't jump to line 276 because the condition on line 273 was always true
274 return _format_html_table(headers, rows)
275 else:
276 raise ValueError(f"Unknown format: {format}")
279def _format_markdown_table(headers: list[str], rows: list[list[Any]]) -> str:
280 """Format table as Markdown."""
281 lines = []
283 # Header row
284 lines.append("| " + " | ".join(str(h) for h in headers) + " |")
285 lines.append("| " + " | ".join("---" for _ in headers) + " |")
287 # Data rows
288 for row in rows:
289 lines.append("| " + " | ".join(str(cell) for cell in row) + " |")
291 return "\n".join(lines)
294def _format_html_table(headers: list[str], rows: list[list[Any]]) -> str:
295 """Format table as HTML."""
296 lines = ['<table class="measurement-table">']
298 # Header
299 lines.append("<thead><tr>")
300 for h in headers:
301 lines.append(f"<th>{h}</th>")
302 lines.append("</tr></thead>")
304 # Body
305 lines.append("<tbody>")
306 for row in rows:
307 lines.append("<tr>")
308 for cell in row:
309 cell_str = str(cell)
310 # Apply CSS classes for status
311 if "PASS" in cell_str:
312 lines.append(f'<td class="pass">{cell}</td>')
313 elif "FAIL" in cell_str:
314 lines.append(f'<td class="fail">{cell}</td>')
315 else:
316 lines.append(f"<td>{cell}</td>")
317 lines.append("</tr>")
318 lines.append("</tbody>")
320 lines.append("</table>")
321 return "\n".join(lines)
324def _format_csv_table(headers: list[str], rows: list[list[Any]]) -> str:
325 """Format table as CSV."""
326 import csv
327 from io import StringIO
329 output = StringIO()
330 writer = csv.writer(output)
332 writer.writerow(headers)
333 for row in rows:
334 writer.writerow(row)
336 return output.getvalue()
339def format_batch_summary_table(
340 batch_results: list[dict[str, Any]],
341 *,
342 format: Literal["dict", "markdown", "html"] = "dict",
343) -> dict[str, Any] | str:
344 """Create batch summary table for multi-DUT testing.
346 Args:
347 batch_results: List of result dictionaries, one per DUT.
348 format: Output format.
350 Returns:
351 Formatted batch summary table.
353 Raises:
354 ValueError: If format is unknown.
356 References:
357 REPORT-009 (Batch Report Aggregation)
358 """
359 if not batch_results:
360 return {"type": "table", "headers": [], "data": []}
362 headers = ["DUT ID", "Total Tests", "Passed", "Failed", "Yield"]
363 rows = []
365 for i, result in enumerate(batch_results):
366 dut_id = result.get("dut_id", f"DUT-{i + 1}")
367 total = result.get("total_count", 0)
368 passed = result.get("pass_count", 0)
369 failed = total - passed
370 yield_pct = (passed / total * 100) if total > 0 else 0
372 rows.append([dut_id, total, passed, failed, f"{yield_pct:.1f}%"])
374 # Add summary row
375 total_tests = sum(r.get("total_count", 0) for r in batch_results)
376 total_passed = sum(r.get("pass_count", 0) for r in batch_results)
377 total_failed = total_tests - total_passed
378 overall_yield = (total_passed / total_tests * 100) if total_tests > 0 else 0
380 rows.append(
381 [
382 "TOTAL",
383 total_tests,
384 total_passed,
385 total_failed,
386 f"{overall_yield:.1f}%",
387 ]
388 )
390 if format == "dict":
391 return {"type": "table", "headers": headers, "data": rows}
392 elif format == "markdown":
393 return _format_markdown_table(headers, rows)
394 elif format == "html": 394 ↛ 397line 394 didn't jump to line 397 because the condition on line 394 was always true
395 return _format_html_table(headers, rows)
396 else:
397 raise ValueError(f"Unknown format: {format}")