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' or type: 'histogram2d' |
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 |
| Area | type: 'scatter', fill: 'tozeroy' | 'tozerox' | 'toself' |
Area chart |
| Stacked Area | type: 'scatter' + stackgroup |
Stacked area |
| 100% Stacked Area | stackgroup + groupnorm: 'percent' | 'fraction' |
Normalized area |
| Funnel | type: 'funnel' |
Funnel chart |
| Waterfall | type: 'waterfall' |
Waterfall chart |
| Error Bars | error_y or error_x on a scatter or bar trace |
Error bars |
| Sunburst | type: 'sunburst' |
Sunburst |
| Icicle | type: 'icicle' |
Icicle |
| Treemap | type: 'treemap' |
Treemap |
| Sankey | type: 'sankey' |
Sankey |
| Gauge / Bullet | type: 'indicator' with gauge in mode |
Gauge |
| Radar | type: 'scatterpolar' |
Radar |
| Polar Area / Rose | type: 'barpolar' |
Polar area |
| Parallel Coordinates | type: 'parcoords' |
Parallel coordinates |
| Ridgeline | type: 'violin' with side: 'positive' |
Ridgeline |
| Gantt / Timeline | horizontal bar traces with a base array on a date axis |
Gantt chart |
| Diverging Bar / Pyramid | barmode: 'relative' + bar traces with opposed signs |
Population pyramid |
| Dot Plot | type: 'scatter', mode: 'markers', one marker per category |
Dot plot |
| Word Cloud | type: 'scatter', mode: 'text', array textfont.size |
Word cloud |
| Choropleth | type: 'choropleth' |
Choropleth map |
| Contour | type: 'contour' or type: 'histogram2dcontour' |
Contour plot |
| Mosaic / Marimekko | stacked bar traces declaring meta: { maidr: { type: 'mosaic' } } |
Mosaic plot |
| 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.A filled scatter is an area chart. Naming a
stackgroupmakes it a stacked one, and addinggroupnormmakes that a 100% stacked one; each stack group in a panel becomes its own layer, so two independent stacks are never merged into a running total neither of them draws. The layer carries each band's own value rather than the running edge Plotly draws it at — MAIDR derives the totals and each band's share of them — except undergroupnorm, where the rescaled heights Plotly drew are read from its calculated data so the announced number matches the percentage axis. A filled trace whoseline.shapeis step-wise stays a step chart: with nothing accumulating, the fill is decoration and the staircase convention is the part worth announcing.Error bars are a modifier rather than a trace type, so a scatter or bar trace whose
error_y(orerror_x) is visible becomes an error-bar layer instead of a scatter, line, or bar one. The interval is what such a chart is drawn to show, and a scatter reading announces the estimate and drops it. MAIDR reads the absolute bounds Plotly resolved for each sample, whichever way they were declared —array,percent,constant, orsqrt— and navigates them as three rows: lower bound, value, upper bound.A funnel is read as a bar chart whose order means something: the pitch carries the retention between adjacent stages rather than the count, since the drop-off is what a funnel is read for and a ratio is what a listener cannot take by ear. The counts are announced alongside it.
A waterfall step carries both numbers the bar draws: the contribution (the bar's height, which is what the pitch follows) and the running total it produced (the bar's position). Steps whose
measureistotalorabsoluteare announced as totals, so a subtotal is not mistaken for a contribution.A sunburst, an icicle and a treemap are one tree drawn three ways, so MAIDR reads all three the same: the hierarchy navigates as a hierarchy on the arrow keys that already exist — up to the parent, down to the first child, left and right between siblings — and each node announces its share of its parent. Plotly stratifies
labels/parentsinto a tree and, unless the trace setssort: false, reorders every node's children largest first, so the adapter walks the tree Plotly computed rather than the arrays as written. A hierarchy Plotly has not computed yet is still read out, in the authored order, but without selectors — sector k is then not slice k.A sankey names both ends of every flow. Plotly stores them as indices into
node.label, so the adapter resolves them back to the labels: "34 from Coal to Electricity" is the reading and "34 from 0 to 1" is not. The nodes are derived from the flows, so nothing is emitted for them separately. Flows Plotly drops before drawing — a non-positive value, an endpoint that is not a node — are dropped here too, since a ribbon nothing draws would still put a node in the graph.An
indicatortrace is a gauge only when it draws one. Withoutgaugein itsmodeit is a number set in text, which a screen reader already reaches, so it is skipped rather than announced twice. A gauge'sgauge.threshold.valuebecomes the target;delta.referencestands in when there is no threshold, except when it equals the measure, which is the value Plotly defaults it to and not a target anyone set. Plotly's steps carry a range and a colour but no name, so the qualitative bands are announced only when the author named every step — an invented "band 2" would say nothing the numbers do not.A polar trace has no axis pair either:
scatterpolarandbarpolarname asubplot(polar,polar2, …) and are positioned bylayout.polar.domain, so each polar subplot becomes its own MAIDR panel. Both read as spokes and values, the way a multi-line chart reads as samples and values, and the circle is carried in the panning rather than in the payload. Plotly's schema has no title for the angular axis, so the spokes are namedSpoke; the radial axis title is read when there is one.A
parcoordslayer is transposed on the way in: Plotly stores a column of values per axis, and MAIDR reads a row per observation. The pitch is scaled per axis rather than for the layer, since the columns are different quantities and one range for all of them would sonify the units instead of the data. Plotly draws these lines to a canvas rather than to SVG, so the layer carries no selectors — audio, text, braille, and navigation all work, visual highlighting alone does not.A
histogram2dis read as the heatmap of counts it draws. The grid comes from the calculated data rather than the trace, because a 2D histogram is given samples and plotly bins them while it draws — the same place ahistogram2dcontour's grid comes from. Each cell is named by the bin it stands for (-0.5 to 1.5) rather than by an index, since the extent is most of what a histogram is read for, and the fill axis is calledCountunless the trace sets ahistfuncother than counting or names its colorbar.A
contour(and ahistogram2dcontour) is read as one curve per level. Plotly computes its curves while it draws and keeps none of them — the calculated data holds the grid and nothing else — so MAIDR walks the grid itself by marching squares, at the levels the trace states incontours.start,.endand.size. Those are resolved by Plotly even whenautocontourchose them, so nothing is guessed: a chart whose levels are not resolved is skipped rather than read at invented ones. The level is announced on the field's own axis — named from the colorbar title when there is one — and each point announces the distance to the nearest point on the adjacent level, which is what the crowding of the lines conveys visually. Where a level is drawn as several disjoint curves, they are walked as one row, in the order they were found. Acontours.type: 'constraint'trace shades the region satisfying an inequality rather than drawing a level ladder, so it is skipped instead of being announced as one. Plotly draws a level as whole curves with no element per vertex, so the visual highlight is synthesised along the drawn curve; each level's selector is scoped to that level's own group, so a highlight always lands on the level being read. A fill-only contour — one that turnedcontours.showlinesoff — is drawn as one filled path per level with no curve elements at all, so it is read and sonified but not highlighted.A mosaic (marimekko) is declared, not detected. Plotly draws one as stacked
bartraces with a per-columnwidth, butwidthis ordinary bar styling that any bar trace may carry, so reading a non-uniform array as data would announce every column's width as a share of all observations — a number the chart does not contain. Writingmeta: { maidr: { type: 'mosaic' } }on one of the panel's bar traces is what says otherwise; see Declaring a mosaic.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.
Declaring a mosaic
Most Plotly charts name themselves in trace.type, and MAIDR reads them with
no configuration at all. A marimekko is the exception: it is stacked bars plus
a convention, and the convention is invisible to the schema. It is declared on
meta, Plotly's own metadata attribute — ordinary trace config, so it survives
Plotly.react and a JSON-authored figure.
{
type: 'bar',
name: 'Survived',
x: centres, // cumulative column centres, computed by the author
y: proportions,
width: passengers, // each column's width, in x-axis units
customdata: rows, // the author's own rows, one per column
meta: {
maidr: {
type: 'mosaic',
width: 'share', // optional: the column of `customdata` holding the share
count: 'count' // optional: the column holding the cell's own count
}
}
}
Only the bare { type: 'mosaic' } is required, and only on one of the panel's
bar traces — it declares the layer, not the series. Then:
- The share of all observations comes from
widthwhen the declaration names a column ofcustomdatathat carries it, and otherwise from the drawn widths, normalised by their own total. A chart authored in counts, in percentages or already in fractions therefore all read the same. - The cell count is emitted only when a column holds it. A mosaic drawn from proportions alone genuinely has no counts, and multiplying a rounded share back out would put a number in the announcement that the data does not contain.
customdatarows must be objects,{ share: 0.3, count: 203 }rather than['First', 0.3, 203]. A declared field is a column name, and Plotly's other canonical shape — one array per point — has no names to resolve against, so a declared column is reported as unresolved rather than guessed at by position.- The column names come from the trace's
text, then itshovertext, and then the axisticktextat the matchingtickvals. A marimekko'sxis a precomputed position rather than a category, so without one of those a column is announced by the number it sits at.
The declaration is checked when it is read. An unknown key, a value of the
wrong kind, or a type that is not a MAIDR trace type is reported on the
console and the chart is read as the undeclared one — never dropped, and never
read as something it is not. A declaration this adapter cannot honour is
reported too: a mosaic on a trace that draws no bars, or on a panel whose
bars Plotly drew side by side, leaves the panel read as the grouped bar chart
it is.
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>
Give the samples on x instead and plotly draws the boxes on their side;
MAIDR announces a horizontal box plot and reads the group off the y axis, which
is where it is drawn. The five numbers themselves do not move.
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>
Contour Plot
<div id="contour-chart" style="width: 700px; height: 500px"></div>
<script>
Plotly.newPlot('contour-chart', [{
type: 'contour',
z: field, // a 2D array of magnitudes
x: eastings, // one coordinate per column
y: northings, // one coordinate per row
contours: { start: 0.2, end: 1.4, size: 0.2 },
colorbar: { title: { text: 'Concentration' } }
}], {
title: { text: 'Concentration over the sampling area' },
xaxis: { title: { text: 'Easting (km)' } },
yaxis: { title: { text: 'Northing (km)' } }
});
</script>
contours may be left out entirely — Plotly picks the ladder itself and MAIDR
reads whichever one it resolved.
Mosaic (Marimekko)
<div id="mosaic-chart" style="width: 700px; height: 500px"></div>
<script>
// Column centres are cumulative: everything before the column, plus half
// of it. The widths are what makes it a mosaic rather than a stacked bar.
Plotly.newPlot('mosaic-chart', [{
type: 'bar',
name: 'Survived',
x: centres,
y: [0.62, 0.41, 0.25, 0.24],
width: [325, 285, 706, 885],
meta: { maidr: { type: 'mosaic' } }
}, {
type: 'bar',
name: 'Died',
x: centres,
y: [0.38, 0.59, 0.75, 0.76],
width: [325, 285, 706, 885]
}], {
title: { text: 'Titanic survival by class' },
barmode: 'stack',
bargap: 0,
xaxis: {
title: { text: 'Class' },
tickmode: 'array',
tickvals: centres,
ticktext: ['First', 'Second', 'Third', 'Crew']
},
yaxis: { title: { text: 'Proportion' } }
});
</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>
Give the samples on y instead of x and plotly bins up the y axis, drawing
the counts across the page. MAIDR reads that as a horizontal histogram — the
bin range is announced against the axis the bins run along, and the count
against the other — with no extra configuration.
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 | 29 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.