Switch to dark theme
Search documentation
On this page

Rect

A rect series represents data as rectangular regions, making it useful for showing time intervals, occupied ranges, or cells in a heatmap. Use x, x2, y, and y2 to define the bounds, or place rectangles within categorical bands.

Categorical cells

Compare hourly request volume across a week, with busier daytime hours and quieter weekends. Each cell represents one hour on one day, with color showing the request count.

const days = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
const hours = Array.from({ length: 24 }, (_, hour) => String(hour).padStart(2, '0'))
const data = days.flatMap((day, d) => hours.map((hour, h) => {
const daytime = Math.exp(-(((h - 14) / 5) ** 2))
const variation = 0.9 + 0.1 * Math.sin(h * 1.7 + d * 2.3)
const requests = Math.round((120 + 880 * daytime) * (d < 5 ? 1 : 0.4) * variation)
return { day, hour, requests }
}))
const weekly_heatmap = plot({
series: [
plot.rect({
data,
x: 'hour',
y: 'day',
inset: 0.75,
color: pure(row => ({
light: 'hsl(174, 65%, ' + (94 - Number(row.requests) * 0.06) + '%)',
dark: 'hsl(174, 55%, ' + (16 + Number(row.requests) * 0.04) + '%)',
})),
tooltip: pure(({ row }) => h("span", {}, row.day + ' ' + row.hour + ':00 · ' + row.requests + ' requests')),
}),
],
height: 260,
margin: { top: 5, right: 8, bottom: 20, left: 38 },
axis: { x: { categories: hours, label: '' }, y: { categories: days, label: '' } },
background: { light: '#ffffff', dark: '#151b28' },
chrome_color: { light: '#e2e8f0', dark: '#334155' },
grid: false,
border: false,
zoom_pan: false,
})
// In Antithesis notebook, plot() returns a Printable. Uncomment below to display it:
// print(weekly_heatmap)
// presentation.blank("Weekly heatmap").print(weekly_heatmap)

Intervals with explicit bounds

Set x and x2 to show durations within categorical rows. Each rectangle retains its start and end values as the plot resizes.

const data = [
{ task: 'Fetch', start: 0.5, end: 3.2 },
{ task: 'Parse', start: 2.8, end: 5.1 },
{ task: 'Build', start: 4.5, end: 9.4 },
{ task: 'Test', start: 7.8, end: 11.2 },
{ task: 'Ship', start: 10.8, end: 12.5 },
]
const task_intervals = plot({
series: [
plot.rect({
data,
x: 'start',
x2: 'end',
y: 'task',
inset: 9,
color: { light: '#c4b5fd', dark: '#6d28d9' },
stroke: { color: { light: '#7c3aed', dark: '#c4b5fd' }, width: 1 },
hover_span_y: true,
tooltip: pure(({ row }) => h("span", {}, row.task + ': ' + row.start + '–' + row.end + ' s')),
}),
],
height: 260,
margin: { top: 5, right: 8, bottom: 20, left: 44 },
axis: { x: { min: 0, max: 14, label: '' }, y: { label: '' } },
background: { light: '#ffffff', dark: '#151b28' },
chrome_color: { light: '#e2e8f0', dark: '#334155' },
grid: false,
border: false,
zoom_pan: false,
})
// In Antithesis notebook, plot() returns a Printable. Uncomment below to display it:
// print(task_intervals)
// presentation.blank("Task intervals").print(task_intervals)

Fixed pixel dimensions

Set size to draw rectangles with a consistent width and height at numeric coordinates. An outline keeps nearby marks distinct.

const data = Array.from({ length: 32 }, (_, i) => ({
x: 8 + (i * 23 % 84),
y: 18 + (i * 13 % 65),
value: 20 + (i * 17 % 80),
}))
const sized_rectangles = plot({
series: [
plot.rect({
data,
size: { width: 16, height: 9 },
color: pure(row => Number(row.value) > 65
? { light: '#c2410c', dark: '#fdba74' }
: { light: '#0891b2', dark: '#67e8f9' }),
stroke: { color: { light: '#ffffff', dark: '#151b28' }, width: 1 },
tooltip: pure(({ row }) => h("span", {}, 'Value: ' + row.value)),
}),
],
height: 260,
margin: { top: 5, right: 8, bottom: 20, left: 30 },
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: false,
})
// In Antithesis notebook, plot() returns a Printable. Uncomment below to display it:
// print(sized_rectangles)
// presentation.blank("Sized rectangles").print(sized_rectangles)

Run history

Place fixed-size rectangles on a time axis in Passed and Failed lanes. Use band_align to anchor both sets of marks to the shared divider, with passing runs above and failures below.

const start = 1767258000000
const data = Array.from({ length: 36 }, (_, i) => ({
run: i + 1,
end_ms: start + (i * 3 + (i % 3)) * 60000,
lane: [5, 6, 14, 23, 24, 25, 32].includes(i) ? 'Failed' : 'Passed',
}))
const run_history = plot({
series: [
plot.rect({
data: data.filter(row => row.lane === 'Passed'),
x: 'end_ms',
y: 'lane',
size: { width: 10, height: 40 },
band_align: 'end',
inset: 0,
color: { light: '#189e3e', dark: '#4ade80' },
stroke: { color: { light: '#ffffff', dark: '#151b28' }, width: 0.5 },
tooltip: pure(({ row }) => h("span", {}, 'Run ' + row.run + ': ' + row.lane)),
}),
plot.rect({
data: data.filter(row => row.lane === 'Failed'),
x: 'end_ms',
y: 'lane',
size: { width: 10, height: 40 },
band_align: 'start',
inset: 0,
color: { light: '#c41e5e', dark: '#fb7185' },
stroke: { color: { light: '#ffffff', dark: '#151b28' }, width: 0.5 },
tooltip: pure(({ row }) => h("span", {}, 'Run ' + row.run + ': ' + row.lane)),
}),
],
height: 260,
margin: { top: 5, right: 8, bottom: 24, left: 56 },
axis: {
x: {
scale: 'utc',
min: start - 5 * 60000,
max: start + 115 * 60000,
label: '',
line: false,
tick_label: { format: pure(value => String(Math.floor(Number(value) / 3600000) % 24).padStart(2, '0') + ':' + String(Math.floor(Number(value) / 60000) % 60).padStart(2, '0')) },
},
y: { categories: ['Failed', 'Passed'], grid_align: 'edge', label: '', line: false },
},
background: { light: '#ffffff', dark: '#151b28' },
chrome_color: { light: '#e2e8f0', dark: '#334155' },
grid: true,
border: false,
theme_invert: false,
zoom_pan: false,
})
// In Antithesis notebook, plot() returns a Printable. Uncomment below to display it:
// print(run_history)
// presentation.blank("Run history").print(run_history)

Rect arguments

Field Type Default Description
data {[key: string]: any}[] Required Data points to plot.
x? FieldArg 'x' Horizontal field name or accessor.
x2? FieldArg 'x2' when present in data Opposite horizontal corner. Can be combined with size.height, but not size.width or a numeric size. A numeric x axis requires either x2 or a width supplied through size.
y? FieldArg 'y' Vertical field name or accessor.
y2? FieldArg 'y2' when present in data Opposite vertical corner. Can be combined with size.width, but not size.height or a numeric size. A numeric y axis requires either y2 or a height supplied through size.
span? 'x' | 'y' None Stretch the rectangle across the full plot on this axis.
size? number | { width?: number; height?: number } Derived from bounds or band Fixed pixel size. A number sets both dimensions and cannot be combined with x2 or y2. Use { width } to set the width in pixels while y and y2 determine the height, or { height } to set the height in pixels while x and x2 determine the width.
min_size? number 0 Minimum width in pixels when using x2, and minimum height in pixels when using y2.
band_align? 'center' | 'start' | 'end' 'center' Alignment of a fixed-size rectangle within a categorical band.
hover_span_x? boolean false Extend the area that responds to tooltips, selection, and hover highlights along the x axis.
hover_span_y? boolean false Extend the area that responds to tooltips, selection, and hover highlights along the y axis.
inset? number 1/6 of band width Space left on each side within a categorical band, in pixels.
offset? { x?: number; y?: number } { x: 0, y: 0 } Pixel offsets applied to each rectangle.
color? ColorArg Black Series color. Theme pairs provide separate light and dark colors.
stroke? StrokeArg None Outline color and optional width.
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 draws rectangles bounded by start, end, low, and high, with an outline, tooltips, and a selection callback.

const selected_data = new reactive.StateVar('Select a point to inspect its data.')
const series = [plot.rect({
data: [
{ start: 1, end: 3, low: 2, high: 5 },
{ start: 4, end: 6, low: 1, high: 4 },
],
x: 'start',
x2: 'end',
y: 'low',
y2: 'high',
min_size: 2,
offset: { x: 0, y: 0 },
color: 'lightsteelblue',
stroke: { color: 'steelblue', 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 bounded_rectangles_plot = plot({
series,
height: 260,
margin: { top: 12, right: 12, bottom: 28, left: 36 },
axis: { x: { min: 0, max: 7, label: '' }, y: { min: 0, max: 6, label: '' } },
background: { light: '#ffffff', dark: '#151b28' },
chrome_color: { light: '#e2e8f0', dark: '#334155' },
grid: true,
border: false,
zoom_pan: false,
})
const bounded_rectangles = h(
'div',
{},
bounded_rectangles_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(bounded_rectangles)
// presentation.blank("Bounded rectangles").print(bounded_rectangles)

For fixed-size marks in categorical bands, use size and band_align. inset controls the space around a band when its size is not fixed:

const series = [plot.rect({
data: [{ group: 'A', row: 'First' }, { group: 'B', row: 'Second' }],
x: 'group',
y: 'row',
size: { width: 12 },
band_align: 'start',
inset: 16,
offset: { x: 2, y: 0 },
hover_span_x: true,
hover_span_y: true,
color: 'steelblue',
tooltip: true,
})]
const band_rectangles = plot({
series,
height: 260,
margin: { top: 12, right: 12, bottom: 28, left: 62 },
axis: { x: { categories: ['A', 'B'], grid_align: 'edge', label: '' }, y: { categories: ['First', 'Second'], grid_align: 'edge', label: '' } },
background: { light: '#ffffff', dark: '#151b28' },
chrome_color: { light: '#e2e8f0', dark: '#334155' },
grid: true,
border: false,
zoom_pan: false,
})
// In Antithesis notebook, plot() returns a Printable. Uncomment below to display it:
// print(band_rectangles)
// presentation.blank("Band rectangles").print(band_rectangles)

Use span to fill one axis, leaving its coordinates unspecified. Add this series before the data series to draw a background region:

const series = [plot.rect({
data: [{ low: 2, high: 5 }],
span: 'x',
y: 'low',
y2: 'high',
color: 'aliceblue',
hoverable: false,
highlight: false,
})]
const background_region = plot({
series,
height: 260,
margin: { top: 12, right: 12, bottom: 28, left: 36 },
axis: { x: { min: 0, max: 10, label: '' }, y: { min: 0, max: 7, label: '' } },
background: { light: '#ffffff', dark: '#151b28' },
chrome_color: { light: '#e2e8f0', dark: '#334155' },
grid: true,
border: false,
zoom_pan: false,
})
// In Antithesis notebook, plot() returns a Printable. Uncomment below to display it:
// print(background_region)
// presentation.blank("Background region").print(background_region)

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.