Skip to content

Commit eb9f6af

Browse files
Add intro / how-to-read / glossary prose to HTML reports
The HTML report ships to manuscript reviewers, conference attendees, and collaborators who haven't run tecap themselves. Until now the only prose in the report was the mechanism legend and the bucket legend; outside readers had no top-level intro, no guide for reading the multi-sample comparison plots, and no glossary for the domain acronyms used throughout (TE, UTR, CDS, polyA, PAS, APA, oligo-dT). Three new constants in tecap/constants.py: - REPORT_INTRO: per-mode (single, compare) intro paragraph. - HOW_TO_READ_COMPARE: reading guide for multi-sample reports. - GLOSSARY: 7 acronyms with definitions. Three new render helpers in tecap/report.py (_intro_html, _how_to_read_html, _glossary_html) wired into both build_single_report and build_compare_report. Two new CSS classes (.intro, .howto) for framing. Tests cover both report modes (47/47 passing). CHANGELOG: added under [Unreleased]; no version bump.
1 parent b636cca commit eb9f6af

4 files changed

Lines changed: 187 additions & 0 deletions

File tree

CHANGELOG.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,26 @@
11
# Changelog
22

3+
## [Unreleased]
4+
5+
### Added
6+
- Self-explanatory prose blocks in HTML reports for non-expert
7+
readers (manuscript reviewers, conference attendees, collaborators
8+
who haven't run tecap themselves). Three additions, sourced from
9+
new constants in `tecap/constants.py`:
10+
- `REPORT_INTRO`: top-level intro paragraph rendered above every
11+
single-sample and multi-sample report. Names what the report is,
12+
what was measured, and where the input came from.
13+
- `HOW_TO_READ_COMPARE`: reading guide rendered only in
14+
multi-sample comparison reports. Maps polylines / grouped bars /
15+
colours back to samples.
16+
- `GLOSSARY`: two-column glossary table at the bottom of every
17+
report. Defines TE, UTR, CDS, polyA, PAS, APA, oligo-dT.
18+
- New report helpers `_intro_html`, `_how_to_read_html`,
19+
`_glossary_html` in `tecap/report.py`. Wired into both
20+
`build_single_report` and `build_compare_report`.
21+
- New CSS classes `.intro` and `.howto` for visually framed prose
22+
callouts (matches existing `.tile` styling).
23+
324
## [0.3.2] — 2026-05-04
425

526
Prose-only patch. No code, schema, CLI, or behavior change.

src/tecap/constants.py

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,3 +218,95 @@
218218
"protocol-specific mispriming."
219219
),
220220
}
221+
222+
223+
# Top-of-page intro paragraph rendered above every report. Tells the
224+
# reader what the report is, what was measured, and where the input
225+
# came from. Two variants because single-sample and multi-sample
226+
# reports answer different questions.
227+
REPORT_INTRO = {
228+
"single": (
229+
"This report summarises 3' terminal exon capture diagnostics for one "
230+
"long-read RNA-seq sample. Every multi-exon read is classified into "
231+
"one of nine mechanism buckets based on where its 3' end falls "
232+
"relative to the terminal exon, the 3' UTR, and a polyA cluster atlas. "
233+
"Reference base composition in a short window downstream of each "
234+
"cleavage site is then summarised per bucket to flag internal-priming "
235+
"artefacts. The mechanism legend below names every bucket; the plots "
236+
"and tables that follow break the sample down by bucket, by polyA "
237+
"hexamer status, and by 3' UTR length."
238+
),
239+
"compare": (
240+
"This report compares 3' terminal exon capture diagnostics across "
241+
"multiple long-read RNA-seq samples. Every multi-exon read in every "
242+
"sample is classified into one of nine mechanism buckets based on "
243+
"where its 3' end falls relative to the terminal exon, the 3' UTR, "
244+
"and a polyA cluster atlas. Reference base composition in a short "
245+
"window downstream of each cleavage site is summarised per bucket "
246+
"to flag internal-priming artefacts. The plots and tables below put "
247+
"the samples side-by-side so that protocol-specific differences "
248+
"(capture rate, read length, mispriming pattern) are directly "
249+
"comparable. See the 'How to read this report' block before the "
250+
"first plot."
251+
),
252+
}
253+
254+
255+
# Reading guide rendered only in multi-sample comparison reports. Tells
256+
# the reader how to map plot elements (polylines, grouped bars, colors)
257+
# back to samples, and where to look for what.
258+
HOW_TO_READ_COMPARE = (
259+
"Each sample is assigned one colour, used consistently across every "
260+
"plot and table in the report. In the mechanism breakdown plot, the "
261+
"left panel is a grouped horizontal bar chart: each row is one of the "
262+
"nine mechanism buckets, and within each row there is one bar per "
263+
"sample (read top-to-bottom in the same order as the report's title "
264+
"line). The middle and right panels of that plot are line charts: "
265+
"each polyline is one sample, x-axis is 3' UTR length bin, y-axis is "
266+
"the per-bin rate of MechA-correct (middle) or Captured (right). "
267+
"Track one colour across all three panels to follow one sample. "
268+
"In the basecomp plot, each subplot is one bucket; within a subplot "
269+
"each step-line is one sample's %A distribution in the downstream "
270+
"window. Per-sample tables under each plot give the underlying "
271+
"counts and fractions. Acronyms used throughout (UTR, TE, CDS, APA, "
272+
"polyA, oligo-dT, PAS) are defined in the glossary at the end."
273+
)
274+
275+
276+
# Glossary rendered at the bottom of every report. Covers every domain
277+
# acronym that appears in the legends, captions, or tables. Two-column
278+
# table; ordered roughly by where the term first appears in the report.
279+
GLOSSARY = [
280+
("TE",
281+
"Terminal exon. The last exon of a transcript, where the 3' UTR "
282+
"and the canonical polyA site live. tecap classifies reads by "
283+
"where their 3' end lands relative to the TE."),
284+
("UTR",
285+
"Untranslated region. The 3' UTR is the part of the terminal "
286+
"exon downstream of the stop codon, between the CDS end and the "
287+
"polyA site. Most real polyA cleavage happens here."),
288+
("CDS",
289+
"Coding sequence. The protein-coding portion of a transcript. "
290+
"Reads with 3' end inside the terminal exon's CDS portion (i.e. "
291+
"before the UTR begins) are flagged as IP-TE-CDS, a strong "
292+
"internal-priming signal."),
293+
("polyA",
294+
"Polyadenylation. The 3' processing step that cleaves the pre-mRNA "
295+
"and adds a poly(A) tail. tecap uses the PolyASite atlas to know "
296+
"where real polyA cleavage clusters are located."),
297+
("PAS",
298+
"Polyadenylation signal. The canonical AAUAAA-like hexamer found "
299+
"10-30 nt upstream of a real polyA cleavage site. tecap splits "
300+
"MechA-correct and MechB-APA reads into PAS+ (cluster carries the "
301+
"hexamer) and PAS- (no hexamer) for higher-confidence calling."),
302+
("APA",
303+
"Alternative polyadenylation. Production of mRNA isoforms that "
304+
"differ in their 3' end through use of upstream polyA sites on "
305+
"earlier exons. MechB-APA captures these biological events and "
306+
"distinguishes them from internal-priming artefacts."),
307+
("oligo-dT",
308+
"A short DNA oligo of consecutive deoxythymidines, used in cDNA "
309+
"library prep to capture poly(A)-tailed mRNA. Internal priming "
310+
"happens when oligo-dT anneals to A-rich genomic stretches "
311+
"instead of the real poly(A) tail."),
312+
]

src/tecap/report.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,13 @@
1616
BUCKETS,
1717
CATEGORIES,
1818
COLORS,
19+
GLOSSARY,
20+
HOW_TO_READ_COMPARE,
1921
MECH_A_CORRECT,
2022
MECH_B_APA,
2123
MECHANISM_DEFINITIONS,
2224
PLOT_CAPTIONS,
25+
REPORT_INTRO,
2326
SCHEMA_VERSION,
2427
)
2528
from tecap.io import read_json
@@ -60,6 +63,11 @@
6063
background: #f6f8fa; padding: .5em .8em; border-radius: 3px;
6164
font-size: 0.85em; overflow-x: auto; white-space: pre-wrap;
6265
word-break: break-all; }
66+
.intro { background: #f6f8fa; border-left: 4px solid #3498db;
67+
padding: 1em 1.2em; border-radius: 4px; margin: 1em 0 1.5em; }
68+
.howto { background: #fdf6e3; border-left: 4px solid #f39c12;
69+
padding: 1em 1.2em; border-radius: 4px; margin: 1em 0 1.5em; }
70+
.intro p, .howto p { margin: 0; }
6371
footer { color: #777; font-size: 0.85em; margin-top: 3em;
6472
border-top: 1px solid #eee; padding-top: 1em; }
6573
"""
@@ -140,6 +148,34 @@ def _bucket_legend_html():
140148
)
141149

142150

151+
def _intro_html(mode):
152+
return (
153+
f"<section class='intro'><p>{escape(REPORT_INTRO[mode])}</p></section>"
154+
)
155+
156+
157+
def _how_to_read_html():
158+
return (
159+
f"<section class='howto'><h2 style='margin-top:0;border:none'>"
160+
f"How to read this report</h2>"
161+
f"<p>{escape(HOW_TO_READ_COMPARE)}</p></section>"
162+
)
163+
164+
165+
def _glossary_html():
166+
rows = []
167+
for term, definition in GLOSSARY:
168+
rows.append(
169+
f"<tr><td><strong>{escape(term)}</strong></td>"
170+
f"<td>{escape(definition)}</td></tr>"
171+
)
172+
return (
173+
"<table><thead><tr><th style='width:8em'>Term</th>"
174+
"<th>Definition</th></tr></thead><tbody>"
175+
+ "".join(rows) + "</tbody></table>"
176+
)
177+
178+
143179
def _classify_table(summary):
144180
counts = summary["counts"]
145181
fractions = summary["fractions"]
@@ -318,6 +354,7 @@ def build_single_report(classify_json, basecomp_json=None, out_dir=None):
318354

319355
parts = [
320356
f"<h1>tecap report — {escape(sample)}</h1>",
357+
_intro_html("single"),
321358
"<table><tbody>",
322359
f"<tr><th>Sample</th><td>{escape(sample)}</td></tr>",
323360
f"<tr><th>Platform</th><td>{escape(summary.get('platform','?'))}</td></tr>",
@@ -366,6 +403,8 @@ def build_single_report(classify_json, basecomp_json=None, out_dir=None):
366403
parts += [
367404
"<h2>Run command</h2>",
368405
f"<pre class='cmd'>{escape(cmd_line)}</pre>",
406+
"<h2>Glossary</h2>",
407+
_glossary_html(),
369408
]
370409

371410
body = "\n".join(parts)
@@ -398,6 +437,8 @@ def build_compare_report(classify_jsons, basecomp_jsons=None, out_dir=None):
398437

399438
parts = [
400439
f"<h1>tecap comparison report — {escape(', '.join(samples))}</h1>",
440+
_intro_html("compare"),
441+
_how_to_read_html(),
401442
"<h2>Mechanism legend</h2>",
402443
_mechanism_legend_html(),
403444
"<h2>Mechanism breakdown across samples</h2>",
@@ -425,6 +466,8 @@ def build_compare_report(classify_jsons, basecomp_jsons=None, out_dir=None):
425466
parts.append(f"<h4>{escape(s.get('sample','?'))}</h4>")
426467
parts.append(_basecomp_table(s))
427468

469+
parts += ["<h2>Glossary</h2>", _glossary_html()]
470+
428471
body = "\n".join(parts)
429472
return _TEMPLATE.substitute(
430473
title=f"tecap comparison — {', '.join(samples)}",

tests/test_report.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,3 +135,34 @@ def test_compare_report_with_basecomp(tmp_path):
135135
html = build_compare_report(cpaths, bpaths, out_dir=str(tmp_path))
136136
assert "Base composition" in html
137137
assert (tmp_path / "comparison_basecomp.png").exists()
138+
139+
140+
def test_single_report_has_intro_and_glossary(tmp_path):
141+
cj = tmp_path / "S1_terminal_exon.json"
142+
write_json(str(cj), _classify_summary("S1"))
143+
html = build_single_report(str(cj))
144+
145+
assert "class='intro'" in html
146+
assert "summarises" in html and "terminal exon capture diagnostics" in html
147+
assert "How to read this report" not in html
148+
assert "<h2>Glossary</h2>" in html
149+
assert "Untranslated region" in html
150+
assert "Polyadenylation signal" in html
151+
152+
153+
def test_compare_report_has_intro_howto_and_glossary(tmp_path):
154+
cpaths = []
155+
for s in ("S1", "S2"):
156+
p = tmp_path / f"{s}_terminal_exon.json"
157+
write_json(str(p), _classify_summary(s))
158+
cpaths.append(str(p))
159+
160+
html = build_compare_report(cpaths, out_dir=str(tmp_path))
161+
162+
assert "class='intro'" in html
163+
assert "compares" in html and "terminal exon capture diagnostics" in html
164+
assert "class='howto'" in html
165+
assert "How to read this report" in html
166+
assert "Each sample is assigned one colour" in html
167+
assert "<h2>Glossary</h2>" in html
168+
assert "Polyadenylation" in html

0 commit comments

Comments
 (0)