Scale¶
Scales are functions that map abstract data values (e.g., a type of a point mutation) to visual values (e.g., colors that indicate the type).
By default, GenomeSpy configures scales automatically based on the data type
(e.g., "ordinal"), the visual channel, and the data domain. As the defaults
may not always be optimal, the scales can be configured explicitly.
Scale defaults can also be configured globally using config.scale and
config.range. For example, color defaults by data type can be set with
nominalColorScheme, ordinalColorScheme, and quantitativeColorScheme. See
Config, Themes, and Styles.
{
"encoding": {
"y": {
"field": "impact",
"type": "quantitative",
"scale": {
"type": "linear",
"domain": [0, 1]
}
}
},
...
}
In composed views, shared scales can also be configured at the view level. See Shared scales in composed views.
Vega-Lite scales¶
GenomeSpy implements most of the scale types of Vega-Lite. The aim is to replicate their behavior identically (unless stated otherwise) in GenomeSpy. Although that has yet to fully materialize, Vega-Lite's scale documentation generally applies to GenomeSpy as well.
The supported scales are: "identity", "linear", "pow", "sqrt",
"symlog", "log", "ordinal", "band", "point", "quantize",
"threshold", and "sequential". The "sequential" type is retained for
backwards compatibility. Disabled scale is supported on quantitative channels
such as x and opacity.
Currently, the following scales are not supported: "time", "utc",
"quantile", "bin-linear", "bin-ordinal".
Relation to Vega scales
In fact, GenomeSpy uses Vega scales, which are based on d3-scale. However, GenomeSpy has GPU-based implementations for the actual scale transformations, ensuring high rendering performance.
GenomeSpy-specific scales¶
GenomeSpy provides two additional scales that are designed for molecular sequence data.
Index scale¶
The "index" scale allows mapping index-based values such as nucleotide or
amino-acid locations to positional visual channels. It has traits from both the
continuous "linear" and the discrete "band" scale. It is linear and zoomable
but maps indices to the range like the band scale does – each index has its own
band. Properties such as padding work just as in the band scale.
Indices can be zero-based or one-based. Configure the scale domain to match the index values in the data. The numbering of axis labels can also be adjusted without transforming the data values.
The index scale is used by default when the field type is "index".
User-facing two-point domains on index scales are inclusive. For example,
"domain": [2, 4] covers the indices 2, 3, and 4. Domains inferred from
observed data also include the last observed index.
Point indices¶
When only the primary positional channel is defined, marks such as "rect" fill
the whole band.
{
"description": "Index scale example with band-filling marks.",
"data": { "values": [0, 2, 4, 7, 8, 10, 12] },
"encoding": {
"x": { "field": "data", "type": "index" }
},
"layer": [
{
"mark": "rect",
"encoding": {
"color": { "field": "data", "type": "nominal", "legend": null }
}
},
{
"mark": "text",
"encoding": {
"text": { "field": "data" }
}
}
]
}
Marks such as "point" that do not support the secondary positional channel are
centered.
{
"description": "Index scale example with centered point marks.",
"data": { "values": [0, 2, 4, 7, 8, 10, 12] },
"mark": "point",
"encoding": {
"x": { "field": "data", "type": "index" },
"color": { "field": "data", "type": "nominal", "legend": null },
"size": { "value": 300 }
}
}
Segment indices¶
When the index scale is used with segments, e.g., a "rect" mark that has both
the x and x2 channels defined, the ranges must be half
open.
For example, if a segment should cover the indices 2, 3, and 4, a half-open
range would be defined as: x = 2 (inclusive), x2 = 5 (exclusive).
Thus, scale.domain uses inclusive bounds, whereas ranged mark encodings such
as x/x2 use half-open interval edges directly.
{
"description": "Index scale example with half-open ranges.",
"data": {
"values": [
{ "from": 0, "to": 2 },
{ "from": 2, "to": 5 },
{ "from": 8, "to": 9 },
{ "from": 10, "to": 13 }
]
},
"encoding": {
"x": { "field": "from", "type": "index" },
"x2": { "field": "to" }
},
"layer": [
{
"mark": "rect",
"encoding": {
"color": { "field": "from", "type": "nominal", "legend": null }
}
},
{
"mark": "text",
"encoding": {
"text": { "expr": "'[' + datum.from + ', ' + datum.to + ')'" }
}
}
]
}
Adjusting the indexing of axis labels¶
When data values are zero-based but axis labels should use one-based numbering,
set numberingOffset to 1. The offset does not transform the data values, but
tick generation takes it into account so labels fall on nice round values.
Consequently, it may change which data indices receive ticks.
{
"description": "Index scale example with one-based axis numbering.",
"data": { "values": [0, 2, 4, 7, 8, 10, 12] },
"encoding": {
"x": {
"field": "data",
"type": "index",
"scale": { "numberingOffset": 1 }
}
},
"layer": [
{
"mark": "rect",
"encoding": {
"color": { "field": "data", "type": "nominal", "legend": null }
}
},
{
"mark": "text",
"encoding": {
"text": { "field": "data" }
}
}
]
}
Including the first index as a tick¶
Use axis.extraValues to add specified values while retaining automatically
generated ticks. Extra values outside the visible domain are omitted. This is
useful for one-based protein residue positions, where automatic tick generation
may start at a round value such as 100; adding 1 identifies the first residue.
extraValues applies only to continuous scales and is ignored on discrete
scales.
{
"description": [
"One-based index axis ticks",
"Uses extra axis values to identify the first residue."
],
"data": {
"values": [
{ "position": 1, "label": "First residue" },
{ "position": 100, "label": "Residue 100" },
{ "position": 1000, "label": "Residue 1000" }
]
},
"mark": { "type": "point", "clip": false, "size": 200 },
"encoding": {
"x": {
"field": "position",
"type": "index",
"scale": { "domain": [1, 1068] },
"axis": {
"title": "Protein position (aa)",
"tickCount": 10,
"extraValues": [1]
}
}
}
}
Locus scale¶
The "locus" scale is similar to the "index" scale, but provides a
genome-aware axis with concatenated chromosomes. See
genomic coordinates for assembly and
coordinate-system details. Locus scales resolve their assembly from
scale.assembly or, if omitted, from the root assembly. If root assembly
is omitted and root genomes has exactly one entry, that entry is used as the
default assembly.
The locus scale is used by default when the field type is "locus".
Note
The locus scale does not map the discrete chromosomes onto the concatenated axis. It's done by the linearizeGenomicCoordinate transform.
Specifying the domain¶
By default, the domain of the locus scale consists of the whole genome. However,
You can specify a custom domain using either linearized or genomic coordinates.
A genomic coordinate consists of a chromosome (chrom) and an optional position
(pos). The left bound's position defaults to zero, whereas the right bound's
position defaults to the size of the chromosome. Thus, the chromosomes are
inclusive.
Two-point locus domains are inclusive and cover both endpoint positions.
For example, chromosomes 3, 4, and 5:
[{ "chrom": "chr3" }, { "chrom": "chr5" }]
Only the chromosome 3:
[{ "chrom": "chr3" }]
A specific region inside the chromosome 3:
[
{ "chrom": "chr3", "pos": 1000000 },
{ "chrom": "chr3", "pos": 2000000 }
]
Somewhere inside the chromosome 1:
[1000000, 2000000]
Example¶
{
"description": "Locus scale example with an explicit genomic domain.",
"assembly": "hg38",
"data": {
"values": [
{ "chrom": "chr3", "pos": 134567890 },
{ "chrom": "chr4", "pos": 123456789 },
{ "chrom": "chr9", "pos": 34567890 }
]
},
"mark": "point",
"scales": {
"x": {
"domain": [{ "chrom": "chr3" }, { "chrom": "chr9" }]
}
},
"encoding": {
"x": {
"chrom": "chrom",
"pos": "pos",
"type": "locus"
},
"size": { "value": 200 }
}
}
Different assemblies¶
Different positional channels can use locus scales with different assemblies.
For example, a synteny view can use the x axis for human hg38 coordinates
and the y axis for mouse mm10 coordinates by setting scale.assembly on
each channel.
{
"description": [
"Human-mouse synteny example",
"Data source: http://bioinfo.konkuk.ac.kr/synteny_portal/"
],
"title": "Human-mouse synteny",
"view": { "stroke": "lightgray" },
"data": { "url": "data/synteny-hg38-mm10.tsv" },
"mark": {
"type": "rule",
"strokeCap": "round",
"size": 2
},
"encoding": {
"x": {
"chrom": "hg38_chrom",
"pos": "hg38_start",
"type": "locus",
"scale": { "type": "locus", "assembly": "hg38" },
"axis": { "chromGrid": true, "chromGridColor": "#bbb" },
"title": "hg38"
},
"x2": { "chrom": "hg38_chrom", "pos": "hg38_end" },
"y": {
"chrom": "mm10_chrom",
"pos": "mm10_start",
"type": "locus",
"scale": { "type": "locus", "assembly": "mm10" },
"axis": { "chromGrid": true, "chromGridColor": "#bbb" },
"title": "mm10"
},
"y2": { "chrom": "mm10_chrom", "pos": "mm10_end" },
"color": { "field": "mm10_chrom", "type": "nominal", "legend": null }
}
}
Domain from Selection Parameters¶
Scale domains can be linked to interval selection parameters:
Use an object-valued domain:
{
"scale": {
"zoom": true,
"domain": {
"param": "brush",
"initial": [10, 20]
}
}
}
Properties¶
encoding- Type: string
Selection interval channel to use.
If omitted, GenomeSpy infers the channel from the scale channel when possible (e.g.,
x->x,x2->x,y->y,y2->y). initial- Type: NumericDomain | string[] | boolean[] | ComplexDomain
Initial configured domain for the linked scale when the linked interval selection is empty.
Only supported when the linked scale is zoomable.
Clearing the linked interval selection resets the domain to the normal default/data-derived domain instead of restoring
initial. paramRequired- Type: string
Name of an interval selection parameter that provides the domain.
Clearing the linked interval selection returns the scale to its normal default
or data-derived domain instead of restoring initial.
Zoomable linked scales automatically synchronize the domain back to the
selection. Non-zoomable linked scales only read the selection. This affects
"index" and "locus" scales as they are zoomable by default.
For detailed brushing-and-linking guidance and interactive examples, see Parameters: Interval selection.
Viewport autoscaling¶
Enable autoscaling by setting domain to { "source": "viewport" }. GenomeSpy
then derives the quantitative scale domain from data in the current positional
viewport. This is useful for autoscaling a genomic signal's y-axis or a scatter
plot's size or color while zooming and panning.
GenomeSpy calculates the domain shortly after navigation pauses and applies the
normal smooth domain transition. Existing scale options such as zero, nice,
domainMin, and domainMax still apply. If a ready viewport has no values, the
scale retains its last nonempty domain.
GenomeSpy uses at least one continuous x or y scale to determine which data are inside the viewport. The domain of that positional scale must not depend on the autoscaled scale. A positional scale cannot be both zoomable and viewport-derived.
{
"description": "Viewport autoscaling for a zoomable signal.",
"data": {
"sequence": { "start": 0, "stop": 200000, "as": "x" }
},
"transform": [
{ "type": "formula", "expr": "random() * 0.682", "as": "u" },
{
"type": "formula",
"expr": "((datum.u % 1e-8 > 5e-9 ? 1 : -1) * (sqrt(-log(max(1e-9, datum.u))) - 0.618)) * 0.6 + sin(datum.x / 10000)",
"as": "y"
}
],
"mark": {
"type": "point",
"size": { "expr": "min(0.5 * pow(zoomLevel, 1.5), 200)" }
},
"encoding": {
"x": {
"field": "x",
"type": "quantitative",
"scale": { "zoom": true }
},
"y": {
"field": "y",
"type": "quantitative",
"scale": { "domain": { "source": "viewport" } }
},
"opacity": { "value": 0.6 }
}
}
Zooming and panning¶
To enable zooming and panning of continuous scales on positional channels, set
the zoom scale property to true. Example:
{
"x": {
"field": "foo",
"type": "quantitative",
"scale": {
"zoom": true
}
}
}
Both "index" and "locus" scales are zoomable by default.
Zoom extent¶
extent- Type: NumericDomain | string[] | boolean[] | ComplexDomain |
"data"|"unbounded"The boundaries that limit zoom and pan interactions.
A domain array sets explicit boundaries.
"data"derives the boundaries from the data that contributes to the scale domain. For index and quantitative scales,"unbounded"allows zooming and panning without fixed boundaries. Locus scales do not support unbounded zoom.With an unbounded extent, the initial scale domain is used as the reference for the
zoomLevelexpression parameter.Default value: The initial scale domain, except the whole genome for locus scales.
For "index" and "locus" scales, two-point zoom extents are inclusive.
The following scale starts at [10, 20] and allows zooming out to the full
data extent:
{
...,
"scale": {
"domain": [10, 20],
"zoom": {
"extent": "data"
}
}
}
Domain transitions¶
By default, domain updates are applied with a smooth transition when that is
possible. Set domainTransition to false to apply the new domain
immediately. ExprRef-driven domains default to domainTransition: false
unless overridden.
Shared scales in composed views¶
The channel-level scale property follows the Vega-Lite style: scale settings
are placed inside an encoding channel. This works well for local scale settings
in simple unit views. However, in composed GenomeSpy views, especially
genome-browser-like multi-track views, a shared positional scale often represents
the viewport of the whole subtree. Placing that viewport domain in one child
encoding makes it harder to see which domain controls the composed view.
For example, the channel-level form places the domain inside a child encoding. This is valid, but it makes a subtree-level setting look local to one participant:
{
"layer": [
{
"mark": "rect",
"encoding": {
"x": {
"chrom": "chrom",
"pos": "start",
"type": "locus",
"scale": {
"domain": [
{ "chrom": "chr15", "pos": 92925000 },
{ "chrom": "chr15", "pos": 92949000 }
]
}
},
"x2": {
"chrom": "chrom",
"pos": "end"
}
}
},
{
"mark": "point",
"encoding": {
"x": {
"chrom": "chrom",
"pos": "pos",
"type": "locus"
}
}
}
]
}
Use view-level scales to define properties for a shared scale at the subtree
that owns it:
{
"scales": {
"x": {
"domain": [
{ "chrom": "chr15", "pos": 92925000 },
{ "chrom": "chr15", "pos": 92949000 }
]
}
},
"layer": [
{
"mark": "rect",
"encoding": {
"x": {
"chrom": "chrom",
"pos": "start",
"type": "locus"
},
"x2": {
"chrom": "chrom",
"pos": "end"
}
}
},
{
"mark": "point",
"encoding": {
"x": {
"chrom": "chrom",
"pos": "pos",
"type": "locus"
}
}
}
]
}
Use resolve.scale to
choose how scales are shared. A view-level scales.<channel> entry owns the
shared scale properties for that view subtree. If the subtree has multiple
independent scales for the same channel, place scales.<channel> closer to the
intended subtree or make the sharing explicit with resolve.scale.
A view-level scale declaration is exclusive. When nested declarations target the same scale resolution, the ancestor declaration shadows the whole descendant declaration; their properties are not merged. Declarations in separate sibling subtrees remain ambiguous and cause an error. This keeps the shared scale under a single, predictable owner. Its effective properties do not depend on which descendant views are present, and imported subtrees can retain declarations that make them useful on their own.
Do not mix view-level scales.<channel> with participating
encoding.<channel>.scale objects for the same shared scale. Keep
encoding.<channel>.type on member encodings; it describes the encoded data and
drives default scale type inference.
Named scales¶
By giving the scale a name, it can be accessed through the API.
{
...,
"scale": {
"name": "myScale"
}
}
Axes¶
Positional scales are usually annotated with axes. See Axis for axis configuration, resolution, styling, inside placement, and genome axes for locus scales.