Chart
A thin, accessibility-corrected wrapper over AG Charts Community. It resolves every mark colour from tokens in JS (a canvas cannot read CSS), honours prefers-reduced-motion and forced colors on the library’s behalf, and gives the chart a real accessible name instead of the generated “chart, N series”.
Preview
A chart ships with its data as a grid — see Text alternative below. The pairing is not decorative; it is what makes the chart conformant.
<esa-chart label="Rainfall by month"></esa-chart>
<!-- data and series are set as PROPERTIES, not attributes -->
<script>
chart.data = [{ month: 'Jan', rain: 20 }, /* … */];
chart.series = [{ type: 'bar', xKey: 'month', yKey: 'rain', yName: 'Rainfall (mm)' }];
</script> Multiple series
With more than one series a legend appears, and each series takes the next colour from the data-viz categorical family in fixed order — never cycled, never re-assigned when a filter changes the series count. Line and scatter series also take a rotating marker shape, so identity is never carried by hue alone.
chart.series = [
{ type: 'bar', xKey: 'month', yKey: 'rain', yName: 'Rainfall (mm)' },
{ type: 'line', xKey: 'month', yKey: 'temp', yName: 'Temperature (C)' },
]; API
| Prop | Type | Default | Description |
|---|---|---|---|
label | string | '' | The chart's accessible name. Required — without it AG Charts announces a generated name like “chart, 2 series”, which names the shape of the thing rather than what it shows. |
data | unknown[] | [] | Row objects, passed to AG Charts untouched. |
series | Array<Record<string, unknown>> | [] | Series definitions. Marker shapes are layered in automatically so series differ by form as well as hue. |
options | Record<string, unknown> | {} | Escape hatch merged over everything the component computes — use it to reach any raw AG Charts option. |
color-scale | 'categorical' | 'sequential' | 'diverging' | 'categorical' | Which tier-2 data-viz family to read: categorical (identity), sequential (magnitude), or diverging (polarity). Property: `colorScale`. |
described-by | string | undefined | — | ID of prose describing the chart’s takeaway. Resolved in the light DOM and applied by element reflection, because an IDREF cannot cross a shadow boundary. Property: `describedBy`. |
height | string | '320px' | Plot height. The chart fills its container’s width. |
Installing
ag-charts-community is an optional peer dependency, loaded
by dynamic import on first render. A spoke that never charts never pays the ~2.8 MB,
and @esa/ecology’s own dependency list stays at lit alone.
npm install ag-charts-community Accessibility
AG Charts was scored in a real browser against the five questions worth asking of any charting library. It passes the two that are hardest to retrofit. The rest is what this wrapper exists to fix.
| Question | AG Charts alone | What esa-chart does |
|---|---|---|
| SVG or canvas? | Canvas — but with a synchronized proxy DOM of real elements carrying the semantics, and the canvas itself marked aria-hidden. | Nothing needed. This is why AG Charts is usable at all where most canvas libraries are not. |
| Default role and name? | Passes — role="figure" unprompted — but the generated name is the generic "chart, N series", and there is no option to change it. | Requires label and writes it onto the proxy root. Warns at runtime when absent rather than shipping the generic name silently. |
| Text or data-table alternative? | None. No table, no figcaption, no offscreen text. Values are reachable only by traversing the chart. | Not solved in the component — solved by pairing it with a grid. |
| Keyboard access to data points? | Passes, on by default. Arrows walk the data, Home/End jump to first and last, and focus is not trapped. | Nothing needed. This is the strongest of the five answers. |
Respects prefers-reduced-motion? | No. The string appears zero times in the library; every matchMedia call is for HiDPI scaling. | Reads the query itself and sets animation.enabled, re-deciding when the preference changes mid-session. |
Three more, found by measuring rather than reading the docs
- Legend hit targets render 16px tall — under the 24px raw floor, and
reported rather than patched. That is not a violation: SC 2.5.8’s spacing
exception passes an undersized target whose 24px circle clears its neighbours’, and
the default legend spacing earns it — axe reports zero target-size violations here.
Forcing it would mean 24px legend markers, since
marker.sizeis the only option that moves the box (item.paddingYis not an option at all; AG Charts accepts and ignores it). The exception is layout-dependent: a legend packed tight enough loses it, so ifnpm run a11yever reports target-size on a chart, the lever isoptions.legend.item.marker.size. - Forced colors cannot reach a canvas. It is a bitmap — no token, no
@mediablock and no assurance profile touches it. The only route in is to re-theme from JS with the system keywords, which this component does. It deliberately discards the data-viz palette in that mode: the user chose those colours, and overriding them would itself be SC 1.4.8. Marks stay apart by shape. - Series told apart by colour alone is SC 1.4.1, Level A. Line, area and scatter series get a rotating marker shape. Bars cannot carry a pattern in Community — for those the answer is direct labels or the paired grid.
What is still yours to do
- Give every chart a real
label. The component warns, but it cannot invent one. - Ship the paired grid. See below.
- Nothing automated proves a screen reader announces any of this correctly. Structure is provable; announcement is not. That needs NVDA/Firefox and VoiceOver/Safari.
Text alternative — pair it with a grid
AG Charts ships no data-table view, and the chart alone does not satisfy the
text-alternative requirement. The answer is a composition rather than a prop:
esa-grid already solves tabular semantics, and a visible
“view as data” gives sighted keyboard users a real affordance instead of an
offscreen table only a screen reader can find.
<esa-chart label="Rainfall by month" .data={rows} .series={series} />
<esa-collapsible summary="View as data">
<esa-grid columnDefs={cols} rowData={rows} />
</esa-collapsible> Theming — why a canvas needs a bridge
A grid re-themes for free because it is DOM. A chart does not: measured, overriding the
library’s own --ag-charts-* custom properties changes
getComputedStyle and leaves the canvas painting its original colours. Those
~60 properties style the DOM chrome — tooltip, legend, menus, focus ring
— and about a third of the theming surface is therefore ordinary CSS. Every
mark, though, is resolved in JS and handed over as a literal.
Which means a canvas does not repaint when the cascade changes. The component watches
data-theme, data-scheme and data-a11y-assurance on
<html> and re-resolves. Without that, switching theme leaves the chart
painted in the old brand with no error anywhere.