diff --git a/examples/d3-bindbar.html b/examples/d3-bindbar.html
new file mode 100644
index 00000000..9a5e94b8
--- /dev/null
+++ b/examples/d3-bindbar.html
@@ -0,0 +1,108 @@
+
+
+
+
+ MAIDR + D3.js Bar Chart Example
+
+
+
+
+
+ MAIDR + D3.js Bar Chart
+ Click on the chart and use arrow keys to navigate. Press 'b' for braille, 't' for text descriptions.
+
+
+
+
+
diff --git a/examples/d3-bindbox.html b/examples/d3-bindbox.html
new file mode 100644
index 00000000..14a0af3e
--- /dev/null
+++ b/examples/d3-bindbox.html
@@ -0,0 +1,83 @@
+
+
+
+
+ MAIDR + D3.js Box Plot Example
+
+
+
+
+
+ MAIDR + D3.js Box Plot
+ Click on the chart and use arrow keys to navigate between boxes.
+
+
+
+
+
diff --git a/examples/d3-bindcandlestick.html b/examples/d3-bindcandlestick.html
new file mode 100644
index 00000000..6890f774
--- /dev/null
+++ b/examples/d3-bindcandlestick.html
@@ -0,0 +1,81 @@
+
+
+
+
+ MAIDR + D3.js Candlestick Chart Example
+
+
+
+
+
+ MAIDR + D3.js Candlestick Chart
+ Click on the chart and use arrow keys to navigate between trading days.
+
+
+
+
+
diff --git a/examples/d3-binddodged.html b/examples/d3-binddodged.html
new file mode 100644
index 00000000..afd5c5be
--- /dev/null
+++ b/examples/d3-binddodged.html
@@ -0,0 +1,76 @@
+
+
+
+
+ MAIDR + D3.js Dodged Bar Chart Example
+
+
+
+
+
+ MAIDR + D3.js Dodged (Grouped) Bar Chart
+ Click on the chart and use arrow keys to navigate bars.
+
+
+
+
+
diff --git a/examples/d3-bindheatmap.html b/examples/d3-bindheatmap.html
new file mode 100644
index 00000000..9651890c
--- /dev/null
+++ b/examples/d3-bindheatmap.html
@@ -0,0 +1,111 @@
+
+
+
+
+ MAIDR + D3.js Heatmap Example
+
+
+
+
+
+ MAIDR + D3.js Heatmap
+ Click on the chart and use arrow keys to navigate cells.
+
+
+
+
+
diff --git a/examples/d3-bindhistogram.html b/examples/d3-bindhistogram.html
new file mode 100644
index 00000000..2de3ab0a
--- /dev/null
+++ b/examples/d3-bindhistogram.html
@@ -0,0 +1,108 @@
+
+
+
+
+ MAIDR + D3.js Histogram Example
+
+
+
+
+
+ MAIDR + D3.js Histogram
+ Click on the chart and use arrow keys to navigate between bins.
+
+
+
+
+
diff --git a/examples/d3-bindline.html b/examples/d3-bindline.html
new file mode 100644
index 00000000..6dc3e00f
--- /dev/null
+++ b/examples/d3-bindline.html
@@ -0,0 +1,144 @@
+
+
+
+
+ MAIDR + D3.js Line Chart Example
+
+
+
+
+
+ MAIDR + D3.js Line Chart
+ Click on the chart and use arrow keys to navigate along the line.
+
+
+
+
+
diff --git a/examples/d3-bindscatter.html b/examples/d3-bindscatter.html
new file mode 100644
index 00000000..c684ab9a
--- /dev/null
+++ b/examples/d3-bindscatter.html
@@ -0,0 +1,112 @@
+
+
+
+
+ MAIDR + D3.js Scatter Plot Example
+
+
+
+
+
+ MAIDR + D3.js Scatter Plot
+ Click on the chart and use arrow keys to navigate between data points.
+
+
+
+
+
diff --git a/examples/d3-bindsmooth.html b/examples/d3-bindsmooth.html
new file mode 100644
index 00000000..c78c1a63
--- /dev/null
+++ b/examples/d3-bindsmooth.html
@@ -0,0 +1,84 @@
+
+
+
+
+ MAIDR + D3.js Smooth Curve Example
+
+
+
+
+
+ MAIDR + D3.js Smooth / Regression Curve
+ Click on the chart and use arrow keys to navigate along the fitted curve.
+
+
+
+
+
diff --git a/examples/d3-bindstacked.html b/examples/d3-bindstacked.html
new file mode 100644
index 00000000..ef4267e6
--- /dev/null
+++ b/examples/d3-bindstacked.html
@@ -0,0 +1,100 @@
+
+
+
+
+ MAIDR + D3.js Stacked Bar Chart Example
+
+
+
+
+
+ MAIDR + D3.js Stacked Bar Chart
+ Click on the chart and use arrow keys to navigate segments.
+
+
+
+
+
diff --git a/package-lock.json b/package-lock.json
index 02c70fcd..6acfdcce 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "maidr",
- "version": "3.50.0",
+ "version": "3.51.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "maidr",
- "version": "3.50.0",
+ "version": "3.51.0",
"dependencies": {
"@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.0",
diff --git a/package.json b/package.json
index ceb2b908..f67df592 100644
--- a/package.json
+++ b/package.json
@@ -13,6 +13,11 @@
"types": "./dist/react.d.mts",
"import": "./dist/react.mjs",
"default": "./dist/react.mjs"
+ },
+ "./d3": {
+ "types": "./dist/d3.d.mts",
+ "import": "./dist/d3.mjs",
+ "default": "./dist/d3.js"
}
},
"main": "dist/maidr.js",
@@ -20,9 +25,10 @@
"dist"
],
"scripts": {
- "build": "vite build && vite build --config vite.react.config.ts",
+ "build": "vite build && vite build --config vite.react.config.ts && vite build --config vite.d3.config.ts",
"build:script": "vite build",
"build:react": "vite build --config vite.react.config.ts",
+ "build:d3": "vite build --config vite.d3.config.ts",
"prepublishOnly": "npm run build",
"prepare": "husky",
"commitlint": "commitlint --from=HEAD~1 --to=HEAD",
diff --git a/src/d3/bindBar.ts b/src/d3/bindBar.ts
new file mode 100644
index 00000000..afaf737a
--- /dev/null
+++ b/src/d3/bindBar.ts
@@ -0,0 +1,97 @@
+/**
+ * D3 binder for bar charts.
+ *
+ * Extracts data from D3.js-rendered bar chart SVG elements and generates
+ * the MAIDR JSON schema for accessible bar chart interaction.
+ */
+
+import type { BarPoint, Maidr, MaidrLayer } from '../type/grammar';
+import type { D3BarConfig, D3BinderResult } from './types';
+import { Orientation, TraceType } from '../type/grammar';
+import { buildAxes, generateId, queryD3Elements, resolveAccessor, scopeSelector } from './util';
+
+/**
+ * Binds a D3.js bar chart to MAIDR, generating the accessible data representation.
+ *
+ * Extracts data from D3-bound SVG elements (``, ``, etc.) and
+ * produces a complete {@link Maidr} data structure for sonification, text
+ * descriptions, braille output, and keyboard navigation.
+ *
+ * @param svg - The SVG element (or container) containing the D3 bar chart.
+ * @param config - Configuration specifying the selector and data accessors.
+ * @returns A {@link D3BinderResult} with the MAIDR data and generated layer.
+ *
+ * @example
+ * ```ts
+ * // D3 bar chart with data bound to elements
+ * const result = bindD3Bar(svgElement, {
+ * selector: 'rect.bar',
+ * title: 'Sales by Quarter',
+ * axes: { x: 'Quarter', y: 'Revenue' },
+ * x: 'quarter', // property name on the bound datum
+ * y: 'revenue', // property name on the bound datum
+ * });
+ *
+ * // Use with maidr-data attribute
+ * svgElement.setAttribute('maidr-data', JSON.stringify(result.maidr));
+ *
+ * // Or use with React
+ *
+ * ```
+ */
+export function bindD3Bar(svg: Element, config: D3BarConfig): D3BinderResult {
+ const {
+ id = generateId(),
+ title,
+ subtitle,
+ caption,
+ axes,
+ format,
+ selector,
+ x: xAccessor = 'x',
+ y: yAccessor = 'y',
+ orientation = Orientation.VERTICAL,
+ } = config;
+
+ const elements = queryD3Elements(svg, selector);
+ if (elements.length === 0) {
+ throw new Error(
+ `No elements found for selector "${selector}". `
+ + `Ensure the D3 chart has been rendered and the selector matches the bar elements.`,
+ );
+ }
+
+ const data: BarPoint[] = elements.map(({ datum, index }) => {
+ if (!datum) {
+ throw new Error(
+ `No D3 data bound to element at index ${index}. `
+ + `Ensure D3's .data() join has been applied to the "${selector}" elements.`,
+ );
+ }
+ return {
+ x: resolveAccessor(datum, xAccessor, index),
+ y: resolveAccessor(datum, yAccessor, index),
+ };
+ });
+
+ const layerId = generateId();
+ const layer: MaidrLayer = {
+ id: layerId,
+ type: TraceType.BAR,
+ title,
+ selectors: scopeSelector(svg, selector),
+ orientation,
+ axes: buildAxes(axes, format),
+ data,
+ };
+
+ const maidr: Maidr = {
+ id,
+ title,
+ subtitle,
+ caption,
+ subplots: [[{ layers: [layer] }]],
+ };
+
+ return { maidr, layer };
+}
diff --git a/src/d3/bindBox.ts b/src/d3/bindBox.ts
new file mode 100644
index 00000000..541cda01
--- /dev/null
+++ b/src/d3/bindBox.ts
@@ -0,0 +1,125 @@
+/**
+ * D3 binder for box plots.
+ *
+ * Extracts data from D3.js-rendered box plot SVG elements and generates
+ * the MAIDR JSON schema for accessible box plot interaction.
+ */
+
+import type { BoxPoint, Maidr, MaidrLayer } from '../type/grammar';
+import type { D3BinderResult, D3BoxConfig } from './types';
+import { Orientation, TraceType } from '../type/grammar';
+import { buildAxes, generateId, getD3Datum, queryD3Elements, resolveAccessor, resolveAccessorOptional, scopeSelector } from './util';
+
+/**
+ * Binds a D3.js box plot to MAIDR, generating the accessible data representation.
+ *
+ * Box plots in D3 are typically constructed from multiple SVG elements per box
+ * (a rect for the IQR, lines for whiskers, a line for the median, and circles
+ * for outliers). This binder extracts statistical summary data from D3-bound
+ * data on the box group elements.
+ *
+ * @param svg - The SVG element containing the D3 box plot.
+ * @param config - Configuration specifying selectors and data accessors.
+ * @returns A {@link D3BinderResult} with the MAIDR data and generated layer.
+ *
+ * @example
+ * ```ts
+ * const result = bindD3Box(svgElement, {
+ * selector: 'g.box',
+ * title: 'Distribution by Category',
+ * axes: { x: 'Category', y: 'Value' },
+ * fill: 'category',
+ * min: 'whiskerLow',
+ * q1: 'q1',
+ * q2: 'median',
+ * q3: 'q3',
+ * max: 'whiskerHigh',
+ * lowerOutliers: 'lowOutliers',
+ * upperOutliers: 'highOutliers',
+ * });
+ * ```
+ */
+export function bindD3Box(svg: Element, config: D3BoxConfig): D3BinderResult {
+ const {
+ id = generateId(),
+ title,
+ subtitle,
+ caption,
+ axes,
+ format,
+ selector,
+ fill: fillAccessor = 'fill',
+ min: minAccessor = 'min',
+ q1: q1Accessor = 'q1',
+ q2: q2Accessor = 'q2',
+ q3: q3Accessor = 'q3',
+ max: maxAccessor = 'max',
+ lowerOutliers: lowerOutliersAccessor = 'lowerOutliers',
+ upperOutliers: upperOutliersAccessor = 'upperOutliers',
+ orientation = Orientation.VERTICAL,
+ } = config;
+
+ const boxGroups = queryD3Elements(svg, selector);
+ if (boxGroups.length === 0) {
+ throw new Error(
+ `No elements found for selector "${selector}". `
+ + `Ensure the D3 chart has been rendered and the selector matches the box group elements.`,
+ );
+ }
+
+ const data: BoxPoint[] = boxGroups.map(({ element, datum, index }) => {
+ // Try to get data from the group element's D3 binding first
+ let effectiveDatum = datum;
+
+ // If no data on the group, try to find it on child elements
+ if (!effectiveDatum) {
+ const firstChild = element.querySelector('rect, line, path');
+ if (firstChild) {
+ effectiveDatum = getD3Datum(firstChild);
+ }
+ }
+
+ if (!effectiveDatum) {
+ throw new Error(
+ `No D3 data bound to box group element at index ${index}. `
+ + `Ensure D3's .data() join has been applied to the "${selector}" elements.`,
+ );
+ }
+
+ // Outlier arrays are optional - use resolveAccessorOptional
+ const lowerOutliers = resolveAccessorOptional(effectiveDatum, lowerOutliersAccessor, index) ?? [];
+ const upperOutliers = resolveAccessorOptional(effectiveDatum, upperOutliersAccessor, index) ?? [];
+
+ return {
+ fill: resolveAccessor(effectiveDatum, fillAccessor, index),
+ lowerOutliers,
+ min: resolveAccessor(effectiveDatum, minAccessor, index),
+ q1: resolveAccessor(effectiveDatum, q1Accessor, index),
+ q2: resolveAccessor(effectiveDatum, q2Accessor, index),
+ q3: resolveAccessor(effectiveDatum, q3Accessor, index),
+ max: resolveAccessor(effectiveDatum, maxAccessor, index),
+ upperOutliers,
+ };
+ });
+
+ const layerId = generateId();
+ const layer: MaidrLayer = {
+ id: layerId,
+ type: TraceType.BOX,
+ title,
+ selectors: scopeSelector(svg, selector),
+ orientation,
+ axes: buildAxes(axes, format),
+ data,
+ };
+
+ const maidr: Maidr = {
+ id,
+ title,
+ subtitle,
+ caption,
+ subplots: [[{ layers: [layer] }]],
+ };
+
+ return { maidr, layer };
+}
diff --git a/src/d3/bindCandlestick.ts b/src/d3/bindCandlestick.ts
new file mode 100644
index 00000000..7d161219
--- /dev/null
+++ b/src/d3/bindCandlestick.ts
@@ -0,0 +1,123 @@
+/**
+ * D3 binder for candlestick charts.
+ *
+ * Extracts data from D3.js-rendered candlestick chart SVG elements and generates
+ * the MAIDR JSON schema for accessible candlestick chart interaction.
+ */
+
+import type { CandlestickPoint, CandlestickTrend, Maidr, MaidrLayer } from '../type/grammar';
+import type { D3BinderResult, D3CandlestickConfig } from './types';
+import { TraceType } from '../type/grammar';
+import { buildAxes, generateId, queryD3Elements, resolveAccessor, resolveAccessorOptional, scopeSelector } from './util';
+
+/**
+ * Binds a D3.js candlestick chart to MAIDR.
+ *
+ * Candlestick charts show OHLC (Open, High, Low, Close) data for financial
+ * time series. This binder extracts data from D3-bound SVG elements
+ * representing candlestick bodies (typically ``) and optional wicks.
+ *
+ * @param svg - The SVG element containing the D3 candlestick chart.
+ * @param config - Configuration specifying the selector and data accessors.
+ * @returns A {@link D3BinderResult} with the MAIDR data and generated layer.
+ *
+ * @example
+ * ```ts
+ * const result = bindD3Candlestick(svgElement, {
+ * selector: 'rect.candle',
+ * title: 'Stock Price',
+ * axes: { x: 'Date', y: 'Price ($)' },
+ * value: 'date',
+ * open: 'open',
+ * high: 'high',
+ * low: 'low',
+ * close: 'close',
+ * volume: 'volume',
+ * });
+ * ```
+ */
+export function bindD3Candlestick(svg: Element, config: D3CandlestickConfig): D3BinderResult {
+ const {
+ id = generateId(),
+ title,
+ subtitle,
+ caption,
+ axes,
+ format,
+ selector,
+ value: valueAccessor = 'value',
+ open: openAccessor = 'open',
+ high: highAccessor = 'high',
+ low: lowAccessor = 'low',
+ close: closeAccessor = 'close',
+ volume: volumeAccessor = 'volume',
+ trend: trendAccessor,
+ } = config;
+
+ const elements = queryD3Elements(svg, selector);
+ if (elements.length === 0) {
+ throw new Error(
+ `No elements found for selector "${selector}". `
+ + `Ensure the D3 chart has been rendered and the selector matches the candlestick elements.`,
+ );
+ }
+
+ const data: CandlestickPoint[] = elements.map(({ datum, index }) => {
+ if (!datum) {
+ throw new Error(
+ `No D3 data bound to element at index ${index}. `
+ + `Ensure D3's .data() join has been applied to the "${selector}" elements.`,
+ );
+ }
+
+ const openVal = resolveAccessor(datum, openAccessor, index);
+ const closeVal = resolveAccessor(datum, closeAccessor, index);
+ const highVal = resolveAccessor(datum, highAccessor, index);
+ const lowVal = resolveAccessor(datum, lowAccessor, index);
+
+ // Compute trend if not provided
+ let trend: CandlestickTrend;
+ if (trendAccessor) {
+ trend = resolveAccessor(datum, trendAccessor, index);
+ } else if (closeVal > openVal) {
+ trend = 'Bull';
+ } else if (closeVal < openVal) {
+ trend = 'Bear';
+ } else {
+ trend = 'Neutral';
+ }
+
+ const volumeVal = resolveAccessorOptional(datum, volumeAccessor, index) ?? 0;
+
+ return {
+ value: resolveAccessor(datum, valueAccessor, index),
+ open: openVal,
+ high: highVal,
+ low: lowVal,
+ close: closeVal,
+ volume: volumeVal,
+ trend,
+ volatility: highVal - lowVal,
+ };
+ });
+
+ const layerId = generateId();
+ const layer: MaidrLayer = {
+ id: layerId,
+ type: TraceType.CANDLESTICK,
+ title,
+ selectors: scopeSelector(svg, selector),
+ axes: buildAxes(axes, format),
+ data,
+ };
+
+ const maidr: Maidr = {
+ id,
+ title,
+ subtitle,
+ caption,
+ subplots: [[{ layers: [layer] }]],
+ };
+
+ return { maidr, layer };
+}
diff --git a/src/d3/bindHeatmap.ts b/src/d3/bindHeatmap.ts
new file mode 100644
index 00000000..ab2464ee
--- /dev/null
+++ b/src/d3/bindHeatmap.ts
@@ -0,0 +1,145 @@
+/**
+ * D3 binder for heatmaps.
+ *
+ * Extracts data from D3.js-rendered heatmap SVG elements and generates
+ * the MAIDR JSON schema for accessible heatmap interaction.
+ */
+
+import type { HeatmapData, Maidr, MaidrLayer } from '../type/grammar';
+import type { D3BinderResult, D3HeatmapConfig } from './types';
+import { TraceType } from '../type/grammar';
+import { buildAxes, generateId, queryD3Elements, resolveAccessor, scopeSelector } from './util';
+
+/**
+ * Binds a D3.js heatmap to MAIDR, generating the accessible data representation.
+ *
+ * Extracts cell data from D3-bound SVG elements (``) organized in a grid
+ * and produces a complete {@link Maidr} data structure. The cells are grouped
+ * by their x and y category values to form the 2D points grid.
+ *
+ * @param svg - The SVG element containing the D3 heatmap.
+ * @param config - Configuration specifying the selector and data accessors.
+ * @returns A {@link D3BinderResult} with the MAIDR data and generated layer.
+ * @throws Error if any cell coordinate pair is missing from the extracted data.
+ *
+ * @example
+ * ```ts
+ * const result = bindD3Heatmap(svgElement, {
+ * selector: 'rect.cell',
+ * title: 'Correlation Matrix',
+ * axes: { x: 'Variable', y: 'Variable', fill: 'Correlation' },
+ * x: 'xVar',
+ * y: 'yVar',
+ * value: 'correlation',
+ * });
+ * ```
+ */
+export function bindD3Heatmap(svg: Element, config: D3HeatmapConfig): D3BinderResult {
+ const {
+ id = generateId(),
+ title,
+ subtitle,
+ caption,
+ axes,
+ format,
+ selector,
+ x: xAccessor = 'x',
+ y: yAccessor = 'y',
+ value: valueAccessor = 'value',
+ } = config;
+
+ const elements = queryD3Elements(svg, selector);
+ if (elements.length === 0) {
+ throw new Error(
+ `No elements found for selector "${selector}". `
+ + `Ensure the D3 chart has been rendered and the selector matches the cell elements.`,
+ );
+ }
+
+ // Extract raw cell data
+ const cells: { x: string; y: string; value: number }[] = elements.map(({ datum, index }) => {
+ if (!datum) {
+ throw new Error(
+ `No D3 data bound to element at index ${index}. `
+ + `Ensure D3's .data() join has been applied to the "${selector}" elements.`,
+ );
+ }
+ return {
+ x: String(resolveAccessor(datum, xAccessor, index)),
+ y: String(resolveAccessor(datum, yAccessor, index)),
+ value: resolveAccessor(datum, valueAccessor, index),
+ };
+ });
+
+ // Build unique x and y labels (preserving order of appearance)
+ const xLabels: string[] = [];
+ const yLabels: string[] = [];
+ const seenX = new Set();
+ const seenY = new Set();
+
+ for (const cell of cells) {
+ if (!seenX.has(cell.x)) {
+ seenX.add(cell.x);
+ xLabels.push(cell.x);
+ }
+ if (!seenY.has(cell.y)) {
+ seenY.add(cell.y);
+ yLabels.push(cell.y);
+ }
+ }
+
+ // Build the 2D points grid using nested Maps to avoid key collisions
+ const cellMap = new Map>();
+ for (const cell of cells) {
+ let row = cellMap.get(cell.y);
+ if (!row) {
+ row = new Map();
+ cellMap.set(cell.y, row);
+ }
+ row.set(cell.x, cell.value);
+ }
+
+ const points: number[][] = [];
+ for (const yLabel of yLabels) {
+ const row: number[] = [];
+ const rowMap = cellMap.get(yLabel);
+ for (const xLabel of xLabels) {
+ const value = rowMap?.get(xLabel);
+ if (value === undefined) {
+ throw new Error(
+ `Missing heatmap cell for y="${yLabel}", x="${xLabel}". `
+ + `Expected a complete grid of ${yLabels.length} x ${xLabels.length} cells `
+ + `but found ${cells.length} elements.`,
+ );
+ }
+ row.push(value);
+ }
+ points.push(row);
+ }
+
+ const data: HeatmapData = {
+ x: xLabels,
+ y: yLabels,
+ points,
+ };
+
+ const layerId = generateId();
+ const layer: MaidrLayer = {
+ id: layerId,
+ type: TraceType.HEATMAP,
+ title,
+ selectors: scopeSelector(svg, selector),
+ axes: buildAxes(axes, format),
+ data,
+ };
+
+ const maidr: Maidr = {
+ id,
+ title,
+ subtitle,
+ caption,
+ subplots: [[{ layers: [layer] }]],
+ };
+
+ return { maidr, layer };
+}
diff --git a/src/d3/bindHistogram.ts b/src/d3/bindHistogram.ts
new file mode 100644
index 00000000..c00685e3
--- /dev/null
+++ b/src/d3/bindHistogram.ts
@@ -0,0 +1,113 @@
+/**
+ * D3 binder for histograms.
+ *
+ * Extracts data from D3.js-rendered histogram SVG elements and generates
+ * the MAIDR JSON schema for accessible histogram interaction.
+ */
+
+import type { HistogramPoint, Maidr, MaidrLayer } from '../type/grammar';
+import type { D3BinderResult, D3HistogramConfig } from './types';
+import { TraceType } from '../type/grammar';
+import { buildAxes, generateId, queryD3Elements, resolveAccessor, scopeSelector } from './util';
+
+/**
+ * Binds a D3.js histogram to MAIDR, generating the accessible data representation.
+ *
+ * D3 histograms are typically created with `d3.bin()` (or `d3.histogram()` in v5),
+ * which produces arrays with `x0` and `x1` properties for bin boundaries. This
+ * binder extracts bin data from D3-bound rect elements.
+ *
+ * @param svg - The SVG element containing the D3 histogram.
+ * @param config - Configuration specifying the selector and data accessors.
+ * @returns A {@link D3BinderResult} with the MAIDR data and generated layer.
+ *
+ * @example
+ * ```ts
+ * // D3 histogram using d3.bin()
+ * const result = bindD3Histogram(svgElement, {
+ * selector: 'rect.bar',
+ * title: 'Age Distribution',
+ * axes: { x: 'Age', y: 'Count' },
+ * x: (d) => `${d.x0}-${d.x1}`,
+ * y: (d) => d.length,
+ * xMin: 'x0',
+ * xMax: 'x1',
+ * yMin: () => 0,
+ * yMax: (d) => d.length,
+ * });
+ * ```
+ */
+export function bindD3Histogram(svg: Element, config: D3HistogramConfig): D3BinderResult {
+ const {
+ id = generateId(),
+ title,
+ subtitle,
+ caption,
+ axes,
+ format,
+ selector,
+ x: xAccessor = 'x',
+ y: yAccessor = 'y',
+ xMin: xMinAccessor = 'x0',
+ xMax: xMaxAccessor = 'x1',
+ yMin: yMinAccessor = (_d: unknown, _i: number) => 0,
+ yMax: yMaxAccessor,
+ } = config;
+
+ const elements = queryD3Elements(svg, selector);
+ if (elements.length === 0) {
+ throw new Error(
+ `No elements found for selector "${selector}". `
+ + `Ensure the D3 chart has been rendered and the selector matches the histogram bar elements.`,
+ );
+ }
+
+ const data: HistogramPoint[] = elements.map(({ datum, index }) => {
+ if (!datum) {
+ throw new Error(
+ `No D3 data bound to element at index ${index}. `
+ + `Ensure D3's .data() join has been applied to the "${selector}" elements.`,
+ );
+ }
+
+ // For D3 bin data, the datum is typically an array with x0/x1 properties.
+ // The "y" value is usually the array length (count of items in the bin).
+ const xValue = resolveAccessor(datum, xAccessor, index);
+ const yValue = resolveAccessor(datum, yAccessor, index);
+ const xMin = resolveAccessor(datum, xMinAccessor, index);
+ const xMax = resolveAccessor(datum, xMaxAccessor, index);
+ const yMin = resolveAccessor(datum, yMinAccessor, index);
+ const yMax = yMaxAccessor
+ ? resolveAccessor(datum, yMaxAccessor, index)
+ : Number(yValue);
+
+ return {
+ x: xValue,
+ y: yValue,
+ xMin,
+ xMax,
+ yMin,
+ yMax,
+ };
+ });
+
+ const layerId = generateId();
+ const layer: MaidrLayer = {
+ id: layerId,
+ type: TraceType.HISTOGRAM,
+ title,
+ selectors: scopeSelector(svg, selector),
+ axes: buildAxes(axes, format),
+ data,
+ };
+
+ const maidr: Maidr = {
+ id,
+ title,
+ subtitle,
+ caption,
+ subplots: [[{ layers: [layer] }]],
+ };
+
+ return { maidr, layer };
+}
diff --git a/src/d3/bindLine.ts b/src/d3/bindLine.ts
new file mode 100644
index 00000000..38952c6b
--- /dev/null
+++ b/src/d3/bindLine.ts
@@ -0,0 +1,223 @@
+/**
+ * D3 binder for line charts.
+ *
+ * Extracts data from D3.js-rendered line chart SVG elements and generates
+ * the MAIDR JSON schema for accessible line chart interaction.
+ */
+
+import type { LinePoint, Maidr, MaidrLayer } from '../type/grammar';
+import type { D3BinderResult, D3LineConfig, DataAccessor } from './types';
+import { TraceType } from '../type/grammar';
+import { buildAxes, generateId, queryD3Elements, resolveAccessor, resolveAccessorOptional, scopeSelector } from './util';
+
+/**
+ * Binds a D3.js line chart to MAIDR, generating the accessible data representation.
+ *
+ * Supports both single-line and multi-line charts. Data can be extracted from:
+ * 1. D3-bound data on point elements (circles, etc.) via `pointSelector`.
+ * When using `pointSelector`, each line path and its associated points
+ * must share the same parent `` group element for correct scoping.
+ * 2. D3-bound data on the path elements themselves (array of points per path).
+ *
+ * @param svg - The SVG element containing the D3 line chart.
+ * @param config - Configuration specifying selectors and data accessors.
+ * @returns A {@link D3BinderResult} with the MAIDR data and generated layer.
+ *
+ * @example
+ * ```ts
+ * // Multi-line chart with paths and point circles
+ * const result = bindD3Line(svgElement, {
+ * selector: 'path.line',
+ * pointSelector: 'circle.data-point',
+ * title: 'Temperature Over Time',
+ * axes: { x: 'Month', y: 'Temperature (F)' },
+ * x: 'month',
+ * y: 'temp',
+ * fill: 'city',
+ * });
+ * ```
+ */
+export function bindD3Line(svg: Element, config: D3LineConfig): D3BinderResult {
+ const {
+ id = generateId(),
+ title,
+ subtitle,
+ caption,
+ axes,
+ format,
+ selector,
+ pointSelector,
+ x: xAccessor = 'x',
+ y: yAccessor = 'y',
+ fill: fillAccessor = 'fill',
+ } = config;
+
+ const lineElements = queryD3Elements(svg, selector);
+ if (lineElements.length === 0) {
+ throw new Error(
+ `No elements found for selector "${selector}". `
+ + `Ensure the D3 chart has been rendered and the selector matches the line path elements.`,
+ );
+ }
+
+ const data: LinePoint[][] = [];
+
+ if (pointSelector) {
+ // Determine whether line paths have distinct parent elements.
+ // Pattern A: Each lives in its own with its points.
+ // Pattern B: All s and s share a single parent .
+ const parents = new Set(
+ lineElements.map(({ element }) => element.parentElement ?? svg),
+ );
+
+ if (parents.size >= lineElements.length) {
+ // Pattern A: distinct parents – scope point queries per parent
+ for (const { element } of lineElements) {
+ const parent = element.parentElement ?? svg;
+ const points = queryD3Elements(parent, pointSelector);
+ const lineData = extractPointsFromElements(
+ points,
+ xAccessor,
+ yAccessor,
+ fillAccessor,
+ pointSelector,
+ );
+ if (lineData.length > 0) {
+ data.push(lineData);
+ }
+ }
+ } else {
+ // Pattern B: shared parent – query all points once and group by fill
+ const allPoints = queryD3Elements(svg, pointSelector);
+ if (allPoints.length === 0) {
+ throw new Error(
+ `No point elements found for selector "${pointSelector}" within the SVG.`,
+ );
+ }
+
+ const lineMap = new Map();
+ const lineOrder: string[] = [];
+
+ for (const { datum, index } of allPoints) {
+ if (!datum) {
+ throw new Error(
+ `No D3 data bound to point element at index ${index}. `
+ + `Ensure D3's .data() join has been applied to the "${pointSelector}" elements.`,
+ );
+ }
+ const point: LinePoint = {
+ x: resolveAccessor(datum, xAccessor, index),
+ y: resolveAccessor(datum, yAccessor, index),
+ };
+ const fill = resolveAccessorOptional(datum, fillAccessor, index);
+ if (fill !== undefined) {
+ point.fill = fill;
+ }
+
+ const key = fill ?? '__default__';
+ if (!lineMap.has(key)) {
+ lineOrder.push(key);
+ lineMap.set(key, []);
+ }
+ lineMap.get(key)!.push(point);
+ }
+
+ for (const key of lineOrder) {
+ data.push(lineMap.get(key)!);
+ }
+ }
+ } else {
+ // Extract data from the path element's bound data directly
+ // D3 line charts typically bind an array of points to each path
+ for (const { datum } of lineElements) {
+ if (!datum) {
+ throw new Error(
+ `No D3 data bound to line path element. `
+ + `Ensure D3's .data() join has been applied to the "${selector}" elements, `
+ + `or provide a pointSelector.`,
+ );
+ }
+
+ const pointArray = Array.isArray(datum) ? datum : [datum];
+ const lineData: LinePoint[] = pointArray.map((d: unknown, index: number) => {
+ const point: LinePoint = {
+ x: resolveAccessor(d, xAccessor, index),
+ y: resolveAccessor(d, yAccessor, index),
+ };
+ const fill = resolveAccessorOptional(d, fillAccessor, index);
+ if (fill !== undefined) {
+ point.fill = fill;
+ }
+ return point;
+ });
+
+ if (lineData.length > 0) {
+ data.push(lineData);
+ }
+ }
+ }
+
+ // Extract legend labels from fill values
+ const legend: string[] = [];
+ for (const lineData of data) {
+ const fill = lineData[0]?.fill;
+ if (fill) {
+ legend.push(fill);
+ }
+ }
+
+ const layerId = generateId();
+ const selectorValue = scopeSelector(svg, selector);
+ const layer: MaidrLayer = {
+ id: layerId,
+ type: TraceType.LINE,
+ title,
+ selectors: selectorValue,
+ axes: buildAxes(axes, format),
+ data,
+ };
+
+ const maidr: Maidr = {
+ id,
+ title,
+ subtitle,
+ caption,
+ subplots: [[{
+ ...(legend.length > 0 ? { legend } : {}),
+ layers: [layer],
+ }]],
+ };
+
+ return { maidr, layer };
+}
+
+/**
+ * Extracts LinePoint data from a set of queried D3 elements.
+ */
+function extractPointsFromElements(
+ points: { element: Element; datum: unknown; index: number }[],
+ xAccessor: DataAccessor,
+ yAccessor: DataAccessor,
+ fillAccessor: DataAccessor,
+ pointSelector: string,
+): LinePoint[] {
+ const lineData: LinePoint[] = [];
+ for (const { datum, index } of points) {
+ if (!datum) {
+ throw new Error(
+ `No D3 data bound to point element at index ${index}. `
+ + `Ensure D3's .data() join has been applied to the "${pointSelector}" elements.`,
+ );
+ }
+ const point: LinePoint = {
+ x: resolveAccessor(datum, xAccessor, index),
+ y: resolveAccessor(datum, yAccessor, index),
+ };
+ const fill = resolveAccessorOptional(datum, fillAccessor, index);
+ if (fill !== undefined) {
+ point.fill = fill;
+ }
+ lineData.push(point);
+ }
+ return lineData;
+}
diff --git a/src/d3/bindScatter.ts b/src/d3/bindScatter.ts
new file mode 100644
index 00000000..cca34f5c
--- /dev/null
+++ b/src/d3/bindScatter.ts
@@ -0,0 +1,87 @@
+/**
+ * D3 binder for scatter plots.
+ *
+ * Extracts data from D3.js-rendered scatter plot SVG elements and generates
+ * the MAIDR JSON schema for accessible scatter plot interaction.
+ */
+
+import type { Maidr, MaidrLayer, ScatterPoint } from '../type/grammar';
+import type { D3BinderResult, D3ScatterConfig } from './types';
+import { TraceType } from '../type/grammar';
+import { buildAxes, generateId, queryD3Elements, resolveAccessor, scopeSelector } from './util';
+
+/**
+ * Binds a D3.js scatter plot to MAIDR, generating the accessible data representation.
+ *
+ * Extracts x/y data from D3-bound SVG point elements (``, `