Plotly.js Integration

MAIDR automatically detects and makes Plotly.js charts accessible — no configuration, no binder, no data attributes needed. Just add one script tag.

Quick Start

Add maidr.js alongside your Plotly.js script. That's it:

<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>My Plotly Chart</title>
    <!-- 1. Load Plotly.js -->
    <script src="https://cdn.plot.ly/plotly-2.35.2.min.js"></script>
    <!-- 2. Load MAIDR — auto-detects Plotly charts -->
    <script src="https://cdn.jsdelivr.net/npm/maidr/dist/maidr.js"></script>
  </head>
  <body>
    <div id="chart" style="width: 700px; height: 500px"></div>

    <script>
      // 3. Create your chart normally — MAIDR hooks in automatically
      Plotly.newPlot('chart', [{
        x: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri'],
        y: [20, 14, 23, 25, 22],
        type: 'bar'
      }], {
        title: { text: 'Tips by Day' },
        xaxis: { title: { text: 'Day' } },
        yaxis: { title: { text: 'Count' } }
      });
    </script>
  </body>
</html>

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

No changes to your Plotly code are required.

How It Works

MAIDR's Plotly adapter runs automatically on page load:

  1. Detection — scans the DOM for .js-plotly-plot elements (added by Plotly during newPlot())
  2. Extraction — reads Plotly's internal data (gd._fullData, gd._fullLayout, gd.calcdata) and produces MAIDR's accessibility schema
  3. Normalization — patches Plotly's SVG structure for compatibility (layout fixes, toolbar accessibility, click-to-focus)
  4. Activation — initializes MAIDR on the chart with full keyboard navigation, audio, text, and braille support

For dynamically-created charts (SPAs, notebooks), a MutationObserver watches for new Plotly divs and initializes them as they appear.

Supported Chart Types

Chart Type Plotly Trace Example
Bar type: 'bar' Bar chart
Scatter type: 'scatter', mode: 'markers' Scatter plot
Line type: 'scatter', mode: 'lines' Line chart
Step type: 'scatter', mode: 'lines', line: { shape: 'hv' | 'vh' | 'hvh' | 'vhv' } Step chart
Box Plot type: 'box' Box plot
Violin Plot type: 'violin' Violin plot
Heatmap type: 'heatmap' Heatmap
Histogram type: 'histogram' Histogram
Candlestick type: 'candlestick' Candlestick
Pie type: 'pie' Pie chart
Grouped Bar barmode: 'group' + multiple bar traces Grouped bar
Stacked Bar barmode: 'stack' + multiple bar traces Stacked bar
Subplots / Facets multiple xaxis/yaxis pairs, layout.grid, or Plotly Express facets Subplots

Notes on chart-type detection:

Code Examples

Bar Chart

<div id="bar-chart" style="width: 700px; height: 500px"></div>
<script>
  Plotly.newPlot('bar-chart', [{
    x: ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday'],
    y: [20, 14, 23, 25, 22],
    type: 'bar',
    marker: { color: '#2ca02c' }
  }], {
    title: { text: 'Number of Tips by Day' },
    xaxis: { title: { text: 'Day' } },
    yaxis: { title: { text: 'Count' } }
  });
</script>

Scatter Plot

<div id="scatter-chart" style="width: 700px; height: 500px"></div>
<script>
  Plotly.newPlot('scatter-chart', [{
    x: [5.1, 4.9, 4.7, 4.6, 5.0, 5.4, 4.6, 5.0],
    y: [1.4, 1.4, 1.3, 1.5, 1.4, 1.7, 1.4, 1.5],
    mode: 'markers',
    type: 'scatter',
    name: 'Setosa',
    marker: { size: 8 }
  }], {
    title: { text: 'Iris Sepal vs Petal Length' },
    xaxis: { title: { text: 'Sepal Length (cm)' } },
    yaxis: { title: { text: 'Petal Length (cm)' } }
  });
</script>

Multi-Line Chart

<div id="line-chart" style="width: 700px; height: 500px"></div>
<script>
  var trace1 = {
    x: [1, 2, 3, 4, 5, 6, 7],
    y: [10, 15, 13, 17, 22, 19, 25],
    mode: 'lines',
    type: 'scatter',
    name: 'Series A'
  };

  var trace2 = {
    x: [1, 2, 3, 4, 5, 6, 7],
    y: [16, 5, 11, 9, 14, 20, 12],
    mode: 'lines',
    type: 'scatter',
    name: 'Series B'
  };

  Plotly.newPlot('line-chart', [trace1, trace2], {
    title: { text: 'Weekly Sales Comparison' },
    xaxis: { title: { text: 'Week' } },
    yaxis: { title: { text: 'Sales ($K)' } }
  });
</script>

Box Plot

<div id="box-chart" style="width: 700px; height: 500px"></div>
<script>
  Plotly.newPlot('box-chart', [
    { y: [2.3, 2.5, 2.8, 3.0, 3.2, 3.4, 3.6, 4.0, 4.5], type: 'box', name: 'Setosa' },
    { y: [4.7, 4.9, 5.2, 5.5, 5.9, 6.0, 6.3, 6.5, 7.0], type: 'box', name: 'Versicolor' },
    { y: [6.0, 6.3, 6.5, 6.9, 7.1, 7.3, 7.5, 7.7, 8.0], type: 'box', name: 'Virginica' }
  ], {
    title: { text: 'Iris Sepal Length Distribution' },
    xaxis: { title: { text: 'Species' } },
    yaxis: { title: { text: 'Sepal Length (cm)' } }
  });
</script>

Violin Plot

A violin becomes two layers in one subplot: the quartile summary (violin_box) you land on, and the density curve (violin_kde) behind it. PageUp and PageDown switch between them. Both come from Plotly's own calculations, so the numbers announced are the ones the chart was drawn from.

<div id="violin-chart" style="width: 700px; height: 500px"></div>
<script>
  Plotly.newPlot('violin-chart', [
    { y: [2.3, 2.5, 2.8, 3.0, 3.2, 3.4, 3.6, 4.0, 4.5], type: 'violin', name: 'Setosa', box: { visible: true }, meanline: { visible: true } },
    { y: [4.7, 4.9, 5.2, 5.5, 5.9, 6.0, 6.3, 6.5, 7.0], type: 'violin', name: 'Versicolor', box: { visible: true }, meanline: { visible: true } }
  ], {
    title: { text: 'Iris Sepal Length Distribution' },
    xaxis: { title: { text: 'Species' } },
    yaxis: { title: { text: 'Sepal Length (cm)' } }
  });
</script>

The inner box (box: { visible: true }) and the mean line (meanline: { visible: true }) are optional: without them the statistics stay navigable, they simply have no drawn element to highlight, and the mean is left out of the sections.

Those two settings are per trace, while the sections a violin plot offers are one list for the whole plot. So when only some traces draw a mean line, every violin still has a mean to read — it is a statistic of each of them — and only the ones drawn with a mean line highlight it.

Heatmap

<div id="heatmap-chart" style="width: 700px; height: 500px"></div>
<script>
  Plotly.newPlot('heatmap-chart', [{
    z: [[1, 20, 30], [20, 1, 60], [30, 60, 1]],
    x: ['Monday', 'Tuesday', 'Wednesday'],
    y: ['Morning', 'Afternoon', 'Evening'],
    type: 'heatmap',
    colorscale: 'Viridis'
  }], {
    title: { text: 'Activity Heatmap' }
  });
</script>

Histogram

<div id="histogram-chart" style="width: 700px; height: 500px"></div>
<script>
  // Generate random data
  var x = [];
  for (var i = 0; i < 500; i++) {
    x.push(Math.random() + Math.random() + Math.random() - 1.5);
  }

  Plotly.newPlot('histogram-chart', [{
    x: x,
    type: 'histogram',
    xbins: { size: 0.5 }
  }], {
    title: { text: 'Distribution of Values' },
    xaxis: { title: { text: 'Value' } },
    yaxis: { title: { text: 'Count' } }
  });
</script>

Candlestick

<div id="candlestick-chart" style="width: 700px; height: 500px"></div>
<script>
  Plotly.newPlot('candlestick-chart', [{
    x: ['2024-01-02', '2024-01-03', '2024-01-04', '2024-01-05'],
    open:  [150.0, 152.5, 151.0, 153.0],
    high:  [153.0, 154.0, 154.5, 155.0],
    low:   [149.0, 151.0, 150.0, 152.0],
    close: [152.5, 151.0, 153.0, 154.5],
    type: 'candlestick'
  }], {
    title: { text: 'Stock Price' },
    xaxis: { title: { text: 'Date' } },
    yaxis: { title: { text: 'Price ($)' } }
  });
</script>

Grouped Bar Chart

<div id="grouped-bar" style="width: 700px; height: 500px"></div>
<script>
  Plotly.newPlot('grouped-bar', [
    { x: ['Q1', 'Q2', 'Q3', 'Q4'], y: [20, 14, 23, 25], name: 'Product A', type: 'bar' },
    { x: ['Q1', 'Q2', 'Q3', 'Q4'], y: [15, 18, 20, 22], name: 'Product B', type: 'bar' },
    { x: ['Q1', 'Q2', 'Q3', 'Q4'], y: [12, 16, 18, 20], name: 'Product C', type: 'bar' }
  ], {
    barmode: 'group',
    title: { text: 'Quarterly Sales by Product' },
    xaxis: { title: { text: 'Quarter' } },
    yaxis: { title: { text: 'Revenue ($M)' } }
  });
</script>

Stacked Bar Chart

<div id="stacked-bar" style="width: 700px; height: 500px"></div>
<script>
  Plotly.newPlot('stacked-bar', [
    { x: ['Q1', 'Q2', 'Q3', 'Q4'], y: [20, 14, 23, 25], name: 'Product A', type: 'bar' },
    { x: ['Q1', 'Q2', 'Q3', 'Q4'], y: [15, 18, 20, 22], name: 'Product B', type: 'bar' }
  ], {
    barmode: 'stack',
    title: { text: 'Quarterly Revenue Breakdown' },
    xaxis: { title: { text: 'Quarter' } },
    yaxis: { title: { text: 'Revenue ($M)' } }
  });
</script>

Pie Chart

<div id="pie-chart" style="width: 700px; height: 500px"></div>
<script>
  Plotly.newPlot('pie-chart', [{
    labels: ['Apples', 'Bananas', 'Cherries', 'Dates'],
    values: [30, 50, 20, 15],
    type: 'pie',
    sort: false
  }], {
    title: { text: 'Units Sold by Fruit' }
  });
</script>

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%". The share is derived from the values themselves, so there is nothing to author for it.

Subplots (2x2 Grid)

Figures with multiple panels — whether built with manual axis pairs, layout.grid, or Python's make_subplots — become a navigable 2D grid. MAIDR reads each panel's axis domains to recover the visual layout (including ragged grids), so arrow keys move between panels in reading order, Enter drills into a panel, and Escape returns to panel navigation. The selected panel is outlined visually.

<div id="subplot-chart" style="width: 900px; height: 600px"></div>
<script>
  Plotly.newPlot('subplot-chart', [
    { x: ['Mon', 'Tue'], y: [20, 14], type: 'bar', name: 'Tips' },
    { x: [1, 2, 3], y: [10, 15, 13], type: 'scatter', mode: 'lines+markers', name: 'Sales', xaxis: 'x2', yaxis: 'y2' },
    { x: [5.1, 4.9, 4.7], y: [1.4, 1.4, 1.3], type: 'scatter', mode: 'markers', name: 'Iris', xaxis: 'x3', yaxis: 'y3' },
    { x: [1.2, 1.9, 2.1, 2.4, 3.0], type: 'histogram', name: 'Distribution', xaxis: 'x4', yaxis: 'y4' }
  ], {
    title: { text: 'Four Views of the Data' },
    grid: { rows: 2, columns: 2, pattern: 'independent' }
  });
</script>

Each panel announces its trace name (e.g. "Subplot 1 of 4") while navigating; inset plots and overlaid dual-axis charts are kept as a flat panel list rather than forced into a grid.

Facets (Plotly Express style)

Faceted figures — shared matches: axes plus facet-label annotations, the pattern Plotly Express emits for facet_row/facet_col — are fully supported:

Both annotation shapes are recognized:

<div id="facet-chart" style="width: 900px; height: 450px"></div>
<script>
  Plotly.newPlot('facet-chart', [
    { x: [16.99, 10.34, 21.01], y: [1.01, 1.66, 3.5], type: 'scatter', mode: 'markers' },
    { x: [8.77, 26.88, 15.04], y: [2.0, 3.12, 1.96], type: 'scatter', mode: 'markers', xaxis: 'x2', yaxis: 'y2' }
  ], {
    xaxis: { domain: [0, 0.48], title: { text: 'Total Bill ($)' } },
    xaxis2: { domain: [0.52, 1], matches: 'x' },
    yaxis: { title: { text: 'Tip ($)' } },
    yaxis2: { matches: 'y', anchor: 'x2' },
    annotations: [
      { text: 'sex=Female', xref: 'x domain', yref: 'y domain', x: 0.5, y: 1.05, showarrow: false },
      { text: 'sex=Male', xref: 'x2 domain', yref: 'y2 domain', x: 0.5, y: 1.05, showarrow: false }
    ]
  });
</script>

Charts generated from Python (plotly.express facets, make_subplots) work the same way — the adapter reads the rendered figure, so no extra configuration is needed.

Dynamic Charts

MAIDR handles charts created after initial page load (common in SPAs and Jupyter notebooks). A MutationObserver watches for new .js-plotly-plot elements and initializes them automatically.

For charts that render asynchronously, MAIDR also listens for the plotly_afterplot event before processing.

The observer disconnects after 30 seconds to avoid unnecessary overhead.

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.

Integration Comparison

Feature Vanilla JS (CDN) React Component Plotly.js Adapter
Setup maidr-data attribute with JSON data prop on <Maidr> Just add <script> tag
Data source Manual JSON schema Manual JSON schema Auto-extracted from Plotly
SVG selectors Manual CSS selectors Manual CSS selectors Auto-generated
Configuration Required Required Zero configuration
Chart types All MAIDR types All MAIDR types 9 Plotly types
Dynamic charts Manual init React lifecycle Auto-detected

Python and R Binders

If you generate Plotly charts from Python or R, you may also be interested in:

API Documentation

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