diff --git a/sfm/report_pdf.py b/sfm/report_pdf.py index b1545e9..87c556a 100644 --- a/sfm/report_pdf.py +++ b/sfm/report_pdf.py @@ -396,18 +396,25 @@ def _render_waveform_layout(fig, rd: ReportData) -> None: ax_stats = fig.add_subplot(gs[2]); ax_stats.axis("off") _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) -def _draw_compliance_panel(fig, gs, rd: ReportData) -> None: - """Large square USBM RI8507 compliance chart in the upper-right, spanning the - mic (row 1) and stats (row 2) rows — matching Blastware's Event Report.""" - bottoms, tops, _lefts, rights = gs.get_grid_positions(fig) - y0, y1 = bottoms[2], tops[1] # bottom of stats row → top of mic row - x0, x1 = 0.64, rights[0] # clear of the stats columns → page right margin - fig.text((x0 + x1) / 2, y1 + 0.004, "USBM RI8507", fontsize=9, weight="bold", - color="#333", ha="center", va="bottom") +# Compliance-chart placement, in figure fractions. Measured directly off a +# Blastware Event Report PDF (ref-stuff/n844lqhbzt0w_bw_pdf.pdf) so the chart +# matches BW's size and position: it spans from just under the header down +# through the stats band, hard against the right page margin. The left edge +# leaves room for the y-axis tick labels + "Velocity (in/s)" title, which the +# compacted stats table (see _draw_channel_stats_waveform) is sized to clear. +_COMPLIANCE_BOX = (0.489, 0.502, 0.951, 0.867) # x0, y0, x1, y1 + + +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: from sfm.compliance import draw_compliance_chart 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) -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).""" - 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) - 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") @@ -592,8 +599,11 @@ def _draw_mic_and_usbm(ax, rd: ReportData) -> None: transform=ax.transAxes, va="top") rows = _mic_rows(rd) 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: - _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 # The USBM compliance chart is drawn as its own large square panel spanning # 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"), ("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)) @@ -708,19 +724,39 @@ def _draw_pvs_summary( table_bottom_y = getattr(ax, "_stats_table_bottom", -0.10) pvs_y = table_bottom_y - 0.04 # small gap below the table border - # Centered for visual balance — looks intentional rather than offset. - # The original BW-replica had a "NA: Not Applicable" caption below - # this line; dropped because we use "—" for missing values and the - # legend was always squished against the PVS line. - ax.text(0.5, pvs_y, line, fontsize=9, weight="bold", - ha="center", va="top", transform=ax.transAxes) + # Centered under the stats table for visual balance — looks intentional + # rather than offset. When the table is compacted (waveform layout), it + # occupies only the left portion of the axes, so center on the table's + # width rather than the full axes (which would push the line under the + # compliance chart). The original BW-replica had a "NA: Not Applicable" + # 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). 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", ""] 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 tbl = ax.table( cellText=table_data, - colWidths=[0.28, 0.14, 0.14, 0.14, 0.10], + colWidths=col_widths, 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.set_fontsize(8) + tbl.set_fontsize(fontsize) for j in range(5): 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_width = bbox_width def _channel_axis_color(ch: str) -> str: