Skip to main content

DotPlotChart

DotPlotChart

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

View Demo

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 title prop is used as aria-label; a description naming the highest and lowest values is generated when description is 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
On this page