Skip to content

Scales

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.

Specifying a scale for a channel
{
  "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. Fractional input values are floored before scaling, so 3.9 is placed at index 3. Scale domains may still have fractional bounds during smooth zooming and panning.

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". Like index inputs, fractional linearized locus values are floored before scaling.

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 }
  }
}

Zooming and panning

To enable zooming and panning on a positional scale, 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.

Categorical scale viewports

Band and point scales keep their full category domain while zooming and panning change a scale viewport in category-rank coordinates. For a domain of ["A", "B", "C", "D"], viewport: [1, 3] shows B and C. Bounds are half-open and may be fractional, so [1.5, 3.5] pans by half a category. Numeric categories remain category values; viewport bounds always refer to ranks in domain order. Intervals narrower than one category expand around their midpoint to span one category.

Omitting viewport, or setting it to null, fits all categories and follows later domain changes. An explicit interval sets the initial and reset window. Navigation preserves rank positions when categories are reordered or added; shrinking the domain moves the window back inside its bounds. Step-based sizes continue to use the full domain and base step.

Categorical zooming is opt-in with zoom: true. Its bounds are always the full domain; zoom.extent options do not apply. Navigation does not support round: true or interval brushing. A scale viewport differs from a scrollable layout viewport, which moves over content that is already laid out.

This example zooms across 78 categorical columns in three alphabetic groups, separated by fixed-pixel gaps:

{
  "description": [
    "Zoomable categorical columns",
    "Zoom and pan across categorical columns. Grouping gaps keep their pixel width."
  ],

  "data": { "sequence": { "start": 0, "stop": 78, "as": "value" } },

  "transform": [
    {
      "type": "formula",
      "expr": "'ABCDEFGHIJKLMNOPQRSTUVWXYZ'[datum.value % 26] + (floor(datum.value / 26) + 1)",
      "as": "category"
    },
    {
      "type": "formula",
      "expr": "datum.value + (datum.value % 2) * 3",
      "as": "value"
    }
  ],

  "mark": "rect",

  "encoding": {
    "x": {
      "field": "category",
      "type": "nominal",
      "scale": {
        "type": "band",
        "zoom": true,
        "gapPositions": ["A2", "A3"],
        "gapWidth": 8
      },
      "axis": { "labelSqueeze": true }
    },
    "color": {
      "field": "value",
      "type": "quantitative",
      "legend": null
    }
  }
}

For point placement, change the x scale to "point", use a point mark, and remove labelSqueeze. Point scales share the rank viewport behavior but have zero bandwidth. The scale API can read, restore, link, and animate categorical rank intervals.

Zoom extent

Continuous scales can constrain navigation with the following options:

extent

Type: NumericDomain | string[] | boolean[] | ComplexDomain | "data" | "unbounded"

The boundaries that limit zoom and pan interactions on continuous scales. Band and point scales use the full categorical domain as their bounds and accept only a boolean zoom property.

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 zoomLevel() expression helper and its channel-specific form, such as zoomLevel("x").

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"
    }
  }
}

Grouping gaps

Grouping gaps separate selected rows or columns in heatmaps and other grouped plots. They are available on positional band, point, index, and locus scales. gapWidth specifies the extra spacing in CSS pixels and stays fixed when the view resizes. Ordinary scale padding continues to apply.

Gaps can be configured in an encoding's scale or a view's scales declaration, including shared scales. Axis domain lines split into groups automatically. The scaleGroups data source provides matching endpoints for custom guides and outlines.

Band and point scales

On a band or point scale, gapPositions lists categories before which gaps appear. For example, gapPositions: ["C"] inserts a gap between B and C in the domain ["A", "B", "C", "D"]. Numeric categories are values, not ranks: gapPositions: [20] cuts before category 20 in the domain [10, 20, 30].

This heatmap separates named rows and columns independently:

{
  "description": [
    "Grouped categorical heatmap",
    "Named rows and columns use independent grouping gaps while preserving a 30 px base step."
  ],

  "data": {
    "values": [
      { "gene": "TP53", "sample": "S1", "value": 1.8 },
      { "gene": "BRCA1", "sample": "S1", "value": 1.2 },
      { "gene": "EGFR", "sample": "S1", "value": -1.1 },
      { "gene": "KRAS", "sample": "S1", "value": -1.6 },
      { "gene": "TP53", "sample": "S2", "value": 1.1 },
      { "gene": "BRCA1", "sample": "S2", "value": 1.5 },
      { "gene": "EGFR", "sample": "S2", "value": -0.8 },
      { "gene": "KRAS", "sample": "S2", "value": -1.2 },
      { "gene": "TP53", "sample": "S3", "value": -1.3 },
      { "gene": "BRCA1", "sample": "S3", "value": -0.9 },
      { "gene": "EGFR", "sample": "S3", "value": 1.7 },
      { "gene": "KRAS", "sample": "S3", "value": 0.8 },
      { "gene": "TP53", "sample": "S4", "value": -1.6 },
      { "gene": "BRCA1", "sample": "S4", "value": -1.2 },
      { "gene": "EGFR", "sample": "S4", "value": 0.9 },
      { "gene": "KRAS", "sample": "S4", "value": 1.4 }
    ]
  },

  "width": { "step": 30 },
  "height": { "step": 30 },

  "mark": "rect",

  "encoding": {
    "x": {
      "field": "gene",
      "type": "nominal",
      "scale": {
        "type": "band",
        "domain": ["TP53", "BRCA1", "EGFR", "KRAS"],
        "gapPositions": ["EGFR"],
        "gapWidth": 8
      },
      "axis": { "title": "Gene", "labelAngle": -45 }
    },
    "y": {
      "field": "sample",
      "type": "nominal",
      "scale": {
        "type": "band",
        "domain": ["S1", "S2", "S3", "S4"],
        "gapPositions": ["S3"],
        "gapWidth": 6
      },
      "axis": { "title": "Sample" }
    },
    "color": {
      "field": "value",
      "type": "quantitative",
      "scale": { "domain": [-2, 2], "scheme": "blueorange" },
      "legend": { "title": "Value" }
    }
  }
}

With band scales, paddingInner controls proportional spacing between every pair of cells, while grouping gaps add pixels at selected boundaries. On point scales, gaps increase the spacing between point centers; large symbols may extend into that spacing.

The domain determines category order. Duplicate positions, absent categories, and the first category add no extra gap. Filtering out a boundary category removes its gap; restoring the category restores the gap.

Band and point gaps do not support round: true. Nested categorical offsets under a gapped band require an explicit offset range. Adding gaps does not enable zooming; enable it separately with zoom: true.

Index and locus scales

For index and locus scales, gapPositions uses numeric boundary coordinates. For example, gapPositions: [5, 12] inserts gaps before indices 5 and 12. Their pixel widths stay fixed during zooming and panning.

{
  "description": [
    "Grouped heatmap",
    "Grouping gaps separate clustered heatmap rows and columns while keeping their pixel widths during zooming."
  ],

  "data": { "sequence": { "start": 0, "stop": 200, "as": "cell" } },

  "transform": [
    { "type": "formula", "expr": "datum.cell % 20", "as": "column" },
    { "type": "formula", "expr": "floor(datum.cell / 20)", "as": "row" },
    {
      "type": "formula",
      "expr": "sin(datum.column * 0.7 + datum.row)",
      "as": "value"
    }
  ],

  "mark": "rect",

  "encoding": {
    "x": {
      "field": "column",
      "type": "index",
      "scale": { "domain": [0, 19], "gapPositions": [5, 12], "gapWidth": 8 }
    },
    "y": {
      "field": "row",
      "type": "index",
      "scale": { "domain": [0, 9], "gapPositions": [4, 7], "gapWidth": 8 }
    },
    "color": {
      "field": "value",
      "type": "quantitative",
      "legend": { "style": null }
    }
  }
}

For chromosome boundaries, set gapPositions: "chromosomes" on a locus scale. The boundaries come from that scale's assembly, so the same configuration works on linear genomic tracks and scales bound to different assemblies.

This example uses hg38 chromosome intervals from the axisGenome source, with a 5 px gap between chromosomes:

{
  "description": [
    "Chromosome grouping gaps",
    "Chromosome intervals in hg38 are separated by fixed 5 px gaps."
  ],

  "assembly": "hg38",

  "data": { "lazy": { "type": "axisGenome", "channel": "x" } },

  "mark": "rect",

  "encoding": {
    "x": {
      "field": "continuousStart",
      "type": "locus",
      "scale": { "gapPositions": "chromosomes", "gapWidth": 5, "zoom": true }
    },
    "x2": { "field": "continuousEnd" },
    "color": {
      "field": "odd",
      "type": "nominal",
      "scale": { "range": ["#c6dbef", "#6baed6"] },
      "legend": null
    }
  }
}

Half-open ranges ending at a cut stop before the gap, while ranges starting there begin after it. A range crossing an internal cut spans the gap normally. Inverting a pointer anywhere in a gap returns its boundary coordinate. A brush entirely within one gap is an active, zero-span selection that matches no data; linked scales keep their last valid domain until the brush has a nonzero span.

Sizing and expressions

With a fixed view width or height, gaps reserve space by reducing cell widths or point spacing. With step-based sizing, gaps increase the plot size while preserving the base step. If gaps consume the whole axis extent, affected marks are hidden until space is available again.

gapWidth and gapPositions accept expressions. Constant zero/null width or constant empty/null positions disable grouping gaps. On a step-sized axis, gap expressions must be independent of the resulting plot width or height; external parameters can control the gaps.

Nested offset scales

A discrete field, datum, or expression on the xOffset or yOffset channel creates a nested band scale when the matching primary position uses a band scale. The offset range is measured in logical pixels and spans the primary band. Point-like marks use subgroup centers, while rectangles cover subgroup band extents.

The primary scale's paddingInner and paddingOuter control spacing between groups and default to 0.2 when a nested offset scale is present. The offset scale's padding controls spacing between marks within each group. Explicit padding values override the defaults.

Nested bands for grouped bars
{
  "width": { "step": 12 },
  "mark": "rect",
  "encoding": {
    "x": { "field": "category", "type": "nominal" },
    "xOffset": {
      "field": "group",
      "type": "nominal",
      "scale": { "paddingInner": 0.15 }
    }
  }
}

As described in Step sizing, a step-based width or height normally describes a positional scale step. When a discrete offset scale is present, it describes each offset step by default. Use { "step": 12, "for": "position" } to make the step describe each primary category instead. An explicit offset-scale range remains a pixel range and is not replaced by nested-band inference. For a primary band with grouping gaps, an explicit offset range is required.

See the grouped bar example.

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.

For dendrogram heights, domain: { "source": "viewport", "mode": "contain" } fits the scale to links whose positional endpoints are both inside the viewport. Boundary-crossing branches then no longer determine the height domain. Clip the link mark on both axes to keep those branches inside the panel. See the clustered heatmap.

mode

Type: string

Determines which positional values contribute to the domain. "intersect" includes intervals that overlap the viewport. "contain" requires both endpoints to be inside it, including its boundaries. Points use inclusive boundaries in either mode. Encoded values are tested before applying band placement or pixel offsets.

Default value: "intersect".

Configured viewport domains on a shared scale must use the same mode. Members without a configured domain inherit that mode.

GenomeSpy calculates the domain shortly after navigation pauses and applies the normal smooth domain transition. Existing scale options such as padding, 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 }
  }
}

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:

Channel-level scale configuration
{
  "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:

View-level scale properties
{
  "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 declaration with an explicit type creates a scale even when no encoding uses it. This allows a container to start without tracks. With resolve.scale.x: "shared", added tracks share the scale, and removing them preserves its zoom state.

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.

Reactive scale properties

Scale domain and range properties can contain expression references that depend on parameters. When a parameter changes, the scale and visualization update automatically. A scale domain can also be linked directly to an interval selection parameter.

Expression-driven domains and ranges

Use an expression reference where a scale property accepts a dynamic value. For example, the upper end of this size range follows the maxSize parameter:

{
  "params": [{ "name": "maxSize", "value": 800 }],
  "mark": "point",
  "encoding": {
    "size": {
      "field": "value",
      "type": "quantitative",
      "scale": { "range": [20, { "expr": "maxSize" }] }
    }
  }
}

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.

Parameter scope

An independent scale resolves parameter names from its unit view or an ancestor. A shared scale resolves them from the composed view that owns the scale resolution or an ancestor of that view. Parameters declared only in a child are not visible to the scale.

Declare reactive shared-scale properties and their controlling parameters on the owning view. If an interaction or input in a child needs to update the parameter, declare a same-named child parameter with "push": "outer":

{
  "description": [
    "Shared Scale Expression Scope",
    "A child slider pushes a size limit to the parameter that owns a shared scale."
  ],

  "params": [{ "name": "maxSize", "value": 800 }],

  "scales": {
    "size": {
      "domain": [0, 10],
      "range": [20, { "expr": "maxSize" }]
    }
  },

  "data": {
    "values": [{ "value": 2 }, { "value": 5 }, { "value": 7.5 }]
  },

  "layer": [
    {
      "params": [
        {
          "name": "maxSize",
          "push": "outer",
          "bind": {
            "input": "range",
            "min": 100,
            "max": 1500,
            "step": 100,
            "name": "Maximum symbol size"
          }
        }
      ],
      "mark": { "type": "point", "color": "#4c78a8" },
      "encoding": {
        "x": { "field": "value", "type": "quantitative" },
        "y": { "value": 0.35 },
        "size": {
          "field": "value",
          "type": "quantitative"
        }
      }
    },
    {
      "mark": { "type": "point", "color": "#f58518" },
      "encoding": {
        "x": { "field": "value", "type": "quantitative" },
        "y": { "value": 0.65 },
        "size": { "field": "value", "type": "quantitative" }
      }
    }
  ]
}

Compatibility behavior

A composed scale resolution used by only one plot unit and with no view-level scales.<channel> declaration currently uses that unit's parameter scope. Do not rely on this fallback in new specifications. Make the scale independent or declare the parameter on the resolution owner. The fallback is planned for removal in GenomeSpy 2.0.

Domain from Selection Parameters

A scale domain can link directly to an interval selection parameter. The named selection must be visible from the scale owner according to the scope rule above:

{
  "scale": {
    "zoom": true,
    "domain": {
      "param": "brush",
      "initial": [10, 20]
    }
  }
}

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.

An interval selection cannot drive the same shared positional scale on which it is defined because that would create a feedback loop. Make the linked scale independent from the selection view's scale.

For detailed brushing-and-linking guidance and interactive examples, see Parameters: Interval selection.

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.

param Required

Type: string

Name of an interval selection parameter that provides the domain. The name resolves from the view that owns the scale resolution.

Scale-dependent expressions

Expressions can read current scale state using functions such as domain, range, bandwidth, and zoomLevel. These expressions are reactive: they are reevaluated when the referenced scale changes. See Scale Functions for the available functions.

An expression parameter can expose the result to mark properties or to properties of another scale.

Responsive genomic scale bars

A scale bar is a practical use of a scale-dependent expression. The example reads the visible span of a shared locus scale, uses tickStep to choose a rounded distance, formats the label, and drives arrow and text properties with the resulting parameters. Zoom the scale to see the bar update while remaining centered at roughly half the viewport width.

{
  "description": [
    "Zoom-responsive genomic scale bars",
    "Zoom the shared genomic scale to change the distance represented by the arrow. A static dummy datum and expression-driven mark properties keep the scale bar centered at roughly half the viewport width."
  ],

  "assembly": "hg38",

  "scales": { "x": { "type": "locus" } },
  "axes": { "x": {} },

  "data": { "values": [{}] },

  "params": [
    { "name": "visibleSpan", "expr": "abs(span(domain('x')))" },
    { "name": "distance", "expr": "max(1, tickStep(0, visibleSpan, 2))" },
    {
      "name": "unit",
      "expr": "distance >= 1e9 ? 1e9 : distance >= 1e6 ? 1e6 : distance >= 1e3 ? 1e3 : 1"
    },
    { "name": "barFraction", "expr": "distance / visibleSpan" }
  ],

  "layer": [
    {
      "name": "scale-bar",
      "mark": {
        "type": "arrow",
        "direction": "both",
        "headPlacement": "inside",
        "headShape": "open",
        "size": 1,
        "color": "black",
        "headWidth": 10,
        "headAngle": 90,
        "yOffset": 2,
        "x": { "expr": "0.5 - barFraction / 2" },
        "x2": { "expr": "0.5 + barFraction / 2" }
      }
    },
    {
      "name": "scale-label",
      "mark": {
        "type": "text",
        "yOffset": -2,
        "baseline": "alphabetic",
        "text": {
          "expr": "format(distance / unit, '.0f') + (unit == 1e9 ? ' Gb' : unit == 1e6 ? ' Mb' : unit == 1e3 ? ' kb' : ' bp')"
        }
      }
    }
  ]
}

Derived scale domains and ranges

A scale-dependent parameter can drive the domain or range of another scale. This is useful when two measurements have a known conversion. Declare the parameter where the source scale can be resolved, and ensure that the dependent scale can see the parameter according to the parameter scope rules above.

The dependent value follows the source scale's effective state, including transition frames. An expression-driven domain does not start a separate transition by default. Set zero and nice to false when an exact converted domain must be preserved. Scale and parameter dependencies must be acyclic; a scale cannot directly or indirectly depend on itself.

Named scales

By giving the scale a name, it can be accessed through the API.

{
  ...,
  "scale": {
    "name": "myScale"
  }
}

Axes

Positional encodings create axes automatically. A view-level scale declaration does not create an axis by itself; declare axes.<channel> when an axis is needed without a positional encoding. See Axis for axis configuration, resolution, styling, inside placement, and genome axes for locus scales.