Skip to contents

0. Installation from GitHub

pak::pak("andrewrlynch/SeqPlotR")

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() — Call grid drawing primitives using the canvas coordinates produced by prep().

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 ignored

4. 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

aes <- function(...) structure(list(...), class = "SeqAes")

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.colaxis.x.line.colaxis.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 axis

SeqTrackR6 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:

  1. Element’s own mapping field (if specified)
  2. Parent seq_track’s mapping field (for any field the element did not specify)
  3. Element’s own data (if specified)
  4. Parent seq_track’s data

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.

aes() to gpar() translation

.aes_to_gpar <- function(a) {
  grid::gpar(
    col      = a$color %||% a$col,
    fill     = a$fill,
    lwd      = a$linewidth %||% a$lwd,
    lty      = a$linetype,
    alpha    = a$alpha,
    cex      = a$size,
    fontsize = a$fontsize
  )
}

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 to rows[[current_row]]
  • seq_track(direction = "under") pushes a new list onto rows and 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:

  1. For each row: sum relative track_width values; assign proportional npc x-spans within the row’s allocated y-band
  2. For each row: max track_height determines the row’s relative height; normalize all rows to fill canvas minus margins and gaps
  3. For each track: for each GRanges window 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 string

Margins 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_windows is set to the same windows GRanges (or a separately provided y_windows argument on seq_track)
  • layoutGrid() builds y_sub_panels for the y-axis using the same proportional sub-panel logic applied to x-axis windows
  • Elements within such a track receive both xscale and yscale as genomic ranges during prep()

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 1

No 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 include start, 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.
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):

bedpe <- data.frame(chrom0="chr1", x0=c(100,300), chrom1="chr1", x1=c(400,600),
                    s0=c("+","-"), s1=c("+","-"), score=c(0.5,0.8))
seq_arch(data=bedpe, map(x0=x0, x1=x1, chrom0=chrom0, chrom1=chrom1,
                          strand0=s0, strand1=s1, height=score))

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.

seq_track(track_id = "A", ...) %+% seq_arc(map(x0=start, x1=end))
# t0 = t1 = "A" automatically

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.

seq_plot() %|%
  seq_track(track_id="A", ...) %+% seq_point() %|%
  seq_track(track_id="B", ...) %+% seq_bar() %+%
  seq_string(data=links_gr, map(x0=start, x1=end, chrom0=seqnames, chrom1=seqnames),
             t0="A", t1="B")
prep = function(layout_all_tracks, track_windows_list, plot_track_index = NULL)
  • layout_all_tracks: named list of panel bounds, keyed by track_id
  • track_windows_list: named list of GRanges windows, keyed by track_id
  • plot_track_index: track_id of 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 in seq_plot$plot_annotations
  • drawElements() must iterate plot_annotations and call draw() 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.

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 colors is not specified)
  • The track_id letter 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-right

8. 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: sparse GRanges with mcols columns i (start bin), j (end bin), score; or a square numeric matrix with rownames/colnames as bin coordinates
  • Internally uses seq_tile with coordinate-transformed polygons
  • rotate = TRUE: each square cell is transformed to a diamond/triangle via rotation matrix applied in prep() — same approach as THEfunc’s SeqTile rotation, 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)