Bar Charts

Vertical, horizontal, grouped and stacked - and why the baseline has to be zero.

Overview

Sorting

A bar chart maps a category to a length. Your eye reads it by comparing those lengths, so the chart works best when the bars are in order. An unsorted bar chart forces the reader to scan back and forth. A sorted one gives the answer at a glance.

Bar charts look simple, and that simplicity hides several design choices that matter. Vertical or horizontal. Grouped or stacked. Labelled on the bar or on the axis. Each choice changes what the reader sees first and what they have to work to find.

The most common mistake is a bar chart whose baseline is not zero. A line chart can start at any value because the reader follows the slope. A bar chart is read by length, and a cropped baseline makes small differences look large. If your data does not vary enough to show on a zero-based axis, a bar chart is the wrong chart for it.

This page covers sorting, labelling, horizontal versus vertical bars, grouped and stacked layouts, error bars, baseline handling, colour choices and the common mistakes that make bar charts misleading. It also includes practical notes on when a bar chart is the right choice and when a dot plot or table would serve better.

matplotlib draws bars with ax.bar (vertical) and ax.barh (horizontal). Both return a container object that you pass to ax.bar_label to add values on each bar. The grouped layout requires manual positioning with np.arange and a width offset, which surprises people the first time they try it.

Bar charts are read by checking lengths. Compare is much easier when the bars are sorted.

Unless the group order means a thing. Months, sizes, a fixed scale. Sort by value. It costs one line with np.argsort and changes the chart from a thing to be studied into a thing to be glanced at.

With barh, note that bars are drawn from the bottom up. Sorting ascending puts the largest bar at the top. This Is where the eye starts.

Worth knowing

barh is usually better than bar when the categories are words — each label gets its own line and needs no rotation.
Sort the bars unless the category order is meaningful; an unsorted chart makes the reader do the comparing.
ax.bar_label(bars) writes values on the bars, which often makes the value axis redundant.
There is no grouped-bar function — you offset the positions yourself and then set_xticks to centre the labels.
bottom= stacks segments. Stacking shows totals well and components badly, since only the bottom segment shares a baseline.
A bar's baseline must be zero. Length encodes value, so a truncated axis misstates the ratios.

Bar Charts

Vertical, flat, grouped and stacked, and why the baseline has to be zero.

bar and barh

Horizontal is usually the better choice when the labels are words.

example_01.pymatplotlib
Output

A bar chart compares quantities across categories. Each bar's length encodes a value. The reader compares lengths, which the eye does well as long as the bars share a common baseline.

Bar charts are the right choice when the data is categorical and the comparison is between totals. Revenue by region, count by category, score by team. They are not the right choice for time series, where a line chart preserves the continuity between points, or for distributions, where a histogram handles continuous bins.

The most common mistake in bar charts is choosing vertical bars when the labels are long. Category names are words, and words need horizontal space. On a vertical chart each label is squeezed into the width of one bar. Rotating the labels 45 degrees is a workaround that makes the chart harder to read. Horizontal bars give each label a full line.

Sorting matters. A bar chart with categories in alphabetical order forces the reader to search for the largest and smallest. Sorting by value makes the ranking visible at a glance. The exception is categories with a natural order, like months or age groups, where the sequence carries meaning.

This page covers vertical and horizontal bars, sorting, labelling, grouped and stacked layouts, the zero-baseline rule, width and spacing, colour usage, negative values, and the cases where bars are not the right chart. It ends with a checklist for a finished bar chart.

The zero-baseline rule is worth learning early. A bar's visual weight is proportional to its length. If the y axis starts at a number other than zero, the lengths no longer represent the values. A bar that is twice as tall does not mean twice the value. Starting at zero is not a style choice. It is a requirement for the chart to be honest.

ax.bar(groups, values) draws vertical bars. ax.barh draws flat ones.

For cat data, flat is often better. The reason is typography. Group labels are words, words are wide, and a vertical chart has only a bar's width in which to put each one. The usual workarounds. Rotating labels 45 degrees, truncating them, shrinking the font. All make the chart harder to read.

barh gives each label a full line of flat space, reading left to right like text.

Vertical bars remain right when the x axis is sorted and quantitative-ish. Months, years, bins of a spread. There the order carries meaning and the labels are short.

Sorting is part of the chart

An unsorted bar chart makes the reader do the comparing.

example_02.pymatplotlib
Output

Labelling the bars

bar_label puts the value on the bar, which often replaces the axis entirely.

example_03.pymatplotlib
Output

Grouped bars

You position them yourself, which is the part that surprises people.

example_04.pymatplotlib
Output

There's no grouped-bar function. You find the spots.

The pattern is: take x = np.arange(n_groups), choose a bar width w. Offset each series by a fraction of it. For two series, x - w/2 and x + w/2. For three, x - w, x, x + w.

Then ax.set_xticks(x) and ax.set_xticklabels(groups) put one label in the middle of each cluster. The default ticks would else fall on the single bars.

Keep the total width of a group below 1 so there's a gap between clusters. A group of three bars of width 0.25 leaves 0.25 of space, which reads well.

Beyond three or four series per group, the chart stops working. Small multiples are the better answer.

Stacked bars, and what they hide

bottom is the running total; only the first segment is easy to compare.

example_05.pymatplotlib
Output

The baseline must be zero

A truncated bar axis misrepresents the data, and it is easy to do by accident.

example_06.pymatplotlib
Output

Labelling

ax.bar_label(bars) writes each bar's value at its end. It takes the holder returned by bar or barh. This Is why that return value is worth keeping here.

fmt="%.1f" controls the format. labels=[...] replaces the text fully. This Is how you show percentages or add units.

Once the values are on the bars, the value axis is often redundant. Dropping it. ax.set_xticks([]) and hiding the spines. Leaves a chart that is quieter and easier to read. This is one of the few cases where deleting a standard chart element is almost always an gain.

Stacked bars

bottom= gives the starting height of each segment. Each call passes the cumulative sum of all below it.

Stacking answers one question well. What is the total — and another badly. Only the bottom segment sits on a common baseline. Only it can be checked across bars by eye. Segments higher up float at other heights. Checking them is truly hard.

So: stack when the total is the message and the breakdown is context. Group when the parts are the message. And if the question is really about proportions, a 100% stacked bar. Each column normed to 1. Answers it better than either.

The zero baseline

This is the one rule about bar charts that isn't a matter of taste.

A bar shows its value by length. If the axis starts at 97, a bar of value 98 has one unit of length and a bar of value 102 has five. The chart shows a five-fold gap where the data has a 4% one.

Line charts don't have this problem. A line codes value by spot. Spot is read against the axis labels. That is why truncating a line chart's y axis is acceptable and truncating a bar chart's isn't.

matplotlib will happily let you do it. It happens by accident when set_ylim is applied to a bar chart to "zoom in".

If the differences are truly small and truly notable, the answer is a other chart. A line, a dot plot, or a chart of the differences themselves. Not a truncated bar.

Width and spacing

width is in data units, and the default of 0.8 leaves a fifth of the spacing as a gap.

Setting it to 1.0 drops the gaps fully. This Turns a bar chart into a thing that reads like a histogram. And that is just the gap the gap sends. Bars with gaps say "these are split groups". Bars without say "these are adjacent intervals of a smooth var".

That is why a histogram has no gaps and a group chart does. Why dropping the gap from a group chart is a small lie.

For grouped bars, the math is: total group width below 1, cut by the number of series. Three series of width 0.27 occupy 0.81 and leave a fifth of a unit between clusters.

Colour on bars

A bar chart often needs one colour. The groups are already distinguished by spot and label. Colouring each bar differently adds nothing and implies a grouping that isn't there.

The exception is highlighting: one bar in a strong colour and the rest in grey says which group the chart is about. Is far more good than an annotation.

colors = ["0.75"] * len(names)
colors[focus] = "crimson"
ax.barh(names, values, color=colors)

Colour turns real again when it codes a second var. Above or below target, plus or minus. And then two colours, not seven.

Negative values

Bars extending below zero are drawn on its own. The baseline should be made visible:

ax.axhline(0, color="black", linewidth=0.8)

Without it, the zero line is implied by the axis and easy to lose.

Colouring by sign is the standard treatment. It's one of the few cases where two colours on one series is right:

colors = ["#2a9d8f" if v >= 0 else "#e76f51" for v in values]

For a diverging bar chart. Change from a baseline — sorting by value puts the largest grows and falls at the two ends. This Is often the most readable setup.

Bars for parts of a whole

A stacked bar normed so each column sums to 100% answers "what is the composition" better than either a pie or an plain stack.

shares = counts / counts.sum(axis=0)

Each segment is then a proportion, the columns are right close. The total. Which is no longer shown. Can go in the axis label or an annotation if it matters.

The left weakness is the same as any stack. Only the bottom and top segments have a fixed baseline. Middle groups are hard to compare across columns. Sorting the groups so the ones being checked are at the bottom mitigates it.

Bars are not always right

Two cases where the default choice is wrong.

Small differences. A bar's message is its length. Length must be read from zero. If the notable differences are 2% of the value, a bar chart can't show them honestly. A dot plot, or a chart of the differences, can.

Many groups. Forty bars is a wall. Sorting helps. Beyond about twenty a dot plot or a lollipop chart uses far less ink per group and stays readable.

ax.hlines plus ax.scatter builds a lollipop in two lines. It's often the better display for a long ranked list.

Labels on and around bars

ax.bar_label handles the common case, and its args cover most of the rest.

padding=3 sets the gap in points. fmt="%.1f%%" formats. label_type="center" puts the value inside the bar rather than beyond its end. This Suits stacked segments where there's no free space at the tip.

For stacked bars, calling it once per holder labels each segment:

for container in ax.containers:
    ax.bar_label(container, label_type="center", fmt="%.0f")

Small segments give shared labels. So Filtering to the ones with room is often needed. labels=[v if v > threshold else "" for v in values].

Once values are on the bars, dropping the value axis fully makes the chart quieter and loses nothing.

The bar chart checklist

Before a bar chart is finished:

Is it sorted? Unless the order means a thing.

Is it flat? Unless the labels are short or the axis is sorted.

Does it start at zero? Always, for bars.

Is there one colour? Unless colour codes a thing, or one bar is highlighted.

Are the values labelled? If so, is the axis now redundant?

Are there too many bars? Past about twenty, a dot plot reads better.

Is the gap between bars visible? It's what says these are groups rather than intervals.

Seven questions, and a chart that answers all of them well is close to as good as a bar chart gets.

Bar Charts, in summary

Flat, sorted, one colour, from zero.

That covers most bar charts, and each part has a reason. Flat because group labels are words, sorted because the reader shouldn't have to rank them, one colour because spot already sets apart the groups, and from zero because a bar codes value as length.

bar_label puts the numbers on the bars. This Often makes the value axis redundant.

Grouped bars need the offsets found by hand. Stacked bars need bottom and hide all above the first segment.

And past about twenty groups, a dot plot or lollipop chart uses far less ink and stays readable. This Is worth reaching for rather than shrinking the bars further.

Bars over time

Bars and lines both show a quantity over time, and the choice says a thing.

A line implies continuity: the quantity existed between the findings and moved smoothly. Right for a reading sampled again and again. A temp, a price, a running total.

Bars imply discreteness: each period is a split quantity. There's nothing between them. Right for a total per period. Monthly revenue, daily counts, quarterly headcount.

Most business time series are period totals and are drawn as lines out of habit. The bar version is often more honest and reads no worse.

Two hands-on points. Bars need the zero baseline. This Limits how much detail can be shown when the variation is small scaled to the level. And with many periods bars become too thin to read, at which point a line or a step is the hands-on choice even so of the semantics.

A step plot is the compromise: discrete like bars, compact like a line.

One more thing

ax.barh draws from the bottom up. A list passed in ascending order appears with the largest at the top.

That is often what you want and is the opposite of the intuition from bar. Here Ascending order puts the largest on the right. Sorting ascending and letting barh reverse it visually is the idiom.

Bar Charts: the short version

Flat, sorted, one colour, from zero. Four defaults that fix most bar charts before anything else is seen as.

The zero baseline is the only rule here that isn't a matter of taste. A bar codes value as length and a truncated axis misstates each ratio on the chart.

Questions people ask about Bar Charts

Should bars always start at zero? Yes. Bar length encodes value, so a truncated baseline exaggerates differences. If zero hides the story, use a dot plot instead.

How do I make horizontal bars? Use ax.barh. It suits long category names, which would collide on a horizontal x axis.

How do I draw grouped bars side by side? Offset the x positions by half a bar width in each direction and set width to less than the offset step.

How do I put values on top of the bars? Call ax.bar_label(bars) on the container that ax.bar returns. It handles placement and formatting.

In what order should the bars go? Sort by value unless the categories have a natural order such as months or age bands. Sorted bars let readers rank at a glance.

When is a bar chart the wrong choice? For trends over many time points, a line reads better. For distributions, use a histogram. Bars suit comparing a few categories.

Reading the code back

A bar chart is one call and a handful of choices made before it. Which orientation, what order, one colour or two, whether the values go on the bars. Whether the axis is then needed at all. Those choices are all made in the data preparation and the arg list rather than in anything clever. This Is why bar charts are quick to give well once the defaults are settled and quick to give badly when they aren't.

Check yourself

0 of 4

Answer without scrolling back up.

  1. Why is `barh` usually better for categorical data?

  2. How do you draw grouped bars?

  3. What does stacking show badly?

  4. Why must a bar chart's axis start at zero, when a line chart's need not?

Cheat sheet

Bar Charts

Build vertical, horizontal, grouped and stacked bar charts in matplotlib. See why the y-axis must start at zero and when to reorder bars.

MATPLOTLIB · vizlearn.in/matplotlib/bar_charts.html

About the author

Ashish Jangra builds and maintains VizLearn. Every module here is written and the visualisation behind it hand-built, so the numbers in a readout come from the same code that draws the picture. Corrections are genuinely welcome and get priority over everything else — if a page states something wrong, or an animation misrepresents what the algorithm does, get in touch.