One row per category with a dot per series, on a value axis that need not start at zero
Tags: chart dot-plot dumbbell comparison ranking reporting
The DotPlotChart draws one row per category and one dot per series, on a value axis fitted to the data instead of starting at zero. It is the clearest way to compare a measure such as average cycle time across many builders, communities or plans. Set connect to join each row's dots into a dumbbell for before/after comparisons, and sizes to scale dots by a sample size.
Svelte only (
@classic-homes/charts-svelte); React support is pending.
Import
<script>
import { DotPlotChart } from '@classic-homes/charts-svelte';
</script>Basic Usage
<script>
import { DotPlotChart } from '@classic-homes/charts-svelte';
const data = {
categories: ['Hilltop Builders', 'Riverbend Homes', 'Oak & Main', 'Summit Custom'],
series: [{ name: 'Avg cycle time', data: [104, 118, 127, 142] }],
};
</script>
<DotPlotChart title="Cycle Time by Builder" {data} valueFormat="days" />Props
Chart-Specific Props
| Prop | Type | Default | Description |
|---|---|---|---|
data |
DotPlotChartData |
required | Categories (one row each) and series (one dot per row) |
dotSize |
number | [number, number] |
12 |
Dot diameter in pixels, or the [smallest, largest] range when series have sizes |
connect |
boolean |
false |
Draw a line between the dots of a row (a dumbbell plot) |
rowHeight |
number |
28 |
Height per row for height="auto" |
showLabels |
boolean |
false |
Show value labels beside the dots |
labelFormatter |
string | ((params: LabelFormatterParams) => string) |
- | Data label template or function |
valueAxisConfig |
AxisConfig |
- | Value axis customisation (min/max default to the data range) |
categoryAxisConfig |
AxisConfig |
- | Category axis customisation |
referenceLines |
ReferenceLine[] |
- | Vertical lines marking targets or statistics |
referenceBands |
ReferenceBand[] |
- | Shaded value ranges |
legend |
ChartLegendConfig |
- | Built-in legend position, alignment, icon and formatter |
onLegendChange |
(visible: Record<string, boolean>) => void |
- | Called when legend items are shown or hidden |
renderLegend |
Snippet<[LegendRenderProps]> |
- | Custom HTML legend (e.g. ChartLegend) instead of the built-in one |
onClick |
(params: DataPointEventParams) => void |
- | Callback when a dot is clicked |
Inherited Base Props
| Prop | Type | Default | Description |
|---|---|---|---|
title |
string |
required | Chart title for accessibility (aria-label) |
description |
string |
- | Description for screen readers (generated from the data when omitted) |
height |
number | string |
'auto' |
Chart height in pixels or CSS value |
minHeight |
number |
- | Minimum height in pixels; also bounds an 'auto' height |
maxHeight |
number |
- | Maximum height in pixels; also bounds an 'auto' height |
loading |
boolean |
false |
Show loading skeleton (an overlay when data is already shown) |
error |
string | null |
- | Error message to display |
onRetry |
() => void |
- | Show a Retry button on the error state, calling this |
emptyMessage |
string |
- | Message shown when data is empty |
theme |
'light' | 'dark' | 'auto' |
'auto' |
Theme preference |
animation |
boolean |
true |
Enable/disable animations (respects prefers-reduced-motion) |
showLegend |
boolean |
true |
Show chart legend |
showTooltip |
boolean |
true |
Show tooltip on hover |
showGrid |
boolean |
true |
Show grid lines |
valueFormat |
ValueFormat |
- | Preset ('number', 'integer', 'compact', 'currency', 'currencyCents', 'percent', 'percentValue', 'days'), Intl.NumberFormatOptions or a function; drives the value axis, tooltip, labels and data table |
locale |
string |
'en-US' |
Locale for valueFormat |
tooltipFormatter |
string | ((items: TooltipFormatterParams[]) => string) |
- | Custom tooltip template or HTML function |
tooltipValueFormatter |
(value: number) => string |
- | Format values in the default tooltip (overrides valueFormat there) |
option |
EChartsOption |
- | Raw ECharts option deep-merged over the generated one |
onReady |
(chart: EChartsType) => void |
- | Called with the ECharts instance when it is created |
events |
ChartEventHandlers |
- | Raw ECharts event handlers keyed by event name ('datazoom', 'legendselectchanged', ...) |
showDataTable |
boolean | 'sr-only' |
'sr-only' |
Data table for screen readers only, a visible "Show data" toggle (true) or none (false) |
exportable |
boolean | ChartExportConfig |
- | Show PNG and CSV download buttons, or { image, csv, filename } |
patterns |
boolean |
false |
Fill series with decal patterns as well as colour |
class |
string |
- | Custom CSS class |
Data Structure
interface DotPlotChartData {
categories: string[]; // One row per category, first at the top
series: Array<{
name: string; // Series name for legend
data: (number | null)[]; // Values by category; null leaves the row empty
sizes?: (number | null)[]; // Per-point weight (e.g. sample size) scaling the dot area
color?: string; // Optional custom color
labels?: (string | null | undefined)[]; // Per-point label text
}>;
}Examples
Dumbbell: This Year vs Last Year
<DotPlotChart
title="Cycle Time, 2025 vs 2026"
data={{
categories: ['Hilltop Builders', 'Riverbend Homes', 'Oak & Main', 'Summit Custom'],
series: [
{ name: '2025', data: [121, 130, 125, 158] },
{ name: '2026', data: [104, 118, 127, 142] },
],
}}
connect
valueFormat="days"
/>Dots Sized by Sample Size
The tooltip shows the weight as n = ....
<DotPlotChart
title="Warranty Response Time by Community"
data={{
categories: ['Cedar Ridge', 'Lakeside', 'Willow Creek'],
series: [{ name: 'Avg response', data: [2.4, 3.1, 1.8], sizes: [140, 36, 88] }],
}}
dotSize={[8, 24]}
valueFormat={{ maximumFractionDigits: 1 }}
referenceLines={[{ value: 3, label: 'SLA' }]}
/>From Records
<script>
import { DotPlotChart, fromRecords } from '@classic-homes/charts-svelte';
let { closings } = $props(); // [{ builder: 'Hilltop Builders', cycleDays: 98 }, ...]
const data = $derived(
fromRecords(closings, {
category: 'builder',
value: 'cycleDays',
aggregate: 'avg',
seriesName: 'Avg cycle time',
sort: 'total-asc',
})
);
</script>
<DotPlotChart title="Cycle Time by Builder" {data} valueFormat="days" />Accessibility
- The
titleprop is used asaria-label; a description naming the highest and lowest values is generated whendescriptionis omitted - The chart data is rendered as a screen-reader table (
showDataTable) - Animations respect
prefers-reduced-motion - Height follows the row count by default, so long lists stay readable
Related Charts
- BarChart - Categorical comparison from zero
- BulletChart - Actual vs target per row
- BoxPlotChart - Spread per category