Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "faba"
version = "0.16.8"
version = "0.16.9"
edition = "2021"
rust-version = "1.91"
license = "MIT"
Expand Down
7 changes: 5 additions & 2 deletions docs/profiling-methods.md
Original file line number Diff line number Diff line change
Expand Up @@ -658,15 +658,18 @@ with no tick, since a tick would read as a position. `s`, in every view, asks fo
that lists the figures already saved where faba runs, newest first, with a thumbnail of each where
the terminal draws images; the list is kept in `.faba-view/` there, so it lasts between sessions.
Saves are PDF only. A name already taken is never overwritten: the prompt offers the next free
one (`name_2`, …).
one (`name_2`, …). A figure saved from `pileup`, `metagene` or `pwm` takes the shape its view has on
screen, each terminal cell 6 × 12 points, so its text sits about as large against the plots as the
terminal's does; in a stack, each gene keeps its pane's shape.

In the `pileup` browser, plots are always drawn as text, and only a saved figure is a picture: a
PDF, its position labels turned to read bottom to top. How the bars are drawn (the y scale, what `c`
counts, mirrored or split, the contrast measure) carries from one gene to the next. `Y` locks the y-axis
maximum for every gene opened after, so they compare; a gene taller than the lock still shows its
bars whole, its axis raised to fit. In the gene list, Space marks genes and Ctrl+R opens the marked
ones stacked top to bottom, each with its tracks: Tab (or a click on a gene) moves the keys from
gene to gene, a click on the bars moves that gene's cursor there, how the bars are drawn applies to
gene to gene, a click on the bars moves that gene's cursor there, a drag (press, move, let go)
pans it as if grabbed, the wheel or a sideways swipe pans the gene under the pointer, how the bars are drawn applies to
all of them, `=`/`-` zoom the gene with the keys while their shifted keys `+`/`_` zoom every
gene (and `)` shows every gene whole, as `0` does one), `Y` locks every gene to the tallest one's
axis, `|` lays them out one under another,
Expand Down
38 changes: 31 additions & 7 deletions src/figure/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -215,18 +215,42 @@ impl Canvas {
}
}

/// Points per terminal cell in a figure saved from a view: a cell is about
/// twice as tall as wide, and the figures' 8- to 12-point text about a row
/// high, as the terminal's is.
pub const CELL: (f64, f64) = (6.0, 12.0);

/// The size in points of a figure saved from a view that took `area` on
/// screen, so it keeps the view's shape; `None` before it was drawn.
/// No smaller than `min` (a view states its own), lest a narrow terminal
/// squeeze the text out.
pub fn page(area: Rect, min: (f64, f64)) -> Option<(f64, f64)> {
(area.width > 0 && area.height > 0).then(|| {
(
(f64::from(area.width) * CELL.0).max(min.0),
(f64::from(area.height) * CELL.1).max(min.1),
)
})
}

/// The `(width, height)` an SVG from [`Canvas::finish`] declares; zero
/// where one is missing.
pub(crate) fn svg_size(svg: &str) -> (f64, f64) {
let get = |key: &str| -> f64 {
let at = svg.find(&format!(" {key}=\"")).map(|i| i + key.len() + 3);
at.and_then(|i| svg[i..].split('"').next()?.parse().ok())
.unwrap_or(0.0)
};
(get("width"), get("height"))
}

/// Figures from [`Canvas::finish`] tiled into one, `cols` to a row, in
/// order: one column stacks them top to bottom, as many columns as figures
/// lays them side by side. Each cell is as wide as the widest figure and
/// each row as tall as its tallest.
pub fn tile_svgs(svgs: &[String], cols: usize) -> String {
let size = |svg: &str, key: &str| -> f64 {
let at = svg.find(&format!(" {key}=\"")).map(|i| i + key.len() + 3);
at.and_then(|i| svg[i..].split('"').next()?.parse().ok())
.unwrap_or(0.0)
};
let cols = cols.max(1);
let cell = svgs.iter().map(|s| size(s, "width")).fold(0.0, f64::max);
let cell = svgs.iter().map(|s| svg_size(s).0).fold(0.0, f64::max);
let mut y = 0.0;
let mut body = String::new();
for row in svgs.chunks(cols) {
Expand All @@ -235,7 +259,7 @@ pub fn tile_svgs(svgs: &[String], cols: usize) -> String {
let x = cell * k as f64;
body.push_str(&svg.replacen("<svg ", &format!("<svg x=\"{x}\" y=\"{y}\" "), 1));
}
y += row.iter().map(|s| size(s, "height")).fold(0.0, f64::max);
y += row.iter().map(|s| svg_size(s).1).fold(0.0, f64::max);
}
let width = cell * cols.min(svgs.len()).max(1) as f64;
format!(
Expand Down
16 changes: 16 additions & 0 deletions src/site_analysis/metagene/tests/tui.rs
Original file line number Diff line number Diff line change
Expand Up @@ -250,3 +250,19 @@ fn enter_stays_and_plus_is_finer() {
press(&mut v, KeyCode::Esc);
assert!(v.done());
}

#[test]
fn a_saved_figure_keeps_the_views_shape() {
let h = hist(true);
let mut v = MetageneView::new("sites", &h);
let size = crate::figure::svg_size;
screen(&mut v, 150, 40);
let (cw, ch) = crate::figure::CELL;
assert_eq!(size(&v.figure()), (150.0 * cw, 40.0 * ch));
screen(&mut v, 90, 50);
assert_eq!(
size(&v.figure()),
(90.0 * cw, 50.0 * ch),
"a taller view, a taller page"
);
}
12 changes: 9 additions & 3 deletions src/site_analysis/metagene/tui.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@

use data_beans::interactive::ui::{header, help_line, panel, Scale, Screen, DIM, HIGHLIGHT};
use ratatui::crossterm::event::{KeyCode, KeyEvent};
use ratatui::layout::{Constraint, Layout};
use ratatui::layout::{Constraint, Layout, Rect};
use ratatui::text::{Line, Span};
use ratatui::widgets::Paragraph;
use ratatui::Frame;
Expand Down Expand Up @@ -127,6 +127,8 @@ fn fitting_merge(hist: &GeneFeatureHistogram, track: Track, width: usize) -> usi

/// State of the profile view, independent of the terminal so it can be tested.
pub struct MetageneView<'a> {
/// Where the last frame drew the view, for a saved figure's shape.
drawn: Rect,
title: String,
hist: &'a GeneFeatureHistogram,
track: Track,
Expand Down Expand Up @@ -165,6 +167,7 @@ impl<'a> MetageneView<'a> {
Show::Sites
},
controls: Controls::new("metagene"),
drawn: Rect::default(),
plot: PlotImage::default(),
done: false,
};
Expand Down Expand Up @@ -235,7 +238,9 @@ impl<'a> MetageneView<'a> {

/// The shown track as a figure, at the shown merge factor.
fn figure(&self) -> String {
let mut c = Canvas::new(680.0, 300.0);
// The view's shape on screen, else a page of its own.
let (w, h) = crate::figure::page(self.drawn, (360.0, 200.0)).unwrap_or((680.0, 300.0));
let mut c = Canvas::new(w, h);
c.bold(16.0, 22.0, &self.title, 12.0, Anchor::Start, INK);
let totals: Vec<String> = self
.track
Expand All @@ -252,7 +257,7 @@ impl<'a> MetageneView<'a> {
MUTED,
);
let title = format!("{} bin(s) per bar", self.shown_merge);
self.draw_track(&mut c, (8.0, 48.0, 664.0, 244.0), None, title);
self.draw_track(&mut c, (8.0, 48.0, w - 16.0, h - 56.0), None, title);
c.finish()
}

Expand Down Expand Up @@ -380,6 +385,7 @@ impl Screen for MetageneView<'_> {
}

fn render(&mut self, frame: &mut Frame) {
self.drawn = frame.area();
let mut keys = vec![
("←/→", "bar"),
("Home", "first"),
Expand Down
84 changes: 84 additions & 0 deletions src/site_analysis/pileup/tests/stack.rs
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,11 @@ fn a_click_gives_the_gene_the_keys_and_moves_its_cursor() {
};
let before = s.views[1].cursor;
assert!(s.mouse(click));
// Let go where it was pressed: a click.
s.mouse(MouseEvent {
kind: MouseEventKind::Up(MouseButton::Left),
..click
});
assert_eq!(s.focus, 1, "the clicked gene has the keys");
assert_ne!(s.views[1].cursor, before, "its cursor moved to the bar");
let (lo, hi) = s.views[1].bar_range(40);
Expand Down Expand Up @@ -217,3 +222,82 @@ fn shifted_zoom_keys_zoom_every_gene() {
assert_eq!(now, whole, "_ zooms every gene back out");
assert!(s.exit.is_none());
}

#[test]
fn a_drag_or_the_wheel_pans_the_gene_under_the_pointer() {
use ratatui::crossterm::event::{MouseButton, MouseEvent, MouseEventKind};
let (a, b) = (positions(1.0), positions(3.0));
let mut s = stack(&a, &b);
screen(&mut s, 100, 40);
// Zoomed in, so there is room to pan.
press(&mut s, KeyCode::Char('+'));
press(&mut s, KeyCode::Char('+'));
screen(&mut s, 100, 40);
let at = |kind, column, row| MouseEvent {
kind,
column,
row,
modifiers: KeyModifiers::NONE,
};
let v = &s.views[1];
let (x, y, before) = (v.chart_x + 50, v.drawn.y + 3, v.window);
// As far as the pointer: the window's span a column.
let ten_columns = 10 * (before.1 - before.0) / v.columns as i64;
s.mouse(at(MouseEventKind::Down(MouseButton::Left), x, y));
s.mouse(at(MouseEventKind::Up(MouseButton::Left), x - 10, y));
assert_eq!(s.focus, 1);
let after = s.views[1].window;
assert_eq!(
after.0 - before.0,
ten_columns,
"dragged left: the view moves right"
);
assert_eq!(after.1 - after.0, before.1 - before.0, "the same width");

// The wheel pans the gene under it, keys or not.
let top = s.views[0].window;
let y0 = s.views[0].drawn.y + 3;
s.mouse(at(MouseEventKind::ScrollDown, x, y0));
assert!(s.views[0].window.0 > top.0, "a notch to the right");
s.mouse(at(MouseEventKind::ScrollLeft, x, y0));
assert_eq!(s.views[0].window, top, "and back");
}

#[test]
fn a_saved_gene_keeps_its_shape_on_screen() {
let (a, b) = (positions(1.0), positions(3.0));
let mut s = stack(&a, &b);
let size = crate::figure::svg_size;
screen(&mut s, 120, 40);
let tall = size(&s.views[0].figure());
press(&mut s, KeyCode::Char('|'));
screen(&mut s, 120, 40);
let wide = size(&s.views[0].figure());
// One under another: each gene is the full width; side by side, half.
assert_eq!(tall.0, 120.0 * crate::figure::CELL.0);
assert_eq!(wide.0, 60.0 * crate::figure::CELL.0);
assert!(wide.1 > tall.1, "side by side, each gene is taller");
}

#[test]
fn a_press_outside_every_gene_lets_go_of_a_grab() {
use ratatui::crossterm::event::{MouseButton, MouseEvent, MouseEventKind};
let (a, b) = (positions(1.0), positions(3.0));
let mut s = stack(&a, &b);
press(&mut s, KeyCode::Char('+'));
screen(&mut s, 100, 40);
let at = |kind, column, row| MouseEvent {
kind,
column,
row,
modifiers: KeyModifiers::NONE,
};
let v = &s.views[0];
let (x, y, before) = (v.chart_x + 30, v.drawn.y + 3, v.window);
// Grabbed, then its release lost (let go outside the terminal).
s.mouse(at(MouseEventKind::Down(MouseButton::Left), x, y));
// A press and release on the footer, outside every gene.
s.mouse(at(MouseEventKind::Down(MouseButton::Left), x + 20, 39));
s.mouse(at(MouseEventKind::Up(MouseButton::Left), x + 20, 39));
assert_eq!(s.views[0].window, before, "no stale grab pans the gene");
}
29 changes: 29 additions & 0 deletions src/site_analysis/pileup/tests/tui.rs
Original file line number Diff line number Diff line change
Expand Up @@ -605,3 +605,32 @@ fn the_browser_draws_text_and_saves_turned_position_labels() {
let _ = s;
assert!(b.figure().contains("rotate(-90)"), "position labels turned");
}

#[test]
fn only_the_bars_grab_and_any_button_clicks() {
use ratatui::crossterm::event::{MouseButton, MouseEvent, MouseEventKind};
let (m, s) = (positions(), sites());
let mut v = view(&m, &s);
press(&mut v, KeyCode::Char('+'));
screen(&mut v, 100, 30);
let at = |kind, column, row| MouseEvent {
kind,
column,
row,
modifiers: KeyModifiers::NONE,
};
let (x, before) = (v.chart_x + 40, v.window);
// A drag from the header row does not pan.
v.mouse(at(MouseEventKind::Down(MouseButton::Left), x, 0));
v.mouse(at(MouseEventKind::Up(MouseButton::Left), x - 10, 0));
assert_eq!(v.window, before);
// A right click on the bars moves the cursor.
let y = v.chart_rows.0 + 2;
let cursor = v.cursor;
v.mouse(at(MouseEventKind::Down(MouseButton::Right), x, y));
v.mouse(at(MouseEventKind::Up(MouseButton::Right), x, y));
assert_ne!(v.cursor, cursor);
// The wheel at the start of the span changes nothing, and says so.
press(&mut v, KeyCode::Char('0'));
assert!(!v.mouse(at(MouseEventKind::ScrollUp, x, y)));
}
Loading
Loading