Dumbbell Charts: Show Gaps and Change Between Two Values

โฑ๏ธ 10 min read ๐Ÿ“Š Visualization

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:

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.

OccupationWomenMenGap (men โˆ’ women)Women as % of men
Legal$1,450$1,900$45076.3%
Management$1,500$1,850$35081.1%
Sales$820$1,100$28074.5%
Healthcare$1,250$1,500$25083.3%
Education$1,100$1,250$15088.0%
Office support$850$900$5094.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 chartGrouped bar chartSlope chart
LayoutOne row per category, one shared axisTwo bars per categoryTwo vertical axes, one line per category
Shows the gap directlyYes, as the line lengthNo, reader compares heightsIndirectly, as steepness
Needs a zero baselineNo (position encoding)Yes (length encoding)No
Many categories (15+)Handles well; long labels fitBecomes a wall of barsEnd labels collide when values are close
Best forGaps between groups; before/after across many categoriesA few categories where the totals themselves matterTwo 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:

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.

VariantWhat it drawsZero baseline?Use when
Cleveland dot plotOne (or more) dots per row on a shared axisNot requiredReplacing a ranked bar chart, especially when values are far from zero
Dumbbell chartTwo dots per row joined by a lineNot requiredThe distance between two values is the message
Arrow chartA dumbbell with an arrowhead on the "after" endNot requiredBefore/after data where some rows rise and others fall
Lollipop chartA stem from the baseline to a dotYesA bar chart with many categories where solid bars feel heavy
Gap lollipop or barOnly the difference, from zeroYesThe 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:

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