report_pdf: PDF histogram aggregation + fix footer/x-axis overlap
Two issues spotted on a histogram event PDF:
1. Footer scale ("Time — /div Amplitude Geo: X in/s/div Mic: Y
psi(L)/div") was overlapping horizontally with the x-axis tick
labels (0, 20, 40, 60...). Both rendered on the same Y row.
Fix: bumped gridspec bottom margin from 0.06 → 0.12, moved the
footer text from y=0.045 → y=0.030 (below the tick labels), moved
the page-bottom Created/Event line from y=0.015 → y=0.005.
Trigger legend on waveforms moved 0.030 → 0.018. Everything
stacks cleanly now without collision.
2. PDF was showing the raw codec output (~150+ bars per histogram)
instead of BW's per-interval aggregation. Why: the aggregation
I'd added to /db/events/{id}/waveform.json wasn't replicated in
the PDF gather path. Now: gather_report_data does the same
max-per-group aggregation when bw_report.histogram.n_intervals is
populated, AND derives per-interval HH:MM:SS labels from the
start time + interval_size_s. Result: histogram PDFs now match
BW's display (one bar per BW interval, x-axis labeled with actual
times) — same fix as the modal chart, applied to the PDF.
For events ingested BEFORE the parser extension (no histogram block
in their sidecar), aggregation is a no-op — they still render with
per-block bars + interval-index x-axis (but the overlap fix applies
to them too). Re-forwarding repopulates the histogram block.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
+55
-11
@@ -285,6 +285,43 @@ def gather_report_data(
|
||||
except Exception as exc:
|
||||
log.warning("gather_report_data: hdf5 read failed: %s", exc)
|
||||
|
||||
# ── Histogram aggregation ──
|
||||
# Codec emits ~N per-block samples (typically 1/sec); BW reports
|
||||
# one bar per configured interval (1 min / 5 min / etc.). When
|
||||
# bw_report.histogram.n_intervals is populated (events ingested
|
||||
# with the parser extension), group max-per-group to match. Also
|
||||
# derives per-interval timestamps for the x-axis. No-op for
|
||||
# waveform events or when n_intervals is missing.
|
||||
if rd.is_histogram and rd.histogram_n_intervals and rd.histogram_n_intervals >= 1:
|
||||
n = int(rd.histogram_n_intervals)
|
||||
for ch, vals in list(rd.channels.items()):
|
||||
if not vals:
|
||||
continue
|
||||
per_group = len(vals) // n
|
||||
remainder = len(vals) % n
|
||||
agg: list = []
|
||||
offset = 0
|
||||
for i in range(n):
|
||||
grp_size = per_group + (1 if i < remainder else 0)
|
||||
if grp_size > 0:
|
||||
grp = vals[offset:offset + grp_size]
|
||||
agg.append(max((abs(v) for v in grp if v is not None), default=0))
|
||||
offset += grp_size
|
||||
else:
|
||||
agg.append(0)
|
||||
rd.channels[ch] = agg
|
||||
# Derive per-interval HH:MM:SS labels if we have the start time + size
|
||||
if rd.histogram_start_str and rd.histogram_interval_size_s and not rd.histogram_interval_times:
|
||||
try:
|
||||
import datetime as _dt
|
||||
start = _dt.datetime.fromisoformat(rd.histogram_start_str)
|
||||
rd.histogram_interval_times = [
|
||||
(start + _dt.timedelta(seconds=(i + 1) * rd.histogram_interval_size_s)).strftime("%H:%M:%S")
|
||||
for i in range(n)
|
||||
]
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return rd
|
||||
|
||||
|
||||
@@ -308,16 +345,18 @@ def render_event_report_pdf(rd: ReportData) -> bytes:
|
||||
else:
|
||||
_render_waveform_layout(fig, rd)
|
||||
|
||||
# Footer (common to both layouts) — Created date + Xmark-like attribution.
|
||||
# Page footer (common to both layouts) — Created date + event id.
|
||||
# Pushed to the very page bottom so it doesn't collide with the
|
||||
# waveform footer scale / trigger legend lines just above.
|
||||
fig.text(
|
||||
0.07, 0.015,
|
||||
0.07, 0.005,
|
||||
f"Created: {rd.server_received_at or '—'} • seismo-relay",
|
||||
fontsize=7, color="#888", ha="left",
|
||||
fontsize=6, color="#888", ha="left",
|
||||
)
|
||||
fig.text(
|
||||
0.93, 0.015,
|
||||
0.93, 0.005,
|
||||
f"Event {rd.event_id[:8] if rd.event_id else '—'}",
|
||||
fontsize=7, color="#888", ha="right",
|
||||
fontsize=6, color="#888", ha="right",
|
||||
)
|
||||
|
||||
buf = io.BytesIO()
|
||||
@@ -331,10 +370,13 @@ def _render_waveform_layout(fig, rd: ReportData) -> None:
|
||||
|
||||
Stats table includes Time (Rel. to Trig), Peak Accel, Peak Disp.
|
||||
Left margin sized to fit the channel labels (MicL/Long/Vert/Tran).
|
||||
Extra bottom margin reserves space for x-axis tick labels +
|
||||
"Amplitude Geo: X in/s/div Mic: Y psi(L)/div" footer + trigger
|
||||
legend without overlap.
|
||||
"""
|
||||
gs = fig.add_gridspec(
|
||||
nrows=4, ncols=1,
|
||||
left=0.11, right=0.94, top=0.97, bottom=0.06,
|
||||
left=0.11, right=0.94, top=0.97, bottom=0.12,
|
||||
height_ratios=[1.7, 2.0, 1.8, 5.5],
|
||||
hspace=0.35,
|
||||
)
|
||||
@@ -355,11 +397,13 @@ def _render_histogram_layout(fig, rd: ReportData) -> None:
|
||||
|
||||
No USBM compliance chart (it's a waveform-only concept). Stats table
|
||||
uses Date + Time-of-peak instead of relative-time + accel + disp.
|
||||
Left margin sized to fit the channel labels.
|
||||
Left margin sized to fit the channel labels. Extra bottom margin
|
||||
leaves room for the x-axis time labels + footer scale legend
|
||||
without overlap.
|
||||
"""
|
||||
gs = fig.add_gridspec(
|
||||
nrows=4, ncols=1,
|
||||
left=0.11, right=0.94, top=0.97, bottom=0.06,
|
||||
left=0.11, right=0.94, top=0.97, bottom=0.12,
|
||||
height_ratios=[1.8, 0.9, 1.7, 5.6],
|
||||
hspace=0.35,
|
||||
)
|
||||
@@ -718,12 +762,12 @@ def _draw_waveform_subplot(fig, gridspec_cell, rd: ReportData) -> None:
|
||||
geo_amp_div = f"{(amax * 1.1 * 2) / 10:.3f}"
|
||||
break
|
||||
fig.text(
|
||||
0.07, 0.045,
|
||||
0.11, 0.030,
|
||||
f"Time(Seconds) {div_s:.2f} sec/div Amplitude Geo: {geo_amp_div} in/s/div Mic: 0.001 psi(L)/div",
|
||||
fontsize=7, color="#444", ha="left",
|
||||
)
|
||||
fig.text(
|
||||
0.07, 0.030,
|
||||
0.11, 0.018,
|
||||
"Trigger = ▶━━━━━ ━━━━━━◀",
|
||||
fontsize=7, color="#444", ha="left",
|
||||
)
|
||||
@@ -789,7 +833,7 @@ def _draw_histogram_subplot(fig, gridspec_cell, rd: ReportData) -> None:
|
||||
geo_amp_div = f"{amax / 5:.3f}"
|
||||
break
|
||||
fig.text(
|
||||
0.07, 0.045,
|
||||
0.11, 0.030,
|
||||
f"Time {interval_str} /div Amplitude Geo: {geo_amp_div} in/s/div Mic: 0.001 psi(L)/div",
|
||||
fontsize=7, color="#444", ha="left",
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user