# 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 | `<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 ```csharp using MatPlotLibNet; double[] months = [1, 2, 3]; double[] revenue = [2.4, 2.8, 3.2]; 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](https://jfly.uni-koeln.de/color/) 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. ```csharp using MatPlotLibNet; using MatPlotLibNet.Styling; double[] x = [1, 2, 3, 4, 5]; double[] y = [2.0, 2.4, 2.9, 3.1, 3.6]; double[] y2 = [1.5, 1.7, 1.8, 2.2, 2.3]; 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: ```csharp using MatPlotLibNet.Styling.ColorMaps; // 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. ```csharp using MatPlotLibNet; using MatPlotLibNet.Styling; double[] x = [1, 2, 3, 4, 5]; double[] y = [2.0, 2.4, 2.9, 3.1, 3.6]; 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: ```csharp using MatPlotLibNet; using MatPlotLibNet.Models; double[] x = [1, 2, 3, 4, 5]; double[] y = [2.0, 2.4, 2.9, 3.1, 3.6]; 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 ```