Switch to dark theme
Search documentation
On this page

Line

A line series connects data points in the order you provide them, showing how values change across a sequence. Line plots can reveal trends, fluctuations, and differences between datasets. Use width and dash to style the line, or add mark and mark_size to show individual samples.

Solid and dashed lines

Compare measured values with a forecast. A solid line follows each fluctuation, while a dashed line distinguishes the expected trend.

const data = Array.from({ length: 97 }, (_, i) => ({
x: i / 4,
expected: 45 + 22 * Math.sin(i / 18) + 12 * Math.cos(i / 7),
measured: 45 + 22 * Math.sin(i / 18) + 12 * Math.cos(i / 7)
+ 7 * Math.sin(i * 1.1) + 4 * Math.cos(i * 2.3),
}))
const line_comparison = plot({
series: [
plot.line({
data,
y: 'expected',
color: { light: '#c2410c', dark: '#fdba74' },
width: 2,
dash: [7, 5],
tooltip: pure(({ y }) => h("span", {}, 'Expected: ' + Math.round(Number(y)))),
}),
plot.line({
data,
y: 'measured',
color: { light: '#0891b2', dark: '#67e8f9' },
width: 2,
tooltip: pure(({ y }) => h("span", {}, 'Measured: ' + Math.round(Number(y)))),
}),
],
height: 260,
margin: { top: 5, right: 8, bottom: 20, left: 30 },
axis: { x: { min: 0, max: 24, label: '' }, y: { min: 0, max: 100, label: '' } },
background: { light: '#ffffff', dark: '#151b28' },
chrome_color: { light: '#e2e8f0', dark: '#334155' },
grid: { x: false, y: true },
border: false,
zoom_pan: false,
})
// In Antithesis notebook, plot() returns a Printable. Uncomment below to display it:
// print(line_comparison)
// presentation.blank("Line comparison").print(line_comparison)

Point marks and outlines

Show individual samples with diamond marks. Set the line width, mark size, and mark outline independently to keep each measurement visible.

const data = Array.from({ length: 13 }, (_, i) => ({
x: i,
y: 35 + i * 2 + 18 * Math.sin(i * 0.8) + 8 * Math.cos(i * 1.7),
}))
const line_markers = plot({
series: [plot.line({
data,
color: { light: '#7c3aed', dark: '#c4b5fd' },
width: 3,
mark: 'diamond',
mark_size: 10,
mark_stroke: { color: { light: '#ffffff', dark: '#151b28' }, width: 2 },
tooltip: pure(({ x, y }) => h("span", {}, 'Sample ' + x + ': ' + Math.round(Number(y)))),
})],
height: 260,
margin: { top: 5, right: 8, bottom: 20, left: 30 },
axis: { x: { min: -0.5, max: 12.5, label: '' }, y: { min: 0, max: 100, label: '' } },
background: { light: '#ffffff', dark: '#151b28' },
chrome_color: { light: '#e2e8f0', dark: '#334155' },
grid: { x: false, y: true },
border: false,
zoom_pan: false,
})
// In Antithesis notebook, plot() returns a Printable. Uncomment below to display it:
// print(line_markers)
// presentation.blank("Line markers").print(line_markers)

Comparing services

Compare response times during the same traffic surge. Search recovers quickly, checkout takes longer to clear its backlog, and catalog stays comparatively steady.

const data = Array.from({ length: 1201 }, (_, i) => {
const minute = i / 10
const surge = Math.exp(-(((minute - 45) / 12) ** 2))
const backlog = minute < 45 ? 0 : (1 - Math.exp(-(minute - 45) / 8)) * Math.exp(-(minute - 45) / 30)
return {
minute,
search: 65 + surge * 90 + 5 * Math.sin(minute * 1.3),
checkout: 95 + surge * 170 + backlog * 220 + 7 * Math.sin(minute * 0.9),
catalog: 40 + surge * 35 + 3 * Math.sin(minute * 1.7),
}
})
const service_latency = plot({
series: [
plot.line({
data, x: 'minute', y: 'search',
color: { light: '#0891b2', dark: '#67e8f9' },
width: 2,
tooltip: pure(({ y }) => h('span', {}, 'Search: ' + Math.round(Number(y)) + ' ms')),
}),
plot.line({
data, x: 'minute', y: 'checkout',
color: { light: '#c2410c', dark: '#fdba74' },
width: 2, dash: [7, 4],
tooltip: pure(({ y }) => h('span', {}, 'Checkout: ' + Math.round(Number(y)) + ' ms')),
}),
plot.line({
data, x: 'minute', y: 'catalog',
color: { light: '#7c3aed', dark: '#c4b5fd' },
width: 2, dash: [2, 4],
tooltip: pure(({ y }) => h('span', {}, 'Catalog: ' + Math.round(Number(y)) + ' ms')),
}),
],
height: 260,
margin: { top: 12, right: 12, bottom: 40, left: 44 },
axis: { x: { min: 0, max: 120, label: 'Minutes' }, y: { min: 0, label: '' } },
background: { light: '#ffffff', dark: '#151b28' },
chrome_color: { light: '#e2e8f0', dark: '#334155' },
grid: { x: false, y: true },
border: false,
zoom_pan: true,
})
// In Antithesis notebook, plot() returns a Printable. Uncomment below to display it:
// print(service_latency)
// presentation.blank("Comparing services").print(service_latency)

Contour map

This contour map demonstrates that lines can loop and change direction instead of progressing from left to right.

const resolution = 64
const terrain = Array.from({ length: (resolution + 1) ** 2 }, (_, index) => {
const x = index % (resolution + 1) / resolution * 100
const y = Math.floor(index / (resolution + 1)) / resolution * 100
const ridgeX = x - 40 + 7 * Math.sin(y / 17)
const height = 180 + 3 * x + 2 * y
+ 1250 * Math.exp(-(ridgeX ** 2 / 750 + (y - 48) ** 2 / 1500))
+ 550 * Math.exp(-((x - 83) ** 2 / 150 + (y - 22) ** 2 / 220))
+ 450 * Math.exp(-((x - 78) ** 2 / 200 + (y - 84) ** 2 / 140))
return { x, y, height }
})
const contours = []
for (let elevation = 300; elevation <= 1700; elevation += 100) {
const edges = new Map()
const connect = (triangle) => {
const crossings = []
for (let side = 0; side < 3; side++) {
const a = triangle[side]
const b = triangle[(side + 1) % 3]
const start = terrain[a]
const end = terrain[b]
if ((start.height < elevation) === (end.height < elevation)) continue
const key = Math.min(a, b) + ':' + Math.max(a, b)
if (!edges.has(key)) {
const t = (elevation - start.height) / (end.height - start.height)
edges.set(key, {
point: { x: start.x + t * (end.x - start.x), y: start.y + t * (end.y - start.y), elevation },
neighbors: [],
})
}
crossings.push(key)
}
if (crossings.length === 2) {
edges.get(crossings[0]).neighbors.push(crossings[1])
edges.get(crossings[1]).neighbors.push(crossings[0])
}
}
for (let y = 0; y < resolution; y++) {
for (let x = 0; x < resolution; x++) {
const a = y * (resolution + 1) + x
const b = a + 1
const c = a + resolution + 1
const d = c + 1
connect([a, b, d])
connect([a, d, c])
}
}
const visited = new Set()
const starts = [...edges.keys()].sort((a, b) => edges.get(a).neighbors.length - edges.get(b).neighbors.length)
for (const start of starts) {
if (visited.has(start)) continue
const data = []
let current = start
while (!visited.has(current)) {
visited.add(current)
const edge = edges.get(current)
data.push(edge.point)
const next = edge.neighbors.find(key => !visited.has(key))
if (next === undefined) {
if (edge.neighbors.includes(start)) data.push(edges.get(start).point)
break
}
current = next
}
const hue = 260 - (elevation - 300) / 1400 * 260
contours.push({
data,
color: { light: 'hsl(' + hue + ' 85% 40%)', dark: 'hsl(' + hue + ' 90% 68%)' },
})
}
}
const contour_map = plot({
series: contours.map(({ data, color }) => plot.line({
data,
color,
width: 1.5,
tooltip: pure(({ row }) => h('span', {}, 'Elevation: ' + row.elevation + ' m')),
})),
height: 260,
margin: { top: 12, right: 12, bottom: 28, left: 32 },
axis: { x: { min: 0, max: 100, label: '' }, y: { min: 0, max: 100, label: '' } },
background: { light: '#ffffff', dark: '#151b28' },
chrome_color: { light: '#e2e8f0', dark: '#334155' },
grid: false,
border: false,
zoom_pan: true,
})
// In Antithesis notebook, plot() returns a Printable. Uncomment below to display it:
// print(contour_map)
// presentation.blank("Contour map").print(contour_map)

Line arguments

Field Type Default Description
data {[key: string]: any}[] Required Data points to plot.
x? FieldArg 'x' Horizontal field name or accessor.
y? FieldArg 'y' Vertical field name or accessor.
color? ThemeColor Black Series color. Theme pairs provide separate light and dark colors.
width? number 2 Line width in pixels.
dash? number[] [] Alternating dash and gap lengths in pixels. An empty array draws a solid line.
mark? MarkShape None Shape drawn at each point.
mark_size? number 6 Point marker diameter in pixels.
mark_stroke? StrokeArg None Outline for the point markers.
tooltip? TooltipArg None Use true for the default tooltip, or a callback for custom content.
on_select? SelectFn None Callback receiving the selected point and its source data.
hoverable? boolean true Enable tooltips, selection callbacks, and hover highlights for this series.
highlight? boolean true Highlight a data point when you hover over it.
highlight_color? ColorArg Automatic Exact hover color, including opacity.

This example connects time and value with a dashed line and outlined circle marks, and adds tooltips and a selection callback.

const selected_data = new reactive.StateVar('Select a point to inspect its data.')
const series = [plot.line({
data: [{ time: 1, value: 3 }, { time: 2, value: 5 }, { time: 3, value: 4 }],
x: 'time',
y: 'value',
color: { light: 'steelblue', dark: 'lightskyblue' },
width: 2,
dash: [6, 3],
mark: 'circle',
mark_size: 8,
mark_stroke: { color: 'navy', width: 1 },
tooltip: pure(({ x, y }) => h("span", {}, `(${x}, ${y})`)),
on_select: pure(({ row }) => selected_data.set(JSON.stringify(row)), { selected_data }),
hoverable: true,
highlight: true,
})]
const line_options_plot = plot({ series, height: 260 })
const line_options = h(
'div',
{},
line_options_plot,
h(
pure(({ selected_data }) =>
h('output', { 'aria-live': 'polite', style: { display: 'block' } }, selected_data.get()),
),
{ selected_data },
),
)
// In Antithesis notebook, plot() returns a Printable. Uncomment below to display it:
// print(line_options)
// presentation.blank("Line options").print(line_options)

Shared configuration

See Install and configure to set up Plot. The argument tables describe omitted fields; examples may override these defaults. See Data fields, Series colors, Strokes, and Mark shapes for shared options. For callbacks, see Tooltips and Selection.