Skip to content

Accessibility

H.P. Gansevoort edited this page Sep 12, 2026 · 8 revisions

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:

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.

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 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

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
**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 |

Where to put it

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.

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 <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.


SVG Semantics

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 has role="img", so the aria-label on a legend, colour bar or series group is never announced. Those labels are still useful for tooling, for a hyperlinked figure (where the role is group and 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.

Setting Alt Text

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.


Keyboard Navigation

All five interactive SVG features are keyboard-accessible:

Legend Toggle

  • Each legend entry receives role="button", tabindex="0", and aria-pressed="false"
  • Press Enter or Space to toggle series visibility
  • aria-pressed updates to reflect current state

Series Highlight

  • Each series group receives tabindex="0"
  • Focus highlights the series (dims siblings); Blur restores all

Zoom / Pan

  • The SVG receives tabindex="0" and aria-roledescription="interactive chart"
  • + or =: zoom in
  • -: zoom out
  • Arrow keys (←→↑↓): pan
  • Home: reset to original view

Selection

  • Escape: cancel an active shift-drag selection rectangle

Tooltips

  • Tooltip div carries role="tooltip" and aria-live="polite"
  • Focus on a <title>-parent element triggers the tooltip; Blur hides it

Color-Blind Safe Palette

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

High-Contrast Theme

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");

Serialization

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 properties

Clone this wiki locally