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:
- Audio sonification — tones representing data values
- Text descriptions — spoken via screen readers
- Braille output — refreshable braille display support
- Keyboard navigation — arrow keys to move between data points
No changes to your Plotly code are required.
How It Works
MAIDR's Plotly adapter runs automatically on page load:
- Detection — scans the DOM for
.js-plotly-plotelements (added by Plotly duringnewPlot()) - Extraction — reads Plotly's internal data (
gd._fullData,gd._fullLayout,gd.calcdata) and produces MAIDR's accessibility schema - Normalization — patches Plotly's SVG structure for compatibility (layout fixes, toolbar accessibility, click-to-focus)
- 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:
A line trace whose
line.shapeis one of the step-wise shapes is piecewise constant — the value is held and then jumps — so it maps to MAIDR's step trace rather than to a line, and is announced and navigated as a step plot (including the Transitions rotor, which moves only between the points where the level changes).hvandvhcarry across to MAIDR'sstepDirectionunchanged, andhvhbecomesmid.vhvbinds as a step but names no convention: its flat segments sit at the mean of the two levels rather than at either one, which none of MAIDR's three conventions describes.A pie trace is bound to no axes — Plotly positions it by its own
domaininstead — so each pie becomes its own MAIDR subplot rather than joining whichever cartesian panel happens to use the first axis pair.axes.xandaxes.yare namedLabelandValue, since there is no drawn axis title to read them from. A doughnut (hole) is the same trace and reads identically.Plotly sorts pie slices by descending value unless the trace sets
sort: false, so the authored order is not necessarily the drawn order. The adapter reads the slices Plotly actually drew where the rendered chart exposes them; where it can only see the authored order and Plotly may have re-sorted, it emits the layer without selectors, since slice k is then not wedge k. That costs visual highlighting only — audio, text, and braille are unaffected. Setsort: falseto keep both.
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:
- Facet labels (e.g.
"sex=Male") become the panel names announced during navigation. - Axis titles carried only by the outer (matched) axis are resolved for every inner panel.
Both annotation shapes are recognized:
- Paper refs (
xref: 'paper',yref: 'paper') — what plotly.py actually emits for Plotly Express facet labels andmake_subplotsrow_titles/column_titles/subplot_titles. These are matched to panels geometrically: column titles above the top row, rotated row titles at the right edge, and per-panel titles (e.g.facet_col_wrap) just above each panel. - Axis-domain refs (
xref: 'x2 domain') — hand-authored facet labels tied explicitly to a panel's axes, as in the example below.
<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.