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→ canvas
Tracks 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.
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.
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, LinearManhattanDisplay, LinearMarkDisplay, LinearMultiRowFeatureDisplay, LinearReferenceSequenceDisplay, LinearScoreDisplay, LinearWiggleDisplay, MultiSampleVariantBaseModel |
GlobalFetchMixin() | One non-regional dataset with no per-region partitioning, plus the render lifecycle. Installs no fetch autoruns; the display adds its own via installGlobalFetchAutorun. | LinearArcDisplay, LinearHicDisplay, LinearPairedArcDisplay, MultiWaySyntenyDisplay, SharedLDModel |
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 both 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 goes on naming what used to be there.
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 | LinearAlignmentsDisplay, LinearArcDisplay, LinearCanvasBaseDisplay, LinearHicDisplay, LinearMafDisplay, LinearManhattanDisplay, LinearMarkDisplay, LinearMultiRowFeatureDisplay, LinearPairedArcDisplay, LinearReferenceSequenceDisplay, LinearScoreDisplay, LinearWiggleDisplay, MultiSampleVariantBaseModel, MultiWaySyntenyDisplay, SharedLDModel |
LegendMixin() | The legend, whole. A display declares the color scales it paints with (colorScales, a getter hook) and the mixin derives the key from them (legendSpec, through legendSpecOf), keeps the showLegend slot's getter and setter, dismisses sections one at a time (dismissLegendSection, undone by re-showing the legend), answers whether there is a key to offer (hasLegendKey) and whether the export parks it beside the plot (svgLegendWidth). DisplayChrome draws the on-screen key and renderDisplaySvg the exported one, so a display places neither | LinearAlignmentsDisplay, LinearCanvasBaseDisplay, LinearHicDisplay, LinearMafDisplay, LinearManhattanDisplay, LinearMarkDisplay, LinearMultiRowFeatureDisplay, LinearWiggleDisplay, MultiSampleVariantBaseModel, MultiWaySyntenyDisplay, SharedLDModel |
ContextMenuMixin() | The right-click state of a display whose menu acts on a | LinearAlignmentsDisplay, LinearCanvasBaseDisplay, LinearMafDisplay, LinearManhattanDisplay, LinearMarkDisplay, LinearMultiRowFeatureDisplay, LinearWiggleDisplay, MultiSampleVariantBaseModel |
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 | LinearManhattanDisplay, LinearMarkDisplay, LinearMultiRowFeatureDisplay, LinearScoreDisplay, LinearWiggleDisplay, MultiSampleVariantBaseModel |
TreeSidebarMixin() | Row set with a dendrogram sidebar. sources (the display rows, named), the treeSidebarConfigSchemaFields slots, plus the run callback naming its own clustering RPC and the sortRows callback naming what a row carries at a column. Brings layout / clusterTree / clusterProvenance / subtreeFilter, the showTree / showBranchLength / showRowLabels / treeAreaWidth getters and setters and the rowDomain getter over those slots, the runClustering / clusterRegion and sortRowsBy declarative launch specs setupTreeSidebarAutoruns consumes, the root, willClearTree and rowOrderIsCustom getters, and the tree-hover and canvas-ref volatiles the shared sidebar draws through | LinearMafDisplay, LinearMultiRowFeatureDisplay, LinearWiggleDisplay, MultiSampleVariantBaseModel |
HiddenGroupsMixin() | The sections a reader hid from an in-track grouping's chips: the hiddenGroups set, hideGroup and showAllGroups over it, the displayHiddenGroupKeys hook a display hides a lane through on its own behalf, hiddenGroupKeys folding both, and the dropGroupState reset that fires when the host's groupKeySpace moves | LinearAlignmentsDisplay, LinearCanvasBaseDisplay, LinearMarkDisplay |
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, LinearMultiRowFeatureDisplay, MultiSampleVariantBaseModel |
ScoreScaleMixin() | Value scale, written in scales.y. valueScaleSchema / scalesSchema. Brings ScoreAxisMixin plus scaleType / scaleTypeChoices / autoscaleType / numStdDev / numQuantile / symlogConstant / manual* and their setters, i.e. the whole ScoreScaleModel interface the shared score menu and SetMinMaxDialog consume | LinearAlignmentsDisplay, LinearMarkDisplay, WiggleScoreConfigMixin |
HeightModeMixin() | Track-height strategy; the one row that must compose after TrackHeightMixin(), whose height and resizeHeight it overrides. growTargetHeight (default = the raw slot). 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 |
Order matters in one place: types.compose gives a collision to the later
argument, so composing HeightModeMixin() before TrackHeightMixin() leaves
grow mode inert. The mixin reports the ordering problem at attach.
Walkthroughs
Two end-to-end guides build the same display, differing only in whether the display writes a shape of its own. 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
shape of its own: a
.slang, a uniform write, a painter and a hit test.
Both are build-step plugins; 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.