Custom track and display types
A new way to visualize data in an existing view is a display type; a track type is a new conceptual category of track.
A track owns the high-level identity (an ID, a name, a default set of displays); a display shows that track inside a particular view and owns the drawing.
Track ─owns→ Display(s) ─draw→ canvasTracks are deliberately thin. Every in-tree registration, with the view each
display renders in — SyntenyTrack and VariantTrack are the two that reach
past LinearGenomeView:
Add a track type only when you need a new conceptual track category, a custom
config schema for that category, or behavior shared across multiple displays.
Register it with pluginManager.addTrackType(...), reusing the base track
config schema.
Every track is given each display registered for its type. A display that can
draw from only some adapters declares the adapter
capabilities it needs, as the
pangenome graph display does with adapterCapabilities: ['getSubgraph'], and a
track whose adapter lacks one is not given it, so its Display types menu
does not offer it. A config that names the display keeps it.
When to add a custom display type
- Drawing chrome over the rendered content (e.g. the Y-scale axis in wiggle tracks, soft-clip indicators in alignments)
- Adding track-menu items that toggle display-only state (e.g. "Show soft clipping", "Modifications")
- Wiring a custom widget into feature
clicks (e.g.
VariantFeatureWidget) - Bundling a specific adapter with drawing code tuned for it, so users get the
right combination by default. The generic pairing a plugin skips here is
FeatureTrackwithLinearBasicDisplay.
The display owns view-specific state, menu items, overlays, and the drawing
itself. A rendering backend is built, never registered: the plugin ABI has no
rendering-backend element type, so the display declares what it draws as a mark
list and its component calls createMarkBackend, which walks the WebGPU →
WebGL2 → Canvas2D ladder over that list — or createCanvas2DBackend for a
drawing that is not instances of a shape. Per-base text over marks, such as the
reference sequence's letters, is an OverlayCanvas beside the backend.
Display foundations
LGV (LinearGenomeView) displays compose one foundation mixin on
BaseDisplay, all sharing baseLinearDisplayConfigSchema. The foundation
answers how the display fetches; how it renders is a separate axis on top.
| Foundation | Brings | Used by |
|---|---|---|
MultiRegionDisplayMixin() | Per-region fetch + render: the fetch autoruns, rpcProps() refetch wiring, and byte gating. The common case. | LinearAlignmentsDisplay, LinearCanvasBaseDisplay, LinearMafDisplay, LinearMarkDisplay, LinearMultiRowFeatureDisplay, LinearMultiSampleVariantDisplay, LinearReferenceSequenceDisplay, LinearScoreDisplay, LinearWiggleDisplay |
GlobalFetchMixin() | One non-regional dataset with no per-region partitioning, plus the render lifecycle. Installs no fetch autoruns; the display installs them with installGlobalFetchAutorun. | LDTrackDisplay, LinearHicDisplay, MultiWaySyntenyDisplay |
ComparativeFetchMixin() | One single-payload fetch keyed on both views' state, drawn onto a canvas the containing view owns — no render lifecycle and no byte gate here. Installs no fetch autoruns; the display adds its own via installComparativeFetchAutorun. | DotplotDisplay, LinearSyntenyDisplay |
The two walkthroughs below use MultiRegionDisplayMixin, the common case. The
architecture spec
goes further into why fetch and render are split.
If your display holds onto what the pointer is over, read the hover rules first: a zoom, a side-scroll, an internal scroll and the region-too-large banner all move content under a stationary cursor without firing a pointer event, so a stored hit keeps naming the content that was there before.
Cross-cutting mixins
Orthogonal to the foundation — compose any of them on top of whichever one you
picked. Each is one mixin with one overridable hook, and composing it is the
opt-in: a display that never overrides the hook gets the default at no extra
cost. Reach for one before writing the behavior yourself; Composed by is
read off the types.compose(...) calls, so it also answers "does anything else
already do this?"
| Mixin | The display supplies | Composed by |
|---|---|---|
TrackHeightMixin() | Internal vertical scroll. scrollContentHeight and scrollViewportHeight (both default 0 = doesn't scroll). Brings the derived scrollableHeight, the clamped setScrollTop and the autorun that re-clamps when content shrinks | HeightModeMixin, LDTrackDisplay, LinearHicDisplay, LinearMafDisplay, LinearMarkDisplay, LinearMultiRowFeatureDisplay, LinearMultiSampleVariantDisplay, LinearReferenceSequenceDisplay, LinearScoreDisplay, LinearWiggleDisplay, MultiWaySyntenyDisplay |
LegendMixin() | The legend, whole. A display declares the color scales it paints with (colorScales, a getter hook, and colorScalesIn where they follow the theme) and the mixin derives the key from them (legendSpec, and legendSpecIn for the SVG export, through legendSpecOf), keeps the legend.show slot's getter and setter (showLegend, setShowLegend), dismisses sections one at a time (dismissLegendSection, undone by re-showing the legend), and answers whether there is a key to offer (hasLegendKey). DisplayChrome draws the on-screen key and the exporting view the exported one, in a gutter right of the plot, so a display places neither | LDTrackDisplay, LinearAlignmentsDisplay, LinearCanvasBaseDisplay, LinearHicDisplay, LinearMafDisplay, LinearMarkDisplay, LinearMultiRowFeatureDisplay, LinearMultiSampleVariantDisplay, LinearWiggleDisplay, MultiWaySyntenyDisplay |
ColorWritesMixin() | The writes a display's encoding.color channel takes from a menu row or a picker: the field it paints by (colorByField) and the constant every feature paints (setColorValue). Each rewrites the object as written, so what a pick leaves alone stays as it was, and a pick of what already paints writes nothing, since every color tier keys on the object's arrays. A pick away from a field parks it (ParkedColorMixin), so picking it again keeps its domain, range and key names. A dialog's Apply button writes the whole object through applyPlot, which rebuilds the display's config for the draft, too dear for a picker writing once per drag frame | ChordVariantDisplay, LinearAlignmentsDisplay, LinearCanvasBaseDisplay, LinearMafDisplay, LinearMultiRowFeatureDisplay, LinearMultiSampleVariantDisplay, MultiWaySyntenyDisplay |
ContextMenuMixin() | The right-click state of a display whose menu acts on a position: the anchor plus whatever the click resolved to (a genomic column, a feature), held as one value so the menu's open-ness and the position its items act on cannot disagree. | LinearAlignmentsDisplay, LinearCanvasBaseDisplay, LinearMafDisplay, LinearMarkDisplay, LinearMultiRowFeatureDisplay, LinearMultiSampleVariantDisplay, LinearWiggleDisplay |
FacetWritesMixin() | The writes a display's facet object takes from a menu row, a dialog or a chip drag: the field the sections split by (facetByField) and their order (setFacetSort). Each rewrites the object as written, so what a pick leaves alone stays, and a pick of the field already grouping writes nothing. A display that resets its scroll when the sections change does it from HiddenGroupsMixin's dropGroupState, which also runs for a facet an agent, a share link or "Edit plot" writes | LinearAlignmentsDisplay, LinearCanvasBaseDisplay, LinearMarkDisplay, LinearMultiRowFeatureDisplay, LinearMultiSampleVariantDisplay |
StoredHoverMixin() | A stored hover. The hit type, as the type parameter. Brings the hoveredFeature getter BaseDisplay declares as a hook, setHoveredFeature, and the clearHoveredFeature the foundations' viewport-change reaction calls | LinearMafDisplay, LinearMarkDisplay, LinearMultiSampleVariantDisplay, LinearScoreDisplay, LinearWiggleDisplay |
TreeSidebarMixin() | Row set with a dendrogram sidebar, its arrangement the display's rows config object and its row colors the rowColor object, each written as a session edit to the track's config so undo, reset and a share link reach it and it survives unticking the track. Brings the sidebar toggles, the runClustering / clusterRegion and sortRowsBy declarative launch specs setupTreeSidebarAutoruns consumes, the row arrangement every shared consumer goes through, the rows derived from it (editableSources, clusterableSources) with the arrangement dialog's applyRowEdits, the root getter, and the tree-hover and canvas-ref volatiles the shared sidebar draws through. A display supplies discoveredRows, and guideTreeNewick where its adapter has a tree, and overrides the hooks its rows need | LinearMafDisplay, LinearMarkDisplay, LinearMultiRowFeatureDisplay, LinearMultiSampleVariantDisplay, LinearWiggleDisplay |
ScoreScaleMixin() | Value scale, read and written at a YScalePaths map's paths (encoding.y, the coverage band's coverage.encoding.y). yChannelSchema / yFieldChannelSchema. Brings ScoreAxisMixin plus scaleType / scaleZero / fenceQuantile / clipFenceQuantile / symlogConstant / manual* and their setters, i.e. the whole ScoreScaleModel interface the Y axis row and its drawer widget consume | LinearAlignmentsDisplay, LinearMafDisplay, LinearMarkDisplay, WiggleScoreConfigMixin |
HiddenGroupsMixin() | The sections a reader hid from an in-track grouping's chips: the facet.hidden keys while the stack groups by the facet's field, hideGroup and showAllGroups writing them, hiddenGroupKeys reading them, groupStateKey (with the ownGroupState hook) for a live figure to key on, and the dropGroupState reset that fires when the host's groupKeySpace moves | LinearAlignmentsDisplay, LinearCanvasBaseDisplay, LinearMarkDisplay |
ParkedColorMixin() | The field the last write took off a color object, with its domain, range and key names, so picking that field again gives them back (colorForField). A written field always paints, so the field a write leaves, for Default, Solid color or another field, is display state rather than config. Each writer parks before it writes, so no reader sees the field gone unparked | ColorWritesMixin, LinearWiggleDisplay, TrackColorsMixin |
RowHeightMixin() | The two-valued row height every multi-row display has. A rowHeightConfigSchemaFields slot whose 0 means fit-to-display-height, and an autoRowHeight getter saying what that fit divides. Brings the raw rowHeight getter, setRowHeight, and the resolved effectiveRowHeight every consumer reads | LinearMafDisplay, LinearMarkDisplay, LinearMultiSampleVariantDisplay |
HeightModeMixin() | Track-height strategy over TrackHeightMixin(), which it composes and whose height and resizeHeight it overrides. growTargetHeight (default = the allotted height). Brings heightMode/autoHeight/fitHeightToDisplay, grownHeight, the reactive height override, setHeightMode, and the grow-aware resizeHeight, and the grow-exit bake reaction that writes the grown height into the slot when the mode leaves grow | LinearAlignmentsDisplay, LinearCanvasBaseDisplay |
MatrixRampMixin() | A matrix display's encoding.color as it paints: the painted field through its preset's ramp (colorEncoding), that ramp's scheme, direction and 256-entry table (colorScheme, colorReverse, colorRamp), and the scheme pick (setColorScheme). A display that may paint a field other than the written one, because the file could not serve it, answers paintedColorField. The domain stays the display's: one follows the loaded values, another spans its statistic's fixed range | LDTrackDisplay, LinearHicDisplay |
PointerHoverMixin() | A hover derived from the pointer. The hit type, as the type parameter. Brings the pointer volatile, setPointer for the chrome's onPointerPosition, the hoverHitAt hook a display overrides, and the hoveredFeature getter BaseDisplay declares as a hook | LinearHicDisplay, LinearMultiRowFeatureDisplay |
TriangleMatrixMixin() | A matrix drawn as a triangle over the view's axis (Hi-C, LD): the fetched payload (rpcData), the canvas box (canvasWidth, matrixHeight under the matrixTop hook), the rotate-and-squash transform and its inverse (cellToScreen, screenToCell), the frame the marks read (triangleFrame), the one-cell region map and canvas-wide block the mark backend draws (matrixRegions, matrixBlocks), and the heightMode slot. Composes after TrackHeightMixin, GlobalFetchMixin and LegendMixin | LDTrackDisplay, LinearHicDisplay |
HeightModeMixin() composes TrackHeightMixin() itself, so a display takes one
or the other, never both.
Walkthroughs
Two end-to-end guides build the same display, differing only in whether the display writes a custom shape. Start with the first:
- Plotting features in a custom display - fetch in a worker, declare a mark over a shared shape. Most displays stop here.
- GPU displays - the same display over a
custom shape: a
.slang, a uniform write, a painter and a hit test.
Each walkthrough is a build-step plugin; Writing a plugin covers the scaffold and build setup they assume.
For a worked in-tree display, read plugins/wiggle/src/LinearWiggleDisplay for
an overlay drawn over rendered content, or
plugins/alignments/src/LinearAlignmentsDisplay for many toggleable menu items
and a custom feature widget.
See also
- Developer guide: Data fetching pipeline
- Developer guide: SVG export