# Brick Plot
A brick plot displays sequences as rows of colored rectangles — one brick per character. It is designed for bioinformatics workflows involving **DNA/RNA sequence visualization** and **tandem-repeat structure analysis**. Each character maps to a color defined by a template; rows are labeled on the y-axis.
**Import paths:** `kuva::plot::BrickPlot`, `kuva::plot::brick::BrickTemplate`
---
## DNA sequences
`BrickTemplate::dna()` provides a standard A / C / G / T color scheme. Pass the `.template` field to `BrickPlot::with_template()`. Use `with_x_offset(n)` to skip a common flanking region so the region of interest starts at x = 0.
```rust,no_run
use std::collections::HashMap;
use kuva::plot::BrickPlot;
use kuva::plot::brick::BrickTemplate;
use kuva::backend::svg::SvgBackend;
use kuva::render::render::render_multiple;
use kuva::render::layout::Layout;
use kuva::render::plots::Plot;
let tmpl = BrickTemplate::new().dna();
let plot = BrickPlot::new()
.with_sequences(vec![
"CGGCGATCAGGCCGCACTCATCATCATCATCATCATCAT",
"CGGCGATCAGGCCGCACTCATCATCATCATCATCATCATCAT",
])
.with_names(vec!["read_1", "read_2"])
.with_template(tmpl.template)
.with_x_offset(18.0); // skip 18-base common prefix
let plots = vec![Plot::Brick(plot)];
let layout = Layout::auto_from_plots(&plots)
.with_title("DNA Repeat Region");
let svg = SvgBackend.render_scene(&render_multiple(plots, layout));
std::fs::write("brick.svg", svg).unwrap();
```
<img src="../assets/brick/dna.svg" alt="Brick plot — DNA sequences with x-offset alignment" width="700">
The 18-character flanking prefix is hidden by `with_x_offset(18.0)`. All rows start at the same x = 0, aligning the CAT repeat region across reads.
---
## Per-row offsets
When reads begin at different positions, `with_x_offsets` accepts one offset per row. Pass `None` for any row that should fall back to the global `x_offset`.
```rust,no_run
# use kuva::plot::BrickPlot;
# use kuva::plot::brick::BrickTemplate;
# use kuva::render::plots::Plot;
let plot = BrickPlot::new()
.with_sequences(sequences)
.with_names(names)
.with_template(BrickTemplate::new().dna().template)
.with_x_offset(12.0) // global fallback
.with_x_offsets(vec![
Some(18.0_f64), // read 1: skip 18
Some(10.0), // read 2: skip 10
Some(16.0), // read 3: skip 16
Some(5.0), // read 4: skip 5
None, // read 5: use global (12)
]);
```
<img src="../assets/brick/per_row_offsets.svg" alt="Brick plot — per-row x offsets" width="700">
Each row is shifted independently, aligning the repeat boundary across reads with different flanking lengths.
---
## Custom template with value overlay
`with_template` accepts any `HashMap<char, String>`. Here a protein secondary-structure alphabet (H, E, C, T) gets custom colors. `with_values()` prints the character label inside each brick.
```rust,no_run
use std::collections::HashMap;
# use kuva::plot::BrickPlot;
# use kuva::render::plots::Plot;
let mut tmpl: HashMap<char, String> = HashMap::new();
tmpl.insert('H', "steelblue".into()); // α-helix
tmpl.insert('E', "firebrick".into()); // β-strand
tmpl.insert('C', "#aaaaaa".into()); // coil
tmpl.insert('T', "seagreen".into()); // turn
let plot = BrickPlot::new()
.with_sequences(vec!["CCCHHHHHHHHHHCCCCEEEEEECCC"])
.with_names(vec!["prot_1"])
.with_template(tmpl)
.with_values(); // show letter labels inside bricks
```
<img src="../assets/brick/custom.svg" alt="Brick plot — custom secondary-structure alphabet with value overlay" width="700">
Any single-character alphabet can be used — amino acids, repeat unit categories, chromatin states, etc.
---
## Strigar mode (tandem-repeat motifs)
`with_strigars` switches to **strigar mode** for structured tandem-repeat data produced by [BLADERUNNER](https://github.com/Psy-Fer/bladerunner). Each read is described by two strings:
- **motif string** — maps local letters to k-mers: `"CAT:A,C:B,T:C"`
- **strigar string** — run-length encoding of those letters: `"10A1B4A1C1A"`
`with_strigars` normalises k-mers across all reads by canonical rotation, assigns global letters (A, B, C, …) by frequency, auto-generates colors, and renders **variable-width** bricks proportional to each motif's nucleotide length.
```rust,no_run
# use kuva::plot::BrickPlot;
# use kuva::render::plots::Plot;
let strigars: Vec<(String, String)> = vec![
("CAT:A,C:B,T:C".to_string(), "10A1B4A1C1A".to_string()),
("CAT:A,T:B".to_string(), "14A1B1A".to_string()),
("CAT:A,C:B,GGT:C".to_string(), "10A1B8A1C5A".to_string()),
// ...
];
let plot = BrickPlot::new()
.with_names(names)
.with_strigars(strigars); // sequences not needed — derived from strigars
```
<img src="../assets/brick/strigar.svg" alt="Brick plot — strigar mode showing CAT tandem repeats with interruptions" width="700">
Bricks are proportional to motif length (CAT = 3 bp wide; single-nucleotide interruptions are narrower). The dominant repeat unit (CAT) is assigned letter A and the first color; rarer motifs receive subsequent letters and colors.
---
## Bladerunner stitched format
Bladerunner's stitched output joins multiple STR candidates with `|` separators. Each `|`-delimited section is its own candidate with its own local letter assignments; `with_strigars` handles this automatically.
### Inter-candidate gaps
Gaps between candidates appear as `N@` in the strigar (where N is the gap width in nucleotides) and render as light grey bricks:
| **Large gap** (above threshold) | *(none)* | `N@` | N nt |
| **Small gap** (below threshold) | `@:SEQUENCE` | `1@` | `len(seq)` nt |
```rust,no_run
# use kuva::plot::BrickPlot;
# use kuva::render::plots::Plot;
// Three stitched candidates; two large gaps between them.
// ACCCTA, TAACCC, CCCTAA are all rotations of the same unit —
// with_strigars assigns them the same global letter and colour.
let strigars: Vec<(String, String)> = vec![
(
"ACCCTA:A | ACCCTA:A | TAACCC:A,T:B | CCCTAA:A,ACCTAACCCTTAA:B".to_string(),
"2A | 36@ | 2A | 213@ | 2A1B3A | 31@ | 2A1B2A".to_string(),
),
];
let plot = BrickPlot::new()
.with_names(vec!["read_1"])
.with_strigars(strigars);
```
### Aligning reads by genomic position
Use `with_start_positions` to pass the reference coordinate where each read begins. Reads are shifted on the shared x-axis so repeat regions line up visually. Combine with `with_x_origin` to anchor a biologically meaningful position (e.g. the repeat start) to x = 0.
```rust,no_run
# use kuva::plot::BrickPlot;
# use kuva::render::plots::Plot;
// read_1: A-repeat (16 nt) + small gap GAA (3 nt) + AGA-repeat.
// AGA region starts at reference position 19.
// read_2: AGA-repeat only, starting at reference position 19.
//
// with_start_positions aligns both reads on the shared reference axis.
// with_x_origin(19) places x=0 at the repeat start; the pre-repeat
// flanking region of read_1 appears at negative x values.
let strigars: Vec<(String, String)> = vec![
("A:A | @:GAA | AGA:B".to_string(), "16A | 1@ | 9B".to_string()),
("AGA:A".to_string(), "12A".to_string()),
];
let plot = BrickPlot::new()
.with_names(vec!["read_1", "read_2"])
.with_strigars(strigars)
.with_start_positions(vec![0.0_f64, 19.0]) // genomic start coord per read
.with_x_origin(19.0); // x=0 at the repeat start
```
`with_start_positions` is equivalent to `with_x_offsets` with negated values but expresses intent clearly: pass the actual reference start coordinate for each read and kuva handles the shift. `with_x_origin` is a separate, independent axis shift applied on top — it does not interact with per-row offsets and can be used with or without `with_start_positions`.
---
## Flanked strigars
`with_flanked_strigars` is a convenience wrapper for workflows where each read carries DNA flanking sequence on both sides of the STR region. Pass an iterator of `(left_flank, motif_string, strigar_string, right_flank)` tuples. Flanks are rendered with standard bioinformatics DNA colors (A=green, C=blue, G=orange, T=red) immediately adjacent to the STR bricks.
```rust,no_run
# use kuva::plot::BrickPlot;
# use kuva::render::plots::Plot;
let flanked = vec![
// (left_flank, motifs, strigar, right_flank)
("ACGTACGT", "CAG:A,CAA:B", "6A1B8A", "TGCATGCA"),
("ACGTACGT", "CAG:A", "16A", "TGCATGCA"),
];
let plot = BrickPlot::new()
.with_names(vec!["consensus", "read_1"])
.with_flanked_strigars(flanked);
```
The motif and strigar strings are processed identically to `with_strigars`. The flank strings are treated as raw DNA sequences — each character becomes one brick with the standard base color.
---
## Right-anchoring
By default all rows are left-aligned (STR start at x = 0 for all reads after offset adjustment). `with_anchor(BrickAnchor::Right)` instead aligns the **trailing edges** of all rows, which is useful when reads end at the same reference position but differ in length.
```rust,no_run
use kuva::plot::brick::BrickAnchor;
# use kuva::plot::BrickPlot;
# use kuva::render::plots::Plot;
let plot = BrickPlot::new()
.with_names(names)
.with_strigars(strigars)
.with_anchor(BrickAnchor::Right);
```
`BrickAnchor::Left` is the default. `BrickAnchor::Right` shifts shorter rows rightward so all trailing edges line up on the same vertical.
---
## Consensus-anchored rotation
When multiple reads cover the same STR locus, different reads may describe the same repeat unit using different rotations of the same k-mer (e.g. `CAG`, `AGC`, `GCA`). By default the rotation chosen for the legend is the most frequent one across all reads. `with_consensus_row(i)` locks the rotation to whatever row `i` uses, so the legend always reflects the reference or assembly read:
```rust,no_run
# use kuva::plot::BrickPlot;
# use kuva::render::plots::Plot;
let strigars = vec![
("CAG:A".to_string(), "12A".to_string()), // consensus — uses CAG
("AGC:A".to_string(), "10A".to_string()), // same unit, different rotation
];
let plot = BrickPlot::new()
.with_names(vec!["consensus", "read_1"])
.with_consensus_row(0) // lock rotation to row 0's k-mers
.with_strigars(strigars); // must be called after with_consensus_row
```
`with_consensus_row` must be set **before** `with_strigars` (or `with_flanked_strigars`), as rotation resolution happens during strigar parsing.
---
## Primary motif marker
`with_mark_primary()` appends `*` to the legend label of global letter A (the dominant motif by brick count). This is a visual cue that A is the canonical repeat unit when the plot is shown alongside other data:
```rust,no_run
# use kuva::plot::BrickPlot;
let plot = BrickPlot::new()
.with_names(names)
.with_mark_primary()
.with_strigars(strigars);
// Legend entry for A reads "CAG*" instead of "CAG"
```
---
## Per-block notation labels
`with_notations` renders `(kmer)count` labels above the bricks for each consecutive run of the same motif. Pass one `Option<String>` per row: `Some(_)` enables labels for that row, `None` disables them. The string content is ignored — labels are always auto-generated from the run-length structure of the expanded strigar.
```rust,no_run
# use kuva::plot::BrickPlot;
# use kuva::render::plots::Plot;
let plot = BrickPlot::new()
.with_names(vec!["consensus", "read_1", "read_2"])
.with_consensus_row(0)
.with_flanked_strigars(flanked)
.with_notations(vec![
Some("".to_string()), // enable labels for consensus row
None, // no labels for read_1
None, // no labels for read_2
]);
```
For a consensus row with strigar `6A1B2A1C10A` and motifs `CAG:A, CAA:B, CCG:C`, this renders five separate labels above the corresponding brick runs: `(CAG)6`, `(CAA)1`, `(CAG)2`, `(CCG)1`, `(CAG)10`. Gap bricks (`@`) are skipped.
When labels from adjacent runs overlap in pixel space they are staggered vertically across up to four tiers. The plot canvas automatically gains extra top-margin headroom when any row has notations enabled.
---
## Row ordering
Row 0 is always rendered at the **top** of the plot. The first entry in `with_names` appears at the top of the y-axis. This matches the natural reading order when row 0 is a reference/consensus sequence.
---
## Bladerunner full-pipeline example
Combining all of the above for a typical bladerunner workflow:
```rust,no_run
use kuva::plot::BrickPlot;
use kuva::plot::brick::BrickAnchor;
use kuva::render::plots::Plot;
use kuva::render::layout::Layout;
use kuva::render::render::render_multiple;
use kuva::backend::svg::SvgBackend;
let flanked = vec![
// consensus row — row 0, shown at the top with notation labels
("ACGTACGT", "CAG:A,CAA:B,CCG:C", "6A1B2A1C10A", "TGCATGCA"),
// supporting reads — no labels
("ACGTACGT", "CAG:A,CCG:B", "8A1B10A", "TGCATGCA"),
("ACGTACGT", "CAG:A", "20A", "TGCA"),
];
let plot = BrickPlot::new()
.with_names(vec!["consensus", "read_1", "read_2"])
.with_consensus_row(0)
.with_mark_primary()
.with_flanked_strigars(flanked)
.with_notations(vec![Some("".to_string()), None, None]);
let plots = vec![Plot::Brick(plot)];
let layout = Layout::auto_from_plots(&plots).with_title("STR locus");
let svg = SvgBackend.render_scene(&render_multiple(plots, layout));
std::fs::write("brick_locus.svg", svg).unwrap();
```
---
## Built-in templates
| `BrickTemplate::new().dna()` | A C G T | green / blue / orange / red |
| `BrickTemplate::new().rna()` | A C G U | green / blue / orange / red |
Access the populated map via `.template` and pass it to `with_template()`.
---
## API reference
| `BrickPlot::new()` | Create with defaults |
| `.with_sequences(iter)` | Load character sequences (one string per row) |
| `.with_names(iter)` | Load row labels (one per sequence); row 0 appears at the top |
| `.with_template(map)` | Set `HashMap<char, CSS color>` |
| `.with_x_offset(f)` | Global x-offset applied to all rows (shift left by f nt) |
| `.with_x_offsets(iter)` | Per-row offsets (`f64` or `Option<f64>`; `None` → global fallback) |
| `.with_start_positions(iter)` | Per-row genomic start coordinates; shifts each read so it begins at that x position |
| `.with_x_origin(f)` | Reference coordinate mapped to x = 0; applied on top of all per-row offsets |
| `.with_values()` | Draw character labels inside bricks |
| `.with_strigars(iter)` | Load strigar data and switch to strigar mode; accepts bladerunner stitched format |
| `.with_flanked_strigars(iter)` | Like `with_strigars` but each row also carries DNA left/right flanking sequences |
| `.with_anchor(BrickAnchor)` | `BrickAnchor::Left` (default) or `BrickAnchor::Right` to align trailing edges |
| `.with_consensus_row(i)` | Lock k-mer rotation to row i's motifs; must be called before `with_strigars` |
| `.with_mark_primary()` | Append `*` to the legend label for global letter A (the dominant motif) |
| `.with_notations(iter)` | Per-row `Option<String>`; `Some(_)` = render per-block `(kmer)count` labels above that row |
| `BrickTemplate::new().dna()` | Pre-built DNA (A/C/G/T) color template |
| `BrickTemplate::new().rna()` | Pre-built RNA (A/C/G/U) color template |