10  Bar

How large is each category? bar draws a rectangle from the baseline (zero) to the value for each row. Bars stand up by default and lie down when the categories are on y; see Horizontal bars.

The bar chart exists because William Playfair ran out of data. His Commercial and Political Atlas of 1786 drew England’s imports and exports as two curves across thirty years, and for Scotland he had a single year, which no curve can show (Playfair, 1786). So he drew a bar for each of Scotland’s trading partners instead, and wrote that the result was the weaker picture for having no time in it. The form he apologized for outlasted the ones he was proud of.

Two names for it are still in circulation, and they disagree. Much of the business world, Excel included, calls this a column chart when the bars stand up and saves bar chart for when they lie down. gog has one bar, and which way it runs is read off the sentence rather than chosen: the categorical axis decides, which is what Horizontal bars is about. Two names for one mark would be a silent letter, and refusing those is what the No Exceptions law is for. A histogram is a third name, and it is a genuinely different plot drawn with the same mark. Karl Pearson coined the word in the 1890s (Pearson, 1895) for the picture of a distribution, where the bars are cut from a continuous axis and touch; that is bar * bin, in Histogram below.

10.1 Basic usage

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 ticks are placed at each bar position automatically. The y-axis always includes zero so bars have a truthful baseline.

10.2 Color

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

10.3 Ordering bars

order() controls the sequence of categories along whichever axis carries them: x on a vertical chart, y on a horizontal one. The key can be any column in the data: the category column itself, the measured column, or a third variable entirely. Naming the category column asks for that column’s own order, which is alphabetical here because country is plain text, and the declared levels if it were a factor.

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

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: Ranked (desc)")
(data(medals) + bar + x(col.country) + y(col.gold) +
  order(col.gold, desc = True) +
  title("Gold Medals: Ranked (desc)"))
data(medals) + bar + x(:country) + y(:gold) + order(:gold, desc = true) +
  title("Gold Medals: Ranked (desc)")
plot(data(medals), bar, x(col.country), y(col.gold),
  order(col.gold, { desc: true }), title("Gold Medals: Ranked (desc)"))
USA China Great Britain Russia Germany 0 10 20 30 40 Gold Medals: Ranked (desc) Gold Country

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

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

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

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

By category name, Z → A, the reverse:

data(medals) + bar + x(country) + y(gold) +
  order(country, desc = TRUE) +
  title("Gold Medals: Reverse Alphabetical")
(data(medals) + bar + x(col.country) + y(col.gold) +
  order(col.country, desc = True) +
  title("Gold Medals: Reverse Alphabetical"))
data(medals) + bar + x(:country) + y(:gold) +
  order(:country, desc = true) +
  title("Gold Medals: Reverse Alphabetical")
plot(data(medals), bar, x(col.country), y(col.gold),
  order(col.country, { desc: true }),
  title("Gold Medals: Reverse Alphabetical"))
USA Russia Great Britain Germany China 0 10 20 30 40 Gold Medals: Reverse Alphabetical 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
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 any other column in the dataset

10.4 Duplicate categories

If the same category appears in multiple rows, bar draws one bar per row; the last silently overlaps the earlier ones.

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

medals_dup <- data.frame(
  country = c("USA", "USA", "GBR", "GBR", "JPN"),
  gold    = c(10, 5, 8, 4, 6)
)
data(medals_dup) + bar * sum + x(country) + y(gold) +
  order(gold, desc = TRUE) + title("Totals: bar * sum")
medals_dup = {"country": ["USA", "USA", "GBR", "GBR", "JPN"], "gold": [10, 5, 8, 4, 6]}
(data(medals_dup) + bar * sum + x(col.country) + y(col.gold) +
  order(col.gold, desc = True) + title("Totals: bar * sum"))
medals_dup = (country = ["USA", "USA", "GBR", "GBR", "JPN"], gold = [10, 5, 8, 4, 6],)
data(medals_dup) + bar * sum + x(:country) + y(:gold) +
  order(:gold, desc = true) + title("Totals: bar * sum")
const medals_dup = { country: ["USA", "USA", "GBR", "GBR", "JPN"], gold: [10, 5, 8, 4, 6] };
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

bar * sum and order() compose freely, because they do different jobs. sum totals the values; order() sorts the categories those totals land on.

bar handles negative values correctly: the bar grows away from zero in whichever direction the value sits.

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 gains differently, notice what is being asked for: color carries a variable, so the variable has to exist. “Is this negative” is a fact about the data rather than about the rectangle, so compute it into a column and map that column, and palette() says which colors the mapping hands out:

balance$result <- factor(ifelse(balance$profit < 0, "loss", "profit"),
                         levels = c("loss", "profit"))
data(balance) + bar + x(quarter) + y(profit) + color(result) +
  palette(c("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

palette() hands its colors out in category order, and that is why result is a factor here instead of a plain character column. Left to itself a character column takes the order its values first appear in, and this data opens on a profit, so the palette would arrive the wrong way round and paint the gains red. Declaring levels = c("loss", "profit") pins the order, the legend reads in it too, and the plot cannot silently invert if next quarter’s numbers start with a loss. This is the same mechanism as Factors decide the order of categories.

Red against green is the convention for money and also the hardest pair to separate for the commonest color blindness. Texture answers that without giving up the colors, by giving each result its own hatch as well as its own hue: see Texture above, and Will it survive a photocopier? in the cookbook.

10.5 Horizontal bars

Swap the bindings. There is no flip atom, because that would be a second way to say one thing. Put the categories on y and the measure on x:

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

gog reads the orientation off the bindings: the axis carrying a number is the one the bars measure along; the other is the one they sit on. Nothing else changes: order, transforms, color and style() all behave identically.

This is what you want whenever the category names are long, because a vertical axis gives each label a whole line instead of a cramped slot:

long <- data.frame(
  department = c("Research & Development", "Sales and Marketing",
                 "Customer Operations", "Finance", "People & Culture"),
  headcount  = c(128.0, 96.0, 74.0, 31.0, 22.0)
)
data(long) + bar + x(headcount) + y(department) +
  order(headcount, desc = TRUE) +
  x_label("Headcount") +
  title("Long labels fit on a vertical axis")
long = {"department": ["Research & Development", "Sales and Marketing",
                 "Customer Operations", "Finance", "People & Culture"], "headcount": [128.0, 96.0, 74.0, 31.0, 22.0]}
(data(long) + bar + x(col.headcount) + y(col.department) +
  order(col.headcount, desc = True) +
  x_label("Headcount") +
  title("Long labels fit on a vertical axis"))
long = (department = ["Research & Development", "Sales and Marketing", "Customer Operations", "Finance", "People & Culture"], headcount = [128, 96, 74, 31, 22],)
data(long) + bar + x(:headcount) + y(:department) +
  order(:headcount, desc = true) + x_label("Headcount") +
  title("Long labels fit on a vertical axis")
const long = { department: ["Research & Development", "Sales and Marketing", "Customer Operations", "Finance", "People & Culture"], headcount: [128, 96, 74, 31, 22] };
plot(data(long), 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: first in sort order reads first. Descending puts the largest bar leftmost on a vertical chart and topmost on a horizontal one.

Orientation is only ambiguous if neither axis is categorical. There gog keeps the long-standing default and draws vertically; x(year) + y(sales) is a column chart, as it always was. To lay that out horizontally, name the measure on x.

A bar needs something to measure, so two categorical axes are refused:

data(medals) + bar + x(country) + y(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 rows per category instead, use `bar * count`.
gog: nothing was rendered. Fix the above, or set GOG_STRICT=0 to draw anyway.

10.6 Texture

A bar’s fill can carry a texture as well as a color: a hatch instead of a flat wash, set with style(pattern = ). A texture reads without hue, so a hatched bar chart stays legible in grayscale or 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

The four fill textures are "hatch", "crosshatch", "grid", and "dots"; the style chapter lays them out beside the strokes’ dash, style(pattern = ) realized once per geometry. One texture covers the whole layer; to give each series in a color split its own hatch, map a column with the pattern() channel, shape()’s texture twin.

10.7 Histogram

Combine bar with the bin transform to create a histogram. bin groups continuous x values into equal-width buckets and counts rows:

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)

Thickness is automatic, and it is where a histogram and a bar chart part ways. A categorical bar fills 80 % of the gap to its neighbor, leaving the space that reads as “separate categories”. A histogram cuts a continuous axis into adjacent intervals, so nothing sits between one bin and the next and the bars touch, with a hairline in the panel color to keep them readable. Nothing was set either time: the transform decides it, measured across whichever axis the bars sit on.

A histogram turns on its side by the same rule. Bind the measured variable to y and the count is drawn along x:

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

10.7.1 Overlaid histograms: one distribution per group

Add color and the histogram splits: one histogram per category, cut on the same bins and drawn in the same panel. This answers the question a single combined histogram cannot: not “how is petal length distributed?” but “how is it distributed for each species?

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

The three histograms overlay: each drawn in place, measured from the shared baseline. Opaque, the last species drawn would bury the others; so each bar is a translucent fill under a solid outline in its own color. The outline is what carries the shape where the fills pile up: setosa, well separated on the left, reads as cleanly as versicolor and virginica, which overlap in the middle and blend where they meet. The bins are shared across the groups, so the bars line up and the overlap is truthful rather than an artifact of three different binnings.

The fill is faint by default so stacked series show through. style(opacity = ) sets how far; the outline stays solid regardless:

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

The outline is the series color by default, but you can recolor it: style(border_color = ) sets the rim, style(border_size = ) its width. A white border cleanly parts the overlapping fills, the way many overlaid histograms are drawn:

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

color is the fill and border_color the outline: two independent settings, so a bar can be one color inside and another at its edge. See Setting vs mapping.

Or go the other way and drop the outline entirely: style(border_size = 0) draws no border at all, so the translucent fills overlap with nothing between them, the plainest overlaid histogram, blending where the groups meet:

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

The split reads the orientation off the bindings, like every other bar: put petal_length on y and the overlaid histograms lie down. And when the groups overlap so heavily that even the outlines crowd, give each its own panel instead: … |facet(species) draws the same three histograms side by side on a shared scale.

See Transforms for details on bin and other transforms.

10.8 Grouped bars: side by side

A color split stacks the groups at one position; to set them side by side, the grouped bar chart, add dodge. Each group’s bar narrows to share the slot, and the split reads as a comparison within each category:

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

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 the whole family.

10.9 Stacked bars: piled up

The other way to place a color split: instead of side by side, pile the groups on top of each other with stack. Each bar’s height becomes the group total, 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 0M 2000M 4000M Population by continent, 1957 and 2007 stacked Population Continent Era 1957 2007

dodge compares the groups (equal baselines, side by side); stack sums them (one bar, segments piled). Reach for stack when the parts add to a meaningful total (populations, counts, revenue) and for dodge when you are comparing the groups against each other. Note the measure: you stack sum population (quantities add), where the dodge example compared mean life (indices do not).

10.9.1 The 100% stacked bar

The plot above answers two questions at once, and one of them drowns out the other: Asia’s two eras come to 5.4 billion against Oceania’s 0.04, so the axis is scaled for Asia and the split inside the small continents is unreadable. When the composition is what you came for and the total is not, stack(share = TRUE) divides each pile by its own bar’s total, so every bar reaches 1 and the segments are the only thing left to read:

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

Now the comparison is between continents rather than inside one, and the two ends of it are worth naming. Africa is the most lopsided: 78% of its two-era total sits in 2007, so it more than tripled. Europe is the flattest at 57%, the only continent whose halves come close to even. Neither is legible in the plot above at any size. What you give up is the totals, which have gone entirely. That is the trade, not a side effect, and Oceania now occupies as much width as Asia.

The axis says Share, because whatever the numbers were before they are fractions of one now, and that holds even though y(population) names a column: the fill rescales an axis somebody already named. It is not proportion under another name. The two divide by different totals. proportion divides by the whole plot’s, so its bars still say how big each slot is. This one divides by the bar’s own. And only this one works on a sum of a column, which proportion has no way to say.

10.10 What you can set

Setting Value
style(color = ) any CSS color name or hex
style(opacity = ) 0 to 1
style(pattern = ) solid, hatch, crosshatch, grid, dots
style(border_color = ) any CSS color name or hex
style(border_size = ) pixels

And these vary per row if you map them to a column instead: color() (categories), pattern() (categories), opacity() (numbers), play() (either).

A bar has no size, and the absence is the point: its extent is already pinned by its position and its value, so a width setting would be a second, conflicting answer to how big it is. What it does have, being a closed fill, is a rim.

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 rim")
(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 rim"))
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 rim")
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 rim"))
USA China Great Britain Russia Germany 0 10 20 30 40 A fill, a texture, and a rim Gold medals Country

The pattern values here are the five fill textures, not the three dashes a line takes. One setting name, one realization per geometry, so a plot that has to survive grayscale printing can separate its bars by texture instead of hue.

The bar’s own knob is not a setting at all: bin(bins = 20) and bin(width = 5) control the histogram bar * bin draws, and they belong to the transform because binning is what has a parameter, not the rectangle.

10.11 What it refuses

A bar measures a length from a baseline, and its refusals mark the edges of that.

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)
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.

A category is already one slot per value, which is what count is for. The refusal says so rather than drawing five bins of nothing.

Texture is the second edge. 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")
Error:
! gog: `style(pattern = )` on a `bar`: `"dashed"` is a stroke's dash, not a fill texture. Use "solid" (the default), "hatch", "crosshatch", "grid", or "dots".
gog: nothing was rendered. Fix the above, or set GOG_STRICT=0 to draw anyway.

The dash is available on the bar’s outline through border_*, which is a stroke and does take one. And a bar given two categorical positions has nothing left to measure, which is the refusal shown above under horizontal bars.