Lambda Phage Gene Map¶
This example recreates a gbdraw lambda phage plot using GenomeSpy's declarative grammar. Forward and reverse coding sequences appear on opposite sides of the genome backbone, with underlined product labels connected to their gene midpoints.
{
"description": [
"Lambda Phage Gene Map",
"Recreates a gbdraw gene map with strand-directed arrows and product labels in a bottom-anchored scrollable track.",
"Data source: NCBI/NLM RefSeq NC_001416.1; coordinate-normalized CDS extract."
],
"title": "Lambda phage (NC_001416.1, 48,502 bp)",
"data": {
"url": "https://data.genomespy.app/datasets/refseq-lambda-phage-cds/v1/lambda-cds.json"
},
"params": [
{ "name": "highlight", "value": null, "persist": false },
{ "name": "labelFontSize", "value": 12 },
{ "name": "laneStep", "value": 20 },
{ "name": "underlineClearance", "value": 4 },
{
"name": "pixelsPerBase",
"expr": "width * abs(scale('x', 1) - scale('x', 0))"
}
],
"transform": [
{
"type": "identifier",
"description": "Share gene identity across labels, connectors, and arrows for coordinated hover highlighting."
},
{
"type": "formula",
"description": "Anchor labels and leaders at the gene midpoint.",
"expr": "(datum.start + datum.end) / 2",
"as": "mid"
},
{ "type": "formula", "expr": "datum.start + 1", "as": "start1" }
],
"background": "white",
"scales": {
"x": {
"name": "genome",
"domain": [5000, 32000],
"zoom": { "extent": [0, 48501] }
}
},
"encoding": {
"x": {
"field": "start",
"type": "index",
"band": 0,
"axis": {
"title": "Genome position (bp)",
"tickCount": 10,
"extraValues": [0],
"grid": false,
"domainColor": "#222222",
"domainWidth": 1.5,
"tickColor": "#222222",
"tickSize": 6,
"labelFontSize": 13,
"labelPadding": 7,
"titleFontSize": 13
}
}
},
"spacing": 0,
"resolve": {
"scale": { "x": "shared", "y": "independent" },
"axis": { "x": "shared" }
},
"vconcat": [
{
"name": "labelPanel",
"description": "Fill the container above the arrow track with a bottom-anchored viewport. Content grows upward as pileup needs more rows.",
"transform": [
{
"type": "measureText",
"description": "Measure label widths using the same font and size as the text marks.",
"field": "product",
"fontSize": { "expr": "labelFontSize" },
"font": "Lato",
"as": "labelWidth"
},
{
"type": "collect",
"description": "Cache measurements so zooming and resizing replay only the layout steps below."
},
{
"type": "filter",
"description": "Label only genes whose midpoint is visible.",
"expr": "datum.mid >= invert('x', 0) && datum.mid <= invert('x', 1)"
},
{
"type": "formula",
"expr": "clamp(width * scale('x', datum.mid) + (datum.mid - floor(datum.mid)) * pixelsPerBase - datum.labelWidth / 2, 0, width - datum.labelWidth)",
"description": "Clamp actual label bounds around the exact gene midpoint; keep spacing out of clamping and flushed starts exactly zero.",
"as": "labelStart"
},
{
"type": "formula",
"expr": "datum.labelStart + datum.labelWidth",
"description": "Use the actual clamped text bounds; pileup adds spacing separately.",
"as": "labelEnd"
},
{
"type": "formula",
"expr": "datum.labelStart + datum.labelWidth / 2 - width * scale('x', datum.mid)",
"description": "Position the label center relative to its genomic anchor using only pixel offsets.",
"as": "labelOffset"
},
{
"type": "collect",
"sort": {
"field": ["labelStart", "mid", "locusTag"],
"order": ["ascending", "ascending", "ascending"]
},
"description": "Sort occupied starts for pileup; break ties by genomic position and ID to keep flushed labels in stable order."
},
{
"type": "pileup",
"description": "Assign collision-free label lanes with an eight-pixel gap between actual label bounds.",
"start": "labelStart",
"end": "labelEnd",
"spacing": 8,
"as": "lane"
}
],
"encoding": {
"x": { "field": "mid", "type": "index", "band": 0 },
"y": { "field": "lane", "type": "index", "band": 0, "axis": null }
},
"layer": [
{
"name": "labelLeaders",
"mark": {
"type": "rule",
"size": 0.8,
"clip": "x",
"tooltip": null
},
"encoding": {
"color": {
"condition": {
"param": "highlight",
"empty": false,
"value": "red"
},
"value": "#929292"
},
"x": { "field": "mid", "type": "index", "band": 0 },
"xOffset": {
"expr": "(datum.mid - floor(datum.mid)) * pixelsPerBase",
"type": "quantitative",
"scale": null
},
"y2": { "value": 0 }
},
"description": "Leaders stop at the lower edge of the label track; the arrow track continues them."
},
{
"name": "labelBackgrounds",
"mark": {
"type": "rect",
"fill": "white",
"tooltip": null,
"yOffset": { "expr": "underlineClearance" },
"y2Offset": { "expr": "-4 - labelFontSize" }
},
"encoding": {
"x": { "field": "mid", "type": "index", "band": 0 },
"x2": { "field": "mid", "band": 0 },
"xOffset": {
"expr": "datum.labelOffset - datum.labelWidth / 2",
"type": "quantitative",
"scale": null
},
"x2Offset": { "expr": "datum.labelOffset + datum.labelWidth / 2" },
"y2": { "field": "lane", "band": 0 }
}
},
{
"name": "labelConnectors",
"description": "Restore only the owning leader across the white clearance below its underline. The extra pixel overlaps the long leader beyond the mask.",
"mark": {
"type": "rule",
"size": 0.8,
"clip": "x",
"tooltip": null,
"y2Offset": { "expr": "underlineClearance + 1" }
},
"encoding": {
"color": {
"condition": {
"param": "highlight",
"empty": false,
"value": "red"
},
"value": "#929292"
},
"x": { "field": "mid", "type": "index", "band": 0 },
"xOffset": {
"expr": "(datum.mid - floor(datum.mid)) * pixelsPerBase",
"type": "quantitative",
"scale": null
},
"y2": { "field": "lane", "band": 0 }
}
},
{
"name": "labelUnderlines",
"mark": {
"type": "rule",
"size": 0.8,
"tooltip": null
},
"encoding": {
"color": {
"condition": {
"param": "highlight",
"empty": false,
"value": "red"
},
"value": "#929292"
},
"x": { "field": "mid", "type": "index", "band": 0 },
"x2": { "field": "mid", "band": 0 },
"xOffset": {
"expr": "datum.labelOffset - datum.labelWidth / 2",
"type": "quantitative",
"scale": null
},
"x2Offset": { "expr": "datum.labelOffset + datum.labelWidth / 2" }
}
},
{
"name": "labelText",
"params": [
{
"name": "highlight",
"select": {
"type": "point",
"on": "pointerover",
"clear": "mouseout"
},
"push": "outer",
"persist": false
}
],
"mark": {
"type": "text",
"font": "Lato",
"size": { "expr": "labelFontSize" },
"align": "center",
"baseline": "bottom",
"dy": -4,
"clip": true,
"tooltip": null
},
"encoding": {
"color": {
"condition": {
"param": "highlight",
"empty": false,
"value": "red"
},
"value": "#171717"
},
"x": { "field": "mid", "type": "index", "band": 0 },
"xOffset": {
"field": "labelOffset",
"type": "quantitative",
"scale": null
},
"text": { "field": "product" }
}
}
],
"height": { "step": { "expr": "laneStep" } },
"scales": {
"y": {
"name": "labelLanes",
"type": "index",
"zoom": false,
"domainMin": -1,
"domainTransition": false,
"padding": 0
}
},
"viewportHeight": { "grow": 1, "anchor": "end" }
},
{
"name": "arrowPanel",
"description": "Two fixed strand bands replace pixel expressions for arrow positions.",
"height": { "step": 30 },
"encoding": {
"y": {
"field": "strand",
"type": "nominal",
"scale": {
"name": "strandRows",
"type": "band",
"domain": ["+", "-"],
"reverse": true,
"padding": 0
},
"axis": null
}
},
"layer": [
{
"name": "backbone",
"data": { "values": [{}] },
"mark": {
"type": "rule",
"color": "#d0d0d0",
"size": 3,
"tooltip": null
},
"encoding": {
"x": { "datum": 0, "type": "index", "band": 0 },
"x2": { "datum": 48502, "band": 0 },
"y": { "value": 0.5 }
}
},
{
"name": "arrowLeaders",
"description": "Continue midpoint leaders from the top edge to the strand row; arrows cover the endpoints.",
"mark": {
"type": "rule",
"size": 0.8,
"y2Offset": -0.5,
"tooltip": null,
"clip": "x"
},
"encoding": {
"color": {
"condition": {
"param": "highlight",
"empty": false,
"value": "red"
},
"value": "#929292"
},
"x": { "field": "mid", "type": "index", "band": 0 },
"xOffset": {
"expr": "(datum.mid - floor(datum.mid)) * pixelsPerBase",
"type": "quantitative",
"scale": null
},
"y2": { "value": 1 }
}
},
{
"name": "geneArrows",
"params": [
{
"name": "highlight",
"select": {
"type": "point",
"on": "pointerover",
"clear": "mouseout"
},
"push": "outer",
"persist": false
}
],
"zindex": 1,
"mark": {
"type": "arrow",
"size": { "band": 0.7 },
"headWidth": 1,
"headAngle": 56,
"headPlacement": "inside",
"minStemLength": 0,
"fill": "#55b9e9",
"strokeWidth": 1,
"clip": true
},
"encoding": {
"order": {
"condition": { "param": "highlight", "empty": false, "value": 1 },
"value": 0
},
"stroke": {
"condition": {
"param": "highlight",
"empty": false,
"value": "red"
},
"value": "#8b8b8b"
},
"x2": { "field": "end", "band": 0 },
"direction": {
"field": "strand",
"type": "nominal",
"scale": { "domain": ["+", "-"], "range": ["forward", "reverse"] }
},
"tooltip": [
{ "field": "product", "title": "Product" },
{ "field": "gene", "title": "Gene" },
{ "field": "locusTag", "title": "Locus tag" },
{ "field": "start1", "title": "Start (1-based)", "format": ",d" },
{ "field": "end", "title": "End (inclusive)", "format": ",d" },
{ "field": "strand", "title": "Strand" },
{ "field": "proteinId", "title": "Protein" }
]
}
}
]
}
]
}
Data: NCBI/NLM RefSeq NC_001416.1 (GenBank snapshot, 2023-01-11). The preparation recipe mirrors all 73 CDS features as zero-based, half-open intervals, preserving strands, product names, and overlaps; sequences and translations are omitted. Reuse follows NCBI's molecular data usage policy and disclaimer, including rights asserted by submitters.
What to notice¶
The initial view spans 5,000–32,000 bp. Zooming and panning reveal the rest of the 48,502 bp reference genome. Arrowheads stay inside their intervals, so genuine overlaps between adjacent genes remain visible. Hovering a gene shows its product, identifiers, strand, and source coordinates.
Hover a label or arrow to highlight that gene's label, connectors, and arrow outline in red. Hovered arrows are drawn above overlapping neighbors.
Label placement responds to the visible region and plot width. The spec
measures product names, clamps their text bounds to the viewport, and uses
pileup spacing to leave eight pixels between labels. Sorting those
intervals by their left edge, gene midpoint, and locus tag keeps labels in a
stable order when several become flush with the viewport edge.
Vertical concatenation separates the label and arrow tracks while sharing the genomic x scale. A non-zoomable index y scale gives label lanes 20-pixel steps; a band scale places arrows in two strand rows. The label viewport fills the remaining container height above the arrows.
Bottom anchoring keeps the lowest labels next to the arrows, including when the labels need less space than the viewport. When labels overflow, scroll vertically to inspect higher lanes. Changes in the lane count or viewport size preserve the scroll distance from the bottom, keeping the arrow track steady while navigating.
White rectangles interrupt leaders that pass through another label. Short connectors restore each label's own leader below its underline. Vertical rules clip only horizontally so leaders meet across the track boundary.
GenomeSpy Features¶
- An
indexscale aligns gene intervals and label anchors while allowing zooming across the full reference. arrowmarks encode genomic intervals and strand direction.measureTextsupplies label widths in pixels.collectcaches text measurements before viewport-dependent calculations and sorts occupied label intervals.pileupassigns non-overlapping label lanes using pixel intervals and separate spacing.vconcatseparates the label and arrow tracks, whilelayercombines marks within each track.- A scrollable viewport
with
anchor: "end"keeps the label track aligned to its bottom edge.