A one-page summary. It has a headline that states the main finding with its number, a deck, a row of three or four key figures, one hero chart with a takeaway callout, two smaller supporting exhibits, a short list of actions and a source note.
Dumbbell chart
Shows two values per category as dots joined by a line, so the length of the line shows the difference.
- Use when
- Before / after or group A / group B across up to about 20 categories, when the size of the difference is what the reader needs.
- Avoid when
- A single value per category (use lollipop chart or bar chart); when the direction of change matters more than the two levels (use arrow chart); more than two points in time (use slope chart or line chart).
Preview
Life expectancy in South Korea rose from 76.0 years in 2000 to 83.6 in 2023
Life expectancy at birth, years, 2000 and 2023
import { DumbbellChart } from "insightcharts";
const data = [
{ country: "South Korea", y2000: 76, y2023: 83.6 },
{ country: "Turkey", y2000: 71.1, y2023: 77.3 },
{ country: "Poland", y2000: 73.8, y2023: 78.6 },
{ country: "Japan", y2000: 81.2, y2023: 84.1 },
{ country: "Germany", y2000: 78.2, y2023: 81.2 },
{ country: "United States", y2000: 76.7, y2023: 78.4 },
];
export function Example() {
return (
<DumbbellChart
data={data}
category="country"
from="y2000"
to="y2023"
names={{ y2000: "2000", y2023: "2023" }}
gapLabel="Gain"
format="decimal:1"
height={280}
title="Life expectancy in South Korea rose from 76.0 years in 2000 to 83.6 in 2023"
subtitle="Life expectancy at birth, years, 2000 and 2023"
source="OECD Health Statistics (rounded)"
note="Figures are approximate, for illustration"
/>
);
}{
"type": "dumbbell-chart",
"data": [
{ "country": "South Korea", "y2000": 76, "y2023": 83.6 },
{ "country": "Turkey", "y2000": 71.1, "y2023": 77.3 },
{ "country": "Poland", "y2000": 73.8, "y2023": 78.6 },
{ "country": "Japan", "y2000": 81.2, "y2023": 84.1 },
{ "country": "Germany", "y2000": 78.2, "y2023": 81.2 },
{ "country": "United States", "y2000": 76.7, "y2023": 78.4 }
],
"category": "country",
"from": "y2000",
"to": "y2023",
"names": { "y2000": "2000", "y2023": "2023" },
"gapLabel": "Gain",
"format": "decimal:1",
"height": 280,
"title": "Life expectancy in South Korea rose from 76.0 years in 2000 to 83.6 in 2023",
"subtitle": "Life expectancy at birth, years, 2000 and 2023",
"source": "OECD Health Statistics (rounded)",
"note": "Figures are approximate, for illustration"
}Render with import { Insight } from "insightcharts" and <Insight spec={spec} />. Nested specs resolve by name; nothing else to import.
Composition
Used in
1 template uses dumbbell chart.
Templates
1Installation
$ pnpm dlx shadcn@latest add @insightcharts/dumbbell-chartThe CLI copies the source into your project and needs the @insightcharts registry in components.json; see installation. With the package, import it by name:
import { DumbbellChart } from "insightcharts";Examples
Two groups, highlighted row
Women in South Korea earned 68.8% of men's median pay in 2022
Median full-time earnings, men = 100
import { DumbbellChart } from "insightcharts";
const data = [
{ country: "South Korea", men: 100, women: 68.8 },
{ country: "Japan", men: 100, women: 78.7 },
{ country: "United States", men: 100, women: 83 },
{ country: "United Kingdom", men: 100, women: 85.5 },
{ country: "Germany", men: 100, women: 85.6 },
{ country: "France", men: 100, women: 88.4 },
{ country: "Sweden", men: 100, women: 92.7 },
{ country: "Belgium", men: 100, women: 98.8 },
];
export function Example() {
return (
<DumbbellChart
data={data}
category="country"
from="women"
to="men"
kind="groups"
names={{ men: "Men", women: "Women" }}
highlight="South Korea"
gapLabel="Gap"
title="Women in South Korea earned 68.8% of men's median pay in 2022"
subtitle="Median full-time earnings, men = 100"
source="OECD gender wage gap, 2022 (rounded)"
note="Figures are approximate, for illustration"
/>
);
}{
"type": "dumbbell-chart",
"data": [
{ "country": "South Korea", "men": 100, "women": 68.8 },
{ "country": "Japan", "men": 100, "women": 78.7 },
{ "country": "United States", "men": 100, "women": 83 },
{ "country": "United Kingdom", "men": 100, "women": 85.5 },
{ "country": "Germany", "men": 100, "women": 85.6 },
{ "country": "France", "men": 100, "women": 88.4 },
{ "country": "Sweden", "men": 100, "women": 92.7 },
{ "country": "Belgium", "men": 100, "women": 98.8 }
],
"category": "country",
"from": "women",
"to": "men",
"kind": "groups",
"names": { "men": "Men", "women": "Women" },
"highlight": "South Korea",
"gapLabel": "Gap",
"title": "Women in South Korea earned 68.8% of men's median pay in 2022",
"subtitle": "Median full-time earnings, men = 100",
"source": "OECD gender wage gap, 2022 (rounded)",
"note": "Figures are approximate, for illustration"
}Render with import { Insight } from "insightcharts" and <Insight spec={spec} />. Nested specs resolve by name; nothing else to import.
Long format
Rent in Lisbon rose from €860 in 2015 to €1,720 in 2024
Typical monthly rent for a one-bedroom flat, euros
import { DumbbellChart } from "insightcharts";
const data = [
{ city: "Lisbon", period: "2015", value: 860 },
{ city: "Lisbon", period: "2024", value: 1720 },
{ city: "Dublin", period: "2015", value: 1390 },
{ city: "Dublin", period: "2024", value: 2380 },
{ city: "Berlin", period: "2015", value: 780 },
{ city: "Berlin", period: "2024", value: 1310 },
{ city: "Madrid", period: "2015", value: 790 },
{ city: "Madrid", period: "2024", value: 1260 },
{ city: "Vienna", period: "2015", value: 840 },
{ city: "Vienna", period: "2024", value: 1090 },
];
export function Example() {
return (
<DumbbellChart
data={data}
category="city"
series="period"
value="value"
from="2015"
to="2024"
format="currency:EUR"
height={260}
title="Rent in Lisbon rose from €860 in 2015 to €1,720 in 2024"
subtitle="Typical monthly rent for a one-bedroom flat, euros"
note="Illustrative figures."
/>
);
}{
"type": "dumbbell-chart",
"data": [
{ "city": "Lisbon", "period": "2015", "value": 860 },
{ "city": "Lisbon", "period": "2024", "value": 1720 },
{ "city": "Dublin", "period": "2015", "value": 1390 },
{ "city": "Dublin", "period": "2024", "value": 2380 },
{ "city": "Berlin", "period": "2015", "value": 780 },
{ "city": "Berlin", "period": "2024", "value": 1310 },
{ "city": "Madrid", "period": "2015", "value": 790 },
{ "city": "Madrid", "period": "2024", "value": 1260 },
{ "city": "Vienna", "period": "2015", "value": 840 },
{ "city": "Vienna", "period": "2024", "value": 1090 }
],
"category": "city",
"series": "period",
"value": "value",
"from": "2015",
"to": "2024",
"format": "currency:EUR",
"height": 260,
"title": "Rent in Lisbon rose from €860 in 2015 to €1,720 in 2024",
"subtitle": "Typical monthly rent for a one-bedroom flat, euros",
"note": "Illustrative figures."
}Render with import { Insight } from "insightcharts" and <Insight spec={spec} />. Nested specs resolve by name; nothing else to import.
Stress: 22 rows, long names, axis instead of labels
Remote working in information and communication rose from 48% in 2019 to 66% in 2024
Share of employees working from home at least one day a week, by sector
import { DumbbellChart } from "insightcharts";
const data = [
{ sector: "Professional, scientific and technical services", before: 41, after: 63 },
{ sector: "Information and communication", before: 48, after: 66 },
{ sector: "Financial and insurance activities", before: 39, after: 58 },
{ sector: "Public administration and defence", before: 22, after: 44 },
{ sector: "Education", before: 18, after: 33 },
{ sector: "Real estate activities", before: 21, after: 35 },
{ sector: "Administrative and support services", before: 14, after: 26 },
{ sector: "Arts, entertainment and recreation", before: 11, after: 21 },
{ sector: "Wholesale and retail trade", before: 8, after: 15 },
{ sector: "Manufacturing", before: 9, after: 15 },
{
sector: "Electricity, gas, steam and air conditioning supply",
before: 19,
after: 31,
},
{ sector: "Construction", before: 6, after: 11 },
{ sector: "Human health and social work", before: 7, after: 13 },
{ sector: "Transportation and storage", before: 5, after: 9 },
{ sector: "Mining and quarrying", before: 8, after: 11 },
{ sector: "Water supply, sewerage and waste management", before: 7, after: 10 },
{ sector: "Agriculture, forestry and fishing", before: 12, after: 14 },
{ sector: "Accommodation and food services", before: 3, after: 4 },
{ sector: "Other service activities", before: 10, after: 17 },
{ sector: "Activities of households as employers", before: 2, after: 3 },
{ sector: "Extraterritorial organisations", before: 30, after: 47 },
{ sector: "Repair of motor vehicles", before: 4, after: 6 },
];
export function Example() {
return (
<DumbbellChart
data={data}
category="sector"
from="before"
to="after"
names={{ before: "2019", after: "2024" }}
format={"{value}%"}
title="Remote working in information and communication rose from 48% in 2019 to 66% in 2024"
subtitle="Share of employees working from home at least one day a week, by sector"
note="Illustrative figures."
/>
);
}{
"type": "dumbbell-chart",
"data": [
{
"sector": "Professional, scientific and technical services",
"before": 41,
"after": 63
},
{ "sector": "Information and communication", "before": 48, "after": 66 },
{ "sector": "Financial and insurance activities", "before": 39, "after": 58 },
{ "sector": "Public administration and defence", "before": 22, "after": 44 },
{ "sector": "Education", "before": 18, "after": 33 },
{ "sector": "Real estate activities", "before": 21, "after": 35 },
{ "sector": "Administrative and support services", "before": 14, "after": 26 },
{ "sector": "Arts, entertainment and recreation", "before": 11, "after": 21 },
{ "sector": "Wholesale and retail trade", "before": 8, "after": 15 },
{ "sector": "Manufacturing", "before": 9, "after": 15 },
{
"sector": "Electricity, gas, steam and air conditioning supply",
"before": 19,
"after": 31
},
{ "sector": "Construction", "before": 6, "after": 11 },
{ "sector": "Human health and social work", "before": 7, "after": 13 },
{ "sector": "Transportation and storage", "before": 5, "after": 9 },
{ "sector": "Mining and quarrying", "before": 8, "after": 11 },
{
"sector": "Water supply, sewerage and waste management",
"before": 7,
"after": 10
},
{ "sector": "Agriculture, forestry and fishing", "before": 12, "after": 14 },
{ "sector": "Accommodation and food services", "before": 3, "after": 4 },
{ "sector": "Other service activities", "before": 10, "after": 17 },
{ "sector": "Activities of households as employers", "before": 2, "after": 3 },
{ "sector": "Extraterritorial organisations", "before": 30, "after": 47 },
{ "sector": "Repair of motor vehicles", "before": 4, "after": 6 }
],
"category": "sector",
"from": "before",
"to": "after",
"names": { "before": "2019", "after": "2024" },
"format": "{value}%",
"title": "Remote working in information and communication rose from 48% in 2019 to 66% in 2024",
"subtitle": "Share of employees working from home at least one day a week, by sector",
"note": "Illustrative figures."
}Render with import { Insight } from "insightcharts" and <Insight spec={spec} />. Nested specs resolve by name; nothing else to import.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
| categoryrequired | string | Category field: one row of the chart each. | |
| fromrequired | string | Field with the first value, or (long data) the series value that marks it. | |
| torequired | string | Field with the second value, or (long data) the series value that marks it. | |
| series | string | Long data: the field whose values are from and to. | |
| value | string | "value" | Long data: the measure field. |
| kind | "change" | "groups" | "change" | change draws a tint then the accent (before / after); groups uses two hues (two peers). |
| sort | "gap" | "from" | "to" | "none" | "gap" | Row order. |
| order | "desc" | "asc" | "desc" | Sort direction. |
| labels | boolean | Value labels beside both dots. Default: on for up to 14 rows; otherwise a value axis is drawn. | |
| gapLabel | boolean | string | false | Print the gap in a right-hand column; a string sets the column header. |
| zero | boolean | false | Start the value axis at zero. |
| names | Record<string, string> | Display names for the from and to keys. |
Common chart props 29
Every chart built with defineChart accepts these on top of its own props.
| Prop | Type | Description |
|---|---|---|
| data | DataInput | Rows, CSV text, a URL, { url, path }, an async function, or an Arrow-style table. Omit inside <InsightData>. |
| transform | Transform | Transform[] | A pipeline of transforms applied before drawing. |
| title | string | The title. One plain statement of what the data shows, with the number and the period ("Monthly churn fell from 4.2% to 2.1% in 2024"), or a plain label ("Monthly churn, 2024"). One clause, no figures of speech. |
| subtitle | string | Measure, unit, population, period. |
| source | string | Data source, shown as "Source: ...". |
| note | string | Method notes or caveats. |
| height | number | string | CSS height of the plot. Default 360. |
| aspect | number | Width / height. Overrides height. |
| highlight | string | string[] | The entity (or entities) the title is about. Every other series is drawn in grey. |
| annotations | Annotation[] | Reference lines, shaded bands and callouts, in data coordinates. |
| format | FormatSpec | How to print values: "compact", "percent", "currency:USD", "{value} kg", Intl options, or a function. |
| legend | boolean | Show the legend. Default: shown when there are two or more series. |
| select | boolean | string | Make marks clickable to filter. Inside <InsightData> the selection filters every other chart. Pass a field name, or true to use the chart's category field. |
| onSelect | (values: string[], field: string) => void | Called with the selected values whenever the selection changes. |
| option | EChartsOption | ((option: EChartsOption, ctx: BuildContext) => EChartsOption) | Deep-merged over the generated ECharts option, or a function that receives and returns it. Use it for ECharts settings no prop covers. |
| theme | ThemeInput | A theme, or a partial set of tokens merged over the surrounding theme, for this chart only. |
| mode | ModeInput | "auto" (default) follows the page, "system" follows the OS, or force "light" / "dark". |
| renderer | RendererInput | "auto" (default) uses SVG for light charts and canvas for heavy ones. |
| animate | boolean | Set false to disable animation. Reduced-motion preferences are always respected. |
| lazy | boolean | Mount the chart only when it scrolls near the viewport. Default true. |
| group | string | Link tooltips with other charts sharing this group id. |
| exportable | boolean | ExportFormat[] | Show an export menu: true or a list of formats from png, jpeg, svg, pdf, csv, json. |
| table | boolean | Show a collapsible "View data" table under the chart. |
| alt | string | Accessible description: the chart type and measures, then the main pattern and the notable values. |
| id | string | Stable id for this chart's selection in the shared store. |
| onEvents | Record<string, ChartEventHandler> | ECharts event handlers, e.g. { click: (params, chart) => {} }. Prefix a name with zr: to bind to the canvas itself. |
| onReady | (chart: EChartsInstance) => void | Called with the ECharts instance once it is created. |
| className | string | Class name for the outer <figure>. |
| style | CSSProperties | Inline style for the outer <figure>. |