10  Bar

How large is each category? bar draws a rectangle from the baseline (zero) to the value for each row. Bars are vertical when the categories are on x and horizontal when they are on y; see Horizontal bars.

The bar chart exists because William Playfair had too little data. His Commercial and Political Atlas of 1786 drew England’s imports and exports as two curves across thirty years (Playfair, 1786). For Scotland he had a single year, and no curve can show one year. So he drew a bar for each of Scotland’s trading partners instead, and wrote that the result was the weaker picture, because it showed no change over time. The form he apologized for became the one everyone knows.

Two names for it are still in use, and they name opposite orientations. Much of the business world, Excel included, calls this a column chart when the bars are vertical, and uses bar chart only when they are horizontal. gog has one bar, and its direction comes from whichever axis carries the category. A third name, histogram, belongs to a different plot drawn with the same mark. Karl Pearson introduced the word in the 1890s (Pearson, 1895) for the picture of a distribution. Its bars cover intervals cut from a continuous axis, and neighboring bars touch. In gog that plot is bar * bin, shown in Histogram below.

10.1 Basic usage

Which country won the most gold medals? Most examples in this chapter start from the smallest sentence that draws a bar: a source, the mark, and its two positions.

medals: all 5 rows
country gold silver bronze
USA 46 37 38
China 38 31 22
Great Britain 29 17 19
Russia 19 18 9
Germany 17 10 15
data(medals) + bar + x(country) + y(gold)
data(medals) + bar + x(col.country) + y(col.gold)
data(medals) + bar + x(:country) + y(:gold)
plot(data(medals), bar, x(col.country), y(col.gold))
USA China Great Britain Russia Germany 0 10 20 30 40 Gold Country

“Given the medals: bars, x is country, y is gold.”

The x axis places a tick at each bar position automatically. The y axis reaches zero unless you state your own limits, so a bar’s length is its value.

10.2 Color

Every bar is the same color until you map a column to color. With a color for each bar, a reader can find one country quickly and match it to the same country in another plot of the same table. Bind color to the column that is already on x. color fills each bar with a distinct color:

data(medals) + bar + x(country) + y(gold) +
  color(country)
(data(medals) + bar + x(col.country) + y(col.gold) +
  color(col.country))
data(medals) + bar + x(:country) + y(:gold) + color(:country)
plot(data(medals), bar, x(col.country), y(col.gold), color(col.country))
USA China Great Britain Russia Germany 0 10 20 30 40 Gold Country Country USA China Great Britain Russia Germany

“Given the medals: bars, x is country, y is gold, color by country.”

The legend repeats the names on the x axis, because the same column is on both. color(country, legend = FALSE) keeps the colors and leaves the legend out.

10.3 Ordering bars

In what order should the bars appear? A reader looking for the largest value wants it first. A reader looking for one name wants the names in alphabetical order. order() controls the sequence of categories along whichever axis carries them: x on a vertical plot, y on a horizontal one. You can sort on the categorical column itself, on the measured column, or on a third column of numbers. After bar * count, you can also sort on the count. Naming the categorical column asks for that column’s own order. Here that is alphabetical, because order() sorts plain text such as country from A to Z. A column with a declared order uses that order instead.

Default, data order (rows as they appear in the table):

data(medals) + bar + x(country) + y(gold) +
  title("Gold medals: data order")
(data(medals) + bar + x(col.country) + y(col.gold) +
  title("Gold medals: data order"))
data(medals) + bar + x(:country) + y(:gold) +
  title("Gold medals: data order")
plot(data(medals), bar, x(col.country), y(col.gold),
  title("Gold medals: data order"))
USA China Great Britain Russia Germany 0 10 20 30 40 Gold medals: data order Gold Country

By value, descending, tallest bar first, the most common pattern for ranked comparisons:

data(medals) + bar + x(country) + y(gold) +
  order(gold, desc = TRUE) +
  title("Gold medals: largest first")
(data(medals) + bar + x(col.country) + y(col.gold) +
  order(col.gold, desc = True) +
  title("Gold medals: largest first"))
data(medals) + bar + x(:country) + y(:gold) + order(:gold, desc = true) +
  title("Gold medals: largest first")
plot(data(medals), bar, x(col.country), y(col.gold),
  order(col.gold, { desc: true }), title("Gold medals: largest first"))
USA China Great Britain Russia Germany 0 10 20 30 40 Gold medals: largest first Gold Country

“Given the medals: bars, x is country, y is gold, ordered by gold, largest first.”

These bars are in the same order as the plot titled Gold medals: data order. The medals table already lists the largest count first, so ordering by gold changes nothing here. Writing the order anyway is still safer, because the plot then stays sorted even if next year’s rows arrive in a different order. The plots below each put the bars in a different order.

By value, ascending, smallest first, useful when scanning for the lowest values:

data(medals) + bar + x(country) + y(gold) +
  order(gold) +
  title("Gold medals: smallest first")
(data(medals) + bar + x(col.country) + y(col.gold) +
  order(col.gold) +
  title("Gold medals: smallest first"))
data(medals) + bar + x(:country) + y(:gold) + order(:gold) +
  title("Gold medals: smallest first")
plot(data(medals), bar, x(col.country), y(col.gold), order(col.gold),
  title("Gold medals: smallest first"))
Germany Russia Great Britain China USA 0 10 20 30 40 Gold medals: smallest first Gold Country

By category name, A → Z, natural for names and labels:

data(medals) + bar + x(country) + y(gold) +
  order(country) +
  title("Gold medals: A to Z")
(data(medals) + bar + x(col.country) + y(col.gold) +
  order(col.country) +
  title("Gold medals: A to Z"))
data(medals) + bar + x(:country) + y(:gold) + order(:country) +
  title("Gold medals: A to Z")
plot(data(medals), bar, x(col.country), y(col.gold), order(col.country),
  title("Gold medals: A to Z"))
China Germany Great Britain Russia USA 0 10 20 30 40 Gold medals: A to Z Gold Country

By category name, Z → A, the reverse:

data(medals) + bar + x(country) + y(gold) +
  order(country, desc = TRUE) +
  title("Gold medals: Z to A")
(data(medals) + bar + x(col.country) + y(col.gold) +
  order(col.country, desc = True) +
  title("Gold medals: Z to A"))
data(medals) + bar + x(:country) + y(:gold) +
  order(:country, desc = true) + title("Gold medals: Z to A")
plot(data(medals), bar, x(col.country), y(col.gold),
  order(col.country, { desc: true }), title("Gold medals: Z to A"))
USA Russia Great Britain Germany China 0 10 20 30 40 Gold medals: Z to A Gold Country

By a third column, order countries by their silver medal count, while plotting gold:

data(medals) + bar + x(country) + y(gold) +
  order(silver, desc = TRUE) +
  title("Gold medals: ordered by silver")
(data(medals) + bar + x(col.country) + y(col.gold) +
  order(col.silver, desc = True) +
  title("Gold medals: ordered by silver"))
data(medals) + bar + x(:country) + y(:gold) +
  order(:silver, desc = true) + title("Gold medals: ordered by silver")
plot(data(medals), bar, x(col.country), y(col.gold),
  order(col.silver, { desc: true }),
  title("Gold medals: ordered by silver"))
USA China Russia Great Britain Germany 0 10 20 30 40 Gold medals: ordered by silver Gold Country

By the count, when each bar is a count of rows. Each row of gapminder_2007 is one country. The count transform, written bar * count, draws how many countries each continent has:

gapminder_2007: first 5 of 142 rows
country continent year life population gdp
Afghanistan Asia 2007 43.828 31889923 974.5803
Albania Europe 2007 76.423 3600523 5937.0295
Algeria Africa 2007 72.301 33333216 6223.3675
Angola Africa 2007 42.731 12420476 4797.2313
Argentina Americas 2007 75.320 40301927 12779.3796
data(gapminder_2007) + bar * count + x(continent) +
  order(count, desc = TRUE) +
  title("Countries per continent: largest first")
(data(gapminder_2007) + bar * count + x(col.continent) +
  order(col.count, desc = True) +
  title("Countries per continent: largest first"))
data(gapminder_2007) + bar * count + x(:continent) +
  order(:count, desc = true) +
  title("Countries per continent: largest first")
plot(data(gapminder_2007), layer(bar, count), x(col.continent),
  order(col.count, { desc: true }),
  title("Countries per continent: largest first"))
Africa Asia Europe Americas Oceania 0 20 40 Countries per continent: largest first Count Continent

“Given gapminder 2007: bars derived by count, x is continent, ordered by count, largest first.”

Africa, with 52 countries, comes first, and Oceania, with 2, comes last. The table has no count column. bar * count writes one, and order() names it like any other column of numbers. After bar * proportion, write order(proportion) in the same way. The Transforms chapter draws that plot.

Expression Order
(none) Data order (rows as they appear)
order(gold) Ascending by gold (smallest first)
order(gold, desc = TRUE) Descending by gold (largest first)
order(country) A → Z by country name
order(country, desc = TRUE) Z → A by country name
order(silver, desc = TRUE) By another column of numbers
order(count, desc = TRUE) Descending by the count that bar * count writes (largest first)

10.4 Duplicate categories

A table often holds several rows for one category, one per event or one per year. The question is still how large each category is in total. Without a transform, bar draws one bar per row, and rows that share a category share a slot. This table has two rows each for USA and GBR:

medals_dup <- data.frame(
  country = c("USA", "USA", "GBR", "GBR", "JPN"),
  gold    = c(10, 5, 8, 4, 6)
)
data(medals_dup) + bar + x(country) + y(gold) +
  title("Duplicate rows: no transform")
medals_dup = {
  "country": ["USA", "USA", "GBR", "GBR", "JPN"],
  "gold": [10, 5, 8, 4, 6],
}
(data(medals_dup) + bar + x(col.country) + y(col.gold) +
  title("Duplicate rows: no transform"))
medals_dup = (
  country = ["USA", "USA", "GBR", "GBR", "JPN"],
  gold = [10, 5, 8, 4, 6],
)
data(medals_dup) + bar + x(:country) + y(:gold) +
  title("Duplicate rows: no transform")
const medals_dup = {
  country: ["USA", "USA", "GBR", "GBR", "JPN"],
  gold: [10, 5, 8, 4, 6],
};
plot(data(medals_dup), bar, x(col.country), y(col.gold),
  title("Duplicate rows: no transform"))
USA GBR JPN 0 2 4 6 8 10 Duplicate rows: no transform Gold Country

Every bar grows from zero, so the shorter ones are drawn inside the tallest. USA has rows of 10 and 5, and its bar reads 10 rather than 15. Each bar is drawn slightly translucent, so the color is darker where two bars overlap. For USA, that darker band ends at 5, where the shorter bar stops. That band is the only sign that a category has extra rows.

Use the sum transform to total the values per category first:

data(medals_dup) + bar * sum + x(country) + y(gold) +
  order(gold, desc = TRUE) + title("Totals: bar * sum")
(data(medals_dup) + bar * sum + x(col.country) + y(col.gold) +
  order(col.gold, desc = True) + title("Totals: bar * sum"))
data(medals_dup) + bar * sum + x(:country) + y(:gold) +
  order(:gold, desc = true) + title("Totals: bar * sum")
plot(data(medals_dup), layer(bar, sum), x(col.country), y(col.gold),
  order(col.gold, { desc: true }), title("Totals: bar * sum"))
USA GBR JPN 0 5 10 15 Totals: bar * sum Gold Country

“Given the medals dup table: bars derived by sum, x is country, y is gold, ordered by gold, largest first.”

sum gives one number for each category. It reads every row that shares a category and totals them. USA’s 10 and 5 become 15, and GBR’s 8 and 4 become 12. Five rows become three bars, one for each country, and the darker band is gone.

10.5 Negative values

A bar can also point the other way, when a value is below zero, such as the profit of a quarter that lost money. Every bar starts at zero. That rule made the duplicate bars overlap, and it is also why a negative bar points down. So a negative value needs no special treatment:

balance <- data.frame(quarter = c("Q1", "Q2", "Q3", "Q4"),
                      profit  = c(12.0, -5.0, 8.0, -2.0))
data(balance) + bar + x(quarter) + y(profit) +
  title("Negative values grow downward from zero")
balance = {
  "quarter": ["Q1", "Q2", "Q3", "Q4"],
  "profit": [12.0, -5.0, 8.0, -2.0],
}
(data(balance) + bar + x(col.quarter) + y(col.profit) +
  title("Negative values grow downward from zero"))
balance = (
  quarter = ["Q1", "Q2", "Q3", "Q4"],
  profit = [12, -5, 8, -2],
)
data(balance) + bar + x(:quarter) + y(:profit) +
  title("Negative values grow downward from zero")
const balance = {
  quarter: ["Q1", "Q2", "Q3", "Q4"],
  profit: [12, -5, 8, -2],
};
plot(data(balance), bar, x(col.quarter), y(col.profit),
  title("Negative values grow downward from zero"))
Q1 Q2 Q3 Q4 -5 0 5 10 Negative values grow downward from zero Profit Quarter

To color the losses and the profits differently, you need a column that says which is which. color maps a column, so that column has to exist. “Is this negative” is a fact about the data, not about the rectangle. So put it in a column, map that column, and let palette() say which colors the mapping uses. The table below sets the column’s values by hand. In practice you compute it from profit with one condition. Above zero is a profit, and anything else is a loss.

balance <- data.frame(
  quarter = c("Q1", "Q2", "Q3", "Q4"),
  profit  = c(12.0, -5.0, 8.0, -2.0),
  result  = factor(c("profit", "loss", "profit", "loss"),
                   levels = c("loss", "profit"))
)
data(balance) + bar + x(quarter) + y(profit) + color(result) +
  palette(c("firebrick", "seagreen")) +
  title("Red below zero, green above")
balance = {
  "quarter": ["Q1", "Q2", "Q3", "Q4"],
  "profit": [12.0, -5.0, 8.0, -2.0],
  "result": ordered(["profit", "loss", "profit", "loss"], ["loss", "profit"]),
}
(data(balance) + bar + x(col.quarter) + y(col.profit) + color(col.result) +
  palette(["firebrick", "seagreen"]) +
  title("Red below zero, green above"))
balance = (
  quarter = ["Q1", "Q2", "Q3", "Q4"],
  profit = [12, -5, 8, -2],
  result = ordered(["profit", "loss", "profit", "loss"], ["loss", "profit"]),
)
data(balance) + bar + x(:quarter) + y(:profit) + color(:result) +
  palette(["firebrick", "seagreen"]) +
  title("Red below zero, green above")
const balance = {
  quarter: ["Q1", "Q2", "Q3", "Q4"],
  profit: [12, -5, 8, -2],
  result: ordered(["profit", "loss", "profit", "loss"], ["loss", "profit"]),
};
plot(data(balance), bar, x(col.quarter), y(col.profit), color(col.result),
  palette(["firebrick", "seagreen"]),
  title("Red below zero, green above"))
Q1 Q2 Q3 Q4 -5 0 5 10 Red below zero, green above Profit Quarter Result loss profit

“Given the balance table: bars, x is quarter, y is profit, color by result, with a palette of firebrick and seagreen.”

palette() assigns its colors in category order, and that is why result carries a declared order here instead of being plain text. palette() does not sort plain text, and order() does not move its colors: it assigns them in the order the values first appear in the table. The first row here is a profit, so the palette would give firebrick to the profits. Declaring the order as loss, then profit, gives firebrick to the losses, and the legend lists them in that order. The colors then stay with their results, even if next year’s table starts with a loss. R spells that with a factor’s levels, and the other three bindings with ordered(). This is the same mechanism as Declaring the order of categories.

Red for loss and green for profit is one common convention. It is also the hardest pair to separate for red-green color blindness, the most common kind. Texture solves that and keeps the colors, by giving each result its own texture as well as its own color: see Texture below, and Will it read in gray? in the cookbook.

10.6 Horizontal bars

Sometimes the bars should be horizontal. The category names may be too long for the bottom axis. Or there may be many categories, and a tall list of horizontal bars reads more easily than a crowded row of vertical ones. Swap the bindings, and put the categories on y and the measure on x:

data(medals) + bar + x(gold) + y(country) +
  title("Categories on y: horizontal bars")
(data(medals) + bar + x(col.gold) + y(col.country) +
  title("Categories on y: horizontal bars"))
data(medals) + bar + x(:gold) + y(:country) +
  title("Categories on y: horizontal bars")
plot(data(medals), bar, x(col.gold), y(col.country),
  title("Categories on y: horizontal bars"))
0 10 20 30 40 Germany Russia Great Britain China USA Categories on y: horizontal bars Country Gold

If you know ggplot2’s coord_flip() or matplotlib’s barh(), gog has no such word, because it would be a second way to say one thing. gog takes the orientation from the bindings. The axis with the number is the one the bars grow along, so their length is read on it. The axis with the category is the one the bars are placed on, one slot each. Nothing else changes: order, transforms such as sum and bin, color and style() all behave identically.

Horizontal bars help most when the category names are long. On a vertical axis each label is written across the page, so it does not have to fit inside one slot:

departments: all 5 rows
department headcount
Research & Development 128
Sales and Marketing 96
Customer Operations 74
Finance 31
People & Culture 22
data(departments) + bar + x(headcount) + y(department) +
  order(headcount, desc = TRUE) +
  x_label("Headcount") +
  title("Long labels fit on a vertical axis")
(data(departments) + bar + x(col.headcount) + y(col.department) +
  order(col.headcount, desc = True) +
  x_label("Headcount") +
  title("Long labels fit on a vertical axis"))
data(departments) + bar + x(:headcount) + y(:department) +
  order(:headcount, desc = true) + x_label("Headcount") +
  title("Long labels fit on a vertical axis")
plot(data(departments), bar, x(col.headcount), y(col.department),
  order(col.headcount, { desc: true }), x_label("Headcount"),
  title("Long labels fit on a vertical axis"))
0 50 100 People & Culture Finance Customer Operations Sales and Marketing Research & Development Long labels fit on a vertical axis Department Headcount

order means the same thing in both orientations: the first category in sort order takes the first slot. Descending puts the largest bar leftmost on a vertical plot and topmost on a horizontal one.

Orientation is only ambiguous if both axes hold plain numbers, not dates. In that case gog draws vertically, so x(year) + y(sales) gives vertical bars. Swapping the two does not make them horizontal. With two columns of plain numbers, gog always measures the one on y. So x(sales) + y(year) still draws vertical bars, and each bar’s length is a year, such as 2019. For horizontal bars, make the year categorical and put it on y. A column of numbers becomes categorical when you declare the order of its values, and each year then gets a slot of its own. A column of dates needs no such step. A bar never measures a date, so dates on y make the bars horizontal, as Bars on a date axis shows.

A bar needs something to measure, so two categorical axes are refused. What it refuses shows that refusal.

10.7 Texture

The profit and loss bars in Negative values are red and green, the pair hardest to distinguish for a reader with red-green color blindness. Give each result its own texture as well, and that reader can separate the bars without seeing color at all.

A bar’s fill can carry a texture instead of one solid color. On a fill, a texture is a hatch: a repeated pattern of lines or dots, drawn in the bar’s color. You set it with style(pattern = ). A texture does not need color, so a hatched bar chart stays legible in grayscale or in a black-and-white print:

data(medals) + bar + x(country) + y(gold) +
  style(pattern = "hatch") +
  title("A hatched bar reads without color")
(data(medals) + bar + x(col.country) + y(col.gold) +
  style(pattern = "hatch") +
  title("A hatched bar reads without color"))
data(medals) + bar + x(:country) + y(:gold) + style(pattern = "hatch") +
  title("A hatched bar reads without color")
plot(data(medals), bar, x(col.country), y(col.gold),
  style({ pattern: "hatch" }), title("A hatched bar reads without color"))
USA China Great Britain Russia Germany 0 10 20 30 40 A hatched bar reads without color Gold Country

“Given the medals: bars, x is country, y is gold, with pattern hatch.”

style(pattern = ) gives every bar in the layer the same texture. To give each category its own texture, map a column with the pattern() channel, which does for texture what shape() does for glyphs. The plot below maps the texture column with pattern(). Each bar then shows one of the six fill textures, in this order: "solid" (the default, a plain fill), "hatch", "crosshatch", "stripes", "grid" and "dots":

textures <- data.frame(
  texture = c("solid", "hatch", "crosshatch", "stripes", "grid", "dots"),
  height  = c(6, 5, 4, 3, 2, 1)
)
data(textures) + bar + x(texture) + y(height) + pattern(texture) +
  x_label("") + y_label("") + title("The six fill textures")
textures = {
  "texture": ["solid", "hatch", "crosshatch", "stripes", "grid", "dots"],
  "height": [6, 5, 4, 3, 2, 1],
}
(data(textures) + bar + x(col.texture) + y(col.height) + pattern(col.texture) +
  x_label("") + y_label("") + title("The six fill textures"))
textures = (
  texture = ["solid", "hatch", "crosshatch", "stripes", "grid", "dots"],
  height = [6, 5, 4, 3, 2, 1],
)
data(textures) + bar + x(:texture) + y(:height) + pattern(:texture) +
  x_label("") + y_label("") + title("The six fill textures")
const textures = {
  texture: ["solid", "hatch", "crosshatch", "stripes", "grid", "dots"],
  height: [6, 5, 4, 3, 2, 1],
};
plot(data(textures), bar, x(col.texture), y(col.height),
  pattern(col.texture), x_label(""), y_label(""),
  title("The six fill textures"))
solid hatch crosshatch stripes grid dots 0 2 4 6 The six fill textures Texture solid hatch crosshatch stripes grid dots

“Given the textures table: bars, x is texture, y is height, pattern by texture.”

The style chapter shows them beside the dashes a stroke takes, because style(pattern = ) is one name whose meaning depends on the shape: a hatch on a fill, a dash on a stroke. For the profit and loss bars, write pattern(result) beside color(result). The losses then stay solid and the profits are hatched, so each result has its own color and its own texture.

10.8 Histogram

How are the world’s life expectancies spread out, and where do most countries fall? That is a question about one continuous column, and a bar needs a category to be drawn in. The bin transform makes those categories: it cuts the x axis into equal-width intervals, called bins, and counts the rows in each. Combine bar with bin to create a histogram:

data(gapminder_2007) + bar * bin + x(life) + y(count) +
  x_label("Life expectancy (years)") + y_label("Count") +
  title("Histogram of life expectancy, 2007")
(data(gapminder_2007) + bar * bin + x(col.life) + y(col.count) +
  x_label("Life expectancy (years)") + y_label("Count") +
  title("Histogram of life expectancy, 2007"))
data(gapminder_2007) + bar * bin + x(:life) + y(:count) +
  x_label("Life expectancy (years)") + y_label("Count") +
  title("Histogram of life expectancy, 2007")
plot(data(gapminder_2007), layer(bar, bin), x(col.life), y(col.count),
  x_label("Life expectancy (years)"), y_label("Count"),
  title("Histogram of life expectancy, 2007"))
50 60 70 80 0 10 20 30 Histogram of life expectancy, 2007 Count Life expectancy (years)

“Given gapminder 2007: bars derived by bin, x is life, y is count.”

gapminder_2007 has no count column. bin writes the counts into a new column named count, and y(count) refers to it. count is the only name y() takes here. To label the axis in your own words, use y_label() (The name on a measure axis).

Thickness is automatic, and it is the clearest way a histogram and a bar chart differ on the page. A categorical bar fills 80% of its slot, which leaves a gap between neighboring bars. That gap is what reads as separate categories. A histogram cuts a continuous axis into adjacent intervals, so there is no space between one bin and the next, and the bars touch. A thin line in the panel’s background color is drawn between neighboring bars, so each one can still be seen. You set nothing in either plot. The gold medal chart in Basic usage and this histogram both take their thickness from the axis the bars are on.

A bar can also be drawn on a continuous axis without bin. Its slot is then the smallest distance between two neighboring values. Two countries here differ in GDP by less than a dollar, so the slot is narrower than a pixel. gog then draws every bar one pixel thick, at its own value:

data(gapminder_2007) + bar + x(gdp) + y(life) +
  title("Bars on a continuous axis, without bin")
(data(gapminder_2007) + bar + x(col.gdp) + y(col.life) +
  title("Bars on a continuous axis, without bin"))
data(gapminder_2007) + bar + x(:gdp) + y(:life) +
  title("Bars on a continuous axis, without bin")
plot(data(gapminder_2007), bar, x(col.gdp), y(col.life),
  title("Bars on a continuous axis, without bin"))
0 10K 20K 30K 40K 50K 0 20 40 60 80 Bars on a continuous axis, without bin Life Gdp

“Given gapminder 2007: bars, x is gdp, y is life.”

Each bar is one country. Where many countries have a similar GDP, the bars touch and form one solid block. To see how the values spread, cut the axis into bins with bin. To read each country’s position, draw point.

bin is not the bar’s own transform. Four other marks can draw the same bins, and only the shape changes:

Mark derived by bin Shape
point * bin one dot at the top of each bin
line * bin the frequency polygon: straight lines through the bin centers
area * bin the same polygon, filled to the baseline
step * bin the staircase outline, drawn in Step
bar * bin filled bars, the histogram above

zone * bin and surface * bin cut two axes instead of one: the heatmap in Zone and the sheet in Surface.

The width of each bin decides what the histogram shows. With no argument, bin picks the number of bins from the number of rows, by Sturges’ rule (Sturges, 1926). The rule is simple: each time the number of rows doubles, the histogram gets one more bin. So 100 rows get 8 bins, the 142 countries here get 9, and 1,000 rows get 11. Name a width to cut the axis in the data’s own units, here five years:

data(gapminder_2007) + bar * bin(width = 5) + x(life) + y(count) +
  x_label("Life expectancy (years)") + y_label("Count") +
  title("Bins five years wide")
(data(gapminder_2007) + bar * bin(width = 5) + x(col.life) + y(col.count) +
  x_label("Life expectancy (years)") + y_label("Count") +
  title("Bins five years wide"))
data(gapminder_2007) + bar * bin(width = 5) + x(:life) + y(:count) +
  x_label("Life expectancy (years)") + y_label("Count") +
  title("Bins five years wide")
plot(data(gapminder_2007), layer(bar, bin({ width: 5 })), x(col.life),
  y(col.count), x_label("Life expectancy (years)"), y_label("Count"),
  title("Bins five years wide"))
40 50 60 70 80 0 10 20 30 40 Bins five years wide Count Life expectancy (years)

“Given gapminder 2007: bars derived by bin at width 5, x is life, y is count.”

bin(30) sets the number of bins instead. Both belong to the transform, and Choosing the bin count shows what each does to the picture. Choosing a width explains how to pick a width for your own column.

A histogram can also be horizontal, by the rule in Horizontal bars. Bind the column you are binning to y, and the count is drawn along x. The sentence does not have to name the count: bin computes it and draws it on the other axis, so y(count) in the first histogram was optional:

data(gapminder_2007) + bar * bin + y(life) +
  y_label("Life expectancy (years)") +
  title("The same histogram, horizontal")
(data(gapminder_2007) + bar * bin + y(col.life) +
  y_label("Life expectancy (years)") +
  title("The same histogram, horizontal"))
data(gapminder_2007) + bar * bin + y(:life) +
  y_label("Life expectancy (years)") +
  title("The same histogram, horizontal")
plot(data(gapminder_2007), layer(bar, bin), y(col.life),
  y_label("Life expectancy (years)"),
  title("The same histogram, horizontal"))
0 10 20 30 50 60 70 80 The same histogram, horizontal Life expectancy (years) Count

10.8.1 Where the bin edges fall

In the histogram titled “Bins five years wide”, the first bin starts at 35, although the shortest life expectancy in the table is 39.6. An edge is a value where one bin ends and the next begins, and where the edges fall is a choice. Each bar counts the rows between its two edges, such as the countries with a life expectancy from 40 to 45. If every edge moves by the same amount, some rows belong to a different bin. The counts change, and the shape can change with them.

When you name a width, gog makes every edge a multiple of that width. The first bin is the one that holds the smallest value. For these countries, the five-year bins therefore run from 35 to 40, from 40 to 45, and so on up to 85. Every gridline in that plot, from 40 to 80, is also a bin edge. A value exactly on an edge is counted in the bin that starts there, so a life expectancy of 40 belongs to the bin from 40 to 45.

Edges on multiples of the width come from Leland Wilkinson’s The Grammar of Graphics, the book that gog is named after (Wilkinson, 2005). He advises one edge at zero. Every edge is then a multiple of the width, a round number that a reader can find on the axis. He also says it should not be a strict rule, because the edges change the shape of the histogram. Round edges are easy to read, but other edges can give a smoother shape.

Wilkinson also treats one kind of column separately: a column that holds only a few distinct values, such as the whole numbers from 0 to 10. With bins one unit wide, every value of such a column falls exactly on an edge. The bar for 3 is then drawn from 3 to 4, not centered on 3. He gives each possible value a bin of its own instead: a column that holds only 3 and 7 gets five bins, for 3, 4, 5, 6 and 7. In gog you ask for this with count instead of bin. bar * count draws a bar for each distinct value, centered on that value, and a value that never occurs leaves a gap.

When you give the number of bins instead of a width, the edges are placed differently. bin(30) divides the range from the smallest value to the largest into thirty equal bins. Here each bin would be 1.43 years wide, so the edges are not round numbers. For round edges, name a width instead. With no argument, bin chooses the number of bins itself, by Sturges’ rule, so its edges also run from the smallest value to the largest.

ggplot2 places its edges differently (Wickham et al., 2026). Given a width, ggplot2 puts the center of a bin at zero, where gog puts an edge. Its five-year bins therefore run from 37.5 to 42.5, from 42.5 to 47.5, and so on. Given the number of bins, ggplot2 makes each bin slightly wider than gog does. Its first bin is then centered on the smallest value, and its last on the largest. A value exactly on an edge goes to the bin that ends there. Both packages count every row once, but the bins cover different years, so the shapes differ. On this table, gog’s last five-year bin, 80 to 85, holds 13 countries, while ggplot2’s, 82.5 to 87.5, holds only Japan.

To put the edges anywhere else, state where the axis starts, and the first bin starts there too. x(life, limits = c(37.5, 87.5)) gives the five-year edges that ggplot2 uses. Scales explains limits.

10.8.2 Overlaid histograms: one distribution per group

The iris flowers table holds measurements of 150 flowers from three species. A single histogram of all the flowers answers “how is petal length distributed?” It cannot answer “how is it distributed for each species?” Add color and the histogram splits: one histogram per category, cut on the same bins and drawn in the same panel.

iris_flowers: first 5 of 150 rows
sepal_length sepal_width petal_length species
5.1 3.5 1.4 setosa
4.9 3.0 1.4 setosa
4.7 3.2 1.3 setosa
4.6 3.1 1.5 setosa
5.0 3.6 1.4 setosa
data(iris_flowers) + bar * bin + x(petal_length) + color(species) +
  x_label("Petal length (cm)") + title("Petal length by species")
(data(iris_flowers) + bar * bin + x(col.petal_length) + color(col.species) +
  x_label("Petal length (cm)") + title("Petal length by species"))
data(iris_flowers) + bar * bin + x(:petal_length) + color(:species) +
  x_label("Petal length (cm)") + title("Petal length by species")
plot(data(iris_flowers), layer(bar, bin), x(col.petal_length),
  color(col.species), x_label("Petal length (cm)"),
  title("Petal length by species"))
2 4 6 0 10 20 30 40 Petal length by species Count Petal length (cm) Species setosa versicolor virginica

“Given the iris flowers: bars derived by bin, x is petal length, color by species.”

The three histograms overlay. None of them is moved aside, and all three measure from the same baseline. If the fills were opaque, the species drawn last would hide the others wherever they overlap. So each bar is a translucent fill under a solid outline in its own color. The outline is what keeps the shape readable where the fills overlap. setosa is alone on the left and reads clearly. versicolor and virginica overlap in the middle, and their outlines still separate them. The groups share the same bins, so their bars align, and an overlap in the plot is an overlap in the data.

The fill is faint by default so the groups underneath show through. style(opacity = ) sets how faint; the outline stays solid whatever you choose:

data(iris_flowers) + bar * bin + x(petal_length) + color(species) +
  style(opacity = 0.6) + x_label("Petal length (cm)") +
  title("A heavier fill: style(opacity = 0.6)")
(data(iris_flowers) + bar * bin + x(col.petal_length) + color(col.species) +
  style(opacity = 0.6) + x_label("Petal length (cm)") +
  title("A heavier fill: style(opacity = 0.6)"))
data(iris_flowers) + bar * bin + x(:petal_length) + color(:species) +
  style(opacity = 0.6) + x_label("Petal length (cm)") +
  title("A heavier fill: style(opacity = 0.6)")
plot(data(iris_flowers), layer(bar, bin), x(col.petal_length),
  color(col.species), style({ opacity: 0.6 }),
  x_label("Petal length (cm)"),
  title("A heavier fill: style(opacity = 0.6)"))
2 4 6 0 10 20 30 40 A heavier fill: style(opacity = 0.6) Count Petal length (cm) Species setosa versicolor virginica

“Given the iris flowers: bars derived by bin, x is petal length, color by species, with opacity 0.6.”

The outline is the group’s color by default, but you can recolor it: style(border_color = ) sets the border, style(border_size = ) its width. A black border separates the overlapping fills, the way many overlaid histograms are drawn. It also stays visible against the light panel, where a white one disappears:

data(iris_flowers) + bar * bin + x(petal_length) + color(species) +
  style(border_color = "black", border_size = 1.5) +
  x_label("Petal length (cm)") + title("Black borders between the fills")
(data(iris_flowers) + bar * bin + x(col.petal_length) + color(col.species) +
  style(border_color = "black", border_size = 1.5) +
  x_label("Petal length (cm)") + title("Black borders between the fills"))
data(iris_flowers) + bar * bin + x(:petal_length) + color(:species) +
  style(border_color = "black", border_size = 1.5) +
  x_label("Petal length (cm)") + title("Black borders between the fills")
plot(data(iris_flowers), layer(bar, bin), x(col.petal_length),
  color(col.species), style({ border_color: "black", border_size: 1.5 }),
  x_label("Petal length (cm)"), title("Black borders between the fills"))
2 4 6 0 10 20 30 40 Black borders between the fills Count Petal length (cm) Species setosa versicolor virginica

“Given the iris flowers: bars derived by bin, x is petal length, color by species, with border color black and border size 1.5.”

The fill and the outline are independent: a bar can be one color inside and another at its edge, whether the fill was mapped (as here) or set. See Setting vs mapping.

Or remove the outline entirely. style(border_size = 0) draws no border, so the translucent fills overlap with nothing between them. This is the plainest overlaid histogram:

data(iris_flowers) + bar * bin + x(petal_length) + color(species) +
  style(border_size = 0) +
  x_label("Petal length (cm)") + title("No border: just the fills")
(data(iris_flowers) + bar * bin + x(col.petal_length) + color(col.species) +
  style(border_size = 0) +
  x_label("Petal length (cm)") + title("No border: just the fills"))
data(iris_flowers) + bar * bin + x(:petal_length) + color(:species) +
  style(border_size = 0) + x_label("Petal length (cm)") +
  title("No border: just the fills")
plot(data(iris_flowers), layer(bar, bin), x(col.petal_length),
  color(col.species), style({ border_size: 0 }),
  x_label("Petal length (cm)"), title("No border: just the fills"))
2 4 6 0 10 20 30 40 No border: just the fills Count Petal length (cm) Species setosa versicolor virginica

“Given the iris flowers: bars derived by bin, x is petal length, color by species, with border size 0.”

When the groups overlap so much that the outlines are hard to distinguish, give each species its own panel instead. Adding | facet(species) to the sentence draws the same three histograms side by side, on one shared scale. Or draw one smooth curve per species, as in Area: curves cross at a point instead of sharing bin edges.

10.8.3 The cumulative histogram

A histogram’s bar counts the rows in one bin. Add cumulative, and each bar counts the rows in its bin and in every bin to its left. Each bar still grows from zero, so its height is the total so far:

data(gapminder_2007) + bar * bin * cumulative + x(life) +
  x_label("Life expectancy (years)") +
  title("bar * bin * cumulative: the cumulative histogram")
(data(gapminder_2007) + bar * bin * cumulative + x(col.life) +
  x_label("Life expectancy (years)") +
  title("bar * bin * cumulative: the cumulative histogram"))
data(gapminder_2007) + bar * bin * cumulative + x(:life) +
  x_label("Life expectancy (years)") +
  title("bar * bin * cumulative: the cumulative histogram")
plot(data(gapminder_2007), layer(bar, bin, cumulative), x(col.life),
  x_label("Life expectancy (years)"),
  title("bar * bin * cumulative: the cumulative histogram"))
50 60 70 80 0 50 100 bar * bin * cumulative: the cumulative histogram Cumulative Life expectancy (years)

“Given gapminder 2007: bars derived by bin and cumulative, x is life.”

The last bar is 142, every country in the table. Each bar is taller than the one before it by the count of its own bin. The three largest rises, between about 68 and 83 years, come from the three bins that hold the most countries. The axis is labeled “Cumulative”, because each bar is a running total, not the count of one bin.

10.9 Grouped bars: side by side

How did each continent’s life expectancy change between 1957 and 2007? That is a comparison inside each category, so each continent needs two bars, one per era. A color split alone draws both bars in one slot, the shorter inside the taller. To set them side by side as a grouped bar chart, add dodge. Each group’s bar narrows to share the slot, and the split reads as a comparison within each category. The gm_eras table holds two years, 1957 and 2007, with one row per country in each. mean averages the countries into one value for each continent and era. era is the column that splits each continent into two bars.

gm_eras: first 5 of 284 rows
country continent year life population gdp era
Afghanistan Asia 1957 30.332 9240934 820.8530 1957
Afghanistan Asia 2007 43.828 31889923 974.5803 2007
Albania Europe 1957 59.280 1476505 1942.2842 1957
Albania Europe 2007 76.423 3600523 5937.0295 2007
Algeria Africa 1957 45.685 10270856 3013.9760 1957
data(gm_eras) + bar * mean * dodge + x(continent) + y(life) + color(era) +
  y_label("Mean life expectancy") +
  title("Mean life expectancy by continent, 1957 vs 2007")
(data(gm_eras) + bar * mean * dodge + x(col.continent) + y(col.life) + color(col.era) +
  y_label("Mean life expectancy") +
  title("Mean life expectancy by continent, 1957 vs 2007"))
data(gm_eras) + bar * mean * dodge + x(:continent) + y(:life) +
  color(:era) + y_label("Mean life expectancy") +
  title("Mean life expectancy by continent, 1957 vs 2007")
plot(data(gm_eras), layer(bar, mean, dodge), x(col.continent),
  y(col.life), color(col.era), y_label("Mean life expectancy"),
  title("Mean life expectancy by continent, 1957 vs 2007"))
Asia Europe Africa Americas Oceania 0 20 40 60 80 Mean life expectancy by continent, 1957 vs 2007 Mean life expectancy Continent Era 1957 2007

“Given the gapminder eras: bars derived by mean and dodge, x is continent, y is life, color by era.”

dodge is a collision modifier: it moves the bars rather than summarizing them, so it composes with the statistic (bar * mean * dodge here, bar * count * dodge for tallies). See Transforms for all four collision modifiers. A text layer over dodged bars takes the same dodge and a split of its own, such as group(era), so each number is printed on its own bar (Labels on dodged bars).

mean is the right statistic here, and gog will not choose it for you. Write sum in its place and gog still draws it:

data(gm_eras) + bar * sum * dodge + x(continent) + y(life) + color(era) +
  y_label("Sum of life expectancies") +
  title("Sum in place of mean: legal, and meaningless")
(data(gm_eras) + bar * sum * dodge + x(col.continent) + y(col.life) + color(col.era) +
  y_label("Sum of life expectancies") +
  title("Sum in place of mean: legal, and meaningless"))
data(gm_eras) + bar * sum * dodge + x(:continent) + y(:life) +
  color(:era) + y_label("Sum of life expectancies") +
  title("Sum in place of mean: legal, and meaningless")
plot(data(gm_eras), layer(bar, sum, dodge), x(col.continent), y(col.life),
  color(col.era), y_label("Sum of life expectancies"),
  title("Sum in place of mean: legal, and meaningless"))
Asia Europe Africa Americas Oceania 0 1K 2K Sum in place of mean: legal, and meaningless Sum of life expectancies Continent Era 1957 2007

“Given the gapminder eras: bars derived by sum and dodge, x is continent, y is life, color by era.”

Africa’s fifty-two life expectancies for 2007 are added into one bar of 2,850 years. A sum of life expectancies has no meaning. You may write either statistic. Only the data says which one is meaningful, and Law 8 says why gog leaves that choice to you.

10.9.1 The gap beside each bar

Look again at the first grouped plot: the two bars of each continent do not touch. Two bars that touch can read as one wide bar, so a dodged group spans four fifths of the slot, as one bar would, and each bar leaves a fifth of its own part empty. style(gap = ) sets that share, from 0, where the bars touch, to just under 1, where almost nothing is left to draw:

data(gm_eras) + bar * mean * dodge + x(continent) + y(life) + color(era) +
  style(gap = 0) + y_label("Mean life expectancy") +
  title("With gap 0, the two bars of a continent touch")
(data(gm_eras) + bar * mean * dodge + x(col.continent) + y(col.life) + color(col.era) +
  style(gap = 0) + y_label("Mean life expectancy") +
  title("With gap 0, the two bars of a continent touch"))
data(gm_eras) + bar * mean * dodge + x(:continent) + y(:life) +
  color(:era) + style(gap = 0) + y_label("Mean life expectancy") +
  title("With gap 0, the two bars of a continent touch")
plot(data(gm_eras), layer(bar, mean, dodge), x(col.continent),
  y(col.life), color(col.era), style({ gap: 0 }),
  y_label("Mean life expectancy"),
  title("With gap 0, the two bars of a continent touch"))
Asia Europe Africa Americas Oceania 0 20 40 60 80 With gap 0, the two bars of a continent touch Mean life expectancy Continent Era 1957 2007

“Given the gapminder eras: bars derived by mean and dodge, x is continent, y is life, color by era, with gap 0.”

The two bars of each continent now touch. The space between continents has not changed, because on dodged bars gap works only inside each bar’s part. The gap matters most when a pattern, not a color, tells the two bars apart.

On a bar that is not dodged, gap is a share of the whole slot, and the default is again a fifth. style(gap = 0.5) leaves half of each slot empty, so each bar is half as wide as its slot. A histogram’s bars leave no gap unless you set one, because its bins are intervals that meet.

10.9.2 Splitting without color

Sometimes another layer already shows which bar belongs to which era, and a color on the bars would say it a second time. color(era) does two things at once. It splits each continent’s rows into one bar per era, and it paints each era its own color. group does only the first: the bars are split and dodged as before, all in one color, and no legend is drawn.

In the plot below, the second layer, interval * confidence(0.95), draws each mean’s 95% confidence interval as an error bar (Interval). style(center = FALSE) leaves out the error bar’s center dot, because the bar already marks the mean. The error bars carry the color and the legend, and the bars stay gray:

data(gm_eras) +
  bar * mean * dodge + group(era) + style(color = "lightgray") +
  interval * confidence(0.95) * dodge + color(era) + style(center = FALSE) +
  x(continent) + y(life) +
  y_label("Mean life expectancy (years)") +
  title("Mean life expectancy by continent, 1957 and 2007")
(data(gm_eras) +
  bar * mean * dodge + group(col.era) + style(color = "lightgray") +
  interval * confidence(0.95) * dodge + color(col.era) + style(center = False) +
  x(col.continent) + y(col.life) +
  y_label("Mean life expectancy (years)") +
  title("Mean life expectancy by continent, 1957 and 2007"))
data(gm_eras) + bar * mean * dodge + group(:era) +
  style(color = "lightgray") + interval * confidence(0.95) * dodge +
  color(:era) + style(center = false) + x(:continent) + y(:life) +
  y_label("Mean life expectancy (years)") +
  title("Mean life expectancy by continent, 1957 and 2007")
plot(data(gm_eras), layer(bar, mean, dodge), group(col.era),
  style({ color: "lightgray" }), layer(interval, confidence(0.95), dodge),
  color(col.era), style({ center: false }), x(col.continent), y(col.life),
  y_label("Mean life expectancy (years)"),
  title("Mean life expectancy by continent, 1957 and 2007"))
Asia Europe Africa Americas Oceania 0 20 40 60 80 Mean life expectancy by continent, 1957 and 2007 Mean life expectancy (years) Continent Era 1957 2007

“Given the gapminder eras: bars derived by mean and dodge, grouped by era, colored lightgray, and also intervals derived by confidence at 95% and dodge, colored by era, with center off, x is continent, y is life.”

A bar takes group only when a statistic, dodge or stack acts on its rows. A statistic such as mean computes one value for each era. dodge sets each era’s bar beside the others, and stack sets it on top of them (Stacked bars, below). With none of these, each bar already draws one row of the table. Splitting the rows would then change nothing, so gog refuses group (What it refuses).

A stack can be split by group too, when each bar’s total matters more than its segments. Every segment then has the same color, so gog separates them with a thin line in the panel’s background color. It is the line that separates the bars of a histogram. The channel_sales table holds each quarter’s revenue from two ways of selling. Its channel column names them, Retail and Online. The table has one row for each quarter and way of selling, so stack needs no statistic:

channel_sales: first 5 of 6 rows
quarter channel revenue
Q1 Retail 4
Q1 Online 3
Q2 Retail 5
Q2 Online 6
Q3 Retail 4
data(channel_sales) + bar * stack + x(quarter) + y(revenue) + group(channel)
data(channel_sales) + bar * stack + x(col.quarter) + y(col.revenue) + group(col.channel)
data(channel_sales) + bar * stack + x(:quarter) + y(:revenue) +
  group(:channel)
plot(data(channel_sales), layer(bar, stack), x(col.quarter),
  y(col.revenue), group(col.channel))
Q1 Q2 Q3 0 5 10 Revenue Quarter

“Given the channel sales table: bars derived by stack, x is quarter, y is revenue, grouped by channel.”

Each bar’s height is the quarter’s total. Retail comes first in the table, so it is the lower segment of each bar. The thin line marks where the retail revenue ends and the online revenue begins. No color or legend tells the two segments apart. When the reader needs to know which is which, another layer has to label them.

10.10 Stacked bars: piled up

Sometimes the question is the total as well as the parts: how much is there altogether, and how much of it does each group hold? A color split can be placed a second way. Instead of side by side, stack piles the groups on top of each other. Each bar’s height becomes the category’s total across the groups, and the segments read as parts of a whole:

data(gm_eras) + bar * sum * stack + x(continent) + y(population) + color(era) +
  y_label("Population") +
  title("Population by continent, 1957 and 2007 stacked")
(data(gm_eras) + bar * sum * stack + x(col.continent) + y(col.population) + color(col.era) +
  y_label("Population") +
  title("Population by continent, 1957 and 2007 stacked"))
data(gm_eras) + bar * sum * stack + x(:continent) + y(:population) +
  color(:era) + y_label("Population") +
  title("Population by continent, 1957 and 2007 stacked")
plot(data(gm_eras), layer(bar, sum, stack), x(col.continent),
  y(col.population), color(col.era), y_label("Population"),
  title("Population by continent, 1957 and 2007 stacked"))
Asia Europe Africa Americas Oceania 0 2B 4B Population by continent, 1957 and 2007 stacked Population Continent Era 1957 2007

“Given the gapminder eras: bars derived by sum and stack, x is continent, y is population, color by era.”

dodge sets the groups side by side, all on one baseline; stack piles them into one bar, whose height is their total. Use stack when the parts add to a meaningful total (populations, counts, revenue), and dodge when you are comparing the groups. The two examples also use different statistics. You stack the sum of population, because quantities add, and you dodge the mean of life, because averages do not.

A stack accepts the wrong statistic as well. gog draws mean in place of sum, and piles two averages into a bar that reads as their total:

data(gm_eras) + bar * mean * stack + x(continent) + y(life) + color(era) +
  y_label("Mean life expectancy, stacked") +
  title("Mean in place of sum: legal, and meaningless")
(data(gm_eras) + bar * mean * stack + x(col.continent) + y(col.life) + color(col.era) +
  y_label("Mean life expectancy, stacked") +
  title("Mean in place of sum: legal, and meaningless"))
data(gm_eras) + bar * mean * stack + x(:continent) + y(:life) +
  color(:era) + y_label("Mean life expectancy, stacked") +
  title("Mean in place of sum: legal, and meaningless")
plot(data(gm_eras), layer(bar, mean, stack), x(col.continent),
  y(col.life), color(col.era), y_label("Mean life expectancy, stacked"),
  title("Mean in place of sum: legal, and meaningless"))
Asia Europe Africa Americas Oceania 0 50 100 150 Mean in place of sum: legal, and meaningless Mean life expectancy, stacked Continent Era 1957 2007

“Given the gapminder eras: bars derived by mean and stack, x is continent, y is life, color by era.”

Oceania’s pile reaches 150 years. A stack reads as parts of a total, so its values must be quantities that add up. That is why this section sums population, while the grouped bars show the mean of life.

10.10.1 The 100% stacked bar

The population stack answers two questions at once: how large each total is, and how it splits. The totals differ so much that the small continents’ splits are hard to read. Together, Asia’s two eras hold 5.4 billion people, and Oceania’s hold 0.04 billion. The axis is scaled for Asia, so Oceania’s bar is too short to show its split at all. When the split is the question and the total is not, ask for shares. stack(share = TRUE) divides each segment by its own bar’s total. Each segment then shows its group’s share of the bar, a number from 0 to 1. Every bar reaches 1, so all the splits are on one scale and can be compared across continents:

data(gm_eras) + bar * sum * stack(share = TRUE) + x(continent) + y(population) +
  color(era) + title("Each continent's 1957/2007 split, filled to one")
(data(gm_eras) + bar * sum * stack(share = True) + x(col.continent) + y(col.population) +
  color(col.era) + title("Each continent's 1957/2007 split, filled to one"))
data(gm_eras) + bar * sum * stack(share = true) + x(:continent) +
  y(:population) + color(:era) +
  title("Each continent's 1957/2007 split, filled to one")
plot(data(gm_eras), layer(bar, sum, stack({ share: true })),
  x(col.continent), y(col.population), color(col.era),
  title("Each continent's 1957/2007 split, filled to one"))
Asia Europe Africa Americas Oceania 0.0 0.2 0.4 0.6 0.8 1.0 Each continent's 1957/2007 split, filled to one Share Continent Era 1957 2007

“Given the gapminder eras: bars derived by sum and stack as shares, x is continent, y is population, color by era.”

With every bar at 1, the question changes. The population stack compared the two eras inside one continent, and only for the large ones. This plot compares the splits across continents: which population grew the most between the two eras. Africa’s population grew the most. Of its two-era total, 78% falls in 2007 and 22% in 1957, so it more than tripled. Europe’s grew the least: 57% of its total falls in 2007, the closest any continent comes to an even split. Neither number can be read from the population stack above. This plot does not show the totals at all, which is why Oceania’s bar is now as tall as Asia’s. The two plots are complementary, each answering half of one question. stack says how much each continent has, and stack(share = TRUE) says how it splits. When you need both answers, draw both plots.

The axis is labeled Share rather than Population, because every value is now a fraction of its bar’s total. Share alone does not say a share of what, so name it yourself with y_label("Share of two-era population"). stack(share = TRUE) is not proportion under another name. The two divide by different totals. proportion divides by the whole plot’s total, so its bars still show how large each category is. This one divides by the bar’s own total.

stack(share = TRUE) works the same way on a histogram. bin cuts the axis into bins, and each segment is then divided by its own bin’s total. The question changes from how many countries fall in each bin to which continents those countries are in:

data(gapminder_2007) + bar * bin(width = 5) * stack(share = TRUE) + x(life) +
  color(continent) + x_label("Life expectancy (years)") +
  y_label("Share of the countries in the bin") +
  title("Each continent's share of each five-year bin, 2007")
(data(gapminder_2007) + bar * bin(width = 5) * stack(share = True) + x(col.life) +
  color(col.continent) + x_label("Life expectancy (years)") +
  y_label("Share of the countries in the bin") +
  title("Each continent's share of each five-year bin, 2007"))
data(gapminder_2007) + bar * bin(width = 5) * stack(share = true) +
  x(:life) + color(:continent) + x_label("Life expectancy (years)") +
  y_label("Share of the countries in the bin") +
  title("Each continent's share of each five-year bin, 2007")
plot(data(gapminder_2007),
  layer(bar, bin({ width: 5 }), stack({ share: true })), x(col.life),
  color(col.continent), x_label("Life expectancy (years)"),
  y_label("Share of the countries in the bin"),
  title("Each continent's share of each five-year bin, 2007"))
40 50 60 70 80 0.0 0.2 0.4 0.6 0.8 1.0 Each continent's share of each five-year bin, 2007 Share of the countries in the bin Life expectancy (years) Continent Asia Europe Africa Americas Oceania

“Given gapminder 2007: bars derived by bin at width 5 and stack as shares, x is life, color by continent.”

Below 60 years, more than 80% of the countries in each bin are in Africa. From 60 to 70, Asia has the largest share, and Africa and the Americas hold the rest. Europe first appears in the bin from 70 to 75. From 75 to 85, about half of the countries in each bin are European. Oceania’s two countries both fall in the last bin.

This plot hides what the histogram titled “Bins five years wide” shows: how many countries each bin holds. The bin from 35 to 40 holds one country and the bin from 70 to 75 holds 39, but both bars reach 1. When the counts matter, draw that histogram beside this one. In ggplot2, stack(share = TRUE) is written position = "fill". Its five-year bins have other edges, so its bars differ (Where the bin edges fall).

10.11 Grouped, stacked or 100% stacked?

You have now seen three ways to place a color split. dodge sets the groups side by side, stack piles them up, and stack(share = TRUE) piles them up as shares. Each way makes one question easy to answer and another question hard, so the choice depends on the question your reader must answer. To see the difference, draw one table all three ways.

The titanic table counts the people aboard the Titanic, the ship that sank in 1912. Each row is one combination of class, sex, age and survived, and n says how many people match it, sometimes none. Each row already holds a count, so the plots add up n with sum rather than counting rows with count. color(survived) then splits each class into the people who survived and the people who died.

The sentence below describes three plots and sets them side by side with |. Each plot is written in its own parentheses, so | joins three whole plots into one arrangement. Composition explains such arrangements in full. legend = FALSE leaves the legend out of the first two plots, because one legend explains all three. The third plot asks for 330 pixels of the arrangement’s width with theme(width = 330), so its legend has room. Outside the outer parentheses, theme(height = 360) sets the height of the whole arrangement.

titanic: first 5 of 32 rows
class sex age survived n
1st Male Adult Yes 57
1st Male Adult No 118
1st Male Child Yes 5
1st Male Child No 0
1st Female Adult Yes 140
((data(titanic) + bar * sum * dodge + x(class) + y(n) +
    color(survived, legend = FALSE) + y_label("People") + title("dodge")) |
  (data(titanic) + bar * sum * stack + x(class) + y(n) +
    color(survived, legend = FALSE) + title("stack")) |
  (data(titanic) + bar * sum * stack(share = TRUE) + x(class) + y(n) +
    color(survived) + y_label("Share of the class") +
    title("stack(share = TRUE)") + theme(width = 330))) +
  theme(height = 360)
(((data(titanic) + bar * sum * dodge + x(col["class"]) + y(col.n) +
    color(col.survived, legend = False) + y_label("People") + title("dodge")) |
  (data(titanic) + bar * sum * stack + x(col["class"]) + y(col.n) +
    color(col.survived, legend = False) + title("stack")) |
  (data(titanic) + bar * sum * stack(share = True) + x(col["class"]) + y(col.n) +
    color(col.survived) + y_label("Share of the class") +
    title("stack(share = TRUE)") + theme(width = 330))) +
  theme(height = 360))
((data(titanic) + bar * sum * dodge + x(:class) + y(:n) +
  color(:survived, legend = false) + y_label("People") + title("dodge")) |
  (data(titanic) + bar * sum * stack + x(:class) + y(:n) +
  color(:survived, legend = false) + title("stack")) |
  (data(titanic) + bar * sum * stack(share = true) + x(:class) + y(:n) +
  color(:survived) + y_label("Share of the class") +
  title("stack(share = TRUE)") + theme(width = 330))) +
  theme(height = 360)
beside(beside(plot(data(titanic), layer(bar, sum, dodge), x(col.class),
  y(col.n), color(col.survived, { legend: false }), y_label("People"),
  title("dodge")),
  plot(data(titanic), layer(bar, sum, stack), x(col.class), y(col.n),
  color(col.survived, { legend: false }), title("stack"))),
  plot(data(titanic), layer(bar, sum, stack({ share: true })),
  x(col.class), y(col.n), color(col.survived),
  y_label("Share of the class"), title("stack(share = TRUE)"),
  theme({ width: 330 })), theme({ height: 360 }))
1st 2nd 3rd Crew 0 200 400 600 800 dodge People Class 1st 2nd 3rd Crew stack Class 1st 2nd 3rd Crew 0.0 0.2 0.4 0.6 0.8 1.0 stack(share = TRUE) Share of the class Class Survived Yes No

“Given titanic: bars derived by sum and dodge, x is class, y is n, color by survived, without a legend, beside bars derived by sum and stack, x is class, y is n, color by survived, without a legend, beside bars derived by sum and stack as shares, x is class, y is n, color by survived.”

All three plots start from the same eight sums, two for each class. The first two read n on the y axis, so they share one axis, drawn once at the left. That axis is sized for the crew’s total, 885, which is why the dodge plot leaves room above its tallest bar, 673. The third plot divides each segment by its class’s total, so it draws a separate axis, from 0 to 1. y_label() names that axis, because Share alone does not say a share of what.

Each plot answers one kind of question best, and each one hides something. For each plot, the table gives both:

Plot Answers best Hides
dodge Did more people survive or die in each class? Did more people die in third class or in the crew? Each class’s total. To see it, you must add two bar heights yourself.
stack How many people were in each class, and how many of them survived? How the deaths compare across classes. The segments of the dead start at different heights.
stack(share = TRUE) In which class was a person most likely to survive? How many people each class held. Second class held 285 people and the crew 885, but both bars reach 1.

dodge answers its questions because every bar starts at zero, so any two bars can be compared by height. In the first plot, for example, first class is the only class with more survivors than dead. In stack, only the bottom group, the survivors, starts at zero. stack(share = TRUE) makes every bar reach 1, so each segment’s height is its share of the class.

The crew and first class show why the choice depends on the question. If you count the survivors, as the first two plots do, the two have nearly the same number: 212 against 203. If you divide by each class’s total, as the third plot does, they are far apart: 24% of the crew survived, against 62% of first class. Both facts are true, and each plot shows only one of them. So choose the plot by the question your reader must answer. When the reader needs more than one answer, draw more than one plot, as this section does.

10.12 What you can set

A setting changes how every bar looks without reading a column, and each mark takes its own. These are a bar’s, with the values each accepts:

Setting Value
style(color = ) any CSS color name or hex
style(opacity = ) 0 to 1
style(pattern = ) solid, hatch, crosshatch, stripes, grid, dots
style(border_color = ) any CSS color name or hex
style(border_size = ) pixels
style(gap = ) a share of the mark’s place, from 0 to just under 1

A bar has no size, and that is deliberate. Its value sets its length, and its slot sets its width, so a size in pixels would be a second, conflicting width. gap does not conflict, because it is a share of the slot: it sets how much of the slot the bar leaves empty (The gap beside each bar). A bar is a shape with an inside and an outline, so it does have a border.

A fill, a texture and a border compose, so one layer can carry all three. The plot uses the medals table, shown in Basic usage:

data(medals) + bar * sum + x(country) + y(gold) +
  style(color = "goldenrod", pattern = "crosshatch",
        border_color = "black", border_size = 1) +
  y_label("Gold medals") + title("A fill, a texture, and a border")
(data(medals) + bar * sum + x(col.country) + y(col.gold) +
  style(color = "goldenrod", pattern = "crosshatch",
        border_color = "black", border_size = 1) +
  y_label("Gold medals") + title("A fill, a texture, and a border"))
data(medals) + bar * sum + x(:country) + y(:gold) +
  style(color = "goldenrod", pattern = "crosshatch", border_color = "black", border_size = 1) +
  y_label("Gold medals") + title("A fill, a texture, and a border")
plot(data(medals), layer(bar, sum), x(col.country), y(col.gold),
  style({ color: "goldenrod", pattern: "crosshatch",
  border_color: "black", border_size: 1 }), y_label("Gold medals"),
  title("A fill, a texture, and a border"))
USA China Great Britain Russia Germany 0 10 20 30 40 A fill, a texture, and a border Gold medals Country

“Given the medals: bars derived by sum, x is country, y is gold, colored goldenrod, with pattern crosshatch, border color black and border size 1.”

The pattern values here are the six fill textures. A line takes five values of its own, and only "solid" is in both lists. The setting has one name, and each mark draws it the way its shape allows. A fill takes a hatch, and a stroke takes a dash.

The histogram’s bins are not a setting. bin(30) and bin(width = 5) belong to the transform, because they change what is counted, not how a bar is drawn. Histogram shows bin(width = 5), and Choosing the bin count shows both.

The grid of every mark and every setting shows which other marks share this list. What a mark maps rather than sets is its row on the companion grid.

10.13 What it refuses

The five refusals below have different causes. The first two belong to the transform rather than the mark: bin needs a continuous axis to cut, and count computes the number it draws. The other three belong to the mark: a bar is a fill rather than a stroke, a bar needs a number to measure, and on a bar with no transform, splitting by group would change nothing.

Binning cuts a continuous axis into intervals, so there is nothing for it to cut on a categorical one:

data(gapminder_2007) + bar * bin + x(continent)
data(gapminder_2007) + bar * bin + x(col.continent)
data(gapminder_2007) + bar * bin + x(:continent)
plot(data(gapminder_2007), layer(bar, bin), x(col.continent))
Error:
! gog: `bin` cuts a continuous axis into intervals, and `x(continent)` is categorical — a category is one slot, with no width to cut. To tally rows per category, `count` is the transform that does it: `bar * count`.
gog: nothing was rendered. Fix the above, or set GOG_STRICT=0 to draw anyway.

To count the rows in each category’s slot, use count. The refusal names bar * count rather than drawing empty bins.

The second refusal comes from count itself. On y, count draws the number of rows in each slot, so a column named on y would never be read. gog refuses it, because the axis would be titled life while its numbers are counts:

data(gapminder_2007) + bar * count + x(continent) + y(life)
data(gapminder_2007) + bar * count + x(col.continent) + y(col.life)
data(gapminder_2007) + bar * count + x(:continent) + y(:life)
plot(data(gapminder_2007), layer(bar, count), x(col.continent),
  y(col.life))
Error:
! gog: `bar * count` counts the rows in each slot, and draws that number on `y`, so `y(life)` names a column it never reads: the axis would be titled `life` over numbers that are not `life`. To summarize `life` in each slot instead of counting rows, name a statistic that reads it, such as `bar * mean`. To title the axis, write `y_label()`.
gog: nothing was rendered. Fix the above, or set GOG_STRICT=0 to draw anyway.

To summarize life in each slot, use a statistic that reads it, such as bar * mean. The first histogram’s y(count) is accepted, because the table has no column named count for the transform to ignore.

Texture is the third refusal. A bar is a fill, so it takes a fill’s textures, and a dash is a stroke’s:

data(medals) + bar + x(country) + y(gold) + style(pattern = "dashed")
data(medals) + bar + x(col.country) + y(col.gold) + style(pattern = "dashed")
data(medals) + bar + x(:country) + y(:gold) + style(pattern = "dashed")
plot(data(medals), bar, x(col.country), y(col.gold),
  style({ pattern: "dashed" }))
Error:
! gog: `style(pattern = )` on a `bar`: `"dashed"` is a stroke's dash, not a fill texture. Use "solid" (the default), "hatch", "crosshatch", "stripes", "grid", "dots".
gog: nothing was rendered. Fix the above, or set GOG_STRICT=0 to draw anyway.

A bar’s outline takes a color and a width, never a dash. border_color and border_size are the only two settings the outline has.

Two categorical positions are the fourth refusal. The axis with the category places the bars, one slot each, and the axis with the number measures how far they reach, as Horizontal bars explained. With a category on both, there is no length left to draw:

data(medals) + bar + x(country) + y(country)
data(medals) + bar + x(col.country) + y(col.country)
data(medals) + bar + x(:country) + y(:country)
plot(data(medals), bar, x(col.country), y(col.country))
Error:
! gog: `bar` has categorical columns on both axes — `x(country)` and `y(country)` — so there is nothing for it to measure. One axis must be a number: that is the length of the bar. To count the rows instead, `bar * count + x(country)` draws one bar for each `country`.
gog: nothing was rendered. Fix the above, or set GOG_STRICT=0 to draw anyway.

The message names two fixes: put a number on one axis, or count the rows per category with bar * count.

The fifth refusal is group on a bar with no transform. Each row of the table is already its own bar, so splitting the rows by era would change nothing in the picture:

data(gm_eras) + bar + x(continent) + y(life) + group(era)
data(gm_eras) + bar + x(col.continent) + y(col.life) + group(col.era)
data(gm_eras) + bar + x(:continent) + y(:life) + group(:era)
plot(data(gm_eras), bar, x(col.continent), y(col.life), group(col.era))
Error:
! gog: `group` on a `bar` needs `dodge`, `stack` or a statistic to act on. Each row is already its own bar, so splitting the rows by `era` alone would change nothing. Add `dodge` to set the bars of each `era` side by side, as in `bar * dodge`, or add a statistic such as `mean` when a slot holds several rows of one `era`, as in `bar * mean * dodge`. Or map `era` to `color`.
gog: nothing was rendered. Fix the above, or set GOG_STRICT=0 to draw anyway.

The message names three fixes. The first, dodge, sets the two eras’ bars side by side in each slot. It is enough when the table holds one row for each bar, as channel_sales does. gm_eras holds one row for each country, so it needs the second fix as well: a statistic such as mean, as in Splitting without color. The third fix writes color(era) in place of group(era), which gives each era its own color and a legend. On this table it still needs mean * dodge.

10.14 In short

The axis with the category places the bars, and the axis with the number measures them. A bar’s length is its value, and that axis reaches zero unless you state your own limits. Without a transform one bar is drawn per row, and rows sharing a category overlap in one slot. A categorical bar fills 80% of its slot unless you set gap. A histogram bar fills its whole bin, and bin() decides how wide a bin is.

10.15 What comes next

The gap on each side of a bar says that the categories are separate. When the positions are continuous, such as the years of a sales series, Area joins each value to the next and fills the region below them, with no gaps.