feat(report): size the USBM compliance chart to match Blastware

The compliance chart on the event-report PDF was correctly drawn but far
too small ("it's tiny") — a ~2.6in square dropped between the mic and
stats rows.  Resize + reposition it to match Blastware's Event Report,
measured directly off a BW reference PDF (n844lqhbzt0w) rasterized with
fitz: the chart data box now spans figure fractions x[0.489,0.951]
y[0.502,0.867] — a ~3.9in square running from just under the header down
through the stats band, hard against the right page margin, exactly as BW
draws it.  Title updated to BW's "USBM RI8507 And OSMRE".

To clear room for the BW-sized chart (waveform layout only):
  * _draw_stats_table gains bbox_width/col_widths/fontsize params; the
    waveform layout packs the Tran/Vert/Long table into the left ~0.42 so
    its columns no longer sit under the chart.  Histogram layout keeps the
    wider defaults (byte-identical output; it has no compliance chart).
  * the mic block's long "Channel Test Passed (Freq … Amp … mv)" line gets
    a tighter indent + one-point-smaller font so it ends before the chart's
    left edge instead of running behind it (_kv gains a fontsize param).
  * the Peak Vector Sum line left-aligns under the compacted table (one pt
    smaller) so it clears the chart's bottom-left tick labels.

Chart placement centralized in the _COMPLIANCE_BOX constant. No change to
the compliance math, the scatter, or the histogram report.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YDXjZCr4RqT2U3QvMDhgzf
This commit is contained in:
2026-09-14 22:56:44 +00:00
co-authored by Claude Opus 4.8
parent 95f926c318
commit dc74c97ade
+62 -25
View File
@@ -396,18 +396,25 @@ def _render_waveform_layout(fig, rd: ReportData) -> None:
ax_stats = fig.add_subplot(gs[2]); ax_stats.axis("off") ax_stats = fig.add_subplot(gs[2]); ax_stats.axis("off")
_draw_channel_stats_waveform(ax_stats, rd) _draw_channel_stats_waveform(ax_stats, rd)
_draw_compliance_panel(fig, gs, rd) _draw_compliance_panel(fig, rd)
_draw_waveform_subplot(fig, gs[3], rd) _draw_waveform_subplot(fig, gs[3], rd)
def _draw_compliance_panel(fig, gs, rd: ReportData) -> None: # Compliance-chart placement, in figure fractions. Measured directly off a
"""Large square USBM RI8507 compliance chart in the upper-right, spanning the # Blastware Event Report PDF (ref-stuff/n844lqhbzt0w_bw_pdf.pdf) so the chart
mic (row 1) and stats (row 2) rows — matching Blastware's Event Report.""" # matches BW's size and position: it spans from just under the header down
bottoms, tops, _lefts, rights = gs.get_grid_positions(fig) # through the stats band, hard against the right page margin. The left edge
y0, y1 = bottoms[2], tops[1] # bottom of stats row → top of mic row # leaves room for the y-axis tick labels + "Velocity (in/s)" title, which the
x0, x1 = 0.64, rights[0] # clear of the stats columns → page right margin # compacted stats table (see _draw_channel_stats_waveform) is sized to clear.
fig.text((x0 + x1) / 2, y1 + 0.004, "USBM RI8507", fontsize=9, weight="bold", _COMPLIANCE_BOX = (0.489, 0.502, 0.951, 0.867) # x0, y0, x1, y1
color="#333", ha="center", va="bottom")
def _draw_compliance_panel(fig, rd: ReportData) -> None:
"""Large USBM RI8507 compliance chart in the upper-right, sized and
positioned to match Blastware's Event Report (see _COMPLIANCE_BOX)."""
x0, y0, x1, y1 = _COMPLIANCE_BOX
fig.text((x0 + x1) / 2, y1 + 0.006, "USBM RI8507 And OSMRE", fontsize=9,
weight="bold", color="#333", ha="center", va="bottom")
if rd.channels and rd.sample_rate_sps: if rd.channels and rd.sample_rate_sps:
from sfm.compliance import draw_compliance_chart from sfm.compliance import draw_compliance_chart
ax = fig.add_axes([x0, y0, x1 - x0, y1 - y0]) ax = fig.add_axes([x0, y0, x1 - x0, y1 - y0])
@@ -495,11 +502,11 @@ def _split_iso_to_date_time(iso: Optional[str]) -> tuple[Optional[str], Optional
return (None, None) return (None, None)
def _kv(ax, x, y, label, value, *, label_w=0.18): def _kv(ax, x, y, label, value, *, label_w=0.18, fontsize=8):
"""Render a 'Label Value' row at axes-coordinates (x, y).""" """Render a 'Label Value' row at axes-coordinates (x, y)."""
ax.text(x, y, label, fontsize=8, color="#555", ha="left", va="top", ax.text(x, y, label, fontsize=fontsize, color="#555", ha="left", va="top",
transform=ax.transAxes) transform=ax.transAxes)
ax.text(x + label_w, y, _fmt(value), fontsize=8, ha="left", va="top", ax.text(x + label_w, y, _fmt(value), fontsize=fontsize, ha="left", va="top",
transform=ax.transAxes, family="monospace") transform=ax.transAxes, family="monospace")
@@ -592,8 +599,11 @@ def _draw_mic_and_usbm(ax, rd: ReportData) -> None:
transform=ax.transAxes, va="top") transform=ax.transAxes, va="top")
rows = _mic_rows(rd) rows = _mic_rows(rd)
y = 0.80 y = 0.80
# Tighter label indent + slightly smaller font so the long "Channel Test
# Passed (Freq = … Amp = … mv)" line clears the enlarged compliance chart's
# left edge (_COMPLIANCE_BOX) instead of running behind it.
for label, value in rows: for label, value in rows:
_kv(ax, 0.0, y, label, value, label_w=0.18) _kv(ax, 0.0, y, label, value, label_w=0.13, fontsize=7)
y -= 0.15 y -= 0.15
# The USBM compliance chart is drawn as its own large square panel spanning # The USBM compliance chart is drawn as its own large square panel spanning
# the mic + stats rows on the right — see _draw_compliance_panel(). # the mic + stats rows on the right — see _draw_compliance_panel().
@@ -647,7 +657,13 @@ def _draw_channel_stats_waveform(ax, rd: ReportData) -> None:
("Peak Displacement", "peak_disp_in", "in"), ("Peak Displacement", "peak_disp_in", "in"),
("Sensor Check", "sensor_check", ""), ("Sensor Check", "sensor_check", ""),
] ]
_draw_stats_table(ax, rd, rows_spec) # Compacted to the left half so the enlarged compliance chart (BW-sized,
# right against the page margin) has room — see _COMPLIANCE_BOX.
_draw_stats_table(
ax, rd, rows_spec,
bbox_width=0.42, fontsize=7.5,
col_widths=[0.185, 0.065, 0.065, 0.065, 0.040],
)
_draw_pvs_summary(ax, rd, n_data_rows=len(rows_spec)) _draw_pvs_summary(ax, rd, n_data_rows=len(rows_spec))
@@ -708,19 +724,39 @@ def _draw_pvs_summary(
table_bottom_y = getattr(ax, "_stats_table_bottom", -0.10) table_bottom_y = getattr(ax, "_stats_table_bottom", -0.10)
pvs_y = table_bottom_y - 0.04 # small gap below the table border pvs_y = table_bottom_y - 0.04 # small gap below the table border
# Centered for visual balance — looks intentional rather than offset. # Centered under the stats table for visual balance — looks intentional
# The original BW-replica had a "NA: Not Applicable" caption below # rather than offset. When the table is compacted (waveform layout), it
# this line; dropped because we use "—" for missing values and the # occupies only the left portion of the axes, so center on the table's
# legend was always squished against the PVS line. # width rather than the full axes (which would push the line under the
ax.text(0.5, pvs_y, line, fontsize=9, weight="bold", # compliance chart). The original BW-replica had a "NA: Not Applicable"
ha="center", va="top", transform=ax.transAxes) # caption below this line; dropped because we use "—" for missing values.
table_w = getattr(ax, "_stats_table_width", 0.80)
if table_w < 0.79:
# Compacted (waveform) layout: left-align under the table, one point
# smaller, so the line clears the enlarged compliance chart's
# bottom-left tick labels on the right.
ax.text(0.0, pvs_y, line, fontsize=8, weight="bold",
ha="left", va="top", transform=ax.transAxes)
else:
ax.text(0.5, pvs_y, line, fontsize=9, weight="bold",
ha="center", va="top", transform=ax.transAxes)
def _draw_stats_table(ax, rd: ReportData, rows_spec: list[tuple[str, str, str]]) -> None: def _draw_stats_table(
ax, rd: ReportData, rows_spec: list[tuple[str, str, str]],
*, bbox_width: float = 0.80, fontsize: float = 8,
col_widths: Optional[list[float]] = None,
) -> None:
"""Render a per-channel stats table (Tran/Vert/Long). """Render a per-channel stats table (Tran/Vert/Long).
rows_spec: list of (label, field_name_in_channel_stats, unit_string) rows_spec: list of (label, field_name_in_channel_stats, unit_string)
``bbox_width`` / ``col_widths`` / ``fontsize`` let a caller compact the
table (the waveform layout packs it into the left half to clear the
compliance chart; the histogram layout keeps the wider defaults).
""" """
if col_widths is None:
col_widths = [0.28, 0.14, 0.14, 0.14, 0.10]
headers = ["", "Tran", "Vert", "Long", ""] headers = ["", "Tran", "Vert", "Long", ""]
ch_lookup = {c["name"]: c for c in rd.channel_stats} ch_lookup = {c["name"]: c for c in rd.channel_stats}
@@ -760,16 +796,17 @@ def _draw_stats_table(ax, rd: ReportData, rows_spec: list[tuple[str, str, str]])
table_bottom = 1.0 - table_height table_bottom = 1.0 - table_height
tbl = ax.table( tbl = ax.table(
cellText=table_data, cellText=table_data,
colWidths=[0.28, 0.14, 0.14, 0.14, 0.10], colWidths=col_widths,
cellLoc="left", edges="open", cellLoc="left", edges="open",
bbox=[0.0, table_bottom, 0.80, table_height], bbox=[0.0, table_bottom, bbox_width, table_height],
) )
tbl.auto_set_font_size(False) tbl.auto_set_font_size(False)
tbl.set_fontsize(8) tbl.set_fontsize(fontsize)
for j in range(5): for j in range(5):
tbl[(0, j)].set_text_props(weight="bold", color="#555") tbl[(0, j)].set_text_props(weight="bold", color="#555")
# Stash the bottom Y so _draw_pvs_summary can position itself below. # Stash the bottom Y + width so _draw_pvs_summary can position itself.
ax._stats_table_bottom = table_bottom ax._stats_table_bottom = table_bottom
ax._stats_table_width = bbox_width
def _channel_axis_color(ch: str) -> str: def _channel_axis_color(ch: str) -> str: