Skip to content

Multiscale Composition

multiscale is a convenience macro for semantic zooming. It expands to a layer composition with generated opacity transitions between child views.

Example

{
  "description": "A three-stage semantic zoom using the multiscale composition operator.",

  "view": { "stroke": "lightgray" },

  "resolve": {
    "scale": { "x": "shared" }
  },

  "encoding": {
    "x": {
      "field": "x",
      "type": "quantitative",
      "scale": { "zoom": true }
    },
    "y": { "field": "y", "type": "quantitative" }
  },

  "stops": [1, 0.1],

  "multiscale": [
    {
      "data": {
        "values": [{}]
      },
      "mark": { "type": "text", "text": "Zoom in to see details", "size": 16 },
      "encoding": {
        "x": { "value": 0.5 },
        "y": { "value": 0.5 },
        "color": { "value": "#666666" }
      }
    },
    {
      "data": {
        "sequence": { "start": 0, "stop": 4000, "step": 16, "as": "x" }
      },
      "transform": [
        { "type": "formula", "expr": "sin(datum.x / 40)", "as": "y" },
        { "type": "formula", "expr": "datum.x + 16", "as": "x2" }
      ],
      "mark": "rect",
      "encoding": {
        "x2": { "field": "x2" }
      }
    },
    {
      "data": {
        "sequence": { "start": 0, "stop": 4000, "step": 1, "as": "x" }
      },
      "transform": [
        {
          "type": "formula",
          "expr": "sin(datum.x / 40) + (random() - 0.5) * 0.2",
          "as": "y"
        }
      ],
      "mark": "point",
      "encoding": {
        "opacity": { "value": 0.7 }
      }
    }
  ]
}

How It Works

multiscale children are ordered from zoomed-out to zoomed-in. For N child views, stops must contain N - 1 values.

At compile time, multiscale is expanded into a regular layer. Normal layer behavior still applies (inherited encodings/data, scale resolution, and opacity multiplication with manually specified child opacity).

By default, each child is wrapped with a zoom-driven opacity ramp that cross-fades adjacent levels. The ramp is tied directly to the zoom metric, so two adjacent levels remain partly visible while the zoom level is between their stops.

By default, channel selection is automatic:

  1. If both x and y are available, the zoom metric is averaged.
  2. If only one is available, that one is used.
  3. Scales that are not visible at the multiscale scope (for example, independent descendant-local scales) are ignored.

For manual opacity control patterns, see layer zoom-driven opacity.

Transitioned Stops

Set stops.transition to select one detail level at each stop and cross-fade only while the selected level changes. See numeric transitions for the transition options. Unlike the default opacity ramp, this is a time-based cross-fade rather than a persistent blend across a zoom range. After it settles, the selected level is fully visible and all other levels are hidden.

Transitioned stops require an explicit "x" or "y" channel and cannot use fade:

{
  "stops": {
    "channel": "x",
    "values": [40000],
    "transition": { "type": "lerp", "halfLife": 60 }
  },
  "multiscale": [
    { "name": "Overview", "mark": "rect" },
    { "name": "Detail", "mark": "point" }
  ]
}
{
  "description": "A three-stage semantic zoom that cross-fades after selecting a discrete multiscale level.",

  "view": { "stroke": "lightgray" },

  "resolve": {
    "scale": { "x": "shared" }
  },

  "encoding": {
    "x": {
      "field": "x",
      "type": "quantitative",
      "scale": { "zoom": true }
    },
    "y": { "field": "y", "type": "quantitative" }
  },

  "stops": {
    "channel": "x",
    "values": [1, 0.1],
    "transition": { "type": "lerp", "halfLife": 100 }
  },

  "multiscale": [
    {
      "data": {
        "values": [{}]
      },
      "mark": { "type": "text", "text": "Zoom in to see details", "size": 16 },
      "encoding": {
        "x": { "value": 0.5 },
        "y": { "value": 0.5 },
        "color": { "value": "#666666" }
      }
    },
    {
      "data": {
        "sequence": { "start": 0, "stop": 4000, "step": 16, "as": "x" }
      },
      "transform": [
        { "type": "formula", "expr": "sin(datum.x / 40)", "as": "y" },
        { "type": "formula", "expr": "datum.x + 16", "as": "x2" }
      ],
      "mark": "rect",
      "encoding": {
        "x2": { "field": "x2" }
      }
    },
    {
      "data": {
        "sequence": { "start": 0, "stop": 4000, "step": 1, "as": "x" }
      },
      "transform": [
        {
          "type": "formula",
          "expr": "sin(datum.x / 40) + (random() - 0.5) * 0.2",
          "as": "y"
        }
      ],
      "mark": "point",
      "encoding": {
        "opacity": { "value": 0.7 }
      }
    }
  ]
}

Schematic Two-Level Cross-Fade Example

This mirrors the layer cross-fading overview/detail example.

{
  "stops": [40000],
  "multiscale": [
    {
      "name": "Overview",
      "mark": "rect"
    },
    {
      "name": "Detail",
      "mark": "point"
    }
  ]
}

Properties

All other properties follow layer view semantics.

stops Required
Type: (number | ExprRef)[] | FadedMultiscaleStops | TransitionedMultiscaleStops

Stop definition that controls transitions between the multiscale levels.

- number[] is shorthand for { metric: "unitsPerPixel", values: ... } - (number | ExprRef)[] supports mixed constants and expressions - Object form allows configuring metric, channel, and fade.

Stop Object Forms

FadedMultiscaleStops

channel
Type: "x" | "y" | "auto"

Which positional channel controls the stop metric.

- "auto" averages x and y when both are available. - "x" uses only the x channel. - "y" uses only the y channel.

Default value: "auto"

fade
Type: number

Relative transition width around each stop.

For each stop value s, the fade transition is evaluated in the range:

- upper edge: s * (1 + fade) - lower edge: s * (1 - fade)

Default value: 0.5

values Required
Type: array

Stop values in descending order.

TransitionedMultiscaleStops

channel Required
Type: "x" | "y"

Positional channel that controls the stop metric.

transition Required
Type: ParamTransition

Cross-fades stages in time after a stop selects a new detail level. The selected stage settles fully visible and all other stages settle hidden. This differs from fade, which keeps adjacent stages partly visible within a zoom range.

Transitioned stops require channel to be either "x" or "y" and cannot be combined with fade.

values Required
Type: array

Stop values in descending order.

Array shorthand:

{
  "stops": [1, 0.1]
}

is equivalent to:

{
  "stops": {
    "metric": "unitsPerPixel",
    "values": [1, 0.1]
  }
}

Expression shorthands are also supported:

{
  "stops": [
    2000,
    { "expr": "windowSize / max(width, 1)" },
    { "expr": "0.2 * windowSize / max(width, 1)" }
  ]
}

unitsPerPixel means data-units per screen pixel. On genomic axes, this is typically base pairs per pixel.