Repository navigation
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.
figure.ToDataTables() returns the figure's data as one ChartDataTable per group of series. Each table can
be written out in three formats:
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(); // <table> with <caption> and scoped <th>; 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.
| 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 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 <title> 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 |
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 |
**Synapse — federation**
| 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… |
**Synapse — federation — 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 |
| Host | How |
|---|---|
| Blazor |
<MplChart Figure="fig" ShowDataTable="true" /> renders a <details> 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.
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 <table> 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.
Every SVG produced by MatPlotLibNet carries:
| Attribute / Element | Value | Purpose |
|---|---|---|
role="img" |
on <svg>, or role="group" when the figure contains a hyperlink |
Marks the SVG as one image for assistive tech |
<title id="chart-title"> |
the alt text, else the figure title, else the tile labels, else empty | Screen-reader label |
<desc id="chart-desc"> |
Figure.Description (optional) |
Longer description |
aria-labelledby="chart-title" |
on <svg>
|
Links to <title>
|
aria-describedby="chart-desc" |
on <svg> when description set |
Links to <desc>
|
aria-label="Chart legend" |
on legend group | Identifies legend section |
aria-label="Color bar" |
on color bar group | Identifies color bar |
aria-label="{series label}" |
on labeled series group | Names each data series |
role="img"hides the group labels from screen readers. Under WAI-ARIA's children-presentational rule, the whole SVG is one node when it hasrole="img", so thearia-labelon a legend, colour bar or series group is never announced. Those labels are still useful for tooling, for a hyperlinked figure (where the role isgroupand the labels are reachable), and for a future version in which the figure is navigable. Today a screen-reader user hears only the<title>and the<desc>: one sentence, however many values the chart contains. That is why the data table above exists.
Plt.Create()
.WithAltText("Monthly revenue Q1 2025")
.WithDescription("Line chart showing revenue growth from Jan to Mar 2025, peaking at $3.2M in March.")
.Plot(months, revenue)
.Save("revenue.svg");WithAltText maps to the SVG <title> element. If omitted, WithTitle is used as the fallback. WithDescription maps to <desc> and is optional.
All five interactive SVG features are keyboard-accessible:
- Each legend entry receives
role="button",tabindex="0", andaria-pressed="false" - Press Enter or Space to toggle series visibility
-
aria-pressedupdates to reflect current state
- Each series group receives
tabindex="0" - Focus highlights the series (dims siblings); Blur restores all
- The SVG receives
tabindex="0"andaria-roledescription="interactive chart" -
+or=: zoom in -
-: zoom out - Arrow keys (←→↑↓): pan
-
Home: reset to original view
- Escape: cancel an active shift-drag selection rectangle
- Tooltip div carries
role="tooltip"andaria-live="polite" -
Focus on a
<title>-parent element triggers the tooltip; Blur hides it
Theme.ColorBlindSafe uses the Okabe-Ito 8-color palette, which is distinguishable under all three common forms of color vision deficiency.
For a chart with more than eight things on it, the colormap registry also carries the three Petroff sequences —
petroff6, petroff8 and petroff10 — from Accessible Color Sequences for Data Visualization. Every pair
inside a sequence stays apart under the common forms of colour vision deficiency, and in grayscale too. Pick the
one whose number matches how many things you are drawing.
Plt.Create()
.WithTheme(Theme.ColorBlindSafe)
.Plot(x, y, s => s.Label = "Revenue")
.Plot(x, y2, s => s.Label = "Cost")
.Save("colorblind.svg");The palette is also available as a colormap:
// Named "okabe_ito" / "okabe_ito_r" in the registry
var map = ColorMaps.Get("okabe_ito");Colors (in cycle order):
| # | Hex | Name |
|---|---|---|
| 1 | #E69F00 |
Orange |
| 2 | #56B4E9 |
Sky blue |
| 3 | #009E73 |
Bluish green |
| 4 | #F0E442 |
Yellow |
| 5 | #0072B2 |
Blue |
| 6 | #D55E00 |
Vermillion |
| 7 | #CC79A7 |
Reddish purple |
| 8 | #000000 |
Black |
Theme.HighContrast targets WCAG AAA (7:1 contrast ratio). Pure white background and pure black text yield a 21:1 contrast ratio. The default font is bold at 13pt; the grid is 1.5px dark.
Plt.Create()
.WithTheme(Theme.HighContrast)
.Plot(x, y)
.Save("high-contrast.svg");Figure.AltText and Figure.Description are included in the JSON round-trip:
var figure = Plt.Create()
.WithAltText("Sales chart")
.WithDescription("Monthly sales data for 2025")
.Plot(x, y)
.Build();
string json = ChartServices.Serializer.ToJson(figure); // includes altText / description
Figure restored = ChartServices.Serializer.FromJson(json); // restores both propertiesMatPlotLibNet 1.18.2