Frappe Charts Integration
MAIDR provides an adapter for Frappe Charts — a lightweight, open-source SVG charting library — that converts your charts into accessible, navigable visualizations with audio sonification, text descriptions, and braille output.
Quick Start
Load maidr.js and frappe.js after Frappe Charts. Frappe renders its SVG at runtime, so call the adapter once the chart has rendered and set the maidr attribute on the chart container:
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>My Frappe Chart</title>
<!-- 1. Load Frappe Charts (v1.6.2 bundles its own styles; no separate CSS file) -->
<script src="https://cdn.jsdelivr.net/npm/frappe-charts@1.6.2/dist/frappe-charts.min.umd.js"></script>
<!-- 2. Load MAIDR core and the Frappe adapter -->
<script src="https://cdn.jsdelivr.net/npm/maidr/dist/maidr.js"></script>
<script src="https://cdn.jsdelivr.net/npm/maidr/dist/frappe.js"></script>
</head>
<body>
<div id="chart"></div>
<script>
const data = {
labels: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri'],
datasets: [{ name: 'Visitors', values: [120, 240, 180, 300, 150] }],
};
const chart = new frappe.Chart('#chart', {
title: 'Daily Website Visitors',
data: data,
type: 'bar',
height: 400,
});
// 3. Frappe runs an entrance animation and RE-CREATES the SVG nodes once
// it finishes. Wait until the chart DOM stops mutating before handing
// it to MAIDR, otherwise MAIDR captures nodes Frappe later replaces and
// highlighting won't track the cursor until the chart is re-initialized.
activateMaidrWhenSettled(document.querySelector('#chart'), {
chartType: 'bar',
title: 'Daily Website Visitors',
axes: { x: 'Day of Week', y: 'Number of Visitors' },
});
function activateMaidrWhenSettled(container, options) {
if (!container.querySelector('svg.frappe-chart')) {
requestAnimationFrame(() => activateMaidrWhenSettled(container, options));
return;
}
let settleTimer;
const observer = new MutationObserver(scheduleSettle);
function scheduleSettle() {
clearTimeout(settleTimer);
settleTimer = setTimeout(finish, 300);
}
function finish() {
observer.disconnect();
const maidr = maidrFrappe.createMaidrFromFrappeChart(chart, container, options);
container.setAttribute('maidr', JSON.stringify(maidr));
}
observer.observe(container, { childList: true, subtree: true, attributes: true });
scheduleSettle();
}
</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
How It Works
The Frappe Charts adapter:
- Reads data - takes the chart's
{ labels, datasets }and converts it to MAIDR's schema - Creates selectors - generates CSS selectors (scoped to the chart container) for the Frappe SVG elements used in visual highlighting. MAIDR highlights one element per data point, so every line-based chart — line, area, bump, scatter, dot — targets the per-point
<circle>dots rather than the one<path>drawn for the whole series. KeeplineOptions.dotSize > 0and leavehideDotsoff. - Returns MAIDR config - produces a complete
Maidrobject you set as themaidrattribute
Wait for the chart to settle
Frappe generates its SVG at runtime and runs an entrance animation, after which it re-creates the data nodes. If you hand the chart to MAIDR before this settles, MAIDR captures references to nodes Frappe then replaces, and highlighting won't track the cursor (a symptom is that highlighting only starts working after you switch browser tabs and return, which re-initializes MAIDR).
The activateMaidrWhenSettled helper in the Quick Start handles this: it waits for svg.frappe-chart to exist, then uses a MutationObserver to wait until the chart DOM stops mutating (~300 ms of quiet) before calling the adapter and setting the maidr attribute. MAIDR's attribute observer then detects the maidr attribute and activates accessibility automatically.
Version note: the adapter's selectors target Frappe Charts v1.6.2. If you upgrade Frappe, verify the SVG class names still match.
Supported Chart Types
| Chart Type | Frappe type |
Adapter chartType |
|---|---|---|
| Bar | 'bar' |
'bar' |
| Line | 'line' |
'line' |
| Multi-line | 'line' (multiple datasets) |
'line' |
| Area | 'line' + lineOptions: { regionFill: 1 } |
'area' (also inferred) |
| Bump (rank over time) | 'line' (one dataset per competitor) |
'bump' |
| Scatter | 'line' + lineOptions: { hideLine: 1 } |
'scatter' |
| Dot plot | 'line' + lineOptions: { hideLine: 1 } |
'dot' |
| Diverging bar | 'bar' (two signed datasets) |
'diverging' |
| Mixed axis (bar + line) | 'axis-mixed' |
'axis-mixed' |
| Pie | 'pie' |
'pie' |
| Donut | 'donut' |
'donut' |
| Percentage | 'percentage' |
'percentage' |
The chartType names above are the adapter's, not Frappe's. Frappe draws several distinct statistical charts with the same type: 'line' or type: 'bar', differing only in their options or in what the numbers mean — nothing a chart instance records — so naming the chart is how you tell MAIDR which one to announce.
Not supported: Frappe's calendar-style Heatmap — a year of days keyed by date, structurally unlike MAIDR's labelled matrix.
Pie note: Frappe aggregates before it draws — it sums every dataset at each label, drops labels whose total is negative, and collapses everything past
maxSlices(default 20) into one "Rest" wedge. The adapter reproduces that aggregation so each announced slice is the wedge it highlights. Pass the chart instance (not a plain{ data }object) when you overridemaxSlices, so the adapter can read it.
Percentage note: A percentage chart is one bar divided into labelled bands, which is a 100% stacked bar whose column count is one — so MAIDR reads it as
stacked_normalized_barwith the bands as its series. Up/Down walks the bands and then the 100% total; Left/Right has nowhere to go. The values announced are shares, not counts:NORMALIZEDdivides nothing itself, so the adapter emits each band's percentage of the whole, the same as every other 100% chart MAIDR reads. The counts the author wrote are not announced — draw a bar chart if they matter. Frappe aggregates this chart with exactly the same code as a pie (AggregationChart.calc()): datasets summed per label, labels whose total is not>= 0dropped, and everything pastmaxSlices(default 20) collapsed into one "Rest" band, largest first. Pass the chart instance rather than a plain{ data }object when you overridemaxSlices.
Area note:
lineOptions.regionFillis an instance field the adapter reads directly, so a chart passed aschartType: 'line'with the region filled is announced as an area chart anyway — you cannot mislabel one as the other. PasschartType: 'area'when you hand the adapter a plain{ data }object, which has no instance to read. Several filled bands in one chart overlap in Frappe rather than stacking, so they stay independent series (area, neverstacked_area). KeepdotSize > 0: the fill is one<path>for the whole series and cannot highlight individual points.
Scatter / dot note: Frappe Charts v1.6.2 has no native
scattertype. Render either chart with a line chart whose connecting line is hidden (lineOptions: { hideLine: 1, dotSize: 6 }). Frappe places its marks at evenly spaced label positions whatever the label holds, so which of the two you have depends on the labels: numeric, evenly spaced labels are a scatter plot (chartType: 'scatter'), and category names are a Cleveland dot plot (chartType: 'dot'). Passing'scatter'with categorical labels is converted as a dot plot with a console warning, becauseNumber('Mon')isNaNand a scatter layer built from those labels would have no x values at all.
Bump note: A bump chart is a multi-dataset line chart whose y values are ranks — one dataset per competitor,
1= best. MAIDR inverts the pitch so first place is the highest note and announces the places gained or lost at each period, which is why the type has to be declared: nothing in the data distinguishes a rank from a magnitude. Emit the true ranks. Frappe v1.6.2 cannot invert or reverse an axis, so a rank chart it draws shows rank 1 at the bottom; pre-inverting the values to make the picture look right would make MAIDR announce the wrong ranks.
Diverging note: A diverging bar chart is two bar datasets with opposite signs, drawn either side of Frappe's zero line — negate the values of the series that should grow downwards and emit them signed, exactly as the chart draws them. MAIDR takes the magnitude for the pitch and names the side. The adapter throws unless the chart is exactly two datasets, one all-negative and one all-positive, rather than announce sides that are not there. Two limits are Frappe's: it has no horizontal bars, so the back-to-back population pyramid orientation is not drawable — only the vertical up/down form — and
barOptions: { stacked: 1 }must not be used, because Frappe cumulates raw values and would stack the negative series onto the positive one.
Code Examples
The snippets below reuse the
activateMaidrWhenSettledhelper from the Quick Start — it waits for Frappe's entrance animation to settle before activating MAIDR. Line-based charts setlineOptions.dotSize > 0so MAIDR can highlight the per-point dots.
Bar Chart
<div id="bar-chart"></div>
<script>
const data = {
labels: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri'],
datasets: [{ name: 'Visitors', values: [120, 240, 180, 300, 150] }],
};
const chart = new frappe.Chart('#bar-chart', { data, type: 'bar', height: 400 });
activateMaidrWhenSettled(document.querySelector('#bar-chart'), {
chartType: 'bar',
title: 'Daily Website Visitors',
axes: { x: 'Day of Week', y: 'Number of Visitors' },
});
</script>
Line Chart
<div id="line-chart"></div>
<script>
const data = {
labels: ['2021', '2022', '2023', '2024', '2025'],
datasets: [{ name: 'Revenue', values: [71, 85, 93, 110, 125] }],
};
// dotSize > 0 renders the per-point dots MAIDR highlights.
const chart = new frappe.Chart('#line-chart', {
data,
type: 'line',
height: 400,
lineOptions: { dotSize: 5 },
});
activateMaidrWhenSettled(document.querySelector('#line-chart'), {
chartType: 'line',
title: 'Annual Revenue',
axes: { x: 'Year', y: 'Revenue (thousands USD)' },
});
</script>
Multi-Line Chart
<div id="multiline-chart"></div>
<script>
const data = {
labels: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun'],
datasets: [
{ name: 'Product A', values: [18, 40, 30, 35, 50, 42] },
{ name: 'Product B', values: [36, 20, 48, 46, 30, 55] },
],
};
const chart = new frappe.Chart('#multiline-chart', {
data,
type: 'line',
height: 400,
lineOptions: { dotSize: 5 },
});
// Multiple datasets are auto-detected; one selector per line + a legend.
activateMaidrWhenSettled(document.querySelector('#multiline-chart'), {
chartType: 'line',
title: 'Monthly Product Sales',
axes: { x: 'Month', y: 'Sales (thousands USD)' },
});
</script>
Area Chart
<div id="area-chart"></div>
<script>
const data = {
labels: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun'],
datasets: [{ name: 'Rainfall', values: [42, 55, 71, 63, 48, 30] }],
};
// regionFill: 1 fills the band between the line and the baseline. The
// adapter reads it off the instance, so `chartType: 'line'` is announced as
// an area chart too — pass 'area' when handing over a plain { data } object.
const chart = new frappe.Chart('#area-chart', {
data,
type: 'line',
height: 400,
lineOptions: { regionFill: 1, dotSize: 5 },
});
activateMaidrWhenSettled(document.querySelector('#area-chart'), {
chartType: 'area',
title: 'Monthly Rainfall',
axes: { x: 'Month', y: 'Rainfall (mm)' },
});
</script>
Bump Chart (Rank Over Time)
<div id="bump-chart"></div>
<script>
// The values are RANKS, 1 = best. Emit them as ranks: MAIDR inverts the
// pitch itself, and Frappe cannot reverse an axis, so the drawn chart shows
// rank 1 at the bottom while the announcements stay correct.
const data = {
labels: ['Week 1', 'Week 2', 'Week 3', 'Week 4'],
datasets: [
{ name: 'Alpha', values: [1, 2, 2, 1] },
{ name: 'Beta', values: [2, 1, 3, 3] },
{ name: 'Gamma', values: [3, 3, 1, 2] },
],
};
const chart = new frappe.Chart('#bump-chart', {
data,
type: 'line',
height: 400,
lineOptions: { dotSize: 6 },
});
activateMaidrWhenSettled(document.querySelector('#bump-chart'), {
chartType: 'bump',
title: 'Weekly League Standings',
axes: { x: 'Week', y: 'Rank' },
});
</script>
Dot Plot
<div id="dot-chart"></div>
<script>
const data = {
labels: ['North', 'South', 'East', 'West', 'Central'],
datasets: [{ name: 'Sales', values: [64, 41, 78, 55, 92] }],
};
// Same rendering as the scatter plot below — a line chart with the line
// hidden. The labels are what tells the two apart: category names place one
// mark per category, which is a dot plot.
const chart = new frappe.Chart('#dot-chart', {
data,
type: 'line',
height: 400,
lineOptions: { hideLine: 1, dotSize: 8 },
});
activateMaidrWhenSettled(document.querySelector('#dot-chart'), {
chartType: 'dot',
title: 'Sales by Region',
axes: { x: 'Region', y: 'Sales (thousands USD)' },
});
</script>
Diverging Bar Chart
<div id="diverging-chart"></div>
<script>
// Two datasets with opposite signs. The downward series is NEGATIVE: MAIDR
// takes the magnitude for the pitch and names the side, so stripping the
// sign would leave it nothing to name the sides with. Do not set
// barOptions.stacked — Frappe cumulates raw values and would stack the
// negative series onto the positive one.
const data = {
labels: ['0-14', '15-29', '30-44', '45-59', '60-74', '75+'],
datasets: [
{ name: 'Men', values: [-1200, -1150, -1080, -990, -720, -310] },
{ name: 'Women', values: [1140, 1100, 1060, 1010, 830, 520] },
],
};
const chart = new frappe.Chart('#diverging-chart', {
data,
type: 'bar',
height: 420,
colors: ['#4c72b0', '#c44e52'],
});
activateMaidrWhenSettled(document.querySelector('#diverging-chart'), {
chartType: 'diverging',
title: 'Population by Age Band',
axes: { x: 'Age band', y: 'People, thousands', z: 'Sex' },
});
</script>
Scatter Plot
<div id="scatter-chart"></div>
<script>
const data = {
labels: [10, 20, 30, 40, 50, 60, 70, 80, 90, 100],
datasets: [{ name: 'Temperature', values: [22, 25, 28, 24, 31, 35, 33, 37, 40, 42] }],
};
// Frappe v1.6.2 has no native 'scatter' type; a line chart with the line
// hidden renders only the dots, which the adapter treats as scatter points.
const chart = new frappe.Chart('#scatter-chart', {
data,
type: 'line',
height: 400,
lineOptions: { hideLine: 1, dotSize: 6 },
});
activateMaidrWhenSettled(document.querySelector('#scatter-chart'), {
chartType: 'scatter',
title: 'Temperature vs Altitude',
axes: { x: 'Altitude (m)', y: 'Temperature (C)' },
});
</script>
Percentage Chart
<div id="percentage-chart"></div>
<script>
const data = {
labels: ['Direct', 'Search', 'Social', 'Referral'],
datasets: [{ name: 'Sessions', values: [400, 300, 200, 100] }],
};
const chart = new frappe.Chart('#percentage-chart', {
data,
type: 'percentage',
height: 200,
});
// Read as a 100% stacked bar of one column: Up/Down walks the four bands
// and then their 100% total, and each is announced as its SHARE (40, 30,
// 20, 10) rather than as the session counts above. `axes.z` names what the
// bands are; `axes.x` names the share and `axes.y` the whole bar.
activateMaidrWhenSettled(document.querySelector('#percentage-chart'), {
chartType: 'percentage',
title: 'Share of Traffic by Channel',
axes: { x: 'Share of sessions (%)', y: 'All traffic', z: 'Channel' },
});
</script>
Mixed Axis Chart (Bar + Line)
<div id="mixed-chart"></div>
<script>
const data = {
labels: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun'],
datasets: [
{ name: 'Sales', chartType: 'bar', values: [50, 70, 85, 60, 95, 110] },
{ name: 'Trend', chartType: 'line', values: [55, 65, 75, 70, 80, 100] },
],
};
// dotSize > 0 renders the per-point dots MAIDR highlights on the line layer.
const chart = new frappe.Chart('#mixed-chart', {
data,
type: 'axis-mixed',
height: 400,
lineOptions: { dotSize: 5 },
});
// Emits one layer per dataset; use PageUp / PageDown to switch layers.
activateMaidrWhenSettled(document.querySelector('#mixed-chart'), {
chartType: 'axis-mixed',
title: 'Monthly Sales with Trend Line',
axes: { x: 'Month', y: 'Value (units)' },
});
</script>
Multi-Panel Figures
Frappe Charts has no native facet/subplot concept — a "multi-panel" chart is simply several new frappe.Chart(...) instances laid out with CSS. createMaidrFromFrappeCharts groups such charts into one MAIDR figure with cross-panel navigation: arrow keys move between panels, Enter drills into a panel, Esc returns to panel navigation.
<div id="dashboard" style="display: grid; grid-template-columns: 1fr 1fr; gap: 1rem;">
<div id="panel-bar"></div>
<div id="panel-line"></div>
</div>
<script>
const barChart = new frappe.Chart('#panel-bar', { type: 'bar', height: 300, data: barData });
const lineChart = new frappe.Chart('#panel-line', {
type: 'line',
height: 300,
lineOptions: { dotSize: 5 },
data: lineData,
});
// The panel grid, in visual reading order (row-major, top-left first),
// matching your CSS layout. A flat array + `columns` also works:
// createMaidrFromFrappeCharts(flatPanels, wrapper, { columns: 2 }).
const panels = [
[
{
chart: barChart,
container: document.querySelector('#panel-bar'),
chartType: 'bar',
title: 'Weekly Visitors',
axes: { x: 'Day', y: 'Visitors' },
},
{
chart: lineChart,
container: document.querySelector('#panel-line'),
chartType: 'line',
title: 'Annual Revenue',
axes: { x: 'Year', y: 'Revenue' },
},
],
];
// Wait until EVERY panel has settled (see "Wait for the chart to settle"),
// then set the `maidr` attribute on the WRAPPER element.
const wrapper = document.querySelector('#dashboard');
Promise.all(panels.flat().map(p => whenChartSettled(p.container))).then(() => {
const maidr = maidrFrappe.createMaidrFromFrappeCharts(panels, wrapper, {
title: 'Store Performance Dashboard',
});
wrapper.setAttribute('maidr', JSON.stringify(maidr));
});
</script>
Key points:
- Each panel is a
FrappePanel:{ chart, container, chartType, title?, axes? }— the same per-chart options as the single-chart API, plus the panel'stitle, which MAIDR announces when navigating between panels. - Grid shape: a 2D array maps 1:1 to subplot rows (ragged rows are fine); a flat array is chunked into rows of
options.columnspanels, or placed in a single row whencolumnsis omitted. Order panels in visual reading order (row-major, top-left first) to match your CSS layout. - Set the
maidrattribute on the wrapper, not on individual panel containers — a per-panel attribute would create N separate MAIDR figures instead of one grid. Every panel container must be a descendant of the wrapper (the adapter throws otherwise). - Wait for all panels to settle before calling the adapter — Frappe's entrance animation re-creates SVG nodes per chart, so wrap the per-container settle wait in
Promise.all(seeexamples/frappe-multipanel.htmlfor a complete 2x2 example, including a promise-basedwhenChartSettledhelper). - Figure-level options (
FrappeChartsGridOptions):id(defaults to the wrapper'sid),title,subtitle,caption, andcolumns(flat input only). - Live updates are not supported for grouped charts: calling
chart.update(...)on a panel after binding re-creates its SVG nodes and invalidates the captured highlight elements. Rebuild and re-set themaidrattribute if panel data changes.
Configuration Options
The adapter accepts a FrappeChartAdapterOptions object:
| Option | Type | Required | Description |
|---|---|---|---|
chartType |
'bar' | 'line' | 'area' | 'bump' | 'scatter' | 'dot' | 'diverging' | 'axis-mixed' | 'pie' | 'donut' |
Yes | Which chart you drew (see Supported Chart Types). Multi-line uses 'line'. |
title |
string |
No | Chart title for accessibility announcements |
axes |
{ x?: string; y?: string; z?: string } |
No | Axis labels. z names the dimension the series themselves vary along (the two sides of a 'diverging' chart, say) and is ignored by the types that have no third dimension. |
id |
string |
No | Unique ID for the MAIDR instance (defaults to the container's id) |
Using with npm/Bundlers
For bundled projects, import the adapter directly:
import { createMaidrFromFrappeChart, createMaidrFromFrappeCharts } from 'maidr/frappe';
// Use after the chart has rendered
const maidr = createMaidrFromFrappeChart(chart, container, {
chartType: 'bar',
title: 'My Chart',
axes: { x: 'Category', y: 'Value' },
});
container.setAttribute('maidr', JSON.stringify(maidr));
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 |
| Switch layers (mixed chart) | Page Up / Page Down | Page Up / Page Down |
| 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.