Dumbbell Charts: Show Gaps and Change Between Two Values
A dumbbell chart (also called a connected dot plot or barbell chart) puts two dots on each category's row, one for each value, and joins them with a line, so the length of the line is the gap. Use it when the story is the difference between two values across many categories: men's vs. women's pay by occupation, this year vs. last year by region, target vs. actual by team.
Quick answer: A dumbbell chart plots two values per category as two dots on a shared axis, joined by a line that makes the gap visible. Use it instead of grouped bars when you have more than a handful of categories or when the gap matters more than the totals, and instead of a slope chart when the two values are groups rather than time points or when labels would collide. Sort the rows by gap size, and don't force a zero baseline: the dots encode position, not length.
What is a dumbbell chart?
It is a dot plot with exactly two dots per row. Each category gets its own horizontal row, both values sit on the same horizontal axis, and a thin line connects them. The two dots are colored consistently (for example orange for women, blue for men) so the reader learns the encoding once and applies it to every row. The connecting line does two jobs: it groups the pair visually and turns the difference into a length you can see without doing arithmetic.
Dumbbells work for two kinds of pairs:
- Gaps between groups: men vs. women, urban vs. rural, your product vs. a competitor, measured on the same metric for every category.
- Before/after change: the same metric at two points in time, such as satisfaction scores in 2024 and 2025 by department.
What does a dumbbell chart of pay gaps by occupation look like?
Here is a worked example with six occupations and median weekly earnings for women and men. The figures are illustrative round numbers chosen to demonstrate the technique, not published statistics.
| Occupation | Women | Men | Gap (men โ women) | Women as % of men |
|---|---|---|---|---|
| Legal | $1,450 | $1,900 | $450 | 76.3% |
| Management | $1,500 | $1,850 | $350 | 81.1% |
| Sales | $820 | $1,100 | $280 | 74.5% |
| Healthcare | $1,250 | $1,500 | $250 | 83.3% |
| Education | $1,100 | $1,250 | $150 | 88.0% |
| Office support | $850 | $900 | $50 | 94.4% |
Sorted by dollar gap, largest at the top, the chart looks like this (each character is roughly $50):
โ Women โ Men Gap
Legal โโโโโโโโโโ $450
Management โโโโโโโโ $350
Sales โโโโโโโ $280
Healthcare โโโโโโ $250
Education โโโโ $150
Office support โโ $50
โฌโโโโโโโโฌโโโโโโโโฌโโโโโโโโฌ
$800 $1,200 $1,600 $2,000
Three things are visible at once that a table hides: where each occupation sits on the pay scale (Sales and Office support are low for both groups), how big each gap is (Legal's line is nine times as long as Office support's), and that the gap runs the same direction in every row. Note the horizontal axis starts at $800, not zero. That is fine for a dumbbell, as explained below.
When should I use a dumbbell chart instead of a grouped bar chart?
Switch to a dumbbell once you have more than three or four categories, or whenever the gap is the point. A grouped bar chart makes the reader compare two bar heights, pair by pair, and the difference is never drawn. Bars also have to start at zero because they encode length, so when both values are large relative to their difference, the bars look nearly identical: for Office support ($850 vs. $900), the $50 gap is under 6% of the taller bar's height. A dumbbell draws the gap as its own line and lets you zoom the axis to the range of the data.
| Dumbbell chart | Grouped bar chart | Slope chart | |
|---|---|---|---|
| Layout | One row per category, one shared axis | Two bars per category | Two vertical axes, one line per category |
| Shows the gap directly | Yes, as the line length | No, reader compares heights | Indirectly, as steepness |
| Needs a zero baseline | No (position encoding) | Yes (length encoding) | No |
| Many categories (15+) | Handles well; long labels fit | Becomes a wall of bars | End labels collide when values are close |
| Best for | Gaps between groups; before/after across many categories | A few categories where the totals themselves matter | Two time points where rank changes and crossings are the story |
Dumbbell chart vs. slope chart: which should I use?
Use a slope chart when the two values are time points and the story is who rose, who fell, and whose lines cross, because slopes and crossings are exactly what that layout makes pop. Use a dumbbell when the two values are groups rather than times (men vs. women has no natural "left" and "right" in time), when you want readers to read exact values off a single axis, or when you have enough categories that labels on a slope chart's end axes would pile on top of each other. A dumbbell gives every category its own row, so labels never overlap no matter how close the values are.
Do dumbbell charts need to start at zero?
No. A bar must start at zero because its length is the value, and a truncated bar misstates it. In a dumbbell, each value is encoded by the position of a dot, and on any linear axis the distance between two dots is proportional to their difference no matter where the axis starts. The $450 gap is always 1.8 times as long as the $250 gap. Starting at zero is acceptable too, but when values are far from zero it squeezes every dumbbell into one edge of the chart. What you should not do is truncate so aggressively that a trivial difference fills the width; see axis truncation ethics for where the line is.
How should I sort a dumbbell chart?
Sort by the gap in most cases, largest at the top, because the gap is usually why you chose a dumbbell. The general rules in sorting chart data apply: alphabetical order is almost never the right choice for a comparison. Three sensible options, depending on the message:
- By absolute gap ($ difference): answers "where is the gap biggest in dollars?"
- By relative gap (ratio or percent): answers "where is the gap biggest in proportion?" In the example this reorders the rows: Sales has only the third-largest dollar gap ($280) but the largest relative one, since women's median is 74.5% of men's, compared with 76.3% in Legal.
- By one of the two values (for example, men's pay): keeps the rows in pay-scale order so readers can see whether gaps widen as pay rises.
Decide which question you are answering before sorting, and say it in the title, because the absolute and relative orderings can tell different stories from the same data.
What are Cleveland dot plots and lollipop charts?
They are the two closest relatives of the dumbbell. A Cleveland dot plot, named for statistician William S. Cleveland, who advocated dot charts in The Elements of Graphing Data (1985), replaces bars with a single dot per category on a shared axis, usually with light gridlines and rows sorted by value; R's base dotchart() function draws one. A dumbbell is a Cleveland dot plot with two dots per row and a connecting line. A lollipop chart is a bar chart drawn as a thin stem ending in a dot. It lightens a bar chart with many categories, but the stem is still a length from the baseline, so unlike a dot plot it needs a zero baseline.
| Variant | What it draws | Zero baseline? | Use when |
|---|---|---|---|
| Cleveland dot plot | One (or more) dots per row on a shared axis | Not required | Replacing a ranked bar chart, especially when values are far from zero |
| Dumbbell chart | Two dots per row joined by a line | Not required | The distance between two values is the message |
| Arrow chart | A dumbbell with an arrowhead on the "after" end | Not required | Before/after data where some rows rise and others fall |
| Lollipop chart | A stem from the baseline to a dot | Yes | A bar chart with many categories where solid bars feel heavy |
| Gap lollipop or bar | Only the difference, from zero | Yes | The gap is the only thing that matters and the underlying levels do not |
The last row is worth keeping in mind. Cleveland and McGill's graphical perception experiments ranked position along a common scale as the most accurately judged encoding, ahead of length. In a dumbbell, the connector lengths start at different points on each row, so comparing two gaps precisely is harder than comparing two aligned bars. If readers only need the gap sizes, plot the differences directly; if they need the levels and the gap together, the dumbbell is the better choice.
How do I make a dumbbell chart in Excel?
Excel has no built-in dumbbell chart type, but two standard workarounds produce one. The quickest gives you vertical dumbbells from a line chart with high-low lines. Put occupations in column A, women in B and men in C, sort the rows by gap first (a line chart plots categories in row order), then:
1. Select A1:C7 โ Insert โ Line โ Line with Markers
2. Chart Design โ Add Chart Element โ Lines โ High-Low Lines
3. Format each series: Line โ No line; Marker โ built-in circle, size 8-10
4. Format the high-low lines: light gray, 2-3 pt
High-low lines connect the highest and lowest series value at each category, which is exactly the dumbbell's connector. The catch is orientation: categories run along the horizontal axis, so long labels get cramped. For horizontal dumbbells, build a scatter chart and draw the connectors with error bars:
Column D (gap): =C2-B2
Column E (row, y): =RANK.EQ(D2,$D$2:$D$7,1)+COUNTIF($D$2:D2,D2)-1
1. Insert โ Scatter (markers only), then Select Data:
Series "Women": X = B2:B7, Y = E2:E7
Series "Men": X = C2:C7, Y = E2:E7
2. Select the Women series โ Chart Elements (+) โ Error Bars
3. Delete the vertical error bars; format the horizontal ones:
Direction: Plus End Style: No Cap
Error Amount: Custom โ Positive Error Value = D2:D7
Line: light gray, 3 pt
4. Women series โ Data Labels โ Label Options: untick Y Value,
tick "Value From Cells" (select A2:A7), Label Position: Left
5. Vertical axis: set Labels to None, bounds 0.5 to 6.5
The rank formula in column E numbers the rows by gap in ascending order (the COUNTIF part breaks ties), so the largest gap gets the highest y value and lands at the top, and the chart re-sorts itself when the data changes. If some rows have the second value below the first, use two helper columns, =MAX(C2-B2,0) and =MAX(B2-C2,0), set the error bar direction to Both, and use them as the positive and negative error values, so every custom error value stays non-negative. "Value From Cells" requires Excel 2013 or later. Step 4 puts each name just left of its row's first dot; if you want the names aligned in a column instead, add a third series with every X set to the axis minimum and no marker, and attach the labels to it.
How do I make a dumbbell chart in ggplot2?
The recommended approach uses plain ggplot2 with no extra packages: one geom_segment() for the connectors and two geom_point() layers for the dots. Mapping a constant string to colour inside aes() gives you a legend without reshaping the data:
library(ggplot2)
pay <- data.frame(
occupation = c("Legal", "Management", "Sales", "Healthcare",
"Education", "Office support"),
women = c(1450, 1500, 820, 1250, 1100, 850),
men = c(1900, 1850, 1100, 1500, 1250, 900)
)
pay$gap <- pay$men - pay$women
# The first factor level is drawn at the bottom of a discrete y axis,
# so ordering by ascending gap puts the largest gap at the top
pay$occupation <- reorder(pay$occupation, pay$gap)
ggplot(pay, aes(y = occupation)) +
geom_segment(aes(x = women, xend = men, yend = occupation),
colour = "grey75", linewidth = 1.5) +
geom_point(aes(x = women, colour = "Women"), size = 3) +
geom_point(aes(x = men, colour = "Men"), size = 3) +
scale_colour_manual(values = c(Women = "#E07A1F", Men = "#1F6FB4")) +
labs(x = "Median weekly earnings (USD)", y = NULL, colour = NULL) +
theme_minimal()
The linewidth argument needs ggplot2 3.4.0 or later; older versions use size for line width. Draw the segment layer first so the dots sit on top of it. For before/after data, add arrow = arrow(length = unit(2, "mm")) to geom_segment() to turn it into an arrow chart.
If you prefer a single geom, the ggalt package provides geom_dumbbell(), which takes x, xend and y aesthetics. Note that ggalt was archived from CRAN in August 2025, so install.packages("ggalt") no longer works. Install it from GitHub with remotes::install_github("hrbrmstr/ggalt"); the current version needs ggplot2 4.0 or later:
# ggalt is not on CRAN; install once from GitHub (needs ggplot2 >= 4.0.0):
# remotes::install_github("hrbrmstr/ggalt")
library(ggalt)
ggplot(pay, aes(x = women, xend = men, y = occupation)) +
geom_dumbbell(colour = "grey75", size_x = 3, size_xend = 3,
colour_x = "#E07A1F", colour_xend = "#1F6FB4") +
labs(x = "Median weekly earnings (USD)", y = NULL) +
theme_minimal()
Because colour_x and colour_xend are fixed parameters rather than mapped aesthetics, geom_dumbbell() draws no legend. Label the two dots directly on the top row or name the colors in the subtitle. For most projects the geom_segment() version above is the better default: it needs nothing beyond ggplot2 from CRAN and gives you full control of each layer.
How do I make a dumbbell chart in matplotlib?
Use ax.hlines() for the connectors and two ax.scatter() calls for the dots, with zorder keeping the dots on top. Sort with np.argsort() on the gap; because matplotlib draws y = 0 at the bottom, ascending order puts the largest gap at the top:
import matplotlib.pyplot as plt
import numpy as np
occupations = ["Legal", "Management", "Sales", "Healthcare",
"Education", "Office support"]
women = np.array([1450, 1500, 820, 1250, 1100, 850])
men = np.array([1900, 1850, 1100, 1500, 1250, 900])
order = np.argsort(men - women) # smallest gap first = bottom row
labels = [occupations[i] for i in order]
women, men = women[order], men[order]
y = np.arange(len(labels))
fig, ax = plt.subplots(figsize=(7, 4))
ax.hlines(y, women, men, color="0.75", linewidth=3, zorder=1)
ax.scatter(women, y, color="#E07A1F", s=60, zorder=2, label="Women")
ax.scatter(men, y, color="#1F6FB4", s=60, zorder=2, label="Men")
ax.set_yticks(y)
ax.set_yticklabels(labels)
ax.set_xlabel("Median weekly earnings (USD)")
ax.legend(frameon=False, loc="lower right")
for side in ("top", "right"):
ax.spines[side].set_visible(False)
plt.tight_layout()
plt.show()
The legend goes in the lower right because the bottom rows (smallest gaps) all end at or below $1,500, which leaves the lower-right corner empty; check your own data before placing it.
Common mistakes
Most bad dumbbell charts fail on ordering, encoding, or chart choice rather than on the code:
- Leaving rows in alphabetical or data-entry order. The gap pattern disappears. Sort by gap or by one of the values.
- Forcing a zero baseline out of bar-chart habit. It pushes every dumbbell into one edge of the chart and shrinks the gaps you are trying to show.
- Making the connector compete with the dots. Draw the line in light gray and thinner than the dot diameter; the dots carry the values.
- Hiding direction in before/after data. If some rows rise and others fall, keep the "before" and "after" colors consistent in every row or add arrowheads, or readers will assume every change goes the same way.
- Picking a color pair many readers cannot tell apart. Red and green is the classic failure; orange and blue is a safer default for two groups.
- Using a dumbbell for three or more time points. It is a two-value chart. For a trend over time, use a line chart; for three or more groups per row, use a multi-dot Cleveland plot. See how to choose the right chart type.
- Mixing absolute and relative framing. Sorting by dollar gap while the title talks about percentage gaps (or the reverse) produces an ordering that contradicts the headline.
Pro Tip: Print the gap as a number at the right end of each row (as in the example above) and label the two dots by name on the top row instead of using a separate legend. Readers get the exact difference without estimating line lengths, and they learn the color encoding right where they start reading.
โ Back to Visualization Tips