Skip to content

View Concatenation

The vconcat and hconcat composition operators place views side-by-side either vertically or horizontally. The vconcat is practical for building genomic visualizations with multiple tracks. The concat operator with the columns property produces a wrapping grid layout.

The spacing (in pixels) between concatenated views can be adjusted using the spacing property (Default: 10).

Example

Vertical

Using vconcat for a vertical layout.

{
  "description": "Vertical concatenation example.",

  "data": { "url": "data/sincos.csv" },

  "spacing": 20,

  "vconcat": [
    {
      "mark": "point",
      "encoding": {
        "x": { "field": "x", "type": "quantitative" },
        "y": { "field": "sin", "type": "quantitative" }
      }
    },
    {
      "mark": "point",
      "encoding": {
        "x": { "field": "x", "type": "quantitative" },
        "y": { "field": "cos", "type": "quantitative" }
      }
    }
  ]
}

Horizontal

Using hconcat for a horizontal layout.

{
  "description": "Horizontal concatenation example.",

  "data": { "url": "data/sincos.csv" },

  "hconcat": [
    {
      "mark": "point",
      "encoding": {
        "x": { "field": "x", "type": "quantitative" },
        "y": { "field": "sin", "type": "quantitative" }
      }
    },
    {
      "mark": "point",
      "encoding": {
        "x": { "field": "x", "type": "quantitative" },
        "y": { "field": "cos", "type": "quantitative" }
      }
    }
  ]
}

Grid

Using concat and columns for a grid layout. For simplicity, the same visualization is used for all panels in the grid.

{
  "description": "Grid concatenation example.",

  "data": { "url": "data/sincos.csv" },

  "encoding": {
    "x": { "field": "x", "type": "quantitative" },
    "y": { "field": "sin", "type": "quantitative" }
  },

  "columns": 3,

  "concat": [
    { "mark": "point" },
    { "mark": "point" },
    { "mark": "point" },
    { "mark": "point" },
    { "mark": "point" },
    { "mark": "point" },
    { "mark": "point" },
    { "mark": "point" },
    { "mark": "point" }
  ]
}

Separators

You can draw separators between child views using the separator property. Separators are centered within the spacing gaps and do not affect layout. Use true to enable the defaults or provide a rule mark style object. Use includePlotMargin: false to keep the separators inside the plot area.

{
  "description": "Concatenation example with separators.",

  "data": { "url": "data/sincos.csv" },

  "encoding": {
    "x": { "field": "x", "type": "quantitative" },
    "y": { "field": "sin", "type": "quantitative" }
  },

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

  "spacing": 29,

  "separator": {
    "color": "#bbb",
    "strokeDash": [6, 4],
    "size": 1,
    "includePlotMargin": true
  },

  "vconcat": [{ "mark": "point" }, { "mark": "point" }, { "mark": "point" }]
}

Child sizing

The concatenation operators mimic the behavior of the CSS flexbox. The child views have an absolute minimum size (px) in pixels and an unitless grow value that specifies in what proportion the possible remaining space should be distributed. The remaining space depends on the parent view's size.

In the following example, the left view has a width of 20 px, the center view has a grow of 1, and the right view has a grow of 2. If you resize the web browser, you can observe that the width of the left view stays constant while the remaining space is distributed in proportions of 1:2.

{
  "description": "Concatenation child sizing example.",

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

  "spacing": 10,

  "hconcat": [
    {
      "width": { "px": 20 },
      "mark": "rect"
    },
    {
      "width": { "grow": 1 },
      "mark": "rect"
    },
    {
      "width": { "grow": 2 },
      "mark": "rect"
    }
  ]
}

SizeDef

grow
Type: number

Share of the remaining space. See child sizing for details.

maxPx
Type: number

Maximum size in pixels.

minPx
Type: number

Minimum size in pixels.

px
Type: number

Size in pixels

The size may have both absolute (px) and proportional (grow) components. When views are nested, both the absolute and proportional sizes are added up. Thus, the width of the above example is { "px": 40, "grow": 3 }. The spacing between the child views is added to the total absolute width.

Views' size properties (width and height) accept both SizeDef objects and shorthands. The SizeDef objects contain either or both of px and grow properties, and may constrain the final size with minPx and maxPx. Numbers are interpreted as absolute sizes, and "container" is the same as { grow: 1 }. A SizeDef with only minPx or maxPx also uses { grow: 1 }. Undefined sizes generally default to "container". Expression references may be used where the resolved value is a number or "container".

Concatenation operators can nested flexibly to build complex layouts as in the following example.

{
  "description": "Nested layout example using SizeDef sizing.",

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

  "hconcat": [
    { "mark": "rect" },
    {
      "vconcat": [{ "mark": "rect" }, { "mark": "rect" }]
    }
  ]
}

Step sizing

For a view with a discrete positional scale, width or height can specify a step size instead of a fixed total size. The view derives its size from the scale's domain and reserves the specified number of logical pixels for each discrete step. The step may also be an expression reference.

{
  "width": { "step": 20 }
}
for
Type: string

Selects which discrete scale the step describes when a positional scale has a nested offset scale. "offset" sizes each subgroup step; "position" sizes each primary category step.

Default value: "offset" when a discrete nested offset scale is present, otherwise "position".

step Required
Type: number | ExprRef

Step size in pixels.

Discrete offset channels can introduce a second scale whose steps determine the view size.

Scrollable viewports

Sometimes the concents of a view are so large that they do not fit into the available space. In such cases, the view can be made scrollable by setting an explicit size for the view using the viewportWidth and viewportHeight properties. They accept the same values as width and height properties except for the step size. Scrollable viewports are particularly useful for categorical data types ("ordinal" and "nominal") and respective scales and axes that do not support zooming and panning.

{
  "description": "Scrollable viewport example.",

  "height": { "step": 20 },

  "viewportHeight": "container",

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

  "data": {
    "sequence": { "start": 0, "stop": 31, "step": 1 }
  },

  "encoding": {
    "x": { "field": "data", "type": "quantitative" },
    "y": { "field": "data", "type": "ordinal" }
  },

  "mark": "point"
}

Collapsed overhang

Axes, titles, and legends normally reserve space outside the plot area. A view can disable reservation on selected edges with the "overhang" property:

{
  "overhang": { "top": false }
}

The element remains visible, but may overlap adjacent content. This is useful for dense composed layouts where the author controls the overlap. Missing edges retain the default reservation behavior. Explicit "padding" remains reserved and can be used to add a deliberate safety margin.

Render order

Concat children render in declaration order by default. Set zindex on a child to override its render order without changing its layout position. Higher values render later, and children with equal values retain declaration order.

This is useful when unclipped marks extend into an adjacent track. For example, a middle protein track can render after lollipop tracks above and below it:

{
  "vconcat": [
    { "name": "upper-lollipops", "mark": "rule" },
    { "name": "protein", "zindex": 1, "mark": "rect" },
    { "name": "lower-lollipops", "mark": "rule" }
  ]
}

Axes, titles, backgrounds, and other view decorations have their own zindex settings relative to composition content.

Track annotations

vconcat and hconcat can draw annotations using ordinary marks and layers across their visible tracks with the annotate array. Annotations require an aligned scale shared across all visible tracks: x for vconcat, y for hconcat (the default in both cases). Annotation layers cannot define their own positional scales or request an independent or excluded shared scale.

Annotation positional fields do not expand the shared track domain. Other encodings, such as color, remain ordinary data-driven encodings. Annotations span the gaps without reserving layout space. They are clipped to the bounding area of the visible track plots, excluding outer axes and titles, and render after the tracks, so they remain in front of track marks even when a track has a higher zindex.

Annotation legends appear around the whole concat using its usual legend regions. Non-positional scales, such as color and size, are shared among annotation entries and remain independent of track scales by default. The concat's resolve.scale.color: "shared" makes tracks and annotations use the same color scale and, by default, one legend.

Sharing is per channel, even when annotations encode different fields. An annotation's resolve.scale.color: "excluded" isolates its color scale from siblings and tracks. An explicit layer entry with resolve.scale.color: "independent" gives its children separate color scales. Legend resolution follows the scale unless configured otherwise.

The example below uses translucent rectangles to mark regions across two tracks:

{
  "description": "Annotations spanning vertically concatenated tracks.",

  "spacing": 16,

  "annotate": [
    {
      "data": {
        "values": [
          { "start": 2, "end": 4, "label": "region A" },
          { "start": 6, "end": 8, "label": "region B" }
        ]
      },
      "mark": { "type": "rect", "fillOpacity": 0.2 },
      "encoding": {
        "x": { "field": "start", "type": "quantitative" },
        "x2": { "field": "end" },
        "color": { "field": "label", "type": "nominal" }
      }
    }
  ],

  "vconcat": [
    {
      "data": {
        "values": [
          { "x": 1, "value": 2 },
          { "x": 3, "value": 5 },
          { "x": 5, "value": 3 },
          { "x": 7, "value": 6 },
          { "x": 9, "value": 4 }
        ]
      },
      "mark": "point",
      "encoding": {
        "x": { "field": "x", "type": "quantitative" },
        "y": { "field": "value", "type": "quantitative" }
      }
    },

    {
      "data": {
        "values": [
          { "x": 1, "value": 8 },
          { "x": 3, "value": 6 },
          { "x": 5, "value": 7 },
          { "x": 7, "value": 3 },
          { "x": 9, "value": 5 }
        ]
      },
      "mark": "point",
      "encoding": {
        "x": { "field": "x", "type": "quantitative" },
        "y": { "field": "value", "type": "quantitative" }
      }
    }
  ]
}

Resolve

By default, all channels have "independent" scales and axes. However, because track-based layouts that resemble genome browsers are such a common use case, vconcat defaults to "shared" resolution for x channel and hconcat defaults to "shared" resolution for y channel.

Shared axes

Concatenation operators support shared axes on channels that also have shared scales. Axis domain line, ticks, and labels are drawn only once for each row or column. Grid lines are drawn for all participating views.

{
  "description": "Concatenation example with shared axes.",

  "data": { "url": "data/sincos.csv" },

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

  "spacing": 20,

  "encoding": {
    "x": { "field": "x", "type": "quantitative" },
    "y": { "field": "sin", "type": "quantitative" }
  },

  "columns": 2,

  "concat": [
    { "mark": "point" },
    { "mark": "point" },
    { "mark": "point" },
    { "mark": "point" }
  ],

  "config": {
    "view": { "stroke": "lightgray" },
    "axisQuantitative": { "grid": true }
  }
}