1. Architecture Overview
SeqPlotR is an R6-based genomic visualization package. All plots are rendered into a single grid viewport, which allows coordinates to be shared across tracks and enables cross-track drawing elements (links, zooms, synteny). The rendering pipeline for every element follows three stages:
-
initialize()— Accept user arguments, store data, mapping, and aesthetics. No computation. -
prep()— Resolve data-driven mappings, clip to windows, transform genomic/data coordinates to npc canvas coordinates. -
draw()— Callgriddrawing primitives using the canvas coordinates produced byprep().
This three-stage pattern is enforced on all elements and must not be bypassed by wrapper functions or composite elements.
2. Package Structure
SeqPlotR/
DESCRIPTION
NAMESPACE
R/
operators.R # %+% (single operator, RHS class dispatch); %|% and %__% as convenience aliases; seq_blank()
map_aes.R # map(), aes(), .resolve_mapping(), .aes_to_gpar()
seq_plot.R # SeqPlotR6, seq_plot()
seq_track.R # SeqTrackR6, seq_track()
scale.R # seq_scale_genomic/continuous/discrete, seq_scale_color_*
element_base.R # SeqElementR6 base class
link_base.R # SeqLinkR6 base class
seq_annotation.R # seq_annotation() — plot-level text/shape overlay
# Primitives
seq_point.R
seq_line.R
seq_segment.R
seq_curve.R
seq_path.R
seq_poly.R
seq_area.R
seq_text.R
# Composites
seq_bar.R
seq_ribbon.R
seq_density.R
seq_tile.R
seq_lollipop.R
seq_gene.R
# Links
seq_arc.R
seq_arch.R
seq_recon.R
seq_string.R
seq_synteny.R
seq_zoom.R
# Wrappers
seq_hic.R
seq_copynumber.R
seq_chip.R
# Utilities
layout.R # .parse_layout_string(), .build_positional_layout()
coord_ops.R # .clip_to_windows(), .data_to_npc(), .npc_to_canvas()
preview.R # seq_preview_layout()
colors.R # flexoki_palette(), color utilities
utilities.R # %||%, common helpers
tests/
testthat/
test-operators.R
test-layout.R
test-mapping.R
test-elements.R
test-links.R
test-wrappers.R
test-preview.R
vignettes/
getting-started.Rmd
patchwork-layouts.Rmd
cross-track-links.Rmd
hic-visualization.Rmd
man/ # auto-generated by Roxygen2
3. Operator Semantics
SeqPlotR uses a single %+% operator meaning “include the next thing.” Dispatch is purely on the class of the right-hand side — there is no context field or operator-driven state on the seq_plot object.
seq_track(direction = ...) argument
Layout position of a track relative to the previous track is specified on the track itself, not the operator:
direction |
Effect |
|---|---|
"right" |
Append to the current row (horizontal) |
"under" |
Close current row, start a new row (vertical) |
The direction of the first track after seq_plot() is always ignored — it is implicitly placed at top-left.
When seq_plot(layout = "...") is given, direction is ignored entirely for all tracks. The layout string is fully authoritative; track positions are determined solely by track_id matching. Tracks with a track_id not present in the layout string are silently skipped.
%+% dispatch table
| RHS class | Effect |
|---|---|
SeqTrack |
Add track to layout (position from direction, or from layout string if given) |
SeqElement (non-link) |
Add element to the most recently added SeqTrack
|
SeqLink |
Validate that t0 and t1 both refer to already-added tracks; error if not. Store in seq_plot$plot_links (deferred, drawn last). |
SeqAnnotation |
Store in seq_plot$plot_annotations (deferred, drawn last) |
Track existence validation for SeqLink: when %+% receives a SeqLink on a seq_plot, it immediately checks that both t0 and t1 resolve to a track_id already present in the plot’s track list. If either is missing, it errors with a clear message naming the unresolved track_id. This ensures the user defines all referenced tracks before defining the link in the chain.
Expression evaluation is left-to-right. The seq_plot object is mutated in-place and returned invisibly at every step.
Convenience aliases
%|% and %__% are kept as shorthand alternatives to reduce verbosity in common cases:
`%|%` <- function(e1, e2) e1 %+% (if (inherits(e2, "SeqTrack")) { e2$direction <- "right"; e2 } else e2)
`%__%` <- function(e1, e2) e1 %+% (if (inherits(e2, "SeqTrack")) { e2$direction <- "under"; e2 } else e2)They are strictly aliases — all logic lives in %+%. Using %|% or %__% with a non-SeqTrack RHS falls through to the same %+% dispatch.
# These are equivalent:
seq_plot() %+% seq_track(direction="right") %+% seq_point() %+% seq_track(direction="under")
seq_plot() %|% seq_track() %+% seq_point() %__% seq_track()
seq_plot() %+%
seq_track(track_id = "Signal", direction = "right", windows = win) %+% seq_area(map(x=start, y=score)) %+%
seq_track(track_id = "CN", direction = "right", windows = win) %+% seq_point(map(x=start, y=logR)) %+%
seq_track(track_id = "Genes", direction = "under", windows = win) %+% seq_gene(map(x=start, type=type, strand=strand)) %+%
seq_string(data=links, map(x=start, y=score), data2=links, mapping2=map(x=end, y=score), t0="Signal", t1="CN") %+%
seq_annotation()
# Row 1: Signal | CN
# Row 2: Genes
# Plot-level: string link + annotation (deferred, drawn last)Full example — patchwork layout
layout <- "
##AA
##AA
BBBC
BBBD
"
seq_plot(layout = layout) %+%
seq_track(track_id = "A") %+% seq_point() %+%
seq_track(track_id = "B") %+% seq_area() %+%
seq_track(track_id = "C") %+% seq_bar() %+%
seq_track(track_id = "D") %+% seq_line() %+%
seq_annotation()
# Positions determined entirely by layout string; direction ignored4. map() and aes()
map() — data-driven aesthetics
map <- function(...) structure(as.list(substitute(list(...)))[-1], class = "SeqMap")Captures unevaluated R expressions. At prep() time, each expression is evaluated with mcols(data) as the evaluation environment and the calling frame as the enclosing environment:
.resolve_mapping <- function(data, mapping, env = parent.frame()) {
if (is.null(data) || is.null(mapping)) return(list())
specials <- list(
start = BiocGenerics::start(data),
end = BiocGenerics::end(data),
width = BiocGenerics::width(data),
mid = (BiocGenerics::start(data) + BiocGenerics::end(data)) / 2
)
# Inject specials into the eval environment alongside mcols so that both
# bare symbols (map(x = start)) and compound expressions (map(x = (start+end)/2))
# resolve correctly without separate branching.
eval_env <- c(specials, as.list(S4Vectors::mcols(data)))
lapply(mapping, function(expr) eval(expr, envir = eval_env, enclos = env))
}Specials (start, end, width, mid) are injected directly into the evaluation environment alongside mcols columns. This means both bare symbols (map(x = start)) and compound expressions (map(x = (start + end) / 2)) resolve correctly in a single eval() call without branching.
aes() — constant aesthetics and theme keys
Captures evaluated values. Used for two purposes:
1. Element aesthetics — static visual properties: aes(color="blue", linewidth=1.5, alpha=0.8)
2. Theme/axis keys — hierarchical dotted-key namespace controlling track chrome and axis appearance. Two equivalent forms are accepted:
# Flat dotted-key form
aes(axis.x.line.col = "red", track.background.fill = "white")
# Nested form (flattened automatically)
aes(axis.x = aes(line = aes(col = "red")), track = aes(background = aes(fill = "white")))Theme key inheritance: keys follow a 3-level hierarchy. A missing specific key falls back to the less-specific parent: - axis.x1.line.col → axis.x.line.col → axis.line.col
Hard break on legacy names: the old flat aesthetic names (xAxisLine, axisColor, trackBackground, etc.) are no longer accepted. All theme control uses the dotted-key namespace.
Theme key namespaces
| Prefix | Controls |
|---|---|
axis.line.* |
Axis line color, width, type |
axis.ticks.* |
Tick mark appearance |
axis.labels.* |
Axis label text, size, rotation |
axis.title.* |
Axis title text, size, rotation |
axis.x1.*, axis.x2.*
|
Primary / secondary x-axis overrides |
axis.y1.*, axis.y2.*
|
Primary / secondary y-axis overrides |
track.background.* |
Track background fill/color |
track.border.* |
Track border color/width |
track.window.background.* |
Window panel background |
track.window.border.* |
Window panel border |
Scale constructors (updated)
seq_scale_continuous(), seq_scale_genomic(), and seq_scale_discrete() now accept additional arguments:
seq_scale_continuous(
limits = NULL, # c(min, max) — explicit range
breaks = NULL, # explicit break positions, or a function(limits) -> breaks
minor_breaks = NULL, # explicit minor break positions, or function, or integer count
expand = c(0.05, 0), # c(multiplicative, additive) expansion beyond data range
cap = "capped", # "capped", "full", "exact", "ticks"
labels = NULL # explicit labels, or a function(breaks) -> labels
)Internal helpers: .compute_scale_breaks(), .expand_limits(), .compute_minor_breaks(), .merge_scale_with_theme().
Axis selector in map()
Elements can be assigned to a specific axis via map(axis.x = 1) or map(axis.x = 2) (and equivalently for axis.y). Default is axis 1. This enables secondary axes:
seq_track(windows=win) %+%
seq_line(data=gr1, map(x=start, y=score1)) %+% # primary y axis
seq_point(data=gr2, map(x=start, y=score2, axis.y=2)) # secondary y axisSeqTrackR6 gains scale_x2, scale_y2, y_windows2, has_axis_x2, has_axis_y2 fields for secondary axis support. Up to 4 axes per track (x1/x2/y1/y2).
Mapping inheritance
When an element is resolved, the following priority applies for each mapping field independently:
- Element’s own
mappingfield (if specified) - Parent
seq_track’smappingfield (for any field the element did not specify) - Element’s own
data(if specified) - Parent
seq_track’sdata
Inheritance is field-level, not object-level. An element can override y while inheriting x and data from the parent track. The axis.x/axis.y selector fields follow the same inheritance rules.
5. Layout System
5a. Positional layout (layout = NULL)
seq_plot maintains an internal rows list of track lists, built as %+% is applied:
-
seq_track(direction = "right")appends torows[[current_row]] -
seq_track(direction = "under")pushes a new list ontorowsand appends the track there - The first track always starts at top-left regardless of
direction
seq_track() margin arguments:
| Argument | Default | Meaning |
|---|---|---|
track_outer_margin |
0.05 |
Space outside the track border (between track and canvas edge / adjacent track) |
track_inner_margin |
0.01 |
Space between the track border and the window panels — this is where axes are drawn |
window_outer_margin |
0.01 |
Space outside each window panel border |
window_inner_margin |
0.005 |
Space between the window panel border and the inner drawing area where data is plotted |
Axis title auto-derivation: drawAxes() derives axis titles by deparsing mapping$x and mapping$y from the track’s mapping. The x-axis title is placed centered in the outer margin band below the panel; the y-axis title is placed in the inner margin band to the left of the panel, rotated 90°. Override with aes(xAxisTitle="custom label", yAxisTitle="custom label"), suppress with aes(xAxisTitle=FALSE, yAxisTitle=FALSE), or control font size with aes(axisTitleSize=0.8) (default 0.8 cex).
At layoutGrid() time:
- For each row: sum relative
track_widthvalues; assign proportional npc x-spans within the row’s allocated y-band - For each row: max
track_heightdetermines the row’s relative height; normalize all rows to fill canvas minus margins and gaps - For each track: for each
GRangeswindow within the track, assign proportional x sub-spans within the track’s x-band (using existing window-gap logic from THEfunc)
5b. Patchwork layout (layout = string)
Parsing:
.parse_layout_string <- function(s) {
# 1. Split on newlines, drop blank lines
# 2. Verify all rows have equal character count (error if not)
# 3. Build a nrow x ncol character matrix
# 4. For each unique non-'#' letter:
# - Find all (row, col) positions
# - Compute min_row, max_row, min_col, max_col
# - Verify every cell in that bounding box is the same letter (error if not — non-rectangular regions are invalid)
# - Store as list: letter -> list(r0, r1, c0, c1)
# 5. Return: list(matrix = m, regions = regions, nrow = nrow, ncol = ncol)
}npc coordinate assignment:
x0 = (c0 - 1) / ncol; x1 = c1 / ncol
y0 = 1 - r1 / nrow; y1 = 1 - (r0 - 1) / nrow # y is top-to-bottom in the stringMargins and gaps are applied inside each region’s bounds when layoutGrid() builds panelBounds.
# cells: parsed and ignored — no track, no background, no border rendered.
seq_blank(): a no-op S3 object. When encountered in operator chains, it occupies a patchwork cell ID but causes layoutGrid() to skip rendering for that cell entirely.
5c. Genomic Y-axis
Detected when mapping$y in a seq_track resolves to a GRanges special field (start, end, mid, width). When detected:
track$uses_genomic_y <- TRUE-
track$y_windowsis set to the samewindowsGRanges (or a separately providedy_windowsargument onseq_track) -
layoutGrid()buildsy_sub_panelsfor the y-axis using the same proportional sub-panel logic applied to x-axis windows - Elements within such a track receive both
xscaleandyscaleas genomic ranges duringprep()
6. Element Specification
Base class: SeqElementR6
SeqElementR6 <- R6Class("SeqElement",
public = list(
data = NULL, # GRanges
mapping = NULL, # SeqMap
aesthetics = NULL, # SeqAes
resolved = NULL, # named list populated by resolve()
coordCanvas = NULL, # list populated by prep()
initialize = function(data = NULL, mapping = NULL, aesthetics = aes(), ...) { ... },
resolve = function(track_data, track_mapping) {
# Apply inheritance rules, call .resolve_mapping(), populate self$resolved
},
prep = function(layout_track, track_windows) {
stop("prep() must be implemented by subclass")
},
draw = function() {
stop("draw() must be implemented by subclass")
},
.infer_scale_y = function() {
# Return a SeqPositionScale inferred from self$resolved$y, or NULL
}
)
)Base class: SeqLinkR6
Extends SeqElementR6. Uses a BEDPE-style single-data API — both anchors are encoded in one data object (GRanges or data.frame), with anchor fields distinguished by map() naming convention.
Fields
t0 = NULL # track_id string (or integer index fallback) for anchor 0
t1 = NULL # track_id string (or integer index fallback) for anchor 1
anchor0_gr = NULL # point GRanges synthesised by resolve() for anchor 0
anchor1_gr = NULL # point GRanges synthesised by resolve() for anchor 1No data2, mapping2, or resolved2 fields — both anchors come from the same data object.
Data shapes
data can be either a GRanges or a data.frame. .resolve_mapping() branches accordingly:
-
GRanges: specials injected into the eval environment includestart,end,width,mid,seqnames,strand(the latter two from the GRanges object directly). Metadata columns are available by name. -
data.frame: columns available by name directly. No genomic specials — all fields must be explicit column references.
map() field vocabulary for links
| Field | Meaning | Required |
|---|---|---|
x0 |
Genomic position of anchor 0 | Yes |
x1 |
Genomic position of anchor 1 | Yes |
chrom0 |
Chromosome of anchor 0 | Yes for cross-chrom links |
chrom1 |
Chromosome of anchor 1 | Yes for cross-chrom links |
strand0 |
Strand of anchor 0 ("+", "-", "*") |
Required by seq_recon
|
strand1 |
Strand of anchor 1 | Required by seq_recon
|
y0 |
Data-scale y position at anchor 0 | Optional (default 0) |
y1 |
Data-scale y position at anchor 1 | Optional (default 0) |
height |
Arch peak height in data-scale units | Optional (default 1) |
color |
Per-link color | Optional |
Example with GRanges:
# GRanges with paired loci encoded as mcols
links_gr <- GRanges("chr1", IRanges(c(100,300), width=1),
x1=c(400,600), chrom1=c("chr1","chr1"),
strand0=c("+","-"), strand1=c("+","-"), score=c(0.5,0.8))
seq_arch(data=links_gr, map(x0=start, x1=x1, chrom0=seqnames, chrom1=chrom1,
strand0=strand0, strand1=strand1, height=score))Example with data.frame (BEDPE):
resolve() behaviour
After resolving all map() fields, SeqLinkR6$resolve() synthesises anchor0_gr and anchor1_gr as single-width GRanges objects from the resolved x0/chrom0 and x1/chrom1 fields. These point GRanges are what prep() uses for findOverlaps() calls.
If x0 or x1 is absent from the resolved fields, resolve() errors immediately with a clear message. If chrom0/chrom1 are absent, the chromosome from the input GRanges seqnames is used as a fallback (for single-chromosome GRanges data).
Track referencing — t0 / t1
Tracks are referenced by track_id string (e.g. t0 = "Signal"). Integer index is accepted as a fallback. track_id takes priority.
Placement rules
Inside a seq_track (via %+%): t0 and t1 are both locked to the parent track’s track_id. No override permitted.
At the seq_plot level (via %+%): stored in seq_plot$plot_links (deferred). Both t0 and t1 must be specified explicitly. Referenced tracks must already exist in the operator chain.
prep() signature for links
-
layout_all_tracks: named list of panel bounds, keyed bytrack_id -
track_windows_list: named list ofGRangeswindows, keyed bytrack_id -
plot_track_index:track_idof the parent track (fallback for within-track links)
Inside prep(), the link: 1. Looks up t0 and t1 in layout_all_tracks (by track_id, then by integer index) 2. Finds overlaps of self$anchor0_gr with t0’s windows 3. Finds overlaps of self$anchor1_gr with t1’s windows 4. Resolves canvas npc coordinates for each matched pair using each track’s xscale/yscale/inner
Primitive elements
All primitives inherit SeqElementR6 and implement prep() + draw().
| Element |
grid call |
Required map() fields |
Optional map() fields |
|---|---|---|---|
seq_point |
grid.points() |
x, y
|
color, fill, size, shape, alpha
|
seq_line |
grid.lines() |
x, y
|
color, linewidth, linetype, alpha
|
seq_segment |
grid.segments() |
x, x_end, y, y_end
|
color, linewidth, linetype
|
seq_curve |
grid.bezierGrob() |
x, y, x_end, y_end
|
curvature, color, linewidth
|
seq_path |
grid.polyline() |
x, y
|
group, color, linewidth
|
seq_poly |
grid.polygon() |
x, y
|
group, fill, color, alpha
|
seq_area |
grid.polygon() |
x, y
|
baseline (default 0), fill, color, alpha
|
seq_text |
grid.text() |
x, y, label
|
size, color, angle, hjust, vjust
|
Composite elements
| Element | Inherits / built on | Key behavior |
|---|---|---|
seq_bar |
seq_poly |
One filled rectangle per observation. x = center position, y = bar height, width = bar width (default: window / n). |
seq_ribbon |
seq_area |
Requires y_min and y_max in map(). Fills the band between them. |
seq_density |
seq_area |
Calls stats::density(resolved$y, bw = aesthetics$bw %||% "nrd0") internally. Plots the resulting density curve as a filled area. |
seq_tile |
seq_poly |
One polygon per observation. aes(rotate = FALSE): rectangle. aes(rotate = TRUE, angle = 45): coordinate-transformed rotated square (triangle at edges). Rotation is a pure coordinate transform — no viewport rotation. |
seq_lollipop |
seq_segment + seq_point
|
Vertical stem from baseline (default 0) to y, with a point at y. |
seq_gene |
seq_poly + seq_segment + seq_text
|
Format-agnostic. map(type = feature_col) determines which rows are exons/UTRs/introns. map(strand = strand_col) determines arrow direction. map(label = name_col) places gene name. All column references are arbitrary — no assumed schema. |
Plot-level features
Plot-level features are attached via %~% and stored on the seq_plot object. They are not SeqElement sub sses and are not associated with any track. They are drawn last in drawElements() after all track elements and links.
seq_annotation() (placeholder — not yet fully specified)
seq_annotation() is reserved as a general-purpose plot-level overlay (text, shapes, arrows, highlighted regions). Its full interface is not yet defined. For now:
- It must be a valid RHS for
%~%and%+%in plot context — i.e. it must return an object of class"SeqAnnotation"that the operators can store inseq_plot$plot_annotations -
drawElements()must iterateplot_annotationsand calldraw()on each, but the draw implementation can be a stub - Do not design the full API until the spec is completed in a future iteration
Implementing agents should create a minimal seq_annotation.R with a constructor that returns a SeqAnnotation S3 object and a no-op draw() method. No further implementation is required in Batch 6.
Link elements
All links inherit SeqLinkR6. Both anchors are encoded in the single data object via the map() field vocabulary (x0, x1, chrom0, chrom1, strand0, strand1, etc.).
| Element | Inherits | Key behavior |
|---|---|---|
seq_arc |
SeqLinkR6 |
Bezier arch, no stems. map(x0=, x1=, height=). Within-track (t0==t1). |
seq_arch |
SeqLinkR6 |
Arch with vertical stems. Supports stubs for half-visible links. map(x0=, x1=, height=). |
seq_recon |
seq_arch |
Colors arches by SV class from map(strand0=, strand1=, chrom0=, chrom1=). Errors at prep() if strand fields missing. |
seq_string |
SeqLinkR6 |
S- or C-curve. Primarily cross-track (t0 ≠ t1). aes(type="s"/"c"). map(x0=, x1=, y0=, y1=). |
seq_synteny |
SeqLinkR6 |
Filled trapezoid between two tracks. map(x0=, x1=, x0_end=, x1_end=, y0=, y1=). |
seq_zoom |
SeqLinkR6 |
Polygon connecting a source region in one track to a destination region in another. |
7. seq_preview_layout() — Layout Preview Function
Purpose
A utility function that renders a fast, schematic toy representation of the intended grid layout — no data required. Allows users to visually verify their layout string or operator chain before adding data.
Signature
seq_preview_layout <- function(
plot_obj = NULL, # a seq_plot object (from operator chain)
layout = NULL, # OR a raw layout string
labels = TRUE, # show track_id labels in each cell
colors = NULL, # optional named vector: track_id -> fill color
margins = TRUE # show margin zones
)Either plot_obj or layout must be supplied. If plot_obj is given, the layout string or positional row structure is extracted from it. If a raw string is given directly, placeholder labels from the string are used.
Rendering
The function opens a new graphics device, calls grid.newpage(), and renders:
- One filled rectangle per unique track region, using soft distinct colors (cycling through a small qualitative palette if
colorsis not specified) - The
track_idletter or label centered in each rectangle, in bold - Light grey cells for
#/ blank regions - A thin outer border around the full canvas to indicate the margin zone if
margins = TRUE - No axes, no data, no windows — purely structural
Output
Returns the parsed layout metadata (region bounding boxes in npc coordinates) invisibly, so it can be inspected or piped into further tooling.
Example
layout <- "
##AA
##AA
BBBC
BBBD
"
seq_preview_layout(layout = layout)
# Renders: blank top-left, A top-right, B bottom-left,
# C middle-right, D bottom-right8. Wrapper Functions
seq_hic(data, windows, rotate = FALSE, angle = 45, ...)
Builds a seq_plot with a single seq_track configured for genomic x and y axes.
-
data: sparseGRangeswithmcolscolumnsi(start bin),j(end bin),score; or a square numeric matrix with rownames/colnames as bin coordinates - Internally uses
seq_tilewith coordinate-transformed polygons -
rotate = TRUE: each square cell is transformed to a diamond/triangle via rotation matrix applied inprep()— same approach as THEfunc’sSeqTilerotation, no viewport transform - Returns a composable
seq_plot
seq_copynumber(data, windows, segment_data = NULL, ...)
Builds a seq_plot with one track: - seq_point for raw log-ratio scatter - Optionally overlays seq_segment for segment means if segment_data is provided - Auto-detects column names: tries log2ratio, logR, log2R, ratio for points; seg.mean, seg_mean, mean for segments; falls back to first numeric mcols column with a warning
seq_chip(data, windows, show_genes = NULL, ...)
Builds a seq_plot with one track (signal) and optionally a second track (genes): - seq_area for coverage signal, with map(x = start, y = score) auto-detected or user-overridden via ... - If show_genes is a GRanges, appends a second track via %__% with seq_gene(data = show_genes)