Last updated

Vega-Lite Integration

MAIDR provides an adapter for Vega-Lite that converts your charts into accessible, navigable visualizations with audio sonification, text descriptions, and braille output.

Quick Start

Load Vega, Vega-Lite, vega-embed, MAIDR core, and the Vega-Lite adapter, then call maidrVegaLite.embed() with your spec — it runs vegaEmbed internally and attaches MAIDR once the chart has rendered:

<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>My Vega-Lite Chart</title>
    <!-- 1. Load Vega-Lite and its dependencies -->
    <script src="https://cdn.jsdelivr.net/npm/vega@5"></script>
    <script src="https://cdn.jsdelivr.net/npm/vega-lite@5"></script>
    <script src="https://cdn.jsdelivr.net/npm/vega-embed@6"></script>
    <!-- 2. Load MAIDR core and the Vega-Lite adapter -->
    <script src="https://cdn.jsdelivr.net/npm/maidr/dist/maidr.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/maidr/dist/vegalite.js"></script>
  </head>
  <body>
    <div id="chart" aria-label="Bar chart loading" style="min-height: 400px"></div>

    <script>
      const spec = {
        $schema: 'https://vega.github.io/schema/vega-lite/v5.json',
        width: 600,
        height: 400,
        title: 'Number of Tips by Day',
        data: {
          values: [
            { day: 'Sat', count: 87 },
            { day: 'Sun', count: 76 },
            { day: 'Thu', count: 62 },
            { day: 'Fri', count: 19 },
          ],
        },
        mark: 'bar',
        encoding: {
          x: { field: 'day', type: 'nominal', title: 'Day' },
          y: { field: 'count', type: 'quantitative', title: 'Count' },
        },
      };

      // 3. Render + bind MAIDR in a single call.
      maidrVegaLite.embed('#chart', spec, { id: 'tips-bar' });
    </script>
  </body>
</html>

embed() returns a Promise<{ view }>await it (or use .then() / .catch()) when you need the underlying Vega view or want to handle render failures.

If you already drive vegaEmbed yourself (e.g. you need the view for additional logic before MAIDR mounts), use bindVegaLite instead. You must await result.view.runAsync() between embedding and binding so the SVG is in the DOM by the time MAIDR mounts:

// renderer must be 'svg' — MAIDR navigates the SVG output, not canvas.
const result = await vegaEmbed('#chart', spec, { renderer: 'svg', actions: false });

// vegaEmbed() resolves when the view is *constructed*, not when it has *rendered*.
// runAsync() resolves only after the first paint — guarantees the SVG exists.
await result.view.runAsync();

maidrVegaLite.bindVegaLite(result.view, spec, { id: 'tips-bar' });

Once the page loads, click on the chart (or Tab to it) and MAIDR activates with:

How It Works

The Vega-Lite adapter:

  1. Inspects the spec — reads the mark, encoding, and any composite blocks (layer, hconcat, vconcat, concat, facet, repeat).
  2. Resolves the data — queries the compiled Vega View for runtime datasets (so transforms such as bin and aggregate are honoured), and falls back to inline data.values when the view isn't available.
  3. Maps the mark to a MAIDR trace type — see the table below.
  4. Builds CSS selectors for visual highlighting against the SVG that Vega renders inside the embed container.
  5. Mounts the MAIDR UI on the rendered SVG.

Because Vega-Lite renders asynchronously through vegaEmbed(), the adapter must be called from inside the vegaEmbed(...).then(...) callback (or after await vegaEmbed(...)), once the SVG and the compiled view are both available.

Supported Chart Types

Vega-Lite mark Encoding hint MAIDR trace type Example
bar (default) Bar vegalite-bindbar.html
bar bin: true on x or y Histogram vegalite-bindhistogram.html
bar color/fill field, default stack Stacked bar vegalite-bindstacked.html
bar color/fill, stack: null or false Dodged (grouped) bar vegalite-binddodged.html
bar color/fill, stack: 'normalize' Normalized stacked bar vegalite-bindnormalized.html
bar stacked, with each series wholly one side of the baseline Diverging bar (pyramid, Likert) vegalite-diverging.html
line, trail, area Line vegalite-bindline.html
line, trail, area interpolate: 'step', 'step-before', 'step-after' Step vegalite-bindline.html
line, trail a window rank/dense_rank whose output column is on y Bump vegalite-bump.html
line, trail a fold transform with detail splitting the polylines Parallel coordinates vegalite-parallel.html
line, trail a regression or loess transform Smooth
area a row facet over a density transform grouped by the facet field Ridgeline vegalite-ridgeline.html
bar x + x2 (or y + y2) fields, other axis nominal/ordinal Gantt (ranged bar) vegalite-gantt.html
bar the same, plus a window sum building a running total Waterfall (either orientation)
point, circle, square, tick Scatter vegalite-bindscatter.html
point, circle, square, tick one positional channel nominal/ordinal Dot plot (vertical & horizontal)
rect Heatmap vegalite-bindheatmap.html
boxplot Box plot (vertical & horizontal) vegalite-bindbox.html
errorbar, errorband Error bar vegalite-errorbar.html
arc theta encoding Pie (mark.innerRadius makes it a doughnut) vegalite-pie.html
arc radius bound to a field Polar area (coxcomb, rose)
geoshape a color or fill field, or a declared value Choropleth map vegalite-choropleth.html
rule + point layers shared category and value channels Lollipop
text text channel, both positional channels continuous Scatter, each point carrying its name
text in a layer:, x/y fields matching a sibling layer's absorbed — the names go to the layer it labels
rule + point layers, or line + point where the line has a detail naming the category two values per category, told apart by color Dumbbell vegalite-dumbbell.html

A trail is read as the line it is: Vega-Lite describes it as a line whose width can vary, and the two compile to the same one-path-per-series geometry over the same channels, so every line reading above applies to a trail unchanged. The varying width is the one thing lost — a line point carries a coordinate pair and no thickness — which is a second encoding of something a reader already reaches by navigating.

An arc mark is only a pie when it has a theta encoding: theta is the channel carrying the slice magnitudes, and an arc without one has no values to sonify, announce, or take percentages of. Such a spec is left unbound rather than announced as a pie whose numbers MAIDR would have to invent. An arc whose radius reads a data field is a polar area instead, where the wedge's length is the magnitude and its angle only says which category it is.

Two of these charts have no mark of their own in Vega-Lite and are authored as a pair of layers: a lollipop is a rule from the baseline plus its dots, and a dumbbell is a connector plus two dots per category (Vega-Lite's own "Ranged Dot Plot"). MAIDR collapses each pair into one trace, so the stem's magnitude and the gap between a pair are announced rather than dropped. The two ends of a dumbbell are named from the colour field — "1995" and "2000" rather than "start" and "end" — since those names are the whole content of the comparison. A line connector has to say what it joins, by naming the category in detail the way the Ranged Dot Plot does: a line under a dot layer without one is the ordinary line-with-markers idiom, and a two-series version of it would otherwise be read as a comparison of its two series.

Five more charts have no mark of their own either and are recognised from the transform that built them. A line is a bump chart only when the ranked column is the one actually plotted on y — the same window rank is also how a spec picks a top-n before drawing the underlying magnitude, which is an ordinary line. A folded line split by detail is parallel coordinates, and the fold's raw value column is handed to the trace rather than any pre-normalised one, so each axis keeps its own units. A row facet of density curves is a ridgeline, whose panel order is read from the compiled view rather than the facet domain, because Vega sorts that domain before laying the cells out. A diverging bar is decided per series — every series wholly one side of the baseline, and both sides used — which recognises a pyramid and a Likert scale while leaving an ordinary stacked bar that merely dips negative alone. And a line or trail over a regression or loess transform is a smooth, because those two transforms replace the rows with a fit: the points on the page are a model evaluated at a grid rather than observations, which is what a smooth means and what every other producer already calls it. A density transform also replaces the rows and is deliberately left alone, since the author picks line or area for it and both are ordinary readings of a curve.

A text mark is read two ways, and which one applies is decided by what else is in the spec rather than by an option. On its own it is a labelled scatter — the countries-against-GDP figure — and both positional channels have to be continuous for that: a text on a categorical axis is a value written over a bar or into a heatmap cell, and the mark beside it already announces that number, so reading it as well would announce every count twice. Inside a layer:, a text layer whose x and y read the same fields as a sibling layer is that layer's labels: it is absorbed rather than read, and its text channel is handed to the layer it labels, which keeps its own trace type. The match is on the fields, not on the order of the layers or where the glyphs land. Only a scatter ends up carrying the names, since a scatter point is the one thing in the grammar that holds one — labels written over a bar or a line are handed over and ignored, leaving those layers' payloads unchanged. A text mark with no text channel has nothing to say either way: standing alone it is left unread, and over another layer it is still absorbed but adds no names.

A geoshape mark is the one mark that names itself: it draws geography and nothing else compiles to it, so a map needs no annotation to be recognised. It is only a choropleth once a value shades it, which is why a color or fill field is required — a geoshape with neither is the outline layer such a map is usually drawn on top of, carries no data, and is left unread rather than announced as a chart with no values.

A ranged bar — gantt or waterfall — needs the axis opposite its two bounds typed in the spec as nominal or ordinal. A type Vega-Lite infers from the data is written down nowhere MAIDR can read it, and without one the bar reads as an ordinary bar whose magnitude is its lower bound alone, so MAIDR warns to the console when it sees a two-bound bar with an untyped category axis.

Highlighting is withheld, rather than pointed at the wrong element, in two cases: an errorband draws every sample into a single <path>, and a gantt whose lanes are interleaved in the data cannot be sliced back apart in DOM order. Both still sonify and announce normally.

Code Examples

Bar chart

const spec = {
  $schema: 'https://vega.github.io/schema/vega-lite/v5.json',
  width: 600,
  height: 400,
  title: 'Number of Tips by Day',
  data: {
    values: [
      { day: 'Sat', count: 87 },
      { day: 'Sun', count: 76 },
      { day: 'Thu', count: 62 },
      { day: 'Fri', count: 19 },
    ],
  },
  mark: 'bar',
  encoding: {
    x: { field: 'day', type: 'nominal', title: 'Day' },
    y: { field: 'count', type: 'quantitative', title: 'Count' },
  },
};

maidrVegaLite.embed('#chart', spec, { id: 'tips-bar' });

See examples/vegalite-bindbar.html for a runnable version.

Stacked bar chart

const spec = {
  $schema: 'https://vega.github.io/schema/vega-lite/v5.json',
  width: 600,
  height: 400,
  title: 'Passengers by Class and Survival',
  data: {
    values: [
      { class: 'First',  survived: 'No',  count: 80 },
      { class: 'First',  survived: 'Yes', count: 136 },
      { class: 'Second', survived: 'No',  count: 97 },
      { class: 'Second', survived: 'Yes', count: 87 },
      { class: 'Third',  survived: 'No',  count: 372 },
      { class: 'Third',  survived: 'Yes', count: 119 },
    ],
  },
  mark: 'bar',
  encoding: {
    x: { field: 'class', type: 'nominal', title: 'Class' },
    y: { field: 'count', type: 'quantitative', title: 'Passengers' },
    color: { field: 'survived', type: 'nominal', title: 'Survived' },
  },
};

See examples/vegalite-bindstacked.html for a runnable version.

Dodged (grouped) bar chart

Set stack: null (or false) on the quantitative axis to switch from stacked to dodged.

const spec = {
  $schema: 'https://vega.github.io/schema/vega-lite/v5.json',
  width: 600,
  height: 400,
  title: 'City Populations (thousands)',
  data: {
    values: [
      { city: 'NYC', year: '2020', pop: 8336 },
      { city: 'NYC', year: '2025', pop: 8258 },
      { city: 'LA',  year: '2020', pop: 3979 },
      { city: 'LA',  year: '2025', pop: 3898 },
      { city: 'CHI', year: '2020', pop: 2693 },
      { city: 'CHI', year: '2025', pop: 2665 },
    ],
  },
  mark: 'bar',
  encoding: {
    x: { field: 'city', type: 'nominal', title: 'City' },
    y: { field: 'pop',  type: 'quantitative', stack: null, title: 'Population' },
    color: { field: 'year', type: 'nominal', title: 'Year' },
  },
};

See examples/vegalite-binddodged.html for a runnable version.

Normalized (100%) stacked bar chart

const spec = {
  $schema: 'https://vega.github.io/schema/vega-lite/v5.json',
  width: 600,
  height: 400,
  title: 'Survival Share by Class',
  data: {
    values: [
      { class: 'First',  survived: 'No',  count: 80 },
      { class: 'First',  survived: 'Yes', count: 136 },
      { class: 'Second', survived: 'No',  count: 97 },
      { class: 'Second', survived: 'Yes', count: 87 },
      { class: 'Third',  survived: 'No',  count: 372 },
      { class: 'Third',  survived: 'Yes', count: 119 },
    ],
  },
  mark: 'bar',
  encoding: {
    x: { field: 'class', type: 'nominal', title: 'Class' },
    y: { field: 'count', type: 'quantitative', stack: 'normalize', title: 'Share' },
    color: { field: 'survived', type: 'nominal', title: 'Survived' },
  },
};

See examples/vegalite-bindnormalized.html for a runnable version.

Histogram

const spec = {
  $schema: 'https://vega.github.io/schema/vega-lite/v5.json',
  width: 600,
  height: 400,
  title: 'Distribution of Values',
  data: {
    values: Array.from({ length: 100 }, () => ({
      value: Math.round(Math.random() * 100),
    })),
  },
  mark: 'bar',
  encoding: {
    x: { field: 'value', type: 'quantitative', bin: true, title: 'Value' },
    y: { aggregate: 'count', title: 'Frequency' },
  },
};

Put the bin on y and the count on x and the same distribution is drawn on its side; MAIDR reads it as a horizontal histogram, announcing the bin range against the axis the bins run along.

See examples/vegalite-bindhistogram.html for a runnable version.

Line chart

const spec = {
  $schema: 'https://vega.github.io/schema/vega-lite/v5.json',
  width: 600,
  height: 400,
  title: 'Monthly Average Temperature',
  data: {
    values: [
      { month: 'Jan', temp: 28 }, { month: 'Feb', temp: 32 },
      { month: 'Mar', temp: 45 }, { month: 'Apr', temp: 55 },
      { month: 'May', temp: 68 }, { month: 'Jun', temp: 78 },
      { month: 'Jul', temp: 82 }, { month: 'Aug', temp: 80 },
      { month: 'Sep', temp: 72 }, { month: 'Oct', temp: 58 },
      { month: 'Nov', temp: 42 }, { month: 'Dec', temp: 30 },
    ],
  },
  mark: 'line',
  encoding: {
    x: { field: 'month', type: 'ordinal',     title: 'Month' },
    y: { field: 'temp',  type: 'quantitative', title: 'Temperature (F)' },
  },
};

See examples/vegalite-bindline.html for a runnable version.

Scatter plot

const spec = {
  $schema: 'https://vega.github.io/schema/vega-lite/v5.json',
  width: 600,
  height: 400,
  title: 'Horsepower vs. Miles per Gallon',
  data: {
    values: [
      { hp: 130, mpg: 18 }, { hp: 165, mpg: 15 }, { hp: 150, mpg: 18 },
      { hp: 150, mpg: 16 }, { hp: 140, mpg: 17 }, { hp: 198, mpg: 15 },
      { hp: 220, mpg: 14 }, { hp: 215, mpg: 14 }, { hp: 225, mpg: 14 },
      { hp: 190, mpg: 15 }, { hp:  97, mpg: 24 }, { hp:  88, mpg: 22 },
      { hp:  70, mpg: 18 }, { hp:  76, mpg: 21 }, { hp:  86, mpg: 27 },
    ],
  },
  mark: 'point',
  encoding: {
    x: { field: 'hp',  type: 'quantitative', title: 'Horsepower' },
    y: { field: 'mpg', type: 'quantitative', title: 'Miles per Gallon' },
  },
};

See examples/vegalite-bindscatter.html for a runnable version.

Heatmap

const spec = {
  $schema: 'https://vega.github.io/schema/vega-lite/v5.json',
  width: 600,
  height: 400,
  title: 'Weekly Activity Heatmap',
  data: {
    values: [
      { day: 'Mon', hour: 'Morning',   value: 5 },
      { day: 'Mon', hour: 'Afternoon', value: 8 },
      { day: 'Mon', hour: 'Evening',   value: 3 },
      { day: 'Tue', hour: 'Morning',   value: 7 },
      { day: 'Tue', hour: 'Afternoon', value: 6 },
      { day: 'Tue', hour: 'Evening',   value: 4 },
      { day: 'Wed', hour: 'Morning',   value: 9 },
      { day: 'Wed', hour: 'Afternoon', value: 5 },
      { day: 'Wed', hour: 'Evening',   value: 7 },
    ],
  },
  mark: 'rect',
  encoding: {
    x: { field: 'day',   type: 'ordinal', title: 'Day' },
    y: { field: 'hour',  type: 'ordinal', title: 'Time of Day' },
    color: { field: 'value', type: 'quantitative', title: 'Activity Level' },
  },
};

See examples/vegalite-bindheatmap.html for a runnable version.

Box plot

Vega-Lite's boxplot is a compound mark — it computes the five-number summary (min, Q1, median, Q3, max) plus outliers internally from raw rows. MAIDR reads that summary back from the rendered SVG so you can navigate each box and its outliers with arrow keys.

const spec = {
  $schema: 'https://vega.github.io/schema/vega-lite/v5.json',
  width: 600,
  height: 400,
  title: 'Score Distribution by Group',
  data: {
    values: [
      // Group A: tight bulk 60–80 + 2 lower & 2 upper outliers
      ...Array.from({ length: 28 }, () => ({
        group: 'A',
        score: 60 + Math.round(Math.random() * 20),
      })),
      { group: 'A', score: 30 },
      { group: 'A', score: 35 },
      { group: 'A', score: 95 },
      { group: 'A', score: 98 },
      // Group B: tight bulk 65–75 + 1 lower & 2 upper outliers
      ...Array.from({ length: 28 }, () => ({
        group: 'B',
        score: 65 + Math.round(Math.random() * 10),
      })),
      { group: 'B', score: 40 },
      { group: 'B', score: 92 },
      { group: 'B', score: 96 },
      // Group C: bulk 50–70 + 2 lower & 1 upper outlier
      ...Array.from({ length: 28 }, () => ({
        group: 'C',
        score: 50 + Math.round(Math.random() * 20),
      })),
      { group: 'C', score: 20 },
      { group: 'C', score: 25 },
      { group: 'C', score: 100 },
    ],
  },
  mark: 'boxplot',
  encoding: {
    x: { field: 'group', type: 'nominal',      title: 'Group' },
    y: { field: 'score', type: 'quantitative', title: 'Score' },
  },
};

Both vertical (categorical x, quantitative y) and horizontal (categorical y, quantitative x) box plots are supported. See examples/vegalite-bindbox.html for a runnable version.

Pie chart

const spec = {
  $schema: 'https://vega.github.io/schema/vega-lite/v5.json',
  width: 400,
  height: 400,
  title: 'Units Sold by Fruit',
  data: {
    values: [
      { fruit: 'Apples', units: 30 },
      { fruit: 'Bananas', units: 50 },
      { fruit: 'Cherries', units: 20 },
      { fruit: 'Dates', units: 15 },
    ],
  },
  mark: 'arc',
  encoding: {
    theta: { field: 'units', type: 'quantitative', title: 'Units' },
    color: { field: 'fruit', type: 'nominal', title: 'Fruit' },
  },
};

maidrVegaLite.embed('#chart', spec, { id: 'fruit-pie' });

An arc has no x or y to name its axes after, so the slice labels come from the color (or fill) channel and the magnitudes from theta, and the layer's axis labels are taken from those two channels' titles. Left and Right move between slices; Up and Down are out of bounds, since a pie is a single row. Each slice announces its label, its value, and its share of the whole — "Apples, 30, 26.1%". Adding mark: { type: 'arc', innerRadius: 60 } makes it a doughnut, which reads identically.

See examples/vegalite-pie.html for a runnable version.

Choropleth map

const states = [
  { id: 53, state: 'Washington', rate: 4.9, lon: -120.4, lat: 47.4 },
  { id: 41, state: 'Oregon', rate: 4.6, lon: -120.6, lat: 43.9 },
  { id: 6, state: 'California', rate: 5.3, lon: -119.7, lat: 37.2 },
  { id: 32, state: 'Nevada', rate: 5.8, lon: -116.6, lat: 39.3 },
];

const spec = {
  $schema: 'https://vega.github.io/schema/vega-lite/v5.json',
  width: 640,
  height: 400,
  title: 'Unemployment rate, western states',
  data: {
    url: 'https://cdn.jsdelivr.net/npm/vega-datasets@2/data/us-10m.json',
    format: { type: 'topojson', feature: 'states' },
  },
  transform: [
    {
      lookup: 'id',
      from: { data: { values: states }, key: 'id', fields: ['state', 'rate', 'lon', 'lat'] },
    },
    { filter: 'isValid(datum.rate)' },
  ],
  projection: { type: 'albersUsa' },
  mark: 'geoshape',
  encoding: {
    color: { field: 'rate', type: 'quantitative', title: 'Unemployment rate (%)' },
  },
  usermeta: {
    maidr: { type: 'choropleth', region: 'state' },
  },
};

maidrVegaLite.embed('#chart', spec, { id: 'states-choropleth' });

A map has no x or y channel, so MAIDR names its two axes after the two things it does carry: what a region is called, and what shades it. The region name is read from the lookup transform's join key — the one field present on every drawn feature and distinct between them, including a nested one such as properties.name. Failing that, from the first tooltip entry that is not the shaded value itself. A map that names its regions in neither place is left unread, because numbering them 0, 1, 2 would announce a position in the file as if it were a place.

Geometry the join left unmatched carries no value. Those regions are dropped rather than shaded zero — a region announced as 0 is a claim the map does not make, and on a rate it is the lowest value on the scale. Once regions are dropped they no longer line up one-to-one with the drawn shapes, so the layer loses its highlighting; a filter on the joined column, as above, keeps the two aligned.

Declaring what a map means

usermeta is Vega-Lite's own slot for third-party metadata, and usermeta.maidr is where a spec says what MAIDR cannot read off the drawing. On a choropleth it carries four optional fields, each named exactly like the value it fills:

field what it names default
region the column holding each region's name the lookup join key, then a tooltip field
value the column holding the value shading it the color / fill field
lon centroid longitude, degrees east a lon, longitude or long column
lat centroid latitude, degrees north a lat or latitude column

region is worth writing whenever the join key is a code rather than a name: the map above joins on the numeric FIPS id, so without it the reader is told "6" rather than "California".

lon and lat are what buy back everything spatial. With them the arrow keys move across the map — up is north, down is south, left is west, right is east — and the description names where the high values sit and which way the gradient runs. Without them the map is read as a region list in declared order, which is a poorer reading but the one the data supports. MAIDR does not invert the projection to synthesise the pair: degrees are the only accepted form, anything else is left out, and a coordinate that is not one is dropped rather than coerced — a wrong compass direction is worse than no compass at all.

A declaration outranks every heuristic, but only where the marks can back it: a maidr block on a spec whose mark is not a geoshape is reported to the console and the chart is read as what was actually drawn. The block belongs on the spec node that becomes one layer — a single-view spec, a layer[i] child, a concat or facet leaf. One written on a composite parent — a layer, concat, facet or repeat node — names none of the layers below it and is ignored. title and name are accepted on any layer and override what it is announced as.

Field names given explicitly are used verbatim, with no fallback: a name that no row carries is a typo worth reporting, and MAIDR says so to the console rather than quietly substituting a column you did not ask for.

See examples/vegalite-choropleth.html for a runnable version.

Multi-panel charts (facet, repeat, concat)

Vega-Lite's view-composition operators all convert to a multi-subplot MAIDR figure — one accessible panel per chart cell. Multi-panel figures start in subplot navigation: arrow keys move between panels (each announces its facet value or repeated field), Enter drills into a panel's data points, Escape returns to panel navigation, and PageUp/PageDown switch layers inside a panel.

Supported compositions:

Composition Spec shape Panel grid
Row/column facet facet: { row?, column? } + spec facet values → grid rows × columns (missing row×column combinations are skipped, matching the rendered chart)
Facet shorthand row / column channels inside encoding same as the facet operator
Wrapped facet facet: { field } + columns: N values wrap into rows of N panels
Repeat repeat: { row?, column? } or repeat: [...] + columns one panel per repeated field (the child's { repeat: ... } field references are substituted per panel)
Concatenation hconcat / vconcat / concat + columns one panel per child spec; concat wraps into rows of columns panels
const spec = {
  $schema: 'https://vega.github.io/schema/vega-lite/v5.json',
  title: 'Barley Yield by Site',
  data: { values: barleyYields },
  facet: { column: { field: 'site', type: 'nominal' } },
  spec: {
    mark: 'bar',
    encoding: {
      x: { field: 'variety', type: 'nominal' },
      y: { field: 'yield', type: 'quantitative' },
    },
  },
};

maidrVegaLite.embed('#chart', spec, { id: 'barley-facet' });

Each panel announces its facet value (e.g. "site: Crookston") or repeated field name as its title, and highlighting is scoped per panel — the adapter stamps each rendered facet cell with a data-maidr-cell attribute at bind time so selectors only ever match their own panel's marks. All selectors are additionally prefixed with the chart container's id (one is generated if the container has none), so several charts on one page — even two copies of the same spec — never highlight each other's elements.

Notes and current limitations:

See examples/vegalite-facet-bar.html (facet operator, wrapped facet, shorthand, and repeat) and examples/vegalite-hconcat-box.html (concatenation) for runnable versions.

API Reference

embed(target, spec, options?)

Renders the Vega-Lite spec and mounts MAIDR in a single call. This is the recommended entry point for most integrations — it wraps vegaEmbed() and bindVegaLite() together and handles the asynchronous render internally.

Argument Type Description
target string | HTMLElement A CSS selector or HTMLElement that hosts the chart.
spec VegaLiteSpec The Vega-Lite specification.
options.id string (optional) Unique ID for the MAIDR instance.
options.title string (optional) Override for the chart title used in announcements.
options.embedOptions EmbedOptions (optional) Forwarded to vegaEmbed. Default: { actions: false }.

Returns a Promise<{ view: vega.View }> so callers can await the underlying Vega view or chain .catch() to handle render failures.

maidrVegaLite.embed('#chart', spec, { id: 'tips-bar' })
  .then(({ view }) => console.log('Chart ready', view))
  .catch((err) => console.error(err));

Requires the global vegaEmbed function (from the vega-embed script) to be loaded on the page. If vegaEmbed is not available, embed() throws a clear error.

bindVegaLite(view, spec, options?)

Lower-level entry point that mounts MAIDR on the SVG produced by an existing vegaEmbed() call. Use this when you need full control over the embed lifecycle (custom vegaEmbed options, post-processing, etc.).

Argument Type Description
view vega.View The compiled Vega view returned by vegaEmbed(...).view.
spec VegaLiteSpec The original Vega-Lite specification.
options.id string (optional) Unique ID for the MAIDR instance. Defaults to the view container's id, or a timestamp fallback.
options.title string (optional) Override for the chart title used in announcements.

Callers must await view.runAsync() before calling bindVegaLite(). vegaEmbed(...) resolves when the view is constructed, not when it has rendered — calling bindVegaLite() immediately after vegaEmbed() resolves can race the first paint and produce a "No SVG found" error on slow or aggregated specs (histograms, complex transforms). The view must also be created with renderer: 'svg'; MAIDR cannot navigate canvas output. Prefer embed() which performs both steps for you.

vegaLiteToMaidr(spec, view?, options?)

Lower-level converter that returns a MAIDR schema object without mounting the UI. Useful when you want to set the maidr attribute manually or post-process the schema.

import { vegaLiteToMaidr } from 'maidr/vegalite';

const maidr = vegaLiteToMaidr(spec, view, { id: 'my-chart' });
container.setAttribute('maidr', JSON.stringify(maidr));

Using with npm / bundlers

import { embed } from 'maidr/vegalite';

await embed('#chart', spec, { id: 'my-chart' });

Or, for fine-grained control:

import { bindVegaLite } from 'maidr/vegalite';
import vegaEmbed from 'vega-embed';

const result = await vegaEmbed('#chart', spec, { renderer: 'svg' });
await result.view.runAsync(); // wait for the first paint
bindVegaLite(result.view, spec, { id: 'my-chart' });

The package exposes both ES (vegalite.mjs) and UMD (vegalite.js) builds plus TypeScript declarations.

Keyboard Controls

Once a chart is focused, use standard MAIDR keyboard shortcuts:

Function Key (Windows) Key (Mac)
Move between data points Arrow keys Arrow keys
Go to extremes Ctrl + Arrow Cmd + Arrow
Toggle Sonification S S
Toggle Braille Mode B B
Toggle Text Mode T T
Toggle Review Mode R R
Auto-play Ctrl + Shift + Arrow Cmd + Shift + Arrow
Stop Auto-play Ctrl Cmd

For the full list, see the Keyboard Controls reference.

API Documentation

For the complete TypeScript API reference, see the API Documentation.