Skip to content
Connect an agent
Other diagram kinds

XY charts

An XY chart plots numbers against places: latency by week, CPU by hour, cost by month, p95 by endpoint. Plotwork draws it with bars, lines, and areas on one value axis, a labelled reference line for the limit that matters, and a callout for the event that explains a change. It is for anyone who writes a design document with metrics in it, and for an agent that has been asked to chart them without making the chart lie.

TL;DR

QuestionAnswer
What is it for?Numbers over a few to a few dozen places: latency, capacity, throughput, error budgets, growth, and cost. Not for parts of one whole (use a pie chart) or for two measures of different scale (draw two charts).
How do I start one?Write xychart "Title", then x "Week" ["W1" "W2" "W3"] for the places, then one bar, line, or area line for each series: line "p95" [310 360 345].
What does the picture show?A rounded plate with the title in a corner tab, quiet gridlines, bars with rounded tops, lines with round joins and an end dot, soft areas, and a dashed line for each threshold.
How are series named?Up to four are named by a pill beside their last point, in the series' own tone mark. More than four get a legend above the plot. One series under a titled axis is not named twice.
What do I never choose?A color. The series take the tones of the theme in a fixed, color-blind-safe order. You may choose a tone for a series, and a threshold is red until you say otherwise.
How is it kept honest?Bars and areas start at zero, ticks are on round numbers, numbers carry thousands marks and their unit, and lint warns for a baseline above zero, more than 6 series, negative bars, a threshold the axis cannot hold, and crowded labels.
What does Mermaid convert?xychart-beta and xychart, with a title, both axes, bar, line, and horizontal. See From Mermaid.
How big can it be?At most 60 places, 8 series, 4 thresholds, and 6 notes.

Ask your agent

text
Use Plotwork to draw an XY chart of the p50 and p95 latency of our checkout API over the last eight weeks. p95 by week: 310, 330, 345, 420, 455, 440, 350, 335. p50 by week: 120, 125, 128, 160, 170, 165, 135, 130. Draw p95 as bars, p50 as a line, our 400 ms objective as a red line, and note the 2.3 release in week 4 and the cache fix in week 7.
text
Use Plotwork to draw a stacked bar chart of our cloud cost per month in dollars, January to June, split into compute, storage, network, and other. Start the axis at zero.
text
Use Plotwork to draw a sideways bar chart that ranks our six slowest endpoints by p95 latency in milliseconds, slowest first, with the 400 ms objective as a line.

Example

Eight weeks of latency against an objective (the SLO, a service level objective). The p95, the 95th percentile of latency, is drawn as bars, so each week that crosses the 400 ms line is red, and the p50, the median, is a line. Each series is named beside its last point, the objective is named at the end of its line, and two notes mark what changed.

API latency against the SLO: 2 series (bar and line) over 8 places on the Week axis, on a scale of 0 ms to 500 ms. Weekly p50 and p95 latency of the checkout API against its 400 ms objective: p95 crossed the line after release 2.3 and came back under it after the cache fix.

The language

One statement to a line. Text is always in double quotes, options are key=value, a list is in square brackets, and # starts a comment. Write a negative number with its sign in front (-5), and put text that holds +, (, <, #, or a bare / in quotes.

StatementMeaningExample
xychart "Title" direction=<vertical|horizontal> stack=<on|off> values=<on|off> desc="<text>"The header. Every option is optional, and so is the title.xychart "Latency by week" stack=off
x "Title" ["<place>" ...] or x "Title" <from> to <to>The horizontal axis, once: named places, or a number range the values are spread over.x "Week" ["W1" "W2" "W3"]
y "Title" unit=<text> min=<number> max=<number>The value axis, once. Every part is optional.y "Latency" unit="ms" min=0
bar|line|area "Name" [<number> ...] tone=<tone> icon=<icon>One series: its name, then one number for each place.line "p95" [310 360 345]
threshold "Label" <number> tone=<tone> breach=<above|below|none>A labelled reference line across the plot.threshold "SLO 400 ms" 400
note "Text" at="<place>"A callout above the plot, with a dotted line down to one place.note "Release 2.3" at="W4"
desc "<text>"The plain-language description. The same as desc= on the header.desc "How latency moved."

A series has a name that is unique in any case, and one value for each place on the axis. A value is a plain number with an optional fraction. A thousands mark, a percent sign, a unit, and an exponent are errors, and the hint says what to write: 1200, not 1,200, and 40 on a y axis with unit="%", not 40%.

Options

OptionOnValuesDefault
direction=headervertical (places along the bottom), horizontal (places down the side)vertical
stack=headeron, offoff
values=headeron, offon
desc=headerQuoted textnone
unit=yUp to 12 characters, such as ms, %, $, req/snone
min=, max=yA number, with a sign in front if it is negativechosen from the data
tone=series, thresholdneutral, blue, violet, aqua, orange, green, amber, redthe next tone of the order below, or red for a threshold
icon=seriesAny bundled icon, such as lucide:activity, or nonenone
breach=thresholdabove, below, noneabove
at=noteA place on the x axis, or an x value on a number axisrequired

Bars, lines, and areas

A bar draws a bar for each place, up to 28px thick, side by side when there are several bar series. Its top has soft corners and its foot is square. A line joins the points with a 2px line, marks each point, and marks the last point larger. An area is a line with a wash of its tone under it. Draw the one that matches the question: bars to compare sizes, lines to show a trend, areas for a quantity that accumulates.

The series take blue, orange, aqua, violet, amber, green, red, and neutral in turn. A tone= on a series keeps that tone, and the others take the next ones that nobody has claimed, so two series never share a hue until all eight are in use. Nobody writes a color value: the theme owns every color, in light and in dark, and every mark keeps at least 3:1 against the plate.

Axes and ticks

Ticks are on round numbers (1, 2, or 5 times a power of ten), and a free end of the axis is pushed out to the next tick so that the data never touches it. Bars and areas always start at zero. A chart of lines alone starts at zero too, unless its smallest value is more than half its largest, where it starts at the tick below the data, because the question is then the change. min= and max= fix an end exactly, and a min= above zero under bars or areas draws a warning.

A symbol unit ($, %) is written on the numbers, and a word unit (ms, req/s) goes in the title of the axis in brackets: Latency (ms). Pills, thresholds, and the text description always write the unit in full. Large ticks are compact: 250K, 1.5M.

A number axis spreads the values of every series evenly over a range, so each series has the same number of values:

Plotwork
x "Requests per second" 0 to 1000line "p99" [110 155 330 880 1700]

Naming the series

Up to four series on an upright chart are named by a pill in the gutter at the right of the plot, level with the series' last point, with the last value after the name. A bar series is named level with its last bar, and a stacked series level with its slice. Pills that would touch are spread by the least movement that keeps them apart. The name is in the ink color, with a small mark in the series' tone beside it, because a light hue is not legible as text.

More than four series, and every sideways chart, are named in a legend of pills above the plot, after the title of the axis. A single series under a titled axis is not named at all, since the title says it.

Thresholds

A threshold is a labelled line across the plot, dashed in its tone, named in a pill at its right end. It is the most useful element of an engineering chart: an objective, a quota, a capacity. Bars and points on the wrong side of it are drawn in its tone, and the text description lists which crossed.

breach= chooses the side: above for a limit that must not be passed, below for a floor, and none for a line that is only a reference. The axis grows to hold the line. A stacked bar keeps its own tones, because its segments are not the value the line is about.

Notes

A note is a pill above the plot with a dotted line down to its place. Two notes that would touch go to separate rows. A note is not available on a sideways chart, where a threshold names a number instead.

Capacity against a limit

An area for the quantity, a line for a second one, and a limit in amber. The points of the area that cross the limit are marked in its tone, and the symbol unit rides on the ticks.

CPU of the API fleet against its limit: 2 series (area and line) over 8 places on the Hour axis, on a scale of 0% to 100%. Average CPU of the API fleet over a day: it climbs through the morning, crosses the 80 percent limit at lunch time and again in the evening peak, and memory follows it a little below.

Stacked bars

stack=on stacks the bar series on one another, in the order written, with a gap of the plate between segments. Each stack is named once by its total above it, and each service is named level with its slice. Use it for parts of a total that changes. A stack hides the size of every segment but the lowest, so keep it to a few series.

Cloud cost per month by service: 4 series (bar) over 6 places on the Month axis, on a scale of $0 to $10,000. Monthly cloud spend in dollars split by service: compute is the largest and growing, storage is steady, and network spiked in April.

A sideways ranking

direction=horizontal puts the places down the side, one row each, which is how a ranking reads and how long names fit. The numbers sit beside the bars, the threshold stands above the plot, and the series is named by the title of the axis.

Slowest endpoints: 1 series (bar) over 6 places on the Endpoint axis, on a scale of 0 ms to 1,000 ms. The 95th percentile latency of the six slowest endpoints of the API in the last week, against the 400 millisecond objective.

Numbers on the bars

A number is written above each bar, in the ink color, when there is room for it. When two numbers would touch, none is written, so a chart never has some numbers and not others for want of room. A number that a line or a threshold would run through is left out alone. values=off leaves them all out. The text description carries every value either way.

Labels on a crowded axis

A place name wraps to two lines at a space. When even the longest word is wider than the room a place has, only every second, third, or later place is named, and lint warns. The other places are still drawn, and the text description names every one. Chart fewer places, or draw the chart sideways, to name them all.

Limits and warnings

Honest charts

A chart can mislead by what it leaves out. Plotwork warns instead of drawing a chart that does. A warning never stops the drawing.

WarningWhen it firesWhat to do
W200, a baseline above zeromin= is above zero and the chart has a bar or an area, so the length of a bar no longer shows its value.Start the axis at zero with min=0, or draw lines.
W200, more than 6 seriesThe colors stop being easy to tell apart.Chart the few that matter, or draw one chart for each group.
W200, negative barsA bar series has a value below zero.Draw a line for a value that can fall below zero.
W200, a threshold outside the axismin= or max= leaves a threshold out, so its line is not drawn.Widen the bounds, or leave them out so the axis grows.
W200, nothing to stackstack=on with fewer than two bar series.Add a bar series, or remove stack=on.
W200, crowded labelsThe names of the places had to be thinned.Chart fewer places, shorten the names, or draw the chart sideways.
W203A name, a title, or a label was cut with an ellipsis to fit its pill.Shorten it. The full text stays in the description.
W202The picture is wider than a Markdown column and its text would shrink.Chart fewer places, or draw the chart sideways.

Limits

LimitValueWhat happens past it
Places on the x axis60E140 before anything is drawn, with the advice to group them.
Series8E140. Lint warns from 7.
Thresholds, notes4, 6E140.
A number1,000,000,000,000E100, at the number.
A unit12 charactersE136, at the value.
A pill240pxThe name is cut with an ellipsis, with W203.

Errors

Every mistake comes back as a result with a code, a line, a column, and a hint. All the codes are the shared ones.

CodeWhen it firesFix
E100A list with no brackets or no closing bracket, a value that is text, has a percent sign, or is too large, a series whose count does not match the places, a chart with no x axis or no series, or a note with no place or on a sideways chart.Read the hint, which gives the line to write.
E101A name that is never closed.Close it with a double quote.
E110An unknown statement.Use x, y, bar, line, area, threshold, note, or desc.
E121, E122An unknown tone or icon.The hint names the nearest.
E130A series name used twice in any case, or a second x or y.Rename the series. A chart has one of each axis, because two scales on one plot invent a link between the series.
E135, E136An unknown option, or a bad direction=, stack=, values=, breach=, unit, bound, range, or note place.The hint lists the choices.
E140More places, series, thresholds, or notes than the limits above.Group, split, or leave some out.

From Mermaid

Mermaid xychart-beta and xychart convert to an XY chart. Ask your agent to convert it, or use the convert_mermaid tool. The migration guide covers the converter in full.

Mermaid
xychart-beta    title "Sales Revenue"    x-axis [jan, feb, mar, apr, may, jun]    y-axis "Revenue (in $)" 4000 --> 11000    bar [5000, 6000, 7500, 8200, 9500, 10500]    line [5000, 6000, 7500, 8200, 9500, 10500]

Mermaid does not name its series, so the converter names them Bars and Line and says so (M204). Rename them so the names beside the lines say what each one is. The axis starts at 4,000 because Mermaid said so, and a bar chart that does not start at zero draws a warning:

Plotwork
xychart "Sales Revenue"
x ["jan" "feb" "mar" "apr" "may" "jun"]y "Revenue (in $)" min=4000 max=11000bar "Bars" [5000 6000 7500 8200 9500 10500]line "Line" [5000 6000 7500 8200 9500 10500]
MermaidPlotworkReport
xychart-beta or xychart, in any casexychartnone
horizontal on the headerdirection=horizontalnone
title Text, title "Text", front matter title:, accTitle:The titlenone
accDescr:desc=none
x-axis [a, b, c], with quotes or withoutx ["a" "b" "c"]none
x-axis "Title" 0 --> 1000x "Title" 0 to 1000none
No x-axisThe places counted from 1none
y-axis "Title" 0 --> 100y "Title" min=0 max=100none
y-axis "Title", or no y-axisThe title only, or none. Plotwork chooses round ticks from zero.none
bar [...], line [...]bar "Bars" [...], line "Line" [...], in the order writtenM204 when there are two or more and Mermaid named none
bar "Name" [...]bar "Name" [...]none
A second x-axis or y-axisThe first is kept.M209
A series whose count does not match the placesNothing is converted, or the series is skipped when the conversion is lenient.M110, or M218
A value with a unit, a percent sign, an exponent, or quotes, a plot that is not a bar or a line, or a line that is not MermaidNothing is converted, or the line is skipped when lenient.M110, or M218
%%{init}%% and front matter config:, such as width, height, and showDataLabelDropped.M200
%% commentsDropped.none
A chart with no titleNo title is written.M214
More than 60 places or 8 seriesNothing is converted.M102
A chart with no seriesNothing is converted.M103

See also

DocWhy
Writing diagramsThe flow, sequence and schema languages, and the rules every kind shares
Examples to copyFinished diagrams with the prompt that gets an agent to write each one
Migrating from MermaidConvert existing Mermaid diagrams, and what is dropped
Pie chartsOne total split into parts
Quadrant chartsItems placed on two scores
Gantt chartsA plan drawn as tasks along a calendar
Language referenceEvery kind, node type, option and diagnostic
Diagram kindsThe kinds Plotwork draws and the header of each