Skip to content

Interactive views, figure export and labelled pileup tracks - #1

Merged
YPARK merged 24 commits into
mainfrom
qc-interactive
Sep 27, 2026
Merged

YPARK merged 24 commits into
mainfrom
qc-interactive

Conversation

@YPARK

@YPARK YPARK commented Sep 26, 2026 •

Copy link
Copy Markdown
Contributor

What

Full-screen views for the stat and QC commands, a shared figure export, and a few fixes they exposed.

Views

  • faba qc -I: the editing-site thresholds combined, before the site cut.
    • Each threshold is a row: its value, the sites it drops alone, and the sites for which it is the first failing check.
    • Beside the rows is a histogram of the column the focused threshold cuts. Sites passing every other threshold are drawn in front, and the dropped bars use the accent colour.
    • Enter applies the thresholds; p prints the matching faba qc flags and writes nothing.
    • Nothing is written until the thresholds are applied, so cancelling leaves no files.
  • faba pwm -I: a sequence logo in information content or frequency, with the site in the accent colour and a cursor reading out each position.
  • faba metagene -I: 5'UTR, CDS and 3'UTR end to end, with the boundaries labelled.
    • The ncRNA track is on Tab.
    • Bins merge within a region to fit the terminal, or to a chosen factor (-/+).
  • faba pileup -I: labelled tracks on one genomic axis.
    • Each frame re-bins the raw positions of the visible window.
    • The cursor pans, zooms and jumps between sites.
    • Without --genes/--regions, it opens a gene list filtered as you type; g returns to the list.

All views are skipped without a terminal on stdin and stdout.

Figures

  • In every view, s asks for a file name and saves the current view as a vector PDF (svg2pdf) and a PNG (resvg), drawn from one SVG.
  • faba qc-report draws its panels to {prefix}.qc_report.pdf/.png and no longer has -I (it duplicated qc -I).
  • In terminals with an image protocol (kitty, iTerm2, WezTerm, Ghostty; via ratatui-image), the plots are drawn from the same SVG as images; i switches to text.
    • The terminal is only queried when its environment names such a terminal, or when FABA_TUI_IMAGES=1, because a terminal that never answers the query would lose the next keystroke.
    • FABA_TUI_IMAGES=0 turns images off.
  • In text mode, the logo draws big letters from bitmaps.

pileup

  • --track LABEL=PATTERN (repeatable) groups the inputs into labelled tracks: each file joins the first track whose pattern (*, ?) matches its path.
    • Tracks are printed, written to the TSV and browsed separately.
    • Without --track, everything is pooled into one matrix track as before.
  • Fix: pileup matched no row of current _site matrices, whose names end in a channel (.../chr:pos/methylated). Rows are now split with data-beans' parse_feature_row, so a gene symbol may also hold /.
  • In the browser (-I):
    • / searches a gene, chr:start-end or chr:pos, from the view or from the gene list.
    • Each read track stacks the converted channel over both channels (methylated/unmethylated for m6A, converted/unconverted for A-to-I), read in one pass per file.
    • Read tracks share one y scale. Two tracks of the same measure share a mirrored row, one above a zero line and one below; m splits them.
    • A leading row compares the first two tracks bar by bar: the difference in converted fraction, or its log2 fold (c).
    • --gtf adds a gene model row (exons on an intron line with strand chevrons): the opened genes, or every gene in view for a searched locus. The annotation is read in the background, keeping only gene and exon lines, and the view redraws when it arrives.
    • --depth adds a read-depth row.
  • Log records raised while a view is open are held and written after it closes, so they no longer tear the screen.

Consistency with qc

  • The site thresholds now live in one table (Criterion in src/qc/sites.rs).
  • SiteFilterArgs::reason walks that table in a fixed check order, and the view counts with the same checks.
  • The written reason labels are unchanged.

Dependencies

  • data-beans 0.6.9 → 0.6.17 (aux, tui): the shared terminal UI pieces, histograms of categories and real values on a shared scale, the mirrored plot, redraws without a key, and held logs.
  • Added: ratatui 0.30, ratatui-image 11.1 (crossterm only), usvg/resvg 0.45, svg2pdf 0.13, image 0.25 (png).
  • Cargo.lock also picks up legume-numeric 0.8.11 and legume-genomic-types 0.4.2.

Testing

  • Unit tests next to each view (src/**/tests/*.rs) drive the keys and render into ratatui's TestBackend. They cover:
    • counts against reason();
    • stepping, merging, zooming and the gene list;
    • saving (a real PDF and PNG);
    • the image path, through the half-block protocol.
  • cargo +1.94 fmt --all --check and cargo +1.94 clippy --all-targets -- -D warnings are clean.
  • Driven by hand on a real output directory in a pseudo-terminal:
    • qc wrote exactly the counts on screen, and cancelling wrote nothing;
    • all five views saved figures, which were checked by eye;
    • the pileup browser on current pipeline output, with tracks, contrast, mirrored row, gene models and depth.
  • Version 0.14.3.

`faba qc -I` opens a full-screen view of the editing-site thresholds
after the cell calls and before the site cut; Enter applies them.
`faba qc-report -I` opens the same view after the sweep and prints the
matching `faba qc` flags on Enter. Without a terminal on stdin and
stdout the view is skipped and the --site-* values stand as given.

The view shows each threshold's value, the sites it drops alone and
the sites for which it is the first failing check, beside a histogram
of the column it cuts. Sites passing every other threshold are drawn
in front, and the bars the threshold drops use the accent colour.
Axes switch between log, sqrt and linear where the column allows it;
p-values default to log bins on -log10 p so the region around the
usual cutoffs is resolved.

To keep the view and the written fileset from disagreeing, the site
thresholds now live in one table (`Criterion` in qc/sites.rs), and
`SiteFilterArgs::reason` walks it in a fixed check order. The written
`reason` labels are unchanged. The view keeps per-site fail bits, so a
key press rechecks only the threshold that moved.

`qc` now holds the per-batch cell QC files in memory and creates the
output directory only once nothing can cancel, so cancelling the view
leaves nothing behind.

Builds on data-beans 0.6.15, which exports its terminal UI pieces
behind the `tui` feature; ratatui 0.30 is added for the frame and
event types.
…nel rows

`faba pwm -I` shows the PWM as a sequence logo: one letter stack per
position, tallest on top, in information content or frequency, with the
site in the accent colour and a cursor reading out each position.

`faba metagene -I` draws the 5'UTR, CDS and 3'UTR bins end to end with
the region boundaries labelled, and the ncRNA track on Tab when it was
profiled. Adjacent bins merge within a region to fit the terminal, or
to a chosen factor; a cursor reads out region, bins, MetaPlotR
coordinates and count.

`faba pileup -I` browses the gene body with the matrix track and the
site track on one genomic axis. Each frame re-bins the raw positions of
the visible window, one bar per column, with the printed pileup's rule;
the cursor pans, zooms and jumps between sites.

All three open after the normal output is written and are skipped
without a terminal. They build on data-beans 0.6.16, whose HistPlot
takes unit-width bins, custom tick labels and real-valued bars.

`pileup` also failed to match any row of current `_site` matrices,
whose names end in a channel (`.../chr:pos/methylated`). Such rows now
pile up the converted channel (methylated or edited) and skip the
unconverted one.
data-beans 0.6.16, legume-numeric 0.8.11, legume-genomic-types 0.4.2.
@YPARK YPARK changed the title Interactive site-threshold picker for qc and qc-report Interactive views for qc, qc-report, pwm, metagene and pileup Sep 26, 2026
A shared figure module draws each view as an SVG and writes it as a
vector PDF (svg2pdf) and a PNG (resvg), so exported figures are the
same whatever the terminal can show. In every full-screen view, `s`
asks for a file name and saves the current view: thresholds and
histogram in `qc`, the logo in `pwm`, the shown track in `metagene`,
and the visible window in `pileup`.

`qc-report` draws its panels to {prefix}.qc_report.pdf/.png and loses
`-I`, which duplicated `qc -I`. `qc -I` gains `p`, which prints the
matching `faba qc` flags and writes nothing.
`--track LABEL=PATTERN` (repeatable) groups the input files into
labelled tracks: each file joins the first track whose pattern (`*`,
`?`) matches its path. Tracks are printed, written to the TSV and
browsed separately; without `--track` every file is pooled into one
`matrix` track as before.

`pileup -I` with no `--genes`/`--regions` reads the inputs' row names
into a list of genes with their site counts and extents, filtered as
you type. Enter opens the gene in the browser; `g` there returns to the
list, which keeps its filter.

The matrix reader now reports how many rows matched instead of failing,
so a track whose files lack the gene is empty rather than an error;
figure mode still fails when nothing matches.
In kitty, iTerm2, WezTerm, Ghostty and other terminals with an image
protocol (ratatui-image), each view's plot area is drawn from the same
SVG the export writes, rasterised to the area's pixel size and rebuilt
only when the view or the area changes. `i` switches between image and
text. Terminals without an image protocol keep the text plots.

The terminal is only queried when its environment names one of those
terminals (or FABA_TUI_IMAGES=1): a terminal that never answers the
query would lose the next keystroke to the query's reader.
FABA_TUI_IMAGES=0 turns images off.

In text, the pwm logo draws letters with room for it from 5x7 bitmaps
instead of repeating the glyph. In image mode, metagene fits its bins
to the pixel width rather than the column width.
@YPARK YPARK changed the title Interactive views for qc, qc-report, pwm, metagene and pileup Interactive views, figure export and labelled pileup tracks Sep 26, 2026
In the browser, `/` takes a gene name, Ensembl ID, `chr:start-end` or
`chr:pos` (commas allowed). A locus inside the current view moves the
window there (a position keeps the zoom); a locus elsewhere loads that
region; a gene opens when one matches and shows the gene list filtered
by the query when several do. What cannot be found is said on the
footer. The gene list takes a typed locus the same way.

Search works from `-q`/`--regions` too: the browser, the gene list and
search now share one loop, which reads the gene list from the inputs'
row names the first time it is needed.
A green, C blue, G amber, T red, as sequence logos usually are, in the
exported figure, the on-screen image and the text letters. The site is
marked by an accent tick and label under position 0.
The rows read "min edit ratio" and "max edit ratio", and each
histogram says what its tail means: a low ratio is weak editing, a high
one looks like a genomic variant.
As in the browser: it clears the filter, and the footer now says the
list takes a gene name or a chr:start-end.
The table's counts get two-line headers and a legend under them:
"drops alone" (dropped if this were the only threshold), the new
"drops only" (sites failing this threshold and passing all others,
i.e. what turning it off keeps), and "reason column" (its count in the
dropped table, which names the first check a site fails). The saved
figure carries the same columns and legend.
- figure::Controls owns the save prompt, image support, the `i`
  toggle, the header tag and the help keys that four views repeated.
  Images are rebuilt only when a key changes the view, not while a
  name or search is typed, and the terminal picker is no longer cloned
  each frame.
- One LineInput for the save and search prompts, one rasteriser for
  PNG export and on-screen images, figure::svg for a view's plot, one
  XML escape for every SVG writer, and term::when_terminal for `-I`.
- pileup: Selector has an explicit exact mode; a lazy Catalog lets
  search load the gene list itself, so the loop no longer pre-parses
  queries; the gene list is read one file at a time; `g` always
  returns to the list.
- metagene fits bins to the text width in both modes, so a saved
  figure does not depend on the display mode.
- qc: one threshold text, one pointer and dropped-bin rule for screen
  and figure, per-bin tick labels computed with the bins, and the
  picker's outcome is `Picked` directly.
- qc-report's ASCII and PDF panels share one grouping; pwm has one
  stacking order.
In the browser, matrix tracks now draw the converted channel
(methylated or edited reads summed over cells) in front of both
channels summed, so the unconverted reads show as the grey above; the
readout gives both, e.g. `wt 152/180`. The unconverted rows are read
for the browser only: the printed pileup and TSV stay converted-only,
and `nnz` is not stacked, since cells would be counted twice.

The site-table track can switch with `c` between methylated /
unmethylated reads (the default, from the `converted` and `coverage`
columns), the same for the control arm, sites per bar, and -log10 p.
Saved figures draw the same stacking.
When the first two tracks carry totals, the browser's top row compares
their converted fractions per bar: the difference in percentage points
or the log2 fold (`c` switches), drawn as signed bars around zero,
accent where the first track is higher. Bars without reads in either
track show nothing; the readout leads with the value at the cursor.
Saved figures carry the same row.

The site-table track keeps its read-count views and -log10 p, now on
`v`; the "sites" view, which only counted sites per bar, is gone.
`pileup -I --gtf FILE` reads the annotation once and draws a genes row
under the tracks: every gene overlapping the window, exons as boxes on
an intron line with strand arrows and the symbol, overlapping genes in
separate lanes. Saved figures draw the same models with the figure
mode's drawer. Without `--interactive`, `--gtf` still makes the figure.

Labels follow the modality: m6A reads are methylated / unmethylated,
A-to-I reads converted / unconverted, in the matrix tracks, the
contrast and the site-table views. The site-table track is now called
"total" (reads pooled in the site table), switched with `t`.
The browser no longer draws the site table's pooled total: with
per-track read counts it adds nothing. The site table still bounds the
gene and feeds the printed pileup and TSV. Tracks now carry a single
series with an optional total, so the switchable views and `t` go.

Gene models are slimmer: half-height exons and tighter lanes in
figures; in text a lane is one line, exons a heavy line on the intron
line, the symbol beside the gene. When the view is a gene, only that
gene's model is drawn; a searched locus shows the genes in view.

A matrix with no unconverted rows is drawn unstacked rather than as a
total equal to its converted count.
`--depth FILE...` adds a depth row to the browser from `_depth`
matrices: the bins overlapping the loaded region, each bin's reads
summed over cells and files, drawn as a step across the columns it
covers. It works for a searched locus as well as a gene, and saved
figures carry it.

With `--gtf`, the annotation is read on a thread, so the browser opens
at once; until it is ready the footer says so and the gene row appears
from the next view.

A test ties pileup to the row names the producers write today
(`{gene}/{modality}/{chr}:{pos}/{channel}` for sites, `{chr}:{start}-{end}`
for depth), so a naming change fails the test instead of emptying the
pileup. Single-series readouts use compact numbers.
Gene models in figures and images get 8-unit exons (from 5) on 18-unit
lanes. Gene models read in the background now land in the view already
open: it keeps a handle on the annotation and draws its gene from the
next key after the models arrive, rather than only in the next view.
A log line printed while a view owns the terminal scrolls it and tears
the picture: toggling the pileup contrast in image mode did this, as
the SVG renderer warned about every zero-height bar. Views now run with
logging paused (restored on exit), and the canvas no longer writes
rectangles of zero or negative size, which is what a bin with no
difference produced.
The browser draws every read track (not depth or the contrast) against
one y-axis top, the tallest bar across them, total included, so `wt`
and `mut` read on the same scale; saved figures and images do too.
Needs data-beans 0.6.17 for HistPlot's y_max.
Rows carry their contrast measure, gene models are filtered to the
view's chromosome and extent, a failed annotation read shows on the
footer, and column ranges come from one BinEdges helper.
When the first two tracks show the same measure they share one row,
the first growing up and the second down on one scale, as a Miami
plot; m splits them back into separate rows. The difference row is
drawn by the same mirrored plot.
- Gene models are chosen by the matched gene keys (by symbol when the
  annotation versions its ids), not parsed back out of the title.
- Views run on run_screen directly: log records are held while a view
  is up, and the browser redraws when the models arrive, no key needed.
- The text mirror and difference rows use data-beans' MirrorPlot.
- Site rows split with parse_feature_row and channels, so a gene symbol
  may hold a slash.
- A track holds reads or depth bins as an enum; the compared pair is
  stored once; each frame bins every track once.
- Gene models draw through Canvas in the figure palette, with smaller
  strand chevrons.
- The GTF reader keeps only gene and exon lines.
@YPARK
YPARK merged commit 10dda0c into main Sep 27, 2026
5 checks passed
@YPARK
YPARK deleted the qc-interactive branch October 1, 2026 17:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant