How to Visualize Likert Scale Data with a Diverging Stacked Bar Chart
Plot agree/disagree survey responses as a diverging stacked bar chart: one horizontal bar per question, disagreement extending left of a center line, agreement extending right, and the neutral category straddling the middle. Naomi Robbins and Richard Heiberger made the case for this design in their 2011 paper "Plotting Likert and Other Rating Scales," and it beats an ordinary 100% stacked bar because the lean of every item is visible against one shared center line.
Quick answer: Use a horizontal diverging stacked bar chart. Keep the segments in scale order (strongly disagree to strongly agree), shift each bar so the middle of the neutral category sits on a common zero line, sort the questions by net agreement (% agree minus % disagree), and color it with a diverging palette: two hues that darken toward the extremes, with gray for neutral. This is the display Robbins and Heiberger (2011) recommend for Likert and other rating scales.
What is a diverging stacked bar chart for Likert data?
It is a stacked bar chart in which each bar is slid sideways so the scale's midpoint lands on a vertical zero line. Everything on the negative side of the scale (strongly disagree, disagree) stacks outward to the left, everything on the positive side (agree, strongly agree) stacks outward to the right, and the neutral category is split in half across the line. Each bar still totals 100% of respondents, but its position now tells you something: a bar that sits mostly right of zero is a question people agreed with, and a bar that sits mostly left is one they rejected. Axis labels show absolute percentages on both sides (40%, 20%, 0%, 20%, 40%) rather than negative numbers.
Why use a diverging bar instead of a 100% stacked bar?
Because a 100% stacked bar has no visual center. It anchors every bar at 0% on the left and 100% on the right, so total disagreement reads from the left edge and total agreement from the right, but to see whether a question leaned positive you have to compare two segments at opposite ends of the bar, and the neutral block floats wherever the disagree segments happen to end. A diverging bar puts the scale's midpoint on one shared line, so each question's lean becomes a position judgment against a common reference: bars shifted right leaned positive, bars shifted left leaned negative. Cleveland and McGill's graphical perception experiments found that people judge position along a common scale more accurately than lengths or positions on unaligned scales, which is why a shared center line makes a long list of questions easier to compare (see the visual encoding hierarchy). The general trade-offs of stacking are covered in when to use stacked bar charts.
| 100% stacked bar | Diverging stacked bar | |
|---|---|---|
| Where each bar is anchored | 0% at the left edge, 100% at the right | The midpoint of the scale, on a shared zero line |
| Can you see which way an item leans? | Only by comparing segments at opposite ends of the bar | Yes, from how far the bar reaches left vs. right of zero |
| Neutral category | Floats mid-bar, wherever the disagree segments end | Centered on zero on every row |
| What it shows well | That each item's responses sum to 100% | The balance of opinion across many items |
Where should the neutral category go?
Split it in half across the zero line unless the neutral share varies a lot between questions. Splitting is what Robbins and Heiberger do, and it is how Heiberger's HH package for R draws an odd-numbered scale by default: half the neutral respondents sit left of zero and half sit right, so the bar stays centered on the scale's true midpoint. The cost is that the agree segments no longer start exactly at zero; they start half a neutral block out, so a question with 30% neutral pushes its agree and disagree blocks further apart than a question with 10% neutral. When that distortion matters, move neutral out of the bar entirely.
| Neutral placement | What sits on the zero line | Use it when |
|---|---|---|
| Split across zero (Robbins & Heiberger) | The middle of the neutral block | Neutral shares are similar across items, or you want the full distribution in one bar |
| Separate neutral bar beside the chart | The inner edges of both the agree and disagree blocks | Neutral shares differ a lot and the agree-vs-disagree comparison is the point |
| All on one side of zero | One edge of neutral | Never: it inflates whichever side receives it |
| Dropped from the chart | Agree and disagree inner edges | Only if you report the neutral share elsewhere; otherwise it hides respondents |
Scales with an even number of points (4 or 6) have no neutral category, so zero falls between the two middle responses. "Don't know" and "Not applicable" are not midpoints of the scale; plot them as a separate bar or footnote, and say which denominator your percentages use.
How should I sort the survey questions?
Sort the questions (the rows) by net agreement, the percentage who agree or strongly agree minus the percentage who disagree or strongly disagree, with the highest at the top. The segments inside each bar always stay in scale order; it is only the order of the questions that changes. Net agreement is a better sort key than % agree alone because it also accounts for how many people pushed back, which breaks ties that % agree cannot. The exception is when the questions have a meaningful order of their own, such as the same question asked every year or items grouped by questionnaire section; keep that order, as covered in how to sort chart data.
Avoid sorting by the mean of 1 to 5 codes. It treats the gap between "neutral" and "agree" as equal to the gap between "agree" and "strongly agree," which an ordinal scale does not guarantee, and it hides whether a middling mean came from consensus or polarization.
What colors should a Likert chart use?
Use a diverging palette: one hue for the disagree side, a contrasting hue for the agree side, darker for the extreme categories, lighter for the moderate ones, and a neutral gray in the middle. The color then encodes the same thing the position does, direction and intensity of opinion, as explained in choosing a color palette. Blue-red, blue-orange, or purple-orange pairs from ColorBrewer (RdBu, PuOr) work well; avoid red-green, which is the pairing most readers with color vision deficiency struggle to separate. If disagreeing is not inherently bad (for example, "I feel stressed at work"), a blue-orange palette avoids implying a verdict that red would.
How do you calculate where each bar starts?
Each bar starts at minus the sum of strongly disagree, disagree, and half of neutral, then its segments are laid end to end from there, so every bar spans exactly 100 points and the neutral midpoint lands on zero. Here are four questions from a hypothetical employee survey of 200 people, in percent of respondents (SD = strongly disagree through SA = strongly agree), already sorted by net agreement:
| Question | SD / D / N / A / SA | Agree | Disagree | Net | Bar spans |
|---|---|---|---|---|---|
| I have the tools I need | 5 / 10 / 15 / 45 / 25 | 70 | 15 | +55 | โ22.5 to 77.5 |
| My manager gives useful feedback | 10 / 15 / 20 / 35 / 20 | 55 | 25 | +30 | โ35 to 65 |
| I see a path to promotion | 15 / 25 / 30 / 20 / 10 | 30 | 40 | โ10 | โ55 to 45 |
| My workload is manageable | 20 / 30 / 20 / 20 / 10 | 30 | 50 | โ20 | โ60 to 40 |
For "I have the tools I need," the bar starts at โ(5 + 10 + 15/2) = โ22.5 and ends at โ22.5 + 100 = 77.5. Notice the bottom two questions: both have exactly 30% agreeing, so a sort on % agree would treat them as a tie. Net agreement separates them, because the workload question has 50% disagreeing versus 40%. This is the same data used in all three code examples below.
How do I make a diverging stacked bar chart in Excel?
Excel has no diverging bar chart type, so build a regular stacked bar chart with an invisible "spacer" series in front of the real data. The spacer pushes each bar right by just enough that every neutral midpoint lines up at the same position. With the question in column A and the five response percentages in B through F:
1. G2 (Left side): =B2+C2+D2/2 ' SD + D + half of Neutral
H2 (Spacer): =MAX($G$2:$G$5)-G2 ' fill both down
I2 (Net): =E2+F2-B2-C2 ' sort the table by this, largest first
2. Select A1:F5, Ctrl-select H1:H5, then Insert โ Column or Bar Chart โ Stacked Bar
(if the questions show up in the legend, click Chart Design โ Switch Row/Column)
3. Chart Design โ Select Data โ select Spacer โ Move Up until it is first
4. Format the Spacer series: No fill, No border; delete it from the legend
5. Format the vertical axis โ tick "Categories in reverse order"
(Excel plots the first row at the bottom by default)
6. Format the horizontal axis โ Vertical axis crosses โ Axis value:
type the largest value in column G (60 in the example); the axis line
now runs through every neutral midpoint and acts as the zero line
7. Format the vertical axis โ Labels โ Label Position: Low
(moves the question labels back to the left edge)
8. Set the series colors to your diverging palette and Gap Width to about 50%
With the example data, column G holds 22.5, 35, 55, and 60, so the spacers are 37.5, 25, 5, and 0, and every bar's neutral midpoint lands at 60. The horizontal axis numbers now measure from the spacer's left edge rather than from the midpoint, so hide them (Label Position: None) and add data labels to the segments instead.
How do I make a Likert chart in Python?
Use matplotlib's barh with a left offset: compute each bar's starting point, draw one call per response level, and move the offset along after each level. There is no built-in Likert chart in matplotlib or pandas, but the offset approach needs only a short loop:
import pandas as pd
import matplotlib.pyplot as plt
from matplotlib.ticker import FuncFormatter
levels = ["Strongly disagree", "Disagree", "Neutral", "Agree", "Strongly agree"]
colors = ["#ca0020", "#f4a582", "#bababa", "#92c5de", "#0571b0"]
df = pd.DataFrame(
[[10, 15, 20, 35, 20],
[ 5, 10, 15, 45, 25],
[20, 30, 20, 20, 10],
[15, 25, 30, 20, 10]],
index=["My manager gives useful feedback",
"I have the tools I need",
"My workload is manageable",
"I see a path to promotion"],
columns=levels,
)
# Net agreement; barh draws the first row at the bottom,
# so sort ascending to put the most positive question on top
net = (df["Agree"] + df["Strongly agree"]) - (df["Strongly disagree"] + df["Disagree"])
df = df.loc[net.sort_values().index]
# Each bar starts left of zero by SD + D + half of Neutral
left = -(df["Strongly disagree"] + df["Disagree"] + df["Neutral"] / 2)
fig, ax = plt.subplots(figsize=(9, 3.5))
for level, color in zip(levels, colors):
ax.barh(df.index, df[level], left=left, color=color, label=level)
left = left + df[level]
ax.set_xlim(-80, 80) # symmetric, so zero is centered
ax.axvline(0, color="black", linewidth=1)
ax.xaxis.set_major_formatter(FuncFormatter(lambda x, _: f"{abs(x):.0f}%"))
ax.legend(ncol=5, loc="lower center", bbox_to_anchor=(0.5, 1.0),
frameon=False, fontsize="small")
plt.tight_layout()
plt.show()
If your raw data has one row per answer (a question column and a response column), build the percentage table first with pd.crosstab(raw["question"], raw["response"], normalize="index") * 100, then put its columns into scale order with .reindex(columns=levels, fill_value=0), which also adds a zero column for any response nobody chose.
How do I make a Likert chart in R?
The quickest route is the likert() function in Heiberger's HH package, the implementation behind Heiberger and Robbins' 2014 Journal of Statistical Software article on diverging stacked bar charts. Give it a matrix of counts with one row per question and one column per response level in scale order; as.percent = TRUE converts each row to percentages and prints the row totals (the n per question) on the right-hand axis:
library(HH)
# Counts from 200 respondents per question
m <- matrix(c(10, 20, 30, 90, 50,
20, 30, 40, 70, 40,
40, 60, 40, 40, 20,
30, 50, 60, 40, 20),
nrow = 4, byrow = TRUE,
dimnames = list(
Question = c("I have the tools I need",
"My manager gives useful feedback",
"My workload is manageable",
"I see a path to promotion"),
Response = c("Strongly disagree", "Disagree", "Neutral",
"Agree", "Strongly agree")))
likert(m, as.percent = TRUE, positive.order = TRUE,
main = "Employee survey", xlab = "Percent")
With an odd number of levels, HH centers the middle (neutral) column on zero automatically. It keeps rows in their original order unless you set positive.order = TRUE, which puts the bar that reaches farthest to the right at the top; that ranks by agreement plus half of neutral, which is close to, but not the same as, net agreement. If you want full control in ggplot2, compute each segment's start and end yourself and draw them with geom_tile():
library(dplyr)
library(tidyr)
library(ggplot2)
scale_levels <- c("Strongly disagree", "Disagree", "Neutral",
"Agree", "Strongly agree")
survey <- tibble::tribble(
~item, ~sd, ~d, ~n, ~a, ~sa,
"My manager gives useful feedback", 10, 15, 20, 35, 20,
"I have the tools I need", 5, 10, 15, 45, 25,
"My workload is manageable", 20, 30, 20, 20, 10,
"I see a path to promotion", 15, 25, 30, 20, 10
)
long <- survey |>
mutate(net = (a + sa) - (sd + d),
start = -(sd + d + n / 2)) |>
pivot_longer(sd:sa, names_to = "response", values_to = "pct") |>
mutate(response = factor(response, levels = c("sd", "d", "n", "a", "sa"),
labels = scale_levels)) |>
arrange(item, response) |>
group_by(item) |>
mutate(xmax = start + cumsum(pct), xmin = xmax - pct) |>
ungroup() |>
mutate(item = reorder(item, net)) # highest net agreement on top
ggplot(long, aes(x = (xmin + xmax) / 2, y = item,
width = pct, fill = response)) +
geom_tile(height = 0.7) +
geom_vline(xintercept = 0) +
scale_fill_manual(values = c("#ca0020", "#f4a582", "#bababa",
"#92c5de", "#0571b0")) +
scale_x_continuous(limits = c(-80, 80),
labels = function(x) paste0(abs(x), "%")) +
labs(x = NULL, y = NULL, fill = NULL) +
theme_minimal() +
theme(legend.position = "top")
What are the common mistakes when charting Likert data?
Most bad Likert charts fail in one of a handful of predictable ways, and each one either misplaces the center of the scale or makes readers work harder to compare questions:
- Stacking neutral entirely on one side of zero. It makes that side look larger than it is. Split it or move it to its own bar.
- Using a categorical or rainbow palette. Five unrelated hues throw away the ordered structure of the scale; readers have to consult the legend for every segment.
- Red-green diverging colors. The most common form of color vision deficiency makes the two ends hard to tell apart.
- Leaving questions in questionnaire order when that order means nothing. Sort by net agreement so the strongest and weakest items sit at the top and bottom.
- Treating "Don't know" or "N/A" as neutral. They are not points on the scale; plotting them in the middle shifts every bar.
- Negative percentages on the axis. "โ40%" of respondents is meaningless; label both sides with absolute values.
- Hiding the sample size. Percentages from 12 respondents and from 1,200 look identical. Put the n in the question label or a subtitle, especially when it differs between questions.
- One pie chart per question. Twenty pies force twenty separate angle judgments; one diverging chart puts them all on a shared scale.
Pro Tip: Print the total % disagree just outside the left end of each bar and the total % agree just outside the right end. Readers get the two numbers they actually quote in meetings without decoding individual segments, and the segments themselves can then go unlabeled, which keeps a 20-question chart readable.
โ Back to Visualization Tips