MAIDR Data Schema
This document describes the JSON data schema used to define plots in maidr.
Schema Structure
Your JSON schema should be a single maidr object with the following properties, or an array of objects if multiple plots exist on the page.
A single plot:
// a single plot
<script>
var maidr = {
id: "barplot_1",
subplots: [
[
{
id: "barplot_1", //add the same id to the svg component
layers: [
{
id: "bar_layer1",
type: "bar",
title: "Sample Bar plot",
axes: {
x: { label: "Category" },
y: { label: "Value" }
},
data: [
{
"x": "A",
"y": 10
},
{
"x": "B",
"y": 24
},
{
"x": "C",
"y": 15
},
{
"x": "D",
"y": 7
}
]
}
]
}
]
]
}
</script>
Or multiple plots:
<script>
var maidr = {
"id": "multipanel_plot",
"subplots": [
[
{
"id": "line1",
"layers": [
{
"id": "line_layer",
"type": "line",
"title": "Line Plot: Random Data",
"axes": {
"x": { "label": "X-axis" },
"y": { "label": "Values" }
},
"data": [
[]
],
}
]
}
],
[
{
"id": "bar1",
"layers": [
{
"id": "bar1_layer",
"type": "bar",
"title": "Bar Plot: Random Values",
"axes": {
"x": { "label": "Categories" },
"y": { "label": "Values" }
},
"data": []
}
]
}
],
[
{
"id": "bar2",
"layers": [
{
"id": "bar2_layer",
"type": "bar",
"title": "Bar Plot 2: Random Values",
"axes": {
"x": { "label": "Categories" },
"y": { "label": "Values" }
},
"data": []
}
]
}
]
]
}
</script>
Object Properties
Use the following to define the object properties:
type: the type of plot. The declarable types arealluvial,area,bar,box,boxen,bump,candlestick,chord,choropleth,contour,directed_graph,diverging_bar,dodged_bar,dot,dumbbell,error_bar,forest,funnel,gantt,gauge,heat,hexbin,hist,icicle,line,lollipop,manhattan,mosaic,network,pack,parallel_coordinates,percentile_band,pie,point,polar_area,pr_curve,radar,ridgeline,roc,rug,sankey,sequence,smooth,stacked_area,stacked_bar,stacked_normalized_area,stacked_normalized_bar,step,sunburst,sunflower,survival,tree,treemap,venn,violin_box,violin_kde,volcano,waterfall,word_cloud.TraceTypeinsrc/type/grammar.tsis the source of truth;candlestick_deltaappears there but is built at runtime from a candlestick and a reference line, so it is not something a page declares. Not all of them are equally settled — see Trace type stability.
candlestick_deltahas no example page, on purpose. It should never be given one: it is a reading mode the model derives at runtime from acandlesticklayer (src/model/candlestickDelta.ts, reached with Alt+L), not a value a page declares.TraceFactory.createinsrc/model/factory.tshas no case for it, so a layer declaring it throwsInvalid trace type: candlestick_deltaand the figure never binds — an example would document something that does not exist. An audit of example coverage that finds this gap has found the correct state of affairs.It is the only deliberate gap. Other declarable types that lack a hand-authored example are genuinely missing one, and adding it is a normal contribution. The last three —
alluvial,areaandmanhattan— were filled byexamples/alluvial.html,examples/area-overlapping.htmlandexamples/manhattan.html, so every declarable type now has a page carrying its JSON. Note thatexamples/area.htmldeclaresstacked_area, notarea;examples/area-overlapping.htmlis the plain one, and the pair is written to be read together, since the difference between independent bands and bands that add up is the thing the missing page had left ambiguous.
id: the id that you added as an attribute of your main SVG.title: the title of the plot. In a single-panel chart with no figuretitle, it also names the chart for screen readers (see below). (optional)typeLabel: what the chart is called when it is read as a type it is not, such as"Deal Pipeline"for a board of deals read as atree. It replaces the type's name in the chart's accessible name, the entry instruction, the description's Chart Type and a layer switch, and the description then leaves out its guide to the type. Written as given in every language, astitleis; a diagram MAIDR knows is better named bydiagramType, below. (optional)typeGuide: the description's "About this chart type" guide to the charttypeLabelnames, as{ "definition": …, "purpose": …, "appearance": … }: what the chart is, what it is used for, and what it looks like, by shape and position rather than by colour alone. It is shown in place of the guide to the type, and only beside atypeLabel, since the guide is headed with the chart's name. A guide with a part missing or blank is not shown, and MAIDR says why in the console. Written as given in every language, astypeLabelis, so a producer whose readers share a language other than English should write both in theirs. (optional)diagramType: the diagram the chart is, when it is one MAIDR knows but reads as a type it is not, such as"flowchart"for a Mermaid flowchart read as adirected_graph. MAIDR names it wherevertypeLabelwould name the chart, and explains it in the description's "About this chart type" guide, both in the reader's language, as it does a type. Where MAIDR knows the diagram it wins overtypeLabelandtypeGuide; give those as well for a MAIDR that does not, and they are read there as before. A diagram MAIDR does not know is said in the console. MAIDR knowsflowchart,swimlane,agent_flow,state,class,entity_relationship,requirement,use_case,c4and its levelsc4_context,c4_container,c4_component,c4_dynamicandc4_deployment,block,architecture,git_graph,event_model,mindmap,kanban,tree_view,ishikawa,timeline,railroad,cynefin,quadrant,user_journey,packetandwardley_map;DiagramTypeinsrc/type/grammar.tssays what each one is. (optional)axes: axes info for your plot. Each axis is a per-axis object:maidr.axes.x,maidr.axes.y, and (when used)maidr.axes.z. Supported properties per axis:label(string),min/max(number bounds),tickStep(number), andformat(anAxisFormatobject controlling numeric / categorical rendering; an axis without one announces numbers rounded to two decimals, with digits grouped in threes from 10,000 up, e.g.1,234,567, using,and.whatever the locale -- declare anumberformat with alocalefor another convention; a numeric category such as an id or a20240115date key is grouped too, so declare"format": { "function": "return String(value);" }on that axis to keep it literal).labelis optional and defaults toX,Y, orLevelfor the respective axis. Bare string values for axes are no longer accepted.data: the main data for your plot. See below.
Top-Level Figure Properties
The top-level maidr object also accepts optional figure-wide metadata that
applies across all subplots:
title,subtitle,caption(string): figure-level text. In a multi-panel figure,l tin the lobby announces the figuretitle, falling back to the focused subplot's own title when no figure title is authored.The title also names the chart. The element a reader focuses is named by the title and the chart's type, such as
Tips by day, Bar Chart, and by the type alone when there is no title; a single-panel chart with no figuretitletakes its first layer's. How to use the chart is its description (aria-describedby), not its name, so a screen reader lists and moves between charts by what they are. A page that describes the chart too, as r-maidr does with an author's alt text and caption, puts its ids before MAIDR's in that attribute: MAIDR keeps them, and keeps its own last.axes(object): figure-wide axis labels shared by every subplot — e.g. a facet grid whose panels all sit on one common X and Y axis. Onlylabelis honored at the figure level (the type isPick<AxisConfig, 'label'>, so a layer'smin/max/tickStep/formathave no figure-wide meaning):var maidr = { id: "facet_grid", title: "Sales by Region", axes: { x: { label: "Year" }, y: { label: "Revenue" } }, subplots: [ /* ... */ ] };In the multi-panel lobby,
l x/l yannounce the figure-wide label when authored ("Figure X label is Year"); otherwise they fall back to the focused subplot's own axis ("Subplot 2, X label is ..."). Omittingaxeskeeps the existing behavior, so this is fully backward compatible. Onlyxandyare read at the figure level — there is no figure-widez, since the Z axis is inherently per-trace, sol zin the lobby always reports the focused subplot's own Z label.
Pointer Interaction
hoverMode("pointermove","click"or"off"): how the pointer moves the reader's position in the chart, and with it the highlight, sound and announcement.pointermove, the default, follows the pointer as it hovers;clickmoves only where the reader clicks;offleaves the pointer out of it, so only the keyboard navigates.var maidr = { id: "quiet_chart", hoverMode: "click", subplots: [ /* ... */ ] };This is the chart's starting value for the reader's Hover Mode setting, not a lock on it. Once a reader changes Hover Mode in the settings, on any chart, their mode is kept on every chart, the default among them. Until then they see each chart's own mode, and saving other settings on a chart leaves their stored mode as it was, so one chart's mode does not follow them to the next. Picking the mode the dialog already shows is not a change, so it leaves them on each chart's own mode. Resetting the settings comes back to the chart's mode. Any other value is ignored with a console warning.
Top-Level Properties for Live Charts
The top-level maidr object accepts two optional properties for realtime/streaming scenarios (see the Live & Streaming Data guide):
live(boolean): enables live mode — in-place data updates viawindow.maidrLive.setData()/appendData()and the M monitor-mode key.maxWidth(number): sliding window size; appending a point beyond this width drops the oldest point(s), keeping at mostmaxWidthpoints per series.
var maidr = {
id: "live_chart",
live: true,
maxWidth: 50,
subplots: [ /* ... */ ]
};
Trace type stability
Not every declarable type carries the same promise, and that is not visible from the list above.
Fifteen of them predate the chart-type coverage roadmap (#814). Thirty-seven
were added by it, most of them inside about two weeks, and rug (#1132),
roc, pr_curve (#1349), percentile_band (#1348), directed_graph (#1347),
and sequence and venn (for Mermaid's sequence and Venn diagrams) after it.
None of the forty-four has been through a user study.
Elsewhere in these docs — the braille guide, the integration guides' supported-type tables and the examples gallery — an experimental type is marked [experimental] after its name; a type with no mark is stable. Wherever they enumerate chart types, the two are also kept apart rather than interleaved: the stable part comes first, under its own heading or in its own table or list, and the experimental part follows it — the same order as the two lists below. py-maidr (Plot Type Stability) and maidr for R (Supported plot types) mark their own docs the same way.
Stable
bar, box, candlestick, dodged_bar, heat, hist, line, pie,
point, smooth, stacked_bar, stacked_normalized_bar, step,
violin_box, violin_kde
These are what MAIDR was built around. Their readings, their announcements and their keyboard model have been exercised by real users over real charts, and a change to any of them changes behaviour people already depend on.
Experimental
alluvial, area, boxen, bump, chord, choropleth, contour,
directed_graph, diverging_bar, dot, dumbbell, error_bar, forest, funnel,
gantt, gauge, hexbin, icicle, lollipop, manhattan, mosaic,
network, pack, parallel_coordinates, percentile_band, polar_area, pr_curve, radar,
ridgeline, roc, rug, sankey, sequence, stacked_area, stacked_normalized_area,
sunburst, sunflower, survival, tree, treemap, venn, volcano,
waterfall, word_cloud
These are prototypes. Treat them as prototypes.
- Under active development. They are being changed as they are used, not maintained against a settled specification.
- Unstable. Point shapes, field names, announcement wording and navigation semantics may change without a deprecation period, including in a patch release. A producer that emits one of these is pinned to the maidr version it was written against.
- Not validated. Each was measured against the drawing it reads, which is what the issues and the tests record. But measuring that a reading is faithful to the chart is a different claim from establishing that it is useful to a reader. Nobody has asked a blind or low-vision reader whether navigating a sunburst by depth, or hearing each parallel-coordinates axis on its own scale, is the right way to read one. Until that happens these are proposals about how a chart could be read, implemented and measured, not answers.
- Not a support commitment. A bug in one of these is worth reporting and is not a promise to keep the current behaviour.
If you are building something that has to keep working, build it on the stable set.
How the split is derived
It is the diff of TraceType against 84d9003, the last commit on main
before #814 was filed:
git show 84d9003:src/type/grammar.ts | grep -oE "= '[a-z_0-9]+'"
test/scripts/schemaStability.test.ts fails if a declarable type appears in
neither list or in both, so a new trace type has to be placed deliberately
rather than inherit either promise by being forgotten.
Shapes deliberately not read
A chart no trace type describes is declined rather than forced into the nearest shape, because a reading that sounds confident and says something false is worse than no reading — the lesson #814 recorded for every chart it added. A decline that is a decision rather than an omission is written down here, once, and an adapter that meets the chart points at this section rather than carrying an argument of its own. Three adapters had each written their own reason for the same chart, and the reasons did not agree (#1190).
Set-overlap diagrams (Venn, Euler)
No longer declined: read by the venn trace, which the Mermaid,
Highcharts (venn, euler), Chart.js (chartjs-chart-venn) and amCharts
(am5venn.Venn) adapters produce. The section stays so the history of the
decision is findable from the adapters that used to point here (#1190).
The objection was never the data. Every one of those producers carries the
author's own declaration of a region — sets: ['A', 'B'], value: 2. What
was missing was a trace whose navigation is set membership: a flat list of
A, B, A∩B says how big each region is, and a reader stepping through
it by index is walking a layout rather than a membership.
venn is that trace: a MovableGraph over
regions addressed as (set count, index among regions of that count), where
up and down move to regions of fewer or more sets and left and right move
between the regions sharing that count.
Two earlier reasons stay retired. That a Venn's areas are drawn approximately is true of the layout and not of what is read — the producers declare the size, and the reading announces the declaration, not the drawing. That a Venn has no axis is true, and no more of an obstacle than it was for the treemap or the sankey.
Selectors
selectors tells MAIDR which drawn element each point of a layer is, so it can
outline the one being read. It is the only part of a layer MAIDR matches
against the rendered chart rather than reading as data, and its shape means
different things to different layer types.
A layer whose selectors resolve to nothing still announces every point, sounds every value and moves the braille cursor. It loses only the outline a sighted or low-vision reader follows, which nobody who only listens will notice. MAIDR therefore says so in the browser console, once per layer:
[MAIDR] Layer "…" (bar): `selectors` is set, but it resolved to no element this layer can highlight, …
Three things cause it: a selector that matches nothing in the rendered SVG, a
shape the layer type does not read (the table below), and a payload attached
before the chart finished drawing. The last one happens to a charting library
that draws asynchronously, such as Plotly: attach the payload once the library
reports the chart drawn, not when its first <svg> appears.
Where a selector is matched
A selector is matched inside its own chart: the element carrying the maidr
or maidr-data attribute, or the children of <Maidr>, and for a Plotly
chart the layers Plotly stacks over it, where its titles, legend and gauges
are drawn. Two charts on one page that share ids, such as one output shown
twice, or whose selectors name no chart at all, as Plotly's do, each find
their own marks. The selector is still tested against the whole document, so a
part naming something round the chart -- #vl-1 svg rect,
[id="svg-id"] path -- matches as it always did.
When nothing inside the chart matches, the rest of the page is searched, leaving out every other chart MAIDR has bound, and the console says so once per selector:
[MAIDR] Selector "…" matches nothing inside its chart, so the elements it matches elsewhere on the page were used. …
Name elements inside the chart; the search beyond it is kept only so that a page which drew a chart's marks elsewhere keeps its highlight.
The shapes
- A string is one CSS selector, resolved with
querySelectorAllwhere the section above says. Several elements can be named with a selector list,"#a rect, #b rect"; the matches come back in document order whatever order the list is written in. - A list of strings means something different per layer type: one element per point, one per series, or each entry resolved and the results concatenated. The table says which.
- A grid
(string | null)[][]names one element per cell.nullsays the chart drew no element for that cell, which is different from a selector that resolves to nothing: that is a mistake, and declines the whole grid. BoxSelector[]andCandlestickSelectorname the parts of a box or a candle; their fields are described under those plot types below.
A named shape -- a <rect>, <path>, <circle> and so on -- is drawn over: a
dark one in the highlight colour, a light one in a darker shade of its own
colour, at least 3:1 against it, keeping an edge it draws in a colour of its
own. In high contrast mode, where the reader's palette paints every mark,
each is drawn in the highlight colour. A label the chart draws over the shape,
as <text> after it, that would read below 4.5:1 on the highlight is shown in
black or white while the highlight is -- in high contrast mode, in the reader's
light or dark colour -- whichever reads better on it; the label itself is left
as drawn. A named group, or any element holding such shapes, is
outlined through them instead: each shape inside it that draws anything is
stroked in the highlight colour, wider than drawn, over whatever paint its own
attributes or the page's stylesheet give it, and left unfilled, so the group
shows through; its text keeps its colour. The outline is drawn last in the
group's parent, above the marks drawn after it.
By layer type
| Layer types | Shapes read | How the matches pair with the data |
|---|---|---|
bar, hist, dot, lollipop, funnel |
string; list with one entry per bar (one row only) | A string pairs in document order; when fewer elements than bars match, bars whose value is 0 are the ones taken to be undrawn. |
dodged_bar, stacked_bar, stacked_normalized_bar, mosaic, diverging_bar |
string; grid [series][category] |
A string is walked series by series. domMapping.order: 'column' walks it category by category instead, each category from its last series unless domMapping.groupDirection is 'forward' (diverging_bar runs forward by default). A grid is in payload order. |
point, sunflower, volcano, manhattan |
string | The n-th match is the mark of the n-th point, so the string should match the marks in the order the points are listed. Each navigation outlines the marks of the points it reads: every point of a row, a column or a grid cell, or the one point it is on. When the number of matches is not the number of points, rows and columns are found by grouping the marks by the position each is drawn at, with a larger y read as lower on the page, and nothing else is outlined. |
pie |
string | Exactly one element per slice, in drawn order; reversed when direction is 'counterclockwise'. |
heat |
string; grid [row][column] |
A string names one element per cell: <rect> cells are read column by column (domMapping.order: 'row' reads them row by row), <path> cells row by row from the top, and a single <image> gets an overlay. A grid's rows run bottom first, the reverse of data.points. |
line, step, survival, smooth, area, stacked_area, stacked_normalized_area, bump, radar, polar_area, parallel_coordinates, roc, pr_curve, contour |
list with one entry per series (a string is one series) | A selector matching one element per point pairs them in document order; otherwise the vertices of the <path>, <polyline> or <polygon> it matches are the points. When a series matches several drawn elements -- a line broken by missing values, which gridSVG writes as sibling polylines -- their vertices are joined in document order and each reading is placed by its x; a missing reading gets no marker. A single element whose subpaths skip the missing values, with one vertex per reading, has its vertices paired with the readings in order. A step layer's staircase may run on to a flat vertex before its first sample and after its last, as MUI X Charts draws one on a category axis. Elements inside <defs>, <clipPath>, <marker>, <symbol>, <pattern> or <mask> (matplotlib's marker templates) are not pieces of the series. domMapping.pointOrder: 'reverse' says the chart draws them the other way round. |
percentile_band |
list with one entry per band, outermost first, and optionally one more for the median's line; or list with one entry per quantile, lowest first | The two lengths never coincide, so the count decides. One per band: entry k names the filled element between the k-th lowest and k-th highest quantile (for TensorBoard's nine levels, four polygons: minimum to maximum, then 6.68th to 93.32nd, 15.87th to 84.13th, 30.85th to 69.15th), and a point of either bound is marked where that bound's edge of the element is drawn at the point's x; the optional extra entry names the median's line, which a marker walks along, and without it the median is marked inside the innermost band, as far between its edges as its value is between the band's bounds -- a place worked out from the values, not one the chart draws, so it is approximate. An entry that matches one path, polyline, polygon or <use> of one is marked this way: at its vertex for each point's x, or, where the renderer dropped vertices a straight run did not need, as Plotly does, at the place the point's x or its position puts it along the shape, when every vertex kept stands at one of those places. One that matches several elements, or another kind, outlines all of them at every point, and so, with a console warning, does a shape whose vertices say neither -- one drawn on a log axis with vertices dropped, or a smoothed curve with more vertices than points. One per quantile: each entry is that quantile's line, read as a line series is (row above), so a marker walks along the bound. Hovering finds a point only in the per-quantile shape. With no selectors, or none that resolve, every point is still announced, sounded and brailled. |
box, violin_box |
BoxSelector[], one per box |
See the box and violin plot types below. |
violin_kde |
string; list with one entry per violin | Markers are drawn at each point's svg_x/svg_y; the list names the curve each violin belongs to. |
candlestick |
string; CandlestickSelector |
A string pairs one element per candle in document order. |
rug |
string; list with one entry per tick | Ticks are walked by position. |
boxen, ridgeline, dumbbell, error_bar, forest, gantt, hexbin, waterfall, word_cloud, gauge, alluvial, chord, sankey, network, choropleth, treemap, sunburst, icicle, tree, pack |
string; list of strings | A list's entries are each resolved and the results concatenated. The total must be one element per item the layer declares -- distribution, group, pair, sample, interval, bin, step, term, flow, link, region or node -- in declared order; gauge uses the first match. A ridgeline's element per group is its curve, and each sample is marked on top of the curve, where the sample's value is drawn; a curve that is not one path, polyline, polygon or <use> of one is outlined whole at every sample, and so, with a console warning, is one whose vertices do not say where each sample is drawn, as a percentile_band's shape is read. |
directed_graph |
string; list of strings | As the row above: one element per declared node, in declared order. A scope is derived from the nodes' paths rather than declared, so it has no element of its own; while the reader is on a closed scope, every declared node inside it is outlined. Edges are not highlighted. A count that does not match drops the highlight for the layer. |
sequence |
string; list of strings | One element per message, notes included, in declared order -- the arrow or the note. A count that does not match drops the highlight for the layer. |
venn |
string; list of strings | One element per declared region, in declared order. A set named only in an overlap has nothing drawn of its own and is not outlined. A count that does not match drops the highlight for the layer. |
Lists written for maidr.js before 4.0
Before 4.0, a list that reached a layer type reading one string was handed to
querySelectorAll as it was, and the DOM turned it into a comma-joined
selector list: ["#bars rect"] worked as "#bars rect". Producers relied on
that for years without knowing it, and 4.0 read those lists as nothing (#750)
or, for bars, as one selector per bar (#991).
That meaning is kept for them. For the bar, segmented, point, pie and
heat rows above, a list of strings that is not one entry per bar is joined
into one selector list and read as a string, and the console says so. A
segmented list that declares no domMapping.order and whose marks are
<rect> is walked category by category, as it was before #1135 made series by
series the default. Emit a string instead; the list form is read only so that
charts made by an older producer keep their highlight.
Changing this contract
What a layer type reads from selectors and domMapping is the one part of
the schema every producer matches against its own rendering, and most
producers -- r-maidr, py-maidr, hand-written pages -- are not in this
repository, so no test here can see them break. A change to it therefore keeps
the shape producers emit today working, with a console warning, for at least
one major version; carries ! and a BREAKING CHANGE: footer naming the shape
that changed; and updates this table. e2e_tests/specs/bindingOutput.spec.ts
drives real r-maidr and py-maidr output through the build, so a change that
breaks one of them fails before it is released.
Data Formats by Plot Type
The data property is defined as a list of objects where each object is a record with fields x and y.
let maidr;
// barplot maidr.data structure: a simple array of values
maidr = {
"data": [
{
"x": "A",
"y": 5.982192824845484
},
{
"x": "B",
"y": 9.309858198175455
},
{
"x": "C",
"y": 7.3531284491571505
},
]
};
// boxplot maidr.data structure: an array of objects with properties lower_outlier, min, q1, q2, q3, max, and upper_outlier
maidr = {
"data": [
{
"lowerOutliers": [
40.0,
50.0
],
"min": 71.35451232573614,
"q1": 92.62315416457983,
"q2": 99.64912548800726,
"q3": 107.6684972253361,
"max": 118.19391634772752,
"upperOutliers": [
150.0,
160.0
],
"fill": "Group 1"
},
],
"orientation": "vert" //vert for vertical box plots, horz for horizontal bar plots
}
// Optional, on the box layer: "whiskerQuantiles": [0.1, 0.9] says the whiskers
// end at those quantiles (here the 10th and 90th percentiles) rather than at
// the data's extremes or 1.5 IQR. `min` and `max` are then announced as
// "10th percentile" and "90th percentile" instead of "Minimum" and "Maximum".
// Omit it, or give [0, 1], when the whisker ends are the minimum and maximum.
// Each end is read on its own: in [0.05, 1] the upper end stays "Maximum".
//candlestick
maidr = {
"data":[
{
'value': '2023-02-16',
'open': 151.61,
'high': 151.82,
'low': 151.59,
'close': 151.8,
'volume': 0
},
]
}
//dodged_bar
maidr = {
"data":[
[
{
"x":"Adelie",
"fill":"Below",
"y":70
}
],
[ {
"x":"Adelie",
"fill":"Above",
"y":90
}]
]
}
// heatmap maidr.data structure: a 2D array of values
maidr = {
"data": {
"points": [
[ 60.5, 86.7, 89.3 ],
[ 18.6, 67.6, 83.9 ],
[ 18.5, 65.4, 78.7 ],
],
"x": [
"CoLA",
"MNLI",
"MRPC",
],
"y": [
"BERT",
"BiLSTM",
"BiLSTM+Attn",
]
}
}
//histogram
//
//A binned axis that declares `"format": { "type": "date" }` carries epoch
//milliseconds: the edges are read as dates, and `Bin width` as a span of
//time ("1 day", "2 weeks") rather than as a count of milliseconds.
maidr = {
"data":[
{
"y": 4.0,
"x": 1.1475,
"xMin": 1.0,
"xMax": 1.295,
"yMin": 0,
"yMax": 4.0
}
]
}
//line
//
//`label` is optional and names the ordinal level that a numeric `y`
//encodes, for a line whose y axis is a category rather than a magnitude
//(a sleep stage, a Likert response, a severity grade). When present it is
//announced INSTEAD of the number, so the reader hears "Sleep stage is
//REM" rather than "Sleep stage is 4"; `y` stays NUMERIC either way,
//because it drives sonification, braille and the min/max range. Omit it
//for the continuous y most line charts have.
maidr = {
"data":[
[
{
"x": 1.0,
"y": 2.0
},
{
"x": 2.0,
"y": 4.0
},
]
//add multiple arrays for multiline plots
]
}
//stacked_area and stacked_normalized_area: nested exactly like `line`, one
//inner array per band. Each point carries its band's OWN value, not the
//height its top edge is drawn at; MAIDR sums the running total itself.
//List the bands from the bottom of the stack up: the first is the bottom
//band, Up moves to the next one, drawn on top of it, and Control + Up to
//the top band. Where the bands' highlight can be measured, MAIDR reads
//which end of the list is drawn on top, so a list in legend order (top
//band first, as ggplot2 stacks it) is walked the same way; a chart drawn
//on a canvas, with nothing to measure, is read bottom band first.
maidr = {
"type": "stacked_area",
"data":[
[ { "x": "2019", "y": 10, "z": "Subscriptions" } ], //the bottom band
[ { "x": "2019", "y": 5, "z": "Services" } ] //drawn on top of it
]
}
//step: piecewise-constant data — the value is HELD across an interval and
//then jumps, rather than being interpolated the way a line implies.
//Data is nested exactly like `line`: one inner array per series.
//
//`y` stays NUMERIC — it drives sonification, braille and the min/max range.
//`label` is the same optional per-point ordinal name documented under
//`line` above, and is read identically here; a hypnogram is its canonical
//case, which is why the example below carries one on every point.
//
//`stepDirection` is a layer-level property (a sibling of `axes` and
//`data`), not a per-point one. It says where the jump happens between two
//consecutive samples:
// "hv" hold y[i] until x[i+1], then jump (matplotlib 'steps-post',
// ggplot2 direction 'hv')
// "vh" jump at x[i], then hold (matplotlib 'steps-pre')
// "mid" jump midway between the two x values (matplotlib 'steps-mid')
//Omit it entirely when the producing library does not report one — the
//description only names a direction the data actually authored.
maidr = {
"type": "step",
"stepDirection": "hv",
"data":[
[
{
"x": 0.0,
"y": 5,
"label": "Awake"
},
{
"x": 0.5,
"y": 3,
"label": "N1"
},
{
"x": 1.0,
"y": 2,
"label": "N2"
}
]
//add multiple arrays for multiple step series
]
}
//bump: nested exactly like `line`, one inner array per competitor, each
//`y` the competitor's RANK in that period, 1 for first place. Emit the
//true ranks: MAIDR makes first place the highest note itself.
//
//`firstPlace` is an optional layer-level property saying where the chart
//draws first place: "top", as a bump chart is usually drawn with its rank
//axis running down the page, or "bottom", on a chart drawn the ordinary
//way up. Up and Down follow the drawing, so on the usual chart Up moves to
//the competitor one place better and Control + Up to first place; `]`
//goes to a competitor's best place and `[` to its worst either way. Left
//out, MAIDR reads where first place is drawn from the highlighted marks;
//with nothing to measure (a canvas, no highlight), Up moves to the next
//larger rank number, as on a line. Say it when the drawing cannot be
//measured; a layer left without it whose drawing turns the keys round from
//that rule logs a console warning asking for it.
maidr = {
"type": "bump",
"firstPlace": "top",
"data":[
[ { "x": "R1", "y": 1, "z": "Ash" }, { "x": "R2", "y": 2, "z": "Ash" } ],
[ { "x": "R1", "y": 2, "z": "Birch" }, { "x": "R2", "y": 1, "z": "Birch" } ]
]
}
//pie: a FLAT array, one object per slice, in the order the slices are
//drawn. Never the nested array the bar-family types use — a pie is one
//row of N slices.
//
//`x` is the slice label and `y` its magnitude. `y` is strictly NUMERIC
//(unlike a bar's), because it is both the sonified value and the
//numerator of the slice's percentage.
//
//There is deliberately no `percentage` field. MAIDR derives the share of
//the whole as `y / sum(y) * 100`, so an authored percentage can never
//disagree with the values it is supposedly derived from. There is no
//`orientation` either: slices sit around a circle, not along an axis.
//
//`axes` here names what the two dimensions mean rather than any drawn
//axis: `x` what the slice labels are, `y` what their values measure.
//`selectors` must resolve to exactly N elements in slice order, so data
//index k and element k are the same wedge; a different count is treated
//as addressing something other than the wedges and the layer is left
//without highlighting rather than highlighting the wrong slice.
//A doughnut is the same layer — the hole is a visual detail.
//
//MAIDR walks a pie CLOCKWISE: Right steps to the next slice round the
//dial the way a clock hand goes, the audio pans each slice to where it
//sits -- and the boundary tone of a move it refuses on that slice with
//it -- and `p` names its clock position. Two optional keys say how the
//slices were laid out so that reading matches the drawing:
//
//`startAngle`: where the first slice begins, in degrees clockwise from
//12 o'clock. Defaults to 0. matplotlib's `startangle=0` (3 o'clock) is
//90 here, and Chart.js's `rotation` carries over unchanged. Plotly's
//`rotation` is clockwise from 12 too, but only a clockwise plotly pie
//starts there: a counterclockwise one (plotly's default) ends its FIRST
//wedge at `rotation` and draws it clockwise of there, so the ring starts
//at `rotation` plus that wedge's sweep -- 50 of 100 at `rotation: 0`
//means `startAngle: 180`.
//
//`direction`: `"clockwise"` (the default) or `"counterclockwise"`, the
//way successive slices follow one another from `startAngle`. Declare
//`"counterclockwise"` for a library that draws that way -- matplotlib,
//base R's `pie()` and plotly all do by default -- and MAIDR walks `data`
//in reverse so Right still moves clockwise, with the highlight, the pan
//and the clock position on the slice being read. `data` and `selectors`
//stay in drawn order either way; only the walk changes.
maidr = {
"type": "pie",
"axes": {
"x": { "label": "Fruit" },
"y": { "label": "Units" }
},
"selectors": "#chart path.slice",
"startAngle": 0,
"direction": "clockwise",
"data":[
{
"x": "Apples",
"y": 30
},
{
"x": "Bananas",
"y": 50
},
{
"x": "Cherries",
"y": 20
}
]
}
// scatterplot
maidr = {
data: [
{
"x": 1.0,
"y": 2.0
},
],
};
// scatterplot on a category axis: `xLabel` / `yLabel` name the category the
// coordinate is a *position* for. A strip plot, a swarm plot and a jittered
// `geom_point` all draw this shape.
//
// The coordinate stays numeric. The trace sorts on it, measures distance
// with it, and resolves the column index that stereo panning uses from it,
// and `'a' - 'b'` is NaN -- a string in `x` alone would give an unstable
// sort, a broken column index and a highlight that lands nowhere. Emit the
// tick position, not the drawn coordinate: on a strip plot the drawn one is
// the jitter, a precise number for a quantity that does not exist.
//
// Either axis may carry the names -- `stripplot(x='g', y='v')` puts them on
// x and `stripplot(y='g', x='v')` puts them on y -- so name the one that
// applies and leave the other off. An empty string counts as absent.
//
// One name per numeric slot: every point sharing an `x` must carry the same
// `xLabel`, since the slot *is* the category. A second, different name for
// the same `x` is dropped rather than splitting the category into two
// columns.
maidr = {
data: [
{ "x": 0, "xLabel": "a", "y": 1.4 },
{ "x": 0, "xLabel": "a", "y": 2.1 },
{ "x": 1, "xLabel": "b", "y": 3.0 }
],
};
// smooth line maidr.data: an object containing x and y properties, each with an array of float values
// note that data is an array here as scatterplots are often combine with line plots
maidr = {
"data":[
[
{
"x": 4.7,
"y": 3.12,
"svg_x": 404.51,
"svg_y": 390.012
},
]
]
}
// violin_box: summary statistics overlay for violin plots
// data is an array of BoxPoint objects, one per violin
maidr = {
"type": "violin_box",
"data": [
{
"fill": "Ideal",
"lowerOutliers": [],
"min": 326,
"q1": 878,
"q2": 1810,
"q3": 4678,
"max": 18806,
"upperOutliers": [18806],
"mean": 3458
},
{
"fill": "Premium",
"lowerOutliers": [],
"min": 326,
"q1": 1046,
"q2": 3185,
"q3": 6296,
"max": 18823,
"upperOutliers": []
}
]
}
// violin_kde: KDE density curve for violin plots
// data is a 2D array: data[violinIndex][curvePosition] = ViolinKdePoint
// points come in left/right pairs at each Y level (do NOT deduplicate)
maidr = {
"type": "violin_kde",
"data": [
[
{ "x": "Ideal", "y": -501.7, "svg_x": 100.41, "svg_y": 281.84, "width": 0.044 },
{ "x": "Ideal", "y": -501.7, "svg_x": 103.84, "svg_y": 281.84, "width": 0.044 },
{ "x": "Ideal", "y": -294.2, "svg_x": 98.25, "svg_y": 279.41, "width": 0.100 },
{ "x": "Ideal", "y": -294.2, "svg_x": 105.99, "svg_y": 279.41, "width": 0.100 }
],
[
{ "x": "Premium", "y": -400.0, "svg_x": 200.0, "svg_y": 270.0, "width": 0.035 },
{ "x": "Premium", "y": -400.0, "svg_x": 205.0, "svg_y": 270.0, "width": 0.035 }
]
]
}
// --- Experimental types ---
// boxen (letter-value) [experimental] maidr.data structure: one object per distribution,
// each with a median and a ladder of quantile pairs. A box plot is this
// shape with exactly one rung; the point of a boxen is that a larger sample
// earns more of them, so the depth varies per distribution and between them.
maidr = {
"type": "boxen",
"data": [
{
"z": "Group 1",
"median": 99.64912548800726,
// Ordered outward from the median. `p` is the *tail*
// probability, which is how letter-value plots are defined
// and how the libraries drawing them report it: p = 0.25 is
// the rung spanning the middle half, p = 0.125 the middle
// three quarters, and so on. `lo` is the p quantile and `hi`
// is the 1 - p quantile.
//
// Getting this backwards is the easy mistake -- a producer
// that sends the *coverage* (0.5, 0.75, 0.875) rather than
// the tail will have every rung announced as the wrong
// percentile while the values stay right. The trace sorts by
// p rather than trusting the order sent, so a ladder built
// inward-first still reads correctly.
//
// 0.5 is out of range rather than a way of naming the median:
// it would put two positions labelled "50th percentile"
// either side of the one already called "median". Rungs
// outside (0, 0.5) are dropped.
"levels": [
{ "p": 0.0625, "lo": 71.35, "hi": 118.19 },
{ "p": 0.125, "lo": 80.11, "hi": 112.44 },
{ "p": 0.25, "lo": 92.62, "hi": 107.67 }
],
// Whatever fell beyond the deepest rung. Optional; a ladder
// drawn to full depth has none.
"lowerOutliers": [40.0, 50.0],
"upperOutliers": [150.0, 160.0]
}
],
"orientation": "vert" // horz when the distributions run across the page
}
// pr_curve [experimental]: a precision-recall curve, one array of points per
// classifier (or class, or run). `x` is the recall and `y` the precision,
// both fractions of one; `threshold` is the decision threshold the point
// was scored at, and `z` names the curve as it names a line. `prevalence`
// is the share of positives in the data the curve was scored on -- the
// precision a classifier that guesses keeps at every recall, and so the
// height of the chart's horizontal chance baseline -- and `ap` is the
// average precision the producer computed; each is read from the first
// point of the curve that carries it. When no point carries `ap`, it is
// measured from the points as `sklearn.metrics.average_precision_score`
// computes it: over each rise in recall, the rise times the precision at
// its top. When no point carries `prevalence`, the baseline is not said.
// The points may be listed in either order; `precision_recall_curve`
// returns them from a recall of 1 down to 0, the last without a threshold.
// Either way the curve is read from its lowest recall up, as it is drawn:
// Right moves toward a higher recall, and Control + Left and Right go to
// its two ends. `selectors` pairs the marks with the points in the order
// the points are listed.
//
// The pitch is the precision on the unit interval for every curve, and
// the pan follows the recall, the boundary tone of a refused move as well
// as the point's own. Each point announces its threshold and how
// far its precision sits above (or below) the baseline; the description
// gives each curve's average precision, the baseline and where each
// average precision stands against it, and the point with the best F1,
// which the extrema dialog also offers.
maidr = {
type: 'pr_curve',
axes: { x: { label: 'Recall' }, y: { label: 'Precision' } },
data: [
[
{ "x": 1, "y": 0.3, "threshold": 0.05, "z": "Logistic", "prevalence": 0.3, "ap": 0.76 },
{ "x": 0.8, "y": 0.7, "threshold": 0.4, "z": "Logistic" },
{ "x": 0, "y": 1, "z": "Logistic" }
],
[
{ "x": 1, "y": 0.3, "threshold": 0.1, "z": "Random forest", "prevalence": 0.3 },
{ "x": 0.6, "y": 0.56, "threshold": 0.5, "z": "Random forest" },
{ "x": 0, "y": 1, "z": "Random forest" }
]
],
};
// roc [experimental]: a receiver operating characteristic curve, one array of operating
// points per classifier. `x` is the false positive rate and `y` the true
// positive rate, both fractions of one; `threshold` is the decision
// threshold the point was scored at, and `z` names the curve as it names
// a line. `auc` is the area the producer computed, read from the first
// point of the curve that carries one; when no point does, the area is the
// trapezoid rule over the curve's own points, which is what
// `sklearn.metrics.auc` and `pROC::auc` compute. The points may be listed
// in either order; the curve is read from its lowest false positive rate
// up, as it is drawn, and `selectors` pairs the marks with the points in
// the order the points are listed.
//
// The pitch is the true positive rate on the unit interval for every
// curve, so two classifiers are comparable by ear; the pan follows the
// false positive rate, the boundary tone of a refused move as well as the
// point's own. Each point announces its threshold and how far it
// sits above (or below) the chance diagonal, and the description gives
// the area under each curve and the best operating point.
maidr = {
type: 'roc',
axes: { x: { label: 'False positive rate' }, y: { label: 'True positive rate' } },
data: [
[
{ "x": 0, "y": 0, "threshold": 1, "z": "Logistic", "auc": 0.896 },
{ "x": 0.1, "y": 0.75, "threshold": 0.6, "z": "Logistic" },
{ "x": 1, "y": 1, "threshold": 0, "z": "Logistic" }
],
[
{ "x": 0, "y": 0, "threshold": 1, "z": "Random forest" },
{ "x": 0.25, "y": 0.6, "threshold": 0.5, "z": "Random forest" },
{ "x": 1, "y": 1, "threshold": 0, "z": "Random forest" }
]
],
};
// percentile_band [experimental]: a distribution at each x drawn as nested
// shaded bands around its median -- TensorBoard's distributions, a forecast
// fan chart, a quantile band. One point per x, in drawn order; `x` is a
// number or a category, and `quantiles` lists the distribution's quantiles
// at that x, each a `level` from 0 to 1 (0 the minimum, 0.5 the median, 1
// the maximum) and its `value` (`null` for a gap). The layer's quantiles
// are every level any point declares; one a point leaves out is a gap
// there. They pair into nested bands from the outside in -- lowest with
// highest -- around the median, so declare them symmetric about 0.5.
//
// The reader enters on the median, walks x with left and right, and steps
// from quantile to quantile with up and down; every quantile is pitched
// against the whole band's range. Each point announces the median and the
// band around it ("Middle 68% is -0.31 to 0.35"), and the description says
// where the band nearest one standard deviation is widest and narrowest,
// and where it widens and narrows most. `axes.x` labels the position,
// `axes.y` the value; `selectors` is one polygon per band, outermost first,
// and optionally the median's line (see "Selectors").
maidr = {
type: 'percentile_band',
axes: { x: { label: 'Step' }, y: { label: 'Weight' } },
selectors: [
"g#bands > polygon:nth-of-type(1)",
"g#bands > polygon:nth-of-type(2)",
"g#bands > polygon:nth-of-type(3)",
"g#bands > polygon:nth-of-type(4)",
"polyline#median"
],
data: [
{
"x": 100,
"quantiles": [
{ "level": 0, "value": -0.92 },
{ "level": 0.0668, "value": -0.61 },
{ "level": 0.1587, "value": -0.31 },
{ "level": 0.3085, "value": -0.12 },
{ "level": 0.5, "value": 0.02 },
{ "level": 0.6915, "value": 0.16 },
{ "level": 0.8413, "value": 0.35 },
{ "level": 0.9332, "value": 0.64 },
{ "level": 1, "value": 0.97 }
]
},
{
"x": 200,
"quantiles": [
{ "level": 0, "value": -1.5 },
{ "level": 0.0668, "value": -1.1 },
{ "level": 0.1587, "value": -0.8 },
{ "level": 0.3085, "value": -0.3 },
{ "level": 0.5, "value": 0.05 },
{ "level": 0.6915, "value": 0.4 },
{ "level": 0.8413, "value": 0.9 },
{ "level": 0.9332, "value": 1.2 },
{ "level": 1, "value": 1.6 }
]
}
],
};
// rug [experimental]: observations marked as ticks along one axis. One position per
// observation and nothing else -- the chart is drawn to show where the
// observations fall and where they bunch up. `x` for a vertical rug (the
// ticks stand on the x axis, the default), `y` for a horizontal one
// (`orientation: 'horz'`); the trace reads the field the orientation
// names and ignores the other, so a producer that already emits a rug as
// a `point` layer with a constant on the other axis switches by changing
// `type`. Listed in any order: the observations are walked from the
// lowest position up, and a flat selector pairs elements in this order.
//
// The pitch and the stereo pan follow the position on the axis, read
// against `axes.x.min` / `max` when they cover the data and the data's
// own span otherwise. The braille is the observation count per bin along
// the axis: `tickStep` sets the bin width when declared, so the strip
// lines up with the ticks the chart draws, and the axis is cut into
// `ceil(sqrt(n))` equal bins when it is not.
maidr = {
type: 'rug',
axes: { x: { label: 'Seconds', min: 0, max: 10, tickStep: 2.5 } },
data: [
{ "x": 2.2 },
{ "x": 1.5 },
{ "x": 9.4 }
],
};
Sankey, alluvial and chord (sankey, alluvial, chord) [experimental]
Weighted flows between nodes. data is one entry per flow; the nodes are
the names the flows give their two ends, so there is no node list to keep in
step with them:
{
type: 'sankey',
axes: { y: { label: 'Petajoules' } }, // what a value measures; defaults to "Value"
selectors: '#chart path.link', // optional: one element per flow, in declared order
flowOptions: { // optional: where the nodes were drawn
columns: [['Gas', 'Solar'], ['Electricity'], ['Heating', 'Homes']],
},
data: [
{ source: 'Solar', target: 'Electricity', value: 40 },
{ source: 'Gas', target: 'Electricity', value: 60 },
{ source: 'Gas', target: 'Heating', value: 45 },
{ source: 'Electricity', target: 'Homes', value: 100 },
],
}
sourceandtargetname the flow's ends, andvaluehow much flows; a flow whose value is not a number is left out.flowOptions.columnslists the columns the chart drew its nodes in, left to right, each naming its nodes top to bottom. Without it MAIDR places each node one column past the furthest node feeding it, and orders a column by the first flow naming each of its nodes. Not every library lays a sankey out that way: d3-sankey's default alignment, which Mermaid uses, puts every node nothing leaves in the last column -- Heating, above -- and reorders each column. Declare the columns when the chart's layout is known, and Up and Down walk each column as it is drawn, the stereo position follows the columns from left to right, and the braille rows and the object list's stages are the drawn columns. They are used only when they name every node once and every flow runs from one column to a later one, so a chord diagram, which cycles, declares none and is read as one stage. Read by these three types and ignored by every other.
Right follows the largest flow out of a node and left the largest flow into it, each step announcing the flow it went along; up and down walk the nodes of the column. The rotor's Outgoing and Incoming units page through the rest of a node's flows. Control + Right and Control + Left follow the largest flows to where they run out; where they go round in a cycle, there is no end to go to, and the jump says so (see Controls).
Directed graph (directed_graph) [experimental]
A computation graph -- a neural network's architecture as TensorBoard's Graphs
dashboard or Keras' plot_model draws it. data is one entry per node;
the edges are declared on the node they arrive at, as inputs, and scopes as a
path, the way a treemap declares its hierarchy:
{
type: 'directed_graph',
axes: { x: { label: 'Layer' } }, // what a node is called; defaults to "Node"
selectors: '#graph g.node', // optional: one element per node, in declared order
data: [
{ id: 'input_1', attributes: { 'Layer type': 'InputLayer', 'Output shape': '(None, 32, 32, 3)' } },
{ id: 'block1/conv', label: 'conv', path: ['block1'], inputs: ['input_1'],
attributes: { 'Layer type': 'Conv2D', 'Parameters': 896 } },
{ id: 'block1/add', label: 'add', path: ['block1'], inputs: ['block1/conv', 'input_1'] },
{ id: 'dense', inputs: ['block1/add'], attributes: { 'Layer type': 'Dense' } },
],
}
idis unique in the layer and is whatinputsrefer to;labelis what is announced, defaulting toid.pathlists the enclosing scopes, outermost first, excluding the node. A scope exists because a path names it; it is never declared on its own.inputsnames the nodes feeding this one. An id naming no declared node, a node naming itself and a repeated input are ignored.inputLabelsnames the edges ininputs, index for index: a flowchart'syesandno, a state machine's event. A label is announced beside the node it leads to or comes from --To: ship (yes), fix (no)-- and in the description's inputs and outputs. A blank ornullentry leaves its edge unnamed. An edge declared twice with different labels carries both, in declared order, apart by semicolons:B (yes; retry). An edge into or out of a collapsed scope stands for every edge between the scope's members and the far end, and is heard with their label when they all carry the same one -- a state'sstopout of a composite state, a flowchart'syesinto a subgraph -- and without one when they differ or some are unnamed.inputEdgessays which drawn edge each edge ininputsis, index for index, where the diagram draws one edge that is declared as several: a line drawn without an arrow, or with an arrow at each end, declared once from each end; an edge into or out of a scope declared once for each node it enters or leaves. Edges sharing an id are counted once in the description'sNumber of edges, a line declared each way is not called a cycle, and the copies arriving from or leaving for one scope are listed as that scope in the table, with their label when they all carry the same one. Moves are unchanged: every declared edge is one a reader can follow. A blank ornullentry is an edge of its own, apart from any drawn edge between the same two nodes: a line and an arrow drawn between them are two edges, and a cycle. Ids are compared as text, so the two directions of a line must carry the same one: with different ids they are two edges, and the line a cycle, as they were beforeinputEdges.attributesis free-form: each entry is announced as a clause, in declared order, and becomes a column of the description's table.
The reader enters on the first top-level item with every scope closed.
Left and right walk the items of one scope in topological order, input to
output; down opens a scope onto its first member and up closes it again;
Ctrl with an arrow goes to the first or last item of the scope, out to the
top-level item, or down to the deepest first member, and plays the edge signal
on an item that is already there. The rotor's Outputs
and Inputs units follow the edges downstream and upstream, each press
taking the next edge of the item the walk started from; an arrow move ends the
walk, so the next press follows the edges of the item the reader is on. An
edge to a node inside a closed scope arrives at the scope. Each move announces the node,
its connections as 2 inputs, 1 output, whether it is a graph input or output
or a branch or merge point, the names on either side, its attributes, and its
place in the scope's order. A scope is announced as a scope rather than by the
x axis's noun for a node -- Scope is block1, Size is 3 nodes inside -- and is
called not connected only when nothing inside it has an edge.
The description speaks of the whole graph at once. Its table lists every
node's own inputs and outputs, as declared, but for the copies of one drawn
edge (inputEdges) that reach a node from one scope or leave it for one,
listed once as that scope. Its branch and merge points are
the items a move announces as one and the object list's filters gather, each
seen from its scope, and are named so each can be found: the skip
connection's input_1 above has two edges of its own, as the table lists,
but both enter the closed block1, so a move there hears one output and it
is no branch point -- Branch points: 0, Merge points: 1: block1 > add.
Sequence diagram (sequence) [experimental]
Participants and the messages between them, in the order they happen. data
is an object, because a participant no message reaches is still a lifeline:
{
type: 'sequence',
selectors: ['#seq [data-id="m0"]', '#seq [data-id="m1"]'], // optional: one element per message
data: {
participants: [
{ id: 'B', label: 'Browser', kind: 'actor' },
{ id: 'S', label: 'Server' },
],
messages: [
{ from: 'B', to: 'S', text: 'GET /', kind: 'sync', number: 1 },
{ from: 'S', to: 'B', text: '200 OK', kind: 'reply', fragments: ['alt cached'] },
],
},
}
participantslists the lifelines left to right.labelis what is announced, defaulting toid;kind(actor,database, ...) is announced in parentheses after the name,Browser (actor);groupnames the box the diagram draws around it. A participant a message names without declaring is added after them.messagesrun in time order.kindis one ofsync(the default),async,reply,create,destroyornote;fragmentslists the enclosing loops and alternatives, outermost first, as the diagram labels them, a later section written as its fragment's label, a comma and the section (alt cached, else), so it is heard with what it is the alternative of and counted with its fragment;numberis the sequence number the diagram prints, omitted for a message it does not number and for a note. The description's table shows only those numbers, a blank where there is none, and has no Number column when nothing is numbered.
Left and right walk the messages in time. Down moves from the row of every
message onto the first participant's lifeline, and on to the next participant,
keeping the moment the reader was at; up comes back. The rotor's Fragments
unit jumps to where the next loop or alternative opens or closes. Each move
announces the message, its route (Browser to Server), its kind, its fragment
and its number. Pitch and pan are the participant the message arrives at, and
the tone glides up for an arrow pointing right and down for one pointing left.
Venn diagram (venn) [experimental]
Sets and their overlaps. data is one entry per region, which names the
sets it is the overlap of:
{
type: 'venn',
data: [
{ sets: ['Desirable'], size: 10 },
{ sets: ['Feasible'], size: 10 },
{ sets: ['Desirable', 'Feasible'], size: 2.5, label: 'Buildable', items: ['MVP'] },
],
}
setsnames the sets the region joins, or the one set it is. The sets are taken in the order the regions first name them; a set named only in an overlap still gets a region of its own.sizeis the set's size, or the overlap's. Without sizes the pitch is the number ofitemswritten in the region.labelnames the region;itemslists what is written inside it.
The first row is the sets, the next every overlap of two, then of three. Left and right walk a row; down goes into the first overlap the region is part of, and up goes back out the way the reader came down: onto the region they went down from, while the overlap they are on is made of it, and otherwise onto the first set it is made of. Control + Up returns to the set they started down from. A set announces the sets it overlaps; an overlap announces its name, its size and its share of the smallest set it joins.
Multilayer Plots
If multiple plots are overlaid on the same SVG, provide the data corresponding to every plot in the layers array.
maidr = {
"id": "multilayer_plot",
"subplots": [
[
{
"id": "445f4f08-b8a5-4204-8c55-0851eda7daec",
"layers": [
{
"id": "f548e01f-ed13-469c-9e0a-cea420ec8b3f",
"type": "bar",
"title": "",
"axes": {
"x": { "label": "X values" },
"y": { "label": "Bar values" }
},
"data": [
{
"x": "0",
"y": 3.0
},
{
"x": "1",
"y": 5.0
},
],
},
{
"id": "f022d8e9-4aff-4ab0-9959-904fd07c9bd2",
"type": "line",
"title": "Multilayer Plot Example",
"axes": {
"x": { "label": "X values" },
"y": { "label": "Line values" }
},
"data": [
[
{
"x": 0.0,
"y": 10.0,
"fill": "Line Data"
},
{
"x": 1.0,
"y": 8.0,
"fill": "Line Data"
},
]
],
}
]
}
]
]
}
Violin Plot (Multilayer)
Violin plots use two layers in the same subplot: violin_box for summary statistics and violin_kde for the KDE density curve. Put violin_box first so it is the default view. Users switch between layers with PageUp/PageDown.
For the full data contract and field reference, see VIOLIN_PLOT_SPEC.md.
maidr = {
"id": "violin_plot",
"subplots": [
[
{
"layers": [
{
"id": "box-layer",
"type": "violin_box",
"title": "Diamond Price Distribution by Cut Quality",
"axes": {
"x": { "label": "Cut Quality" },
"y": { "label": "Price (USD)" }
},
"selectors": [
{
"lowerOutliers": [],
"min": "#box1 .whisker-min",
"iq": "#box1 .iqr-rect",
"q2": "#box1 .median-line",
"max": "#box1 .whisker-max",
"upperOutliers": []
}
],
"data": [
{
"fill": "Ideal",
"lowerOutliers": [],
"min": 326,
"q1": 878,
"q2": 1810,
"q3": 4678,
"max": 18806,
"upperOutliers": []
}
]
},
{
"id": "kde-layer",
"type": "violin_kde",
"title": "Diamond Price Distribution by Cut Quality",
"axes": {
"x": { "label": "Cut Quality" },
"y": { "label": "Price (USD)" }
},
"selectors": [
"#violin-group-1 path"
],
"data": [
[
{ "x": "Ideal", "y": -501.7, "svg_x": 100.4, "svg_y": 281.8, "width": 0.044 },
{ "x": "Ideal", "y": -501.7, "svg_x": 103.8, "svg_y": 281.8, "width": 0.044 },
{ "x": "Ideal", "y": -294.2, "svg_x": 98.3, "svg_y": 279.4, "width": 0.100 },
{ "x": "Ideal", "y": -294.2, "svg_x": 106.0, "svg_y": 279.4, "width": 0.100 }
]
]
}
]
}
]
]
}