| 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 |
7 Choosing a mark
Which visible form should your data take? A mark is that visible form: a dot, a bar, a line. Choosing the mark is the first decision in every plot. The answer depends on the types of your variables and the question you are asking.
There are fourteen marks, and they are not a list to learn before you draw. This chapter helps you with the choice: it shows which mark answers which question, draws the common cases, and stops. Each mark then has a chapter of its own, with examples. The tables in Quick reference are for later, when you have data in front of you and want the row that fits.
If you have used ggplot2, a mark is similar to a geom, but the two differ in one way. Many geoms compute a statistic before they draw. geom_histogram() bins your data and then draws bars. geom_smooth() fits a curve and then draws a line. A mark never does that. A mark is only the shape, and the statistic is a separate word: a histogram is bar * bin, a trend line is line * smooth. A geom tends to name a chart. A mark names a shape.
Each column of your data is one variable, and gog reads its type from the data. There are two types:
- Continuous. A column that holds numbers and has no declared order (
gdp,temperature,score). A date or date-time column is continuous too, and it gets a calendar axis (see Scales). - Categorical. A column that holds text (
continent,country,species), or a column with a declared order.
7.1 One variable
With one variable there is one question: how are its values spread out? The answer depends on whether the column is continuous or categorical.
7.1.1 Continuous variable
Are the life expectancies of 142 countries close to one value, or spread across a wide range? You can cut a continuous column into bands, or estimate a smooth curve over it. bin cuts the column into bands and counts the rows in each. bar * bin draws one bar per band, which is the histogram:
# How is life expectancy distributed? → histogram
data(gapminder_2007) + bar * bin + x(life) +
x_label("Life expectancy (years)") + title("Distribution: bar * bin")(data(gapminder_2007) + bar * bin + x(col.life) +
x_label("Life expectancy (years)") + title("Distribution: bar * bin"))data(gapminder_2007) + bar * bin + x(:life) +
x_label("Life expectancy (years)") + title("Distribution: bar * bin")plot(data(gapminder_2007), layer(bar, bin), x(col.life),
x_label("Life expectancy (years)"), title("Distribution: bar * bin"))“Given gapminder 2007: bars derived by bin, x is life.”
When sharp changes in height from one bar to the next hide the shape, a curve shows the same spread without them. density estimates a smooth curve over the same column, and line * density draws that curve:
# Smooth shape of the distribution → density curve
data(gapminder_2007) + line * density + x(life) +
x_label("Life expectancy (years)") + title("Density: line * density")(data(gapminder_2007) + line * density + x(col.life) +
x_label("Life expectancy (years)") + title("Density: line * density"))data(gapminder_2007) + line * density + x(:life) +
x_label("Life expectancy (years)") + title("Density: line * density")plot(data(gapminder_2007), layer(line, density), x(col.life),
x_label("Life expectancy (years)"), title("Density: line * density"))“Given gapminder 2007: a line derived by density, x is life.”
7.1.2 Categorical variable
A categorical column cannot be cut into bands, so the same question becomes: how often does each category occur? count tallies the rows in each category, and bar * count draws one bar per category:
# How many countries per continent? → frequency bar
data(gapminder_2007) + bar * count + x(continent) +
title("Frequency: bar * count")(data(gapminder_2007) + bar * count + x(col.continent) +
title("Frequency: bar * count"))data(gapminder_2007) + bar * count + x(:continent) +
title("Frequency: bar * count")plot(data(gapminder_2007), layer(bar, count), x(col.continent),
title("Frequency: bar * count"))“Given gapminder 2007: bars derived by count, x is continent.”
A count does not say what part of the whole each continent is. Africa holds 52 of the 142 countries, which is more than a third of them. proportion is the same tally as a share of the whole, so the bars sum to 1:
# What share of countries per continent? → proportion bar
data(gapminder_2007) + bar * proportion + x(continent) +
title("Shares: bar * proportion")(data(gapminder_2007) + bar * proportion + x(col.continent) +
title("Shares: bar * proportion"))data(gapminder_2007) + bar * proportion + x(:continent) +
title("Shares: bar * proportion")plot(data(gapminder_2007), layer(bar, proportion), x(col.continent),
title("Shares: bar * proportion"))“Given gapminder 2007: bars derived by proportion, x is continent.”
7.2 Two variables
With two variables, the pair of column types decides the mark.
7.2.1 Continuous × Continuous
Both variables are continuous, so the questions are about relationships, trends and patterns. Do people in richer countries live longer? point places one glyph per row, and the shape those glyphs make is the first answer:
# Is there a relationship? → scatter
data(gapminder_2007) + point + x(gdp) + y(life) + x_label("GDP per person") +
y_label("Life expectancy") + title("Relationship: point")(data(gapminder_2007) + point + x(col.gdp) + y(col.life) + x_label("GDP per person") +
y_label("Life expectancy") + title("Relationship: point"))data(gapminder_2007) + point + x(:gdp) + y(:life) +
x_label("GDP per person") + y_label("Life expectancy") +
title("Relationship: point")plot(data(gapminder_2007), point, x(col.gdp), y(col.life),
x_label("GDP per person"), y_label("Life expectancy"),
title("Relationship: point"))“Given gapminder 2007: points, x is gdp, y is life.”
Whether the points rise or fall together can matter more than any one country. smooth fits a trend line through the points, and line * smooth draws it over the scatter. color(continent) is written right after point, so it colors only the points, and one line is fitted to all of them:
# Smooth the relationship → fitted trend line
data(gapminder_2007) + x(gdp) + y(life) +
point + color(continent) +
line * smooth +
x_label("GDP per person") + y_label("Life expectancy") +
title("Trend: point + line * smooth")(data(gapminder_2007) + x(col.gdp) + y(col.life) +
point + color(col.continent) +
line * smooth +
x_label("GDP per person") + y_label("Life expectancy") +
title("Trend: point + line * smooth"))data(gapminder_2007) + x(:gdp) + y(:life) + point + color(:continent) +
line * smooth + x_label("GDP per person") + y_label("Life expectancy") +
title("Trend: point + line * smooth")plot(data(gapminder_2007), x(col.gdp), y(col.life), point,
color(col.continent), layer(line, smooth), x_label("GDP per person"),
y_label("Life expectancy"), title("Trend: point + line * smooth"))“Given gapminder 2007: x is gdp, y is life, points colored by continent, and also a line derived by smooth.”
A question about two continuous columns is not always about a relationship. When one column is time and the other an amount, the question can be how much there was. The actuals table records yearly sales:
| year | sales |
|---|---|
| 2019 | 120 |
| 2020 | 135 |
| 2021 | 128 |
| 2022 | 152 |
| 2023 | 168 |
# How much, not just where → filled area
data(actuals) + area + x(year) + y(sales) + y_label("Sales") +
title("Quantity: area")(data(actuals) + area + x(col.year) + y(col.sales) + y_label("Sales") +
title("Quantity: area"))data(actuals) + area + x(:year) + y(:sales) + y_label("Sales") +
title("Quantity: area")plot(data(actuals), area, x(col.year), y(col.sales), y_label("Sales"),
title("Quantity: area"))“Given the actuals: an area, x is year, y is sales.”
line and area draw the same boundary and give it different meanings. A line says where the value was; an area says how much there was, so it fills to zero and the filled region is the quantity. Choose area when zero is a true baseline for your variable, and line when it is not.
Sometimes each x has a spread to show, not one value. ribbon fills between a low boundary and a high one, and a range transform supplies that pair: the minimum and maximum at each x. Name two quantiles and range uses those instead. Add a line * mean over the ribbon, and the line of means is drawn inside the band.
When the order of the rows is what you want to show, path connects them in that order. line sorts its points by x before it joins them. path does not, so the stroke can move left as well as right, cross itself, or return to where it started. That is the connected scatterplot: the two axes carry measurements, and the joining line shows the order in time, here for five Asian countries.
| country | continent | year | life | population | gdp |
|---|---|---|---|---|---|
| China | Asia | 1952 | 44.00000 | 556263527 | 400.4486 |
| China | Asia | 1957 | 50.54896 | 637408000 | 575.9870 |
| China | Asia | 1962 | 44.50136 | 665770000 | 487.6740 |
| China | Asia | 1967 | 58.38112 | 754550000 | 612.7057 |
| China | Asia | 1972 | 63.11888 | 862030000 | 676.9001 |
# The reading order is the table's → path
data(gapminder_asia) + path + x(gdp) + y(life) + color(country) +
style(arrow = "end") +
x_label("GDP per person") + y_label("Life expectancy") +
title("Route: path (the arrow points to 2007)")(data(gapminder_asia) + path + x(col.gdp) + y(col.life) + color(col.country) +
style(arrow = "end") +
x_label("GDP per person") + y_label("Life expectancy") +
title("Route: path (the arrow points to 2007)"))data(gapminder_asia) + path + x(:gdp) + y(:life) + color(:country) +
style(arrow = "end") + x_label("GDP per person") +
y_label("Life expectancy") +
title("Route: path (the arrow points to 2007)")plot(data(gapminder_asia), path, x(col.gdp), y(col.life),
color(col.country), style({ arrow: "end" }), x_label("GDP per person"),
y_label("Life expectancy"),
title("Route: path (the arrow points to 2007)"))“Given gapminder Asia: paths, x is gdp, y is life, color by country, with arrow end.”
Layered over any of the marks above, rule draws a straight stroke at one value on one axis, across the whole of the other: a threshold line, or a rug. A rug is a short tick at the edge of the panel for each observation, so it shows where the observations are. style(reach = "edge") shortens a rule into a rug.
How many countries have a life expectancy below 60, 70 or 80 years, and where are they on the income axis? life_bands holds three threshold values of life expectancy, each with a name:
| life | band |
|---|---|
| 60 | Low |
| 70 | Middle |
| 80 | High |
# One position, the panel supplies the other → rule
data(gapminder_2007) + point + x(gdp) + y(life) + style(opacity = 0.45) +
rule + x(gdp) + style(reach = "edge") +
data(life_bands) + rule + color(band) +
x_label("GDP per person") + y_label("Life expectancy") +
title("Threshold and rug: rule")(data(gapminder_2007) + point + x(col.gdp) + y(col.life) + style(opacity = 0.45) +
rule + x(col.gdp) + style(reach = "edge") +
data(life_bands) + rule + color(col.band) +
x_label("GDP per person") + y_label("Life expectancy") +
title("Threshold and rug: rule"))data(gapminder_2007) + point + x(:gdp) + y(:life) +
style(opacity = 0.45) + rule + x(:gdp) + style(reach = "edge") +
data(life_bands) + rule + color(:band) + x_label("GDP per person") +
y_label("Life expectancy") + title("Threshold and rug: rule")plot(data(gapminder_2007), point, x(col.gdp), y(col.life),
style({ opacity: 0.45 }), rule, x(col.gdp), style({ reach: "edge" }),
data(life_bands), rule, color(col.band), x_label("GDP per person"),
y_label("Life expectancy"), title("Threshold and rug: rule"))“Given gapminder 2007: points, x is gdp, y is life, and also rules, x is gdp, with reach edge, and also rules from the life bands, colored by band.”
The rule names x(gdp) even though the points already named it. That second x(gdp) does not add an axis. Every layer shares the plot’s one x and one y. A rule marks a value on one axis and spans the other, so it has to say which. This rule reads the scatter’s table, which holds both gdp and life, so either column could place it. Without x(gdp), gog refuses rather than guessing; the Rule chapter shows the refusal. The three rules from life_bands need no position, because that table holds life and no gdp, so only one axis can answer.
text is also layered over other marks: it writes a label from a column at each position where point would draw a dot. Layered under the other marks, zone shades a rectangle. On an axis where you give it a pair of columns, the rectangle starts at the value in one column and ends at the value in the other. On an axis where you give it no pair, the rectangle spans the whole panel. Write zone * bin instead, and the same mark covers the panel with a grid of cells and counts the rows in each: the heatmap.
7.2.2 Categorical × Continuous
One axis groups, the other measures. The natural question is how the measurement differs across groups. mean reduces each group to one value, its average. bar * mean draws one bar per group, and the bar’s height is that value:
# Mean value per group → bar
data(gapminder_2007) + bar * mean + x(continent) + y(life) +
y_label("Mean life expectancy") + title("Group summary: bar * mean")(data(gapminder_2007) + bar * mean + x(col.continent) + y(col.life) +
y_label("Mean life expectancy") + title("Group summary: bar * mean"))data(gapminder_2007) + bar * mean + x(:continent) + y(:life) +
y_label("Mean life expectancy") + title("Group summary: bar * mean")plot(data(gapminder_2007), layer(bar, mean), x(col.continent),
y(col.life), y_label("Mean life expectancy"),
title("Group summary: bar * mean"))“Given gapminder 2007: bars derived by mean, x is continent, y is life.”
A mean hides how far apart the countries inside one continent are. Sometimes that spread is what you want to see. Drop the transform, and point draws every country in its continent’s slot:
# All individual points per group → strip plot
data(gapminder_2007) + point + x(continent) + y(life) +
y_label("Life expectancy") +
title("Strip plot: point (one dot per country)")(data(gapminder_2007) + point + x(col.continent) + y(col.life) +
y_label("Life expectancy") +
title("Strip plot: point (one dot per country)"))data(gapminder_2007) + point + x(:continent) + y(:life) +
y_label("Life expectancy") +
title("Strip plot: point (one dot per country)")plot(data(gapminder_2007), point, x(col.continent), y(col.life),
y_label("Life expectancy"),
title("Strip plot: point (one dot per country)"))“Given gapminder 2007: points, x is continent, y is life.”
Nothing is summarized: each dot is one country, placed by its continent and its life expectancy. Dots for countries with the same life expectancy overlap, so a crowded slot hides two things: how many dots it holds, and how they are spread.
There are three ways to draw a crowded group more clearly, and each shows something different. For the shape of each group rather than every point, write box + x(continent) + y(life). box draws a box between the quartiles, a line at the median, whiskers, and outliers as separate dots. ribbon * density draws the whole estimated distribution that the box summarizes: the violin plot. When every point matters but the points overlap, point * jitter spreads them sideways: the jittered strip plot.
The question is not always which group is highest. It can be how the value changes from one group to the next. line, area, step and ribbon accept a category on x too. They join one summary per group instead of drawing a bar on each, and the result is a profile:
# One value per group, joined → profile
data(gapminder_2007) + x(continent) + y(life) +
line * mean + point * mean +
y_label("Mean life expectancy") +
title("Profile: line * mean")(data(gapminder_2007) + x(col.continent) + y(col.life) +
line * mean + point * mean +
y_label("Mean life expectancy") +
title("Profile: line * mean"))data(gapminder_2007) + x(:continent) + y(:life) + line * mean +
point * mean + y_label("Mean life expectancy") +
title("Profile: line * mean")plot(data(gapminder_2007), x(col.continent), y(col.life),
layer(line, mean), layer(point, mean), y_label("Mean life expectancy"),
title("Profile: line * mean"))“Given gapminder 2007: x is continent, y is life, a line derived by mean and also points derived by mean.”
A bar compares one group with another; a profile traces a change across the groups. A profile needs more care than a bar, because its segments cross parts of the panel where nothing was measured. Use it when the categories have an order, such as a rating scale or size groups from smallest to largest. Continents have no order, so that advice says not to draw this plot. gog draws it anyway. The grammar refuses a sentence that is ill-formed, never one that is only unwise; whether a profile suits your categories is your judgment (Law 8).
line is not the only mark that draws a profile. area fills under the same boundary, step holds each value flat until the next category, and ribbon * range fills the spread as a band. Drawn around a circle, the same line * mean profile is the radar chart, in Polar.
7.3 Quick reference
The table below holds the common cases, one question per row. Start from the types of the columns you put on x and y, and find the rows that match. Each row names the mark and transform that answer its question. This chapter drew plots for the most common rows, and each mark’s own chapter covers the rest. A — means that no column of yours goes on that position.
| x | y | Mark + transform | Question |
|---|---|---|---|
| Continuous | — | bar * bin |
Distribution? |
| Continuous | — | line * density |
Smooth distribution? |
| Continuous | — | point * bin * stack |
Distribution, every observation shown? |
| Categorical | — | bar * count |
Frequency? |
| Categorical | — | bar * proportion |
Share? |
| Continuous | Continuous | point |
Relationship? |
| Continuous | Continuous | line |
Trend / time series? |
| Continuous | Continuous | line * smooth |
Smoothed trend? |
| Continuous | Continuous | area |
How much, over a range? |
| Continuous | Continuous | ribbon * range |
Spread / band over a range? |
| Continuous | Continuous | path |
The order of the rows? |
| Categorical | Continuous | bar |
One value per group, already in the table? |
| Categorical | Continuous | bar * mean/sum/… |
Value per group? |
| Categorical | Continuous | point |
All values per group? |
| Categorical | Continuous | box |
Distribution per group, summarized? |
| Categorical | Continuous | ribbon * density |
Distribution per group, in full? |
| Categorical | Continuous | interval * range |
Spread per group? |
| Categorical | Continuous | line * mean |
Value per group, as a profile? |
| Categorical | Continuous | area * mean |
Value per group, filled? |
| Categorical | Continuous | line * mean + polar() |
Value per group, as a radar? |
The table has no row for edge, because its question has no x and no y: what is connected to what? edge draws one stroke for each connection in a table of connections, such as who trades with whom. Nothing in the data says where the two ends of that stroke should be placed, so a layout transform inside the network() space places them. The Network chapter builds the whole diagram.
A third variable takes the third position, z, rather than a new mark. The last row below is different. It has only two columns, both categorical, so count supplies the third number: how many rows have each pair. space() gives that count an axis of its own.
| x | y | z | Mark | Question |
|---|---|---|---|---|
| Continuous | Continuous | Continuous | point |
Relationship, in three dimensions? |
| Continuous | Continuous | Continuous | path |
A route through three dimensions? |
| Continuous | Continuous | Continuous | surface |
A height over a plane? (needs a grid: a z value at every x and y) |
| Categorical | Categorical | — | bar * count + space() |
How many, per pair? |
7.3.1 Swapping x and y
Long category names, such as country names, are easier to read on the vertical axis than along the bottom. In the first table, each Categorical / Continuous row whose mark is drawn in a slot also works with the two axes swapped. bar, box, interval, point and the violin (ribbon * density) are all drawn in a slot on one axis and measure along the other. The column types decide which axis holds the slots: the one with the categorical column. Swapping the two positions therefore draws the same plot horizontally.
box + x(life) + y(continent) is the horizontal box plot, and bar + x(gold) + y(country), on the medals table, is the horizontal bar. Nothing else is needed. If you know ggplot2’s coord_flip() or matplotlib’s barh(), this is the same result with no extra word. gog reads the orientation from which axis carries the categories, as ggplot2 does.
Only the marks with a slot can be drawn horizontally this way. For line, step and area, x is always the domain, the axis the mark is read along, and y is the measure. The same is true of a ribbon drawn as a band rather than as a violin. Put the category on y for any of them, and gog refuses the sentence. Their two columns are often both numbers, so no column type could say which one is the domain, and the mark decides it instead. The line chapter shows what a swap of two continuous columns draws, and what to write when the domain must be the vertical axis.
7.4 Mark families
The mark list looks like a set to memorize, one entry at a time. It is not. The fourteen consonants of Hangeul (한글) are not fourteen unrelated shapes either. The letters are featural: most of them are another letter with a single feature changed. ㅋ (k) is ㄱ (g) with one stroke added, so learning ㄱ teaches you most of ㅋ.
The marks are built the same way. Some pairs have the same rules for every channel: x, y, color, size and the rest. If one mark requires, accepts or refuses a channel, the other mark does the same. They differ in the geometry they draw:
| Family | Same rules for every channel | They differ in |
|---|---|---|
area and ribbon |
✓ | area closes its region on the baseline; ribbon closes it on a second data boundary |
box and interval |
✓ | box computes its own summary of each group; interval spans two ends a transform supplies |
line and step |
✓ | line slopes from one value to the next; step holds each value until it changes |
The second member of a pair is quick to learn. It follows the same rules for every channel as its sibling, so you already know those. The grid of marks and channels in Combinations shows this: the two marks of a family have identical rows there. What is left is what the second mark draws. ribbon and interval also need a transform, such as range, to supply their two ends.
When does gog add a mark? Only for a shape that no existing mark can draw. step was added because “hold the value until it changes” is such a shape: no channel, transform or setting can say it. A thick line was not added as a mark, because style(size = ) already says it. A new chart is never the reason. A histogram, a radar and a pyramid are built from marks that already exist. A new word is spelled with the letters you have, and a new chart is written with the marks you have. The families make the marks quick to learn, but gog never adds a mark only to complete a pair.
7.5 Unpronounceable combinations
Just as some letter combinations cannot be pronounced, some pairs of a transform and a column type have no meaning. gog refuses each one and draws nothing.
bin, density and smooth all work along a continuous axis, so the column on that axis must be continuous. A category on that axis is refused, and each refusal names the atom that does the same job for categories:
| Combination | Problem | Use instead |
|---|---|---|
bar * bin + x(category) |
bin cuts a continuous axis into bands |
bar * count + x(category) |
bar * density + x(category) |
density estimates a continuous distribution |
bar * proportion + x(category) |
point * smooth + x(category) |
smooth fits a curve along a continuous axis |
bar * mean + x(category) + y(value) |
All three appear as live refusals in What bin, density and smooth refuse; the table above is this page’s summary of them.
7.6 What comes next
Choosing the mark is the first decision in a plot, not the last one. Point opens the fourteen chapters that follow, each describing one mark: its channels, its transforms, its settings and its refusals.