# Accessibility
MatPlotLibNet supports accessibility in five areas: the **data table** beside the picture, SVG semantic
structure, keyboard navigation, the color-blind safe palette, and the high-contrast theme.
The data table comes first because it matters most, and because most charting libraries do not have one. A
chart is a complex image, and WCAG 1.1.1 requires a complex image to have a text alternative that carries the
same information. A sentence of alt text does not carry that information. The data table does.
---
## The Data Table
`figure.ToDataTables()` returns the figure's data as one `ChartDataTable` per group of series. Each table can
be written out in three formats:
```csharp
using MatPlotLibNet;
double[] months = [1, 2, 3, 4, 5, 6];
double[] revenue = [2.1, 2.4, 2.2, 2.8, 3.0, 3.2];
double[] cost = [1.6, 1.7, 1.8, 1.9, 2.0, 2.1];
var figure = Plt.Create()
.WithTitle("Monthly Sales")
.AddSubPlot(1, 1, 1, ax =>
{
ax.SetXLabel("Month");
ax.Plot(months, revenue, s => s.Label = "Revenue");
ax.Plot(months, cost, s => s.Label = "Cost");
})
.Build();
foreach (var table in figure.ToDataTables())
{
string html = table.ToHtml(); //
with
and scoped
; capped at 1000 rows
string markdown = table.ToMarkdown(); // for a README, a chat transcript, or a model
string csv = table.ToCsv(); // RFC 4180, CRLF, never capped
}
```
The two series share the same x values, so they go into **one** table with three columns (`Month`, `Revenue`,
`Cost`) instead of two tables that the reader would have to line up by eye. Series that do not share an x each
get their own table. The first column takes the axis label, and shows dates when the axis is a date axis.
### What is in it
| | |
|---|---|
| **All the data, not only what is drawn** | A line with `MaxDisplayPoints` draws a downsampled path, and a streaming series draws its viewport. Both put every point they hold into the table |
| **Per-series column names** | Each series type defines its own columns: an XY series has x and y, an OHLC series has a date and four prices, a heatmap is long-form row/column/value (a row or column the axis names is written as that name), a histogram has its bins, and a treemap is its tree flattened with the depth kept as a column |
| **A caption that matches the picture** | The caption is `figure.AccessibleName()` (the alt text, else the title, else the tile labels), joined with the subplot title and the series label. The SVG `` uses the same rule |
| **Typed cells** | `DataCell` is Empty, Number or Text. A jagged row (for example an OHLC row with no volume) has an empty cell instead of a sentinel number |
| **Columns that know their axis** | `ChartDataColumn` carries a `DataAxis`. The series marks its `start` and `end` columns as x coordinates, only the axes know whether x should be shown as dates, and the composition resolves the two |
| **Two exceptions** | `QuiverKeySeries` and `Text3DSeries` are annotations, not data. They produce no table, and a figure that contains only those series produces no tables |
### On a dashboard
The data table matters most on a control-room dashboard. Three rules apply there:
| | |
|---|---|
| **A tile lists all of its fields** | `StatTileSeries` produces the columns `label · value · target · caption · trend`. The table includes the target and the trend, not only the current value, so the reader can see the comparison the tile shows. A field the tile does not have is an empty cell, never a zero |
| **A row of tiles is one table** | Every tile sits in its own subplot; that is layout, not data. Neighbouring subplots that produce the same single row merge into one table with a row per tile, so the reader gets the row the operator sees instead of five one-row grids to join by hand. Two bullet graphs on one axes merge the same way |
| **A position on a date axis reads as a time** | A state timeline's `start` and `end` are x coordinates, and `Plt.OpsDashboard()` pins a date axis on the shared window, so the table prints `2026-09-12 07:58:20`, not `46277.332`. A cell keeps the precision it was given: a midnight prints as a date, a whole minute as a minute, a thirty-second sample as a second. If the value were rounded to the minute, two samples taken thirty seconds apart would print as the same time |
```
**Order service — gateway**
| label | value | target | caption | trend |
| --- | --- | --- | --- | --- |
| Buses | 15 | | all 15 normal | |
| RFx p99 | 28.1 | 25 | target 25 ms · +3.1 | 24.1, 24.8, 25.3, 26… |
**Order service — gateway — Exchange feed**
| state | start | end |
| --- | --- | --- |
| normal | 2026-09-12 07:55 | 2026-09-12 07:57 |
| degraded | 2026-09-12 07:57 | 2026-09-12 07:58:20 |
```
### A chart built in one call
`Pareto`, `BumpChart`, `Ridgeline` and the other verbs that build a chart in one call add ordinary series, so the table
holds those series and nothing more, and the table names what the axis names. A lollipop, a dumbbell, a slope graph,
a dot plot and parallel coordinates stand on the centres of the slots their axis names, so each row is keyed by that
name, under the axis title or `category`: a Pareto chart is one table (`category`, `Count`, `Cumulative %`) and a dot
plot one table with a column per group. The names come from the axis, by the rule the picture writes them with, so a
line on the centres of named bars joins the bars' table and a line between the names keeps its numbers. A ridgeline
is one table with a column per group, named by the group, of each ridge's height above its baseline (the tallest
reaches 1). A calendar heatmap's table names each cell's weekday and numbers its week column; it holds no dates,
because the chart is a heatmap of its cells. A Marimekko chart's table keeps its column edges (the widths are the
data) and names its segments, not its columns. The tick labels are inside the picture, which a screen reader
announces by its alt text, so say what the chart is in `WithAltText` and `WithDescription`.
### Where to put it
| Host | How |
|---|---|
| Blazor | `` renders a `` disclosure after the chart, in every display mode |
| ASP.NET Core | `app.MapChartTableEndpoint("/chart/sales.table", ctx => figure)` beside `MapChartSvgEndpoint` |
| GraphQL | `{ chartDataTable(chartId: "demo") }` beside `chartSvg` |
| MCP | the `chart_data_table` tool returns markdown a model can read, and the same values as structured content it can compute with |
| Anywhere else | put `table.ToHtml()` into your page, and offer `table.ToCsv()` as a download |
A disclosure immediately after the chart is the placement that the WCAG technique for complex images names. It
sits in the reading order exactly where the reader needs it, and a sighted reader sees only one closed line.
`ShowDataTable` defaults to `false` because the layout is your page's decision. There is no accessibility
reason to leave it off.
The markup carries `class="mpl-data-table"` (and `class="mpl-data-table-details"` on the disclosure) and has no
styling of its own, so the same fragment works in a dark dashboard, a printed report and a plain page.
### Where it stops
HTML and markdown output cap at 1000 rows and end with a `… N more rows` line. A table nobody can page through
is not a usable alternative, and a 50,000-row `
` makes a page that never finishes laying out. `ToCsv()`
never caps: a download has no reading order to protect, and a truncated file would lose data without any
warning. When the cap applies, offer the CSV.
---
## SVG Semantics
Every SVG produced by MatPlotLibNet carries:
| Attribute / Element | Value | Purpose |
|---|---|---|
| `role="img"` | on `