Parameters and interaction¶
Interaction lets a users to change and manipulate a visualization without rebuilding it from scratch. The genome-spy-pythonAPI follows closely the parameter ergonomics of
Altair:
create a handle, attach it to a chart, then reuse that handle in an
encoding, expression, or filter.
In practice, GenomeSpy represents interactive state with named parameters. Scales, transforms, encodings, and expressions can read those names and update when their values change. The GenomeSpy documentation describes the complete parameter model in parameters.
Zoom and pan¶
The simplest interaction needs no parameter at all. This chart’s locus scale is navigable as it stands:
# Each datum is one genomic variant with a score, sequencing depth, and impact.
import genome_spy as gs
VARIANTS = [
{
"id": "v1",
"chrom": "chr17",
"pos": 43_044_295,
"score": 0.42,
"depth": 38,
"impact": "moderate",
},
{
"id": "v2",
"chrom": "chr17",
"pos": 43_057_481,
"score": 0.91,
"depth": 72,
"impact": "high",
},
{
"id": "v3",
"chrom": "chr17",
"pos": 43_070_977,
"score": 0.27,
"depth": 51,
"impact": "low",
},
{
"id": "v4",
"chrom": "chr17",
"pos": 43_082_144,
"score": 0.73,
"depth": 64,
"impact": "high",
},
]
REGION = [
{"chrom": "chr17", "pos": 43_040_000},
{"chrom": "chr17", "pos": 43_090_000},
]
# A plain locus scale already supports zooming and panning.
zoom_chart = (
gs.Chart(VARIANTS)
.mark_point(filled=True, size=110)
.encode(
x=gs.Locus("chrom", "pos").scale(domain=REGION).axis(title="Genomic position"),
y=gs.Y("score:Q").scale(domain=[0, 1]).title("Score"),
color=gs.Color("impact:N"),
tooltip=["id:N", "score:Q", "impact:N"],
)
.properties(assembly="hg38", title="Zoomable locus scale")
)
The scale domain is the state being changed, and no explicit parameter holds it. In a linked-track browser, put the domain on the one shared scale so every track follows it.
Parameters handle interaction¶
A
param()is a named value, such as a cutoff.A
binding_range()is the input widget used to change that value.A
selection_point()stores marks a user clicksA
selection_interval()stores a range they drag over (brush).add_params()puts the parameter on the chart that owns it.
Bind a parameter to an input¶
param() declares a value,
binding_range() makes a slider, and
add_params() attaches
the parameters to the chart:
# A value parameter and its slider binding as a handle.
min_score = gs.param(
"minScore",
value=0.4,
bind=gs.binding_range(
min=0,
max=1,
step=0.05,
name="Minimum score: ",
),
)
point_size = gs.param("pointSize", expr=60 + min_score * 100)
# Keep the categories stable when the slider filters rows out.
VARIANT_DOMAIN = ["v1", "v2", "v3", "v4"]
bound_chart = (
gs.Chart(VARIANTS)
.transform_filter(gs.datum.score >= min_score)
.mark_point(
filled=True,
color="#4c78a8",
size=point_size,
)
.encode(
x=gs.X("id:N").scale(domain=VARIANT_DOMAIN).title("Variant"),
y=gs.Y("score:Q").scale(domain=[0, 1]).title("Score"),
)
.properties(title="Filter with a bound parameter")
.add_params(min_score, point_size)
)
Parameters can be used directly in Python expressions. Here, min_score
controls the filter:
gs.datum.score >= min_score
datum means the current data row. Moving the slider makes GenomeSpy run the
filter again in the browser.
Use transform_collect() before a parameter-dependent transform to replay cached rows instead of returning to the data source when a slider changes.
The second parameter demonstrates a reactive expression:
point_size = gs.param("pointSize", expr=60 + min_score * 100)
GenomeSpy recalculates point_size when min_score changes. Passing the handle
to mark_point(size=...) uses its current value.
The x-axis is told to always show all variant names. Moving the filter slider may hide some points, but it does not make the remaining names shift position.
The GenomeSpy documentation covers the available input widgets in input bindings and reactive parameters in expressions.
More slider examples¶
Thresholds: Manhattan plot, HapMap volcano plot, Airway volcano plot, and Airway MA plot use sliders to change significance or effect-size cutoffs.
Track settings and filtering: BAM read alignments, Sashimi plot, and ASCAT fitting use sliders to filter data or adjust a track’s layout and model settings.
Select marks and style them conditionally¶
A selection parameter stores what the user picks. Use
selection_point() for discrete marks
Use when() to make an encoding conditional: choose one
visual value when a condition matches (.then(...)) and optionally another when it does not (.otherwise(...)). This reacts to a selection or a value parameter, for example, changing a mark’s color, opacity, size, or outline.
# Clicking a point updates this named selection.
selected_variant = gs.selection_point("selectedVariant", empty=False)
# The point opacity and stroke width is controlled with the
# .when().then().otherwise() notation.
selection_chart = (
gs.Chart(VARIANTS)
.mark_point(filled=True, size=140, stroke="black")
.encode(
x=gs.Locus("chrom", "pos").scale(domain=REGION),
y=gs.Y("score:Q").scale(domain=[0, 1]),
color=gs.Color("impact:N"),
key=gs.Key("id"),
opacity=gs.when(selected_variant).then(gs.value(1)).otherwise(gs.value(0.25)),
strokeWidth=gs.when(selected_variant).then(gs.value(2)).otherwise(gs.value(0)),
tooltip=["id:N", "impact:N", "score:Q"],
)
.properties(assembly="hg38", title="Click a variant to select it")
.add_params(selected_variant)
)
Selected points are opaque and outlined; other points are faint.
In the selection definition, empty=False makes the chart start with no
selected points:
selected_variant = gs.selection_point("selectedVariant", empty=False)
In the point encoding, key=gs.Key("id") uses each point’s id value to
identify it. This helps the chart keep the right point selected if its data is
updated or reordered.
See point for more configuration options.
Select intervals with brushing¶
Use selection_interval() or a brush for a dragged range selection. A brush is a translucent rectangle a user drags to choose an area. It is useful, for example, when an overview, such as a chromosome track, should control what other linked tracks show.
In code, create a named value to hold the selected brush range with param(). Then add an interval selection with the same name using selection_interval() to let the user drag a rectangle. Dragging updates brush with the chosen range. Other chart parts can reuse brush to zoom, filter data, or change mark styles.
In the following example, The top row is a map of the chromosomes. Drag across it to choose which part of the genome appears below. That dragged rectangle is the brush. It stores the selected genomic range under the name brush, and both detail tracks read that same range, so they move together.
# The parent owns the selected genomic interval. The overview below updates it;
# the detail tracks read it as their x-scale domain.
DETAIL_REGION = [
{"chrom": "chr6", "pos": 20_000_000},
{"chrom": "chr11", "pos": 40_000_000},
]
BRUSH_VARIANTS = [
{"id": "g1", "chrom": "chr1", "pos": 45_000_000, "score": 0.42, "depth": 38},
{"id": "g2", "chrom": "chr7", "pos": 35_000_000, "score": 0.91, "depth": 72},
{"id": "g3", "chrom": "chr9", "pos": 60_000_000, "score": 0.27, "depth": 51},
{"id": "g4", "chrom": "chr11", "pos": 20_000_000, "score": 0.73, "depth": 64},
{"id": "g5", "chrom": "chr17", "pos": 43_000_000, "score": 0.58, "depth": 46},
{"id": "g6", "chrom": "chr20", "pos": 30_000_000, "score": 0.36, "depth": 57},
]
def build_brush_chart() -> gs.TopLevelSpec:
"""Build the overview-and-detail brush example."""
brush = gs.param("brush")
brush_update = gs.selection_interval(
"brush",
encodings=["x"],
mark=gs.BrushConfig(
clip=False,
fill="#4c78a8",
fillOpacity=0.18,
stroke="#4c78a8",
measure="outside",
zindex=11,
),
push="outer",
persist=False,
)
chromosome_rects = (
gs.Chart()
.mark_rect(tooltip=None)
.encode(
fill=gs.Fill("odd:N")
.scale(domain=[True, False], range=["#e8e8e8", "white"])
.legend(None)
)
)
chromosome_labels = (
gs.Chart()
.mark_text(paddingX=3, paddingY=5, tooltip=None)
.encode(text=gs.Text("name:N"))
)
overview_track = (
gs.layer(chromosome_rects, chromosome_labels)
.encode(
x=gs.X("continuousStart:L").scale(zoom=False).axis(None),
x2=gs.X2("continuousEnd"),
)
.properties(
data=gs.Data(lazy=gs.AxisGenomeData(type="axisGenome", channel="x")),
height=26,
)
.add_params(brush_update)
)
overview = gs.vconcat(overview_track).resolve_scale(x="excluded")
brush_score_track = (
gs.Chart()
.mark_point(filled=True, size=100, color="#4c78a8")
.encode(
x=gs.Locus("chrom", "pos")
.scale(
domain=gs.SelectionDomainRef(param=brush.name, initial=DETAIL_REGION)
)
.axis(None),
y=gs.Y("score:Q").scale(domain=[0, 1]).title(None),
tooltip=["id:N", "score:Q"],
)
.properties(height=80, title=gs.title("Score", orient="left"))
)
brush_depth_track = (
gs.Chart()
.mark_point(filled=True, size=100, color="#f58518", shape="square")
.encode(
x=gs.Locus("chrom", "pos").scale(
domain=gs.SelectionDomainRef(param=brush.name, initial=DETAIL_REGION)
),
y=gs.Y("depth:Q").scale(domain=[0, 80]).title(None),
tooltip=["id:N", "depth:Q"],
)
.properties(height=80, title=gs.title("Depth", orient="left"))
)
return (
gs.vconcat(overview, brush_score_track, brush_depth_track)
.properties(
data=BRUSH_VARIANTS,
assembly="hg38",
padding=gs.Paddings(top=20),
spacing=8,
)
.add_params(brush)
.resolve_scale(x="independent", y="independent")
.resolve_axis(x="independent", y="independent")
)
brush_chart = build_brush_chart()
Dragging the overview updates brush with the selected range. Both detail
tracks use that range as their x-axis domain, so they move together. The
overview always shows the full chromosome layout; drag its brush or zoom a
detail track to change the shared view.
The brush example uses a few extra configuration objects because an overview controls two linked detail tracks.
BrushConfigcontrols how the dragged selection rectangle looks, including its fill and outline. Use it only when the default brush style is not enough.AxisGenomeDataprovides the built-in chromosome overview data for the top row.SelectionDomainRefconnects each detail track’s x-axis tobrush. Itsinitialvalue chooses the range shown when the chart first opens.
See GenomeSpy’s documentation on interval selections and domains from selection parameters for the underlying grammar. The linked brush gallery example applies the same pattern to three genome-wide association tracks.
Add one ruler across linked tracks¶
A ruler parameter follows a coordinate and draws a guide across tracks.
ruler() is GenomeSpy-specific, but it uses
the same create, attach, and reuse pattern as value and selection parameters.
Define one ruler at the common parent of the tracks it should span:
# A ruler is a shared pointer-following guide rather than a data selection.
cursor = gs.ruler(
"cursor",
persist=False,
encodings=["x"],
extent="container",
display="line",
mark=gs.RulerMarkConfig(stroke="#d62728", strokeWidth=1),
)
score_track = (
gs.Chart()
.mark_point(filled=True, size=90, color="#4c78a8")
.encode(y=gs.Y("score:Q").scale(domain=[0, 1]).title("Score"))
.properties(height=90, title=gs.title("Score", orient="left"))
)
depth_track = (
gs.Chart()
.mark_point(filled=True, size=90, color="#f58518")
.encode(y=gs.Y("depth:Q").scale(domain=[0, 80]).title("Depth"))
.properties(height=90, title=gs.title("Read depth", orient="left"))
)
ruler_chart = (
(score_track & depth_track)
.properties(
data=VARIANTS,
assembly="hg38",
scales=gs.scales(x=gs.Scale(domain=REGION)),
axes=gs.axes(x=gs.GenomeAxis(title="Genomic position")),
spacing=8,
)
.add_params(cursor)
.encode(x=gs.Locus("chrom", "pos"))
.resolve_scale(x="shared", y="independent")
.resolve_axis(x="shared", y="independent")
)
In the ruler definition, encodings=["x"] makes the ruler follow the horizontal
position, and extent="container" makes one line span both tracks.
persist=False clears the ruler when the pointer leaves the chart.
RulerMarkConfig controls how that line looks, including
its color and width. Use it only when the default ruler style is not enough.
Pointer rulers follow mouse movement by default. Set source="viewport" when
the ruler should instead follow the centre of the visible range.
The GenomeSpy documentation covers snapping, clearing, and guide styling in ruler parameters.