| 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 |
22 Scales
What do you do when most of your data is crowded into one corner? You change the scale, which decides how a number becomes a position. It is written on the binding, not as an atom of its own:
x(gdp, scale = "log")A scale belongs to one channel, so it is written where that channel is bound. There is no scale() atom to write separately. A separate atom would make you name the channel twice, once to bind it and once to scale it, and the two could then disagree.
Writing the scale on the binding also makes a sentence easier to read. To see how gdp is placed, you look at x(gdp, scale = "log") and nowhere else. Nothing else in the sentence can change it, and there is no second function to look up.
22.1 A scale is not a transform
A log scale is not the only way to see a spread of incomes. You could take the logarithm of the column yourself and plot the result. That is a different operation, and the difference decides what the axis says.
bin and mean are transforms: they read your table and compute new values from it. A scale produces no new values at all. It changes how far along the axis a number lands and what the ticks say, and nothing else.
The difference is visible in the finished plot. Here is gdp plotted with a log scale:
data(gapminder_2007) + point + x(gdp, scale = "log") + y(life) +
color(continent) +
x_label("GDP per person") +
title("Income and life expectancy: log scale")(data(gapminder_2007) + point + x(col.gdp, scale = "log") + y(col.life) +
color(col.continent) +
x_label("GDP per person") +
title("Income and life expectancy: log scale"))data(gapminder_2007) + point + x(:gdp, scale = "log") + y(:life) +
color(:continent) + x_label("GDP per person") +
title("Income and life expectancy: log scale")plot(data(gapminder_2007), point, x(col.gdp, { scale: "log" }),
y(col.life), color(col.continent), x_label("GDP per person"),
title("Income and life expectancy: log scale"))“Given gapminder 2007: points, x is gdp on a log scale, y is life, color by continent.”
The axis reads 1K and 10K. Those are dollars, the units you supplied.
The word log appears nowhere on the axis, and that is deliberate. The units did not change, so the axis label does not claim they did. The ticks are the only sign: each is a whole power of ten, so equal steps along the axis mean equal ratios. Write x_label("GDP per person (log scale)") when a reader needs the scale named in words. The axis label is yours, and gog never writes the scale into it.
Now the same picture, drawn from a column that already holds the logarithm. The gm_logged table is gapminder_2007 with one more column, log_gdp, the base-10 logarithm of gdp:
| country | continent | year | life | population | gdp | log_gdp |
|---|---|---|---|---|---|---|
| Afghanistan | Asia | 2007 | 43.828 | 31889923 | 974.5803 | 2.988818 |
| Albania | Europe | 2007 | 76.423 | 3600523 | 5937.0295 | 3.773569 |
| Algeria | Africa | 2007 | 72.301 | 33333216 | 6223.3675 | 3.794025 |
| Angola | Africa | 2007 | 42.731 | 12420476 | 4797.2313 | 3.680991 |
| Argentina | Americas | 2007 | 75.320 | 40301927 | 12779.3796 | 4.106510 |
data(gm_logged) + point + x(log_gdp) + y(life) +
color(continent) +
title("The same shape, with the axis in log units")(data(gm_logged) + point + x(col.log_gdp) + y(col.life) +
color(col.continent) +
title("The same shape, with the axis in log units"))data(gm_logged) + point + x(:log_gdp) + y(:life) + color(:continent) +
title("The same shape, with the axis in log units")plot(data(gm_logged), point, x(col.log_gdp), y(col.life),
color(col.continent),
title("The same shape, with the axis in log units"))Every point is in the same place as in the first plot, but the axis now reads 2.5 to 4.5. Those numbers are logarithms, not dollars. The tick at 3.0 is where the first plot has 1K, because ten to the power of 3 is 1,000. The tick at 3.5 means ten to the power of 3.5. That is about 3,162 dollars, a number most readers need a calculator to find. A log scale does that calculation for the reader: its ticks are already in dollars.
So the choice between the two is about what the quantity is. Take the logarithm of the column when that logarithm is itself the quantity of interest: a log-odds, a decibel, a pH. Use a log scale when the quantity is still dollars or people, and you only want to see it across orders of magnitude.
22.2 The order of a scale and a transform
Once a transform is in the plot, a scale is no longer only about display, because a transform has to choose which numbers to work on. gog follows one rule, and it has no per-transform exceptions:
A transform groups by one axis and measures on the other. The scale is applied before the transform on the grouping axis, and after it on the measured axis.
22.2.1 Scaling the grouping axis
A histogram of income is the first plot where that rule changes the result. Its bins have to be cut somewhere, either in dollars or in the logarithms of those dollars.
bin groups by x, so a log x is applied first. The bins are cut on the logarithms, not on the dollars. Each bin then spans one constant ratio. Every bar is therefore the same width, and the bars fill the axis from end to end:
data(gapminder_2007) + bar * bin + x(gdp, scale = "log") +
x_label("GDP per person") +
title("Bins cut in log space are evenly spaced")(data(gapminder_2007) + bar * bin + x(col.gdp, scale = "log") +
x_label("GDP per person") +
title("Bins cut in log space are evenly spaced"))data(gapminder_2007) + bar * bin + x(:gdp, scale = "log") +
x_label("GDP per person") +
title("Bins cut in log space are evenly spaced")plot(data(gapminder_2007), layer(bar, bin), x(col.gdp, { scale: "log" }),
x_label("GDP per person"),
title("Bins cut in log space are evenly spaced"))“Given gapminder 2007: bars derived by bin, x is gdp on a log scale.”
Suppose the bins had been cut in dollars and only the drawing had been logarithmic. The first bin alone would cover most of the axis, and the rest would crowd into the right edge. That reads as a mistake rather than as a modeling choice, which is why the grouping axis is scaled first.
22.2.2 The measured value keeps its own units
Put the log on the measured axis instead and a different question follows. Does a bar on a log scale stand for a sum of dollars, or for a sum of logarithms?
sum writes to y, so a log y is applied after the sum. The two bars in this plot stand for 100 and 10:
| store | sales |
|---|---|
| North | 10 |
| North | 90 |
| South | 1 |
| South | 9 |
data(receipts) + bar * sum + x(store) + y(sales, scale = "log") +
title("A sum is still a sum")(data(receipts) + bar * sum + x(col.store) + y(col.sales, scale = "log") +
title("A sum is still a sum"))data(receipts) + bar * sum + x(:store) + y(:sales, scale = "log") +
title("A sum is still a sum")plot(data(receipts), layer(bar, sum), x(col.store),
y(col.sales, { scale: "log" }), title("A sum is still a sum"))“Given the receipts: bars derived by sum, x is store, y is sales on a log scale.”
If the scale had been applied first, gog would have added the logarithms, giving the log of a product, which is not a quantity anyone asked for. gog adds the dollars first and scales the total afterwards, so sum reports a sum of dollars.
If you have used ggplot2, note the order, because ggplot2 does the opposite. Under scale_y_log10(), a stat_summary(fun = mean) averages the logarithms of the values. The result is a geometric mean, the average computed on the logarithms. gog computes the summary in your data’s units and scales the result, so the number the bar stands for is the number you would get by taking the sum or the mean yourself.
22.2.3 smooth: a curve on a log axis
The grouping axis raises the same question for smooth: is the curve fitted to dollars, or to their logarithms? On incomes that run from a few hundred dollars to nearly fifty thousand, the answer changes the shape of the curve.
smooth groups by x, so with a log x the curve is fitted against log income. Life expectancy rises by about the same amount for each tenfold rise in income, so the curve is close to a straight line:
data(gapminder_2007) + point + x(gdp, scale = "log") + y(life) +
style(color = "lightgray") +
line * smooth + x(gdp, scale = "log") + y(life) +
x_label("GDP per person") +
title("A trend fitted against log income")(data(gapminder_2007) + point + x(col.gdp, scale = "log") + y(col.life) +
style(color = "lightgray") +
line * smooth + x(col.gdp, scale = "log") + y(col.life) +
x_label("GDP per person") +
title("A trend fitted against log income"))data(gapminder_2007) + point + x(:gdp, scale = "log") + y(:life) +
style(color = "lightgray") + line * smooth + x(:gdp, scale = "log") +
y(:life) + x_label("GDP per person") +
title("A trend fitted against log income")plot(data(gapminder_2007), point, x(col.gdp, { scale: "log" }),
y(col.life), style({ color: "lightgray" }), layer(line, smooth),
x(col.gdp, { scale: "log" }), y(col.life), x_label("GDP per person"),
title("A trend fitted against log income"))“Given gapminder 2007: points, x is gdp on a log scale, y is life, and also a line derived by smooth, x is gdp on a log scale, y is life.”
22.2.4 straight: a line on a log axis
The rule at the start of this section treats straight’s two axes differently. With a log x, the least-squares line is fitted to log income, so it is drawn as a straight line. With a log y, the line would be fitted to life expectancy in years and then drawn on the log axis, where it would bend. gog refuses that sentence, because straight must draw a straight line. The least-squares line shows both: the line on a log x, and the refusal on a log y.
22.3 What a log scale refuses
Some columns cannot take a log scale, and they are not rare: a depth that goes below zero, a balance that went negative, a count of zero.
A logarithm is undefined at zero and below, so rows with such values have no position on the axis. gog says so rather than dropping them quietly:
| depth | temp |
|---|---|
| 1 | 12 |
| 0 | 14 |
| -4 | 15 |
| 100 | 9 |
data(depth_readings) + point + x(depth, scale = "log") + y(temp)data(depth_readings) + point + x(col.depth, scale = "log") + y(col.temp)data(depth_readings) + point + x(:depth, scale = "log") + y(:temp)plot(data(depth_readings), point, x(col.depth, { scale: "log" }),
y(col.temp))Error:
! gog: `x(depth, scale = "log")` has no place for 2 of 4 rows — a logarithm is undefined at zero and below, and `depth` reaches -4. Filter those rows before plotting, or use a linear scale.
gog: nothing was rendered. Fix the above, or set GOG_STRICT=0 to draw anyway.
The message names how many rows are affected and the smallest value in the column, because “some of your data cannot be drawn” is not enough to act on.
Text has no logarithm either, and a column of text already gets a categorical axis without being asked:
data(gapminder_2007) + point + x(continent, scale = "log") + y(life)data(gapminder_2007) + point + x(col.continent, scale = "log") + y(col.life)data(gapminder_2007) + point + x(:continent, scale = "log") + y(:life)plot(data(gapminder_2007), point, x(col.continent, { scale: "log" }),
y(col.life))Error:
! gog: `x(continent, scale = "log")` needs a number to take the logarithm of, but `continent` is text. A text column already gets a categorical axis — remove `scale = "log"`.
gog: nothing was rendered. Fix the above, or set GOG_STRICT=0 to draw anyway.
22.4 Choosing the base
Not every quantity is best read in powers of ten. Ten is the usual base for money and for population, but a pitch doubles with every octave, and a decay is counted in factors of e.
scale = "log" means base 10 unless you say otherwise:
x(freq, scale = "log", base = 2)The base is a number, not a name. There is no "log2" or "ln" scale. One base argument already covers every base, so a separate name for each would add words without adding meaning. The same objection rules out ggplot2’s scale_x_log10 family.
22.4.1 The base changes the labels, not the picture
You might expect a new base to move the points. It does not move a single one. Any two logarithms of the same column differ by a constant factor, and gog normalizes every axis by its own range. The factor cancels, so the points land in the same places in every base. The transforms are unaffected too: bins of equal width in base 2 are equal width in base 10, so bar * bin cuts at the same values whichever base you name.
What the base actually chooses is where the gridlines fall and how they read. Here are six frequencies, each one an octave above the one before it, on base 10 and then base 2:
| freq | level |
|---|---|
| 55 | 3 |
| 110 | 6 |
| 220 | 9 |
| 440 | 7 |
| 880 | 4 |
data(octaves) + point + x(freq, scale = "log") + y(level) +
x_label("Frequency (Hz)") + title("Base 10: gridlines at 100, 1K")(data(octaves) + point + x(col.freq, scale = "log") + y(col.level) +
x_label("Frequency (Hz)") + title("Base 10: gridlines at 100, 1K"))data(octaves) + point + x(:freq, scale = "log") + y(:level) +
x_label("Frequency (Hz)") + title("Base 10: gridlines at 100, 1K")plot(data(octaves), point, x(col.freq, { scale: "log" }), y(col.level),
x_label("Frequency (Hz)"), title("Base 10: gridlines at 100, 1K"))data(octaves) + point + x(freq, scale = "log", base = 2) + y(level) +
x_label("Frequency (Hz)") + title("Base 2: gridlines on every octave")(data(octaves) + point + x(col.freq, scale = "log", base = 2) + y(col.level) +
x_label("Frequency (Hz)") + title("Base 2: gridlines on every octave"))data(octaves) + point + x(:freq, scale = "log", base = 2) + y(:level) +
x_label("Frequency (Hz)") + title("Base 2: gridlines on every octave")plot(data(octaves), point, x(col.freq, { scale: "log", base: 2 }),
y(col.level), x_label("Frequency (Hz)"),
title("Base 2: gridlines on every octave"))“Given the octaves: points, x is freq on a log scale in base 2, y is level.”
The points have not moved. The second plot puts a gridline on each doubling, which is what “an octave” means. So for pitch, bits, or doubling times, base 2 is the one whose labels name what you are counting.
22.4.2 Natural log
The natural base is for a quantity that decays at a constant rate. The amounts in the decay table shrink by a factor of e every hour, so there is one gridline between each reading and the next. A change by a factor of e is called an e-folding.
R has no constant named e, so you write base = exp(1). The other three bindings have one: Python’s math.e, Julia’s ℯ, and JavaScript’s Math.E.
| hours | amount |
|---|---|
| 0 | 100.000000 |
| 1 | 36.787944 |
| 2 | 13.533528 |
| 3 | 4.978707 |
| 4 | 1.831564 |
data(decay) + point + x(amount, scale = "log", base = exp(1)) + y(hours) +
x_label("Amount remaining") +
title("Gridlines at each e-folding: one step is a factor of e")(data(decay) + point + x(col.amount, scale = "log", base = math.e) + y(col.hours) +
x_label("Amount remaining") +
title("Gridlines at each e-folding: one step is a factor of e"))data(decay) + point + x(:amount, scale = "log", base = ℯ) + y(:hours) +
x_label("Amount remaining") +
title("Gridlines at each e-folding: one step is a factor of e")plot(data(decay), point, x(col.amount, { scale: "log", base: Math.E }),
y(col.hours), x_label("Amount remaining"),
title("Gridlines at each e-folding: one step is a factor of e"))Notice the labels: e, e2, e3, e4, not 2.718, 7.389, 20.086, 54.598. That follows from one rule gog applies to every base:
Label the quantity when it is a short number; otherwise label the power.
Base 10 gives 1, 10, 100, 1K, 10K and so on, up to 100T. K means a thousand, M a million, B a billion (109) and T a trillion (1012). A number before a letter has at most three digits, and no letter comes after T. So from 1015 upward, a tick is labeled as a power instead, such as 1015. Base 2 gives 1, 2, 4 and so on, up to 8388608. A number with more than seven digits is labeled as a power instead, such as 224. Base e has no short quantity above 1, so every tick past the first reads as a power, which is what a reader counting e-foldings wants. If instead you want the axis to read 0, 1, 2, 3 in ln units, you do not want a log scale at all. You want log() applied to the column, and an axis labeled in log units, like the log_gdp plot earlier in this chapter. A scale keeps the reader’s units; a column of logarithms changes them.
A base only means something as the base of a logarithm, so gog refuses one that has no log scale to belong to:
data(octaves) + point + x(freq, base = 2) + y(level)data(octaves) + point + x(col.freq, base = 2) + y(col.level)data(octaves) + point + x(:freq, base = 2) + y(:level)plot(data(octaves), point, x(col.freq, { base: 2 }), y(col.level))Error:
! gog: `x(freq, base = …)` has no scale to be the base of. A base belongs to a logarithm — add `scale = "log"`, or remove the base.
gog: nothing was rendered. Fix the above, or set GOG_STRICT=0 to draw anyway.
22.5 Time: a date column gets a calendar axis
A column of dates raises a question the other columns do not. Where do the ticks go, and what do they say, when the units are years and months rather than tens and hundreds?
The time scale is one you never have to write. A column of dates or date-times (Date or POSIXct in R) is temporal, because its type already says so. gog reads the calendar from the type, the same way a column of text already gets a categorical axis:
| day | sales |
|---|---|
| 1994-06-30 | 52 |
| 1995-06-30 | 57 |
| 1996-06-30 | 55 |
| 1997-06-30 | 61 |
| 1998-06-30 | 64 |
data(revenue) + line + x(day) + y(sales) +
title("Twenty-six years, ticked in years")(data(revenue) + line + x(col.day) + y(col.sales) +
title("Twenty-six years, ticked in years"))data(revenue) + line + x(:day) + y(:sales) +
title("Twenty-six years, ticked in years")plot(data(revenue), line, x(col.day), y(col.sales),
title("Twenty-six years, ticked in years"))The sentence names no scale at all. Writing scale = "time" is allowed and adds nothing, exactly like writing scale = "linear" on a number.
The ticks land on calendar boundaries, because the calendar is not decimal. An axis of years steps 1, 2 or 5 like any number line. For units smaller than a year those steps mean nothing: nobody reads an axis cut into fifths of a year. Months step by 1, 2, 3 or 6, and each of those divides twelve. A gridline therefore always falls on January, and a 3-month step marks each quarter. Days step by 1 or 2, and weeks by 1 or 2, always on Mondays. Hours and minutes step in the units a clock uses.
| day | orders | weekday | week |
|---|---|---|---|
| 2024-03-01 | 20 | Fri | Week 1 |
| 2024-03-02 | 23 | Sat | Week 1 |
| 2024-03-03 | 25 | Sun | Week 1 |
| 2024-03-04 | 28 | Mon | Week 1 |
| 2024-03-05 | 30 | Tue | Week 1 |
# six_weeks is the cast's daily orders table, see the Preface
data(six_weeks) + line + x(day) + y(orders) +
title("Six weeks, ticked on Mondays")(data(six_weeks) + line + x(col.day) + y(col.orders) +
title("Six weeks, ticked on Mondays"))data(six_weeks) + line + x(:day) + y(:orders) +
title("Six weeks, ticked on Mondays")plot(data(six_weeks), line, x(col.day), y(col.orders),
title("Six weeks, ticked on Mondays"))A tick is labeled at its own resolution: 2010 for a year step, Jan 2024 for a month step, Mar 4 for a day step. A column of dates never gets clock ticks, however narrow its range. A column of date-times (POSIXct in R) can, because it stores a time of day:
| at | load |
|---|---|
| 2024-03-04 06:00:00 | 40 |
| 2024-03-04 07:00:00 | 47 |
| 2024-03-04 08:00:00 | 54 |
| 2024-03-04 09:00:00 | 60 |
| 2024-03-04 10:00:00 | 65 |
data(monitoring) + line + x(at) + y(load) +
title("A day and a half, ticked by the clock")(data(monitoring) + line + x(col.at) + y(col.load) +
title("A day and a half, ticked by the clock"))data(monitoring) + line + x(:at) + y(:load) +
title("A day and a half, ticked by the clock")plot(data(monitoring), line, x(col.at), y(col.load),
title("A day and a half, ticked by the clock"))00:00 happens every day, so that label names no particular day. The midnight tick shows the date instead. Every other label on a time axis follows one rule: a tick is labeled at its own resolution, and it leaves out what it shares with its neighbors.
22.5.1 Bars on a date axis
Daily orders make a natural bar chart, one bar per day, and forty-two bars raise the question of where the ticks go.
Bars positioned at dates are an ordinary time series. The ticks still come from the calendar, and there is not a tick for every bar. Wilkinson’s stock-price example puts its ticks on Sundays rather than on every trading day (Wilkinson, 2005), and gog ticks these six weeks at the start of each week:
data(six_weeks) + bar + x(day) + y(orders) +
title("Daily bars, weekly ticks")(data(six_weeks) + bar + x(col.day) + y(col.orders) +
title("Daily bars, weekly ticks"))data(six_weeks) + bar + x(:day) + y(:orders) +
title("Daily bars, weekly ticks")plot(data(six_weeks), bar, x(col.day), y(col.orders),
title("Daily bars, weekly ticks"))A bar never measures a date. A bar’s length is an amount, and a moment in time is not an amount. A bar “reaching” 2007 would be measured from 1970, the year a stored date counts from, which nobody chose as a baseline. So the bars are always placed along the dates, on whichever axis you write them. Write the dates on y and the orders on x, and the same bars are horizontal:
data(six_weeks) + bar + y(day) + x(orders) +
title("Daily bars, drawn horizontally")(data(six_weeks) + bar + y(col.day) + x(col.orders) +
title("Daily bars, drawn horizontally"))data(six_weeks) + bar + y(:day) + x(:orders) +
title("Daily bars, drawn horizontally")plot(data(six_weeks), bar, y(col.day), x(col.orders),
title("Daily bars, drawn horizontally"))With dates on one axis and a category on the other, a bar has nothing to measure. Neither column is an amount. gog refuses it, and the message asks for a column of numbers in place of the category:
data(six_weeks) + bar + x(day) + y(weekday)data(six_weeks) + bar + x(col.day) + y(col.weekday)data(six_weeks) + bar + x(:day) + y(:weekday)plot(data(six_weeks), bar, x(col.day), y(col.weekday))Error:
! gog: `bar` would measure its length along `day`, and that is a date column — a bar's length is an amount, and a moment in time is not an amount. The other axis holds no number to measure. Write a column of numbers on `y` instead, `bar + x(day) + y(<number>)`, or use `point`/`line` for values that are dates.
gog: nothing was rendered. Fix the above, or set GOG_STRICT=0 to draw anyway.
22.5.2 What a time axis refuses
A date is stored as a number underneath. You might therefore expect a date to take a log scale, and a number to take a time scale. Neither is allowed.
A moment has no logarithm. The calendar’s zero is an arbitrary convention, and a logarithm measured from an arbitrary zero measures nothing:
data(revenue) + line + x(day, scale = "log") + y(sales)data(revenue) + line + x(col.day, scale = "log") + y(col.sales)data(revenue) + line + x(:day, scale = "log") + y(:sales)plot(data(revenue), line, x(col.day, { scale: "log" }), y(col.sales))Error:
! gog: `x(day, scale = "log")` — `day` is a date column, and a moment in time has no logarithm: the calendar's zero is an arbitrary origin. Log the measured axis instead, or remove the scale.
gog: nothing was rendered. Fix the above, or set GOG_STRICT=0 to draw anyway.
gog can read the calendar off a date column, but you cannot declare a plain number temporal at the binding. The engine cannot know whether a number like 20656 counts days, seconds, or years:
data(revenue) + point + x(sales, scale = "time") + y(sales)data(revenue) + point + x(col.sales, scale = "time") + y(col.sales)data(revenue) + point + x(:sales, scale = "time") + y(:sales)plot(data(revenue), point, x(col.sales, { scale: "time" }), y(col.sales))Error:
! gog: `x(sales, scale = "time")` — `sales` is not a date column, and a number alone does not say what moment it is. Store it as dates, or as date-times, before plotting; gog reads the calendar from the column's type.
gog: nothing was rendered. Fix the above, or set GOG_STRICT=0 to draw anyway.
The fix belongs in the data, where the answer is known: convert the column to your language’s date or timestamp type, which in R is as.Date() or as.POSIXct().
For the same reason, a plain number cannot set the width of a bin on a time axis. bin(width = 7) does not say whether 7 counts days or seconds. The engine measures time in seconds, so these bins would be seven seconds wide, far too thin to see:
data(six_weeks) + bar * bin(width = 7) + x(day)data(six_weeks) + bar * bin(width = 7) + x(col.day)data(six_weeks) + bar * bin(width = 7) + x(:day)plot(data(six_weeks), layer(bar, bin({ width: 7 })), x(col.day))Error:
! gog: `bin(width = 7)` does not say what unit 7 is in, and `x(day)` holds dates. gog measures time in seconds, so these bins would be 7 seconds wide. Give the number of bins instead, such as `bin(30)`.
gog: nothing was rendered. Fix the above, or set GOG_STRICT=0 to draw anyway.
To cut a time axis into bins, give the number of bins instead: bin(6) cuts these six weeks into six bins.
Time zones are never converted. The engine draws the clock time you gave it. In R, a POSIXct is formatted in its own time zone when it is drawn, so the axis shows the times your session showed you. In JavaScript, a Date is drawn as your computer’s clock shows it, in local time. If two series must share an axis, put them in one time zone before plotting; that decision belongs to the analysis, not the engine.
22.5.3 Time on the other channels
A scale answers how far along? on every channel that measures, and a date column is no exception. Coloring rows by how recent they are is common. An axis tick can read Mar 4, because the ticks beside it place it in a sequence. A legend row is read on its own, so each row carries the full date:
data(six_weeks) + point + x(day) + y(orders) + color(day) +
title("Recent days, darker")(data(six_weeks) + point + x(col.day) + y(col.orders) + color(col.day) +
title("Recent days, darker"))data(six_weeks) + point + x(:day) + y(:orders) + color(:day) +
title("Recent days, darker")plot(data(six_weeks), point, x(col.day), y(col.orders), color(col.day),
title("Recent days, darker"))22.6 The scales
The chapter has used linear, log and time so far, and category comes later, in The column’s type picks the axis. The table names all four and what each one does.
| Scale | What it does |
|---|---|
linear |
Equal steps for equal differences. The default |
log |
Equal steps for equal ratios, on x, y, color, size and opacity, with any base above 1 |
time |
Positions dates and times, chosen from the column type: columns of dates and date-times, on every channel that measures |
category |
One slot per distinct value, chosen from the column type: columns of text, and columns with a declared order |
22.7 The axis fits your data
Where should an axis end, on the data or on the next round number past it? Padding to a round number leaves part of the panel blank, and implies that the data reaches further than it does.
An axis shows round numbers on its ticks, but the range follows your data, not the round numbers. Each tick is a round number that falls inside the data, or at most a little past it into the margin. The axis does not extend to the next multiple of ten only to end on a round number. A span of 1952–2007 is drawn as 1952–2007, with ticks at 1960, 1980 and 2000, not padded to 1940–2020 with a third of the panel left blank:
| 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 |
data(gapminder_asia) + area + x(year) + y(life) +
group(country) + style(opacity = 0.4) +
title("The year axis fits 1952–2007; the ticks fall inside it")(data(gapminder_asia) + area + x(col.year) + y(col.life) +
group(col.country) + style(opacity = 0.4) +
title("The year axis fits 1952–2007; the ticks fall inside it"))data(gapminder_asia) + area + x(:year) + y(:life) + group(:country) +
style(opacity = 0.4) +
title("The year axis fits 1952–2007; the ticks fall inside it")plot(data(gapminder_asia), area, x(col.year), y(col.life),
group(col.country), style({ opacity: 0.4 }),
title("The year axis fits 1952–2007; the ticks fall inside it"))“Given gapminder Asia: areas, x is year, y is life, grouped by country.”
Two more rules refine that fit. When the data sets an end, that end is a free end, and a free end leaves a small margin, so a glyph never touches the panel edge. A point at the exact maximum would otherwise be cut in half by that edge. The plot uses the gapminder_2007 table, shown in A scale is not a transform:
data(gapminder_2007) + point + x(gdp) + y(life) + color(continent) +
x_label("GDP per person") + title("No point touches the panel edge")(data(gapminder_2007) + point + x(col.gdp) + y(col.life) + color(col.continent) +
x_label("GDP per person") + title("No point touches the panel edge"))data(gapminder_2007) + point + x(:gdp) + y(:life) + color(:continent) +
x_label("GDP per person") + title("No point touches the panel edge")plot(data(gapminder_2007), point, x(col.gdp), y(col.life),
color(col.continent), x_label("GDP per person"),
title("No point touches the panel edge"))A baseline gets no margin. The area of Asian life expectancies above shows it: the shape starts on the panel’s bottom edge, not a little way above it. When a bar or an area measures from zero, the zero is part of what it measures. A gap beneath it would show a stretch of axis below zero that no bar can reach, so the baseline is on the panel edge. An area has no glyph for the edge to cut, so it runs from the left edge of the panel to the right. That is why the area plot above spans the full width while the scatterplot of incomes keeps its margins. An area ends exactly at its first and last values. A point’s circle reaches past its value by its radius, so it needs the margin.
A label reaches past its value too, by the width of its letters. When a label near a free end would not fit inside the panel, the axis is widened just enough to hold its letters. Oceania has the longest bar below, and its label is printed past the end of that bar. The labels are a text layer (Number labels):
data(gapminder_2007) + bar * mean + y(continent) + x(life) +
text * mean + label(life, decimals = 1) + style(nudge = "right") +
title("The life axis is widened to fit Oceania's label")(data(gapminder_2007) + bar * mean + y(col.continent) + x(col.life) +
text * mean + label(col.life, decimals = 1) + style(nudge = "right") +
title("The life axis is widened to fit Oceania's label"))data(gapminder_2007) + bar * mean + y(:continent) + x(:life) +
text * mean + label(:life, decimals = 1) + style(nudge = "right") +
title("The life axis is widened to fit Oceania's label")plot(data(gapminder_2007), layer(bar, mean), y(col.continent),
x(col.life), layer(text, mean), label(col.life, { decimals: 1 }),
style({ nudge: "right" }),
title("The life axis is widened to fit Oceania's label"))“Given gapminder 2007: bars derived by mean, y is continent, x is life, and also text derived by mean, label by life with 1 decimal place, with nudge right.”
Oceania’s 80.7 ends just inside the panel’s right edge. Only a free end is widened. A baseline stays where it is. An end you state with limits also stays, and a label past it is cut at the panel edge. A label whose letters take more than a quarter of the panel is not given room either, and is cut.
22.8 A scale is not only for axes
Most of this chapter has put a scale on an axis. A color ramp can crowd its data into one end, just as a linear axis can crowd it into one corner of the panel. x and y turn a number into a position, but so do color, size and opacity: a ramp position, a radius, a transparency. All five answer how far along?, so all five take a scale. In three dimensions z takes a scale too, but it refuses log: see Space.
This matters most for a column spread across orders of magnitude. population runs from about 200 thousand to 1.3 billion. On a linear ramp the median country is in the bottom one percent of that range, so nearly every point gets the same color:
data(gapminder_2007) + point + x(gdp, scale = "log") + y(life) +
color(population) +
x_label("GDP per person") + title("Linear color: nearly one flat color")(data(gapminder_2007) + point + x(col.gdp, scale = "log") + y(col.life) +
color(col.population) +
x_label("GDP per person") + title("Linear color: nearly one flat color"))data(gapminder_2007) + point + x(:gdp, scale = "log") + y(:life) +
color(:population) + x_label("GDP per person") +
title("Linear color: nearly one flat color")plot(data(gapminder_2007), point, x(col.gdp, { scale: "log" }),
y(col.life), color(col.population), x_label("GDP per person"),
title("Linear color: nearly one flat color"))data(gapminder_2007) + point + x(gdp, scale = "log") + y(life) +
color(population, scale = "log") +
x_label("GDP per person") + title("Log color: the whole ramp is used")(data(gapminder_2007) + point + x(col.gdp, scale = "log") + y(col.life) +
color(col.population, scale = "log") +
x_label("GDP per person") + title("Log color: the whole ramp is used"))data(gapminder_2007) + point + x(:gdp, scale = "log") + y(:life) +
color(:population, scale = "log") + x_label("GDP per person") +
title("Log color: the whole ramp is used")plot(data(gapminder_2007), point, x(col.gdp, { scale: "log" }),
y(col.life), color(col.population, { scale: "log" }),
x_label("GDP per person"), title("Log color: the whole ramp is used"))“Given gapminder 2007: points, x is gdp on a log scale, y is life, color by population on a log scale.”
The same is true of size, where a linear scale leaves almost every point at the minimum radius:
data(gapminder_2007) + point + x(gdp, scale = "log") + y(life) +
size(population, scale = "log") + style(color = "steelblue") +
x_label("GDP per person") + title("Log size")(data(gapminder_2007) + point + x(col.gdp, scale = "log") + y(col.life) +
size(col.population, scale = "log") + style(color = "steelblue") +
x_label("GDP per person") + title("Log size"))data(gapminder_2007) + point + x(:gdp, scale = "log") + y(:life) +
size(:population, scale = "log") + style(color = "steelblue") +
x_label("GDP per person") + title("Log size")plot(data(gapminder_2007), point, x(col.gdp, { scale: "log" }),
y(col.life), size(col.population, { scale: "log" }),
style({ color: "steelblue" }), x_label("GDP per person"),
title("Log size"))“Given gapminder 2007: points, x is gdp on a log scale, y is life, size by population on a log scale.”
Look again at the middle row of each of the two color legends. On a linear ramp it is the arithmetic midpoint; on a log ramp it is the geometric one: √(min · max), about 16M rather than 660M. The label has to be the geometric midpoint, because it names the color painted at the middle of the ramp.
shape, pattern and group answer which one?, not how far along?: there is no distance between a circle and a square. So gog refuses scale on all three, as shown here for shape:
data(gapminder_2007) + point + x(gdp) + y(life) +
shape(continent, scale = "log")(data(gapminder_2007) + point + x(col.gdp) + y(col.life) +
shape(col.continent, scale = "log"))data(gapminder_2007) + point + x(:gdp) + y(:life) +
shape(:continent, scale = "log")plot(data(gapminder_2007), point, x(col.gdp), y(col.life),
shape(col.continent, { scale: "log" }))Error:
! gog: `scale` on `shape(continent)`: `shape` distinguishes categories rather than measuring them, so there is no scale for it to run along. Drop `scale`.
gog: nothing was rendered. Fix the above, or set GOG_STRICT=0 to draw anyway.
22.9 limits: stating the range yourself
Every rule so far takes the range from the data. That is the right default, but it is wrong when the data does not cover every value the axis should show. The range an axis should cover is its domain, and it is not always the range the data reaches. The tide table holds a gauge’s readings, one height every three hours from hour 1 to hour 22, so none falls at midnight. polar() draws an axis as a circle, and Polar teaches that space in full. Nothing in a column of hours tells that circle that hour 24 is the same place as hour 0. limits states the domain:
| hour | height |
|---|---|
| 1 | 2.1 |
| 4 | 4.4 |
| 7 | 3.6 |
| 10 | 1.2 |
| 13 | 0.9 |
data(tide) + line + x(hour, limits = c(0, 24)) + y(height) + polar() +
title("The whole day, whatever hours the gauge recorded")(data(tide) + line + x(col.hour, limits = [0, 24]) + y(col.height) + polar() +
title("The whole day, whatever hours the gauge recorded"))data(tide) + line + x(:hour, limits = [0, 24]) + y(:height) + polar() +
title("The whole day, whatever hours the gauge recorded")plot(data(tide), line, x(col.hour, { limits: [0, 24] }), y(col.height),
polar(), title("The whole day, whatever hours the gauge recorded"))“Given the tide: a line, x is hour from 0 to 24, y is height, in polar.”
Without limits, one turn would cover only 1 to 22, and the first and last readings would be drawn on the same spoke. With limits, each hour is drawn at its own fraction of the turn: hour 6 a quarter of the way around, hour 12 halfway. The curve still does not close, and should not. The gauge took no reading at midnight, and a domain says what range the axis covers rather than inventing an observation. The gap is in the readings, and the plot shows it at its true size, the three hours from 22 to 1. Polar describes three cases: two in which the curve closes, and this one, in which it does not.
limits is written on the binding, beside scale and base, for the same reason they are: a domain belongs to one channel, and an atom of its own would name that channel twice. A stated end is drawn exactly where you state it. The margin exists to keep a glyph off the panel edge, and an end you chose is not a data extreme that needs the room.
A domain can widen the axis as easily as it narrows it. Nothing was dropped in that plot: the readings run 1 through 22 and all of them lie inside 0 through 24. Narrowing is the direction that removes rows, and gog reports how many rows it removes:
data(gapminder_2007) + point + x(gdp, limits = c(0, 30000)) + y(life) +
color(continent) + x_label("GDP per person") +
title("A domain of 0 to $30,000")(data(gapminder_2007) + point + x(col.gdp, limits = [0, 30000]) + y(col.life) +
color(col.continent) + x_label("GDP per person") +
title("A domain of 0 to $30,000"))data(gapminder_2007) + point + x(:gdp, limits = [0, 30000]) + y(:life) +
color(:continent) + x_label("GDP per person") +
title("A domain of 0 to \$30,000")plot(data(gapminder_2007), point, x(col.gdp, { limits: [0, 30000] }),
y(col.life), color(col.continent), x_label("GDP per person"),
title("A domain of 0 to $30,000"))gog: `x(gdp, limits = …)` excludes 20 of 142 rows, which fall outside [0, 30000]. Stating a domain is what removes them — widen the limits if they should be drawn.
The note above the plot is that report: it names how many rows fell outside and what the domain was. The plot still draws, because removing those rows is what you asked for, but gog does not remove data without saying so. Ask for a domain no row can satisfy and gog refuses instead, because the alternative is an empty panel with axes drawn over nothing.
Keeping every country on the page and marking one group of them is a different operation, and it has a different word. limits removes rows before the plot is drawn. brush removes nothing: it dims the rows outside its range. Selection shows the two side by side.
When a transform computes the values on an axis, as count does, a domain leaves out each bar or point whose value falls outside it. count writes its tally into a column named count, and y(count) is how the sentence gives that axis a setting (The name on a measure axis). Here the count axis stops at 40, below Africa’s count of 52:
data(gapminder_2007) + bar * count + x(continent) + y(count, limits = c(0, 40))data(gapminder_2007) + bar * count + x(col.continent) + y(col.count, limits = [0, 40])data(gapminder_2007) + bar * count + x(:continent) +
y(:count, limits = [0, 40])plot(data(gapminder_2007), layer(bar, count), x(col.continent),
y(col.count, { limits: [0, 40] }))gog: `y(count, limits = …)` leaves out 1 of 5 computed values, which fall outside [0, 40]. Stating a domain is what removes them — widen the limits if they should be drawn.
“Given gapminder 2007: bars derived by count, x is continent, y is count from 0 to 40.”
Africa keeps its label and has no bar. A bar stopped at the top edge would show 40, not 52. The note above the plot says how many values were left out, so the empty space above Africa does not mean zero.
You can state one end alone. The end you leave out stays a free end, which is how you start an axis at zero and let the top follow the data:
data(gapminder_2007) + point * mean + x(continent) + y(life, limits = c(0, NA)) +
title("y from zero, top still fitted to the data")(data(gapminder_2007) + point * mean + x(col.continent) + y(col.life, limits = [0, None]) +
title("y from zero, top still fitted to the data"))data(gapminder_2007) + point * mean + x(:continent) +
y(:life, limits = [0, missing]) +
title("y from zero, top still fitted to the data")plot(data(gapminder_2007), layer(point, mean), x(col.continent),
y(col.life, { limits: [0, null] }),
title("y from zero, top still fitted to the data"))“Given gapminder 2007: points derived by mean, x is continent, y is life from 0.”
limits reaches more than the axes. A domain is a property of the scale, so it reaches every channel that measures. That is how you keep one color ramp the same across plots built from different data:
data(gapminder_2007) + point + x(gdp, scale = "log") + y(life) +
color(population, scale = "log", limits = c(1e5, 2e9)) +
x_label("GDP per person") + title("A ramp fixed from 100K to 2B")(data(gapminder_2007) + point + x(col.gdp, scale = "log") + y(col.life) +
color(col.population, scale = "log", limits = [1e5, 2e9]) +
x_label("GDP per person") + title("A ramp fixed from 100K to 2B"))data(gapminder_2007) + point + x(:gdp, scale = "log") + y(:life) +
color(:population, scale = "log", limits = [1e+05, 2e+09]) +
x_label("GDP per person") + title("A ramp fixed from 100K to 2B")plot(data(gapminder_2007), point, x(col.gdp, { scale: "log" }),
y(col.life),
color(col.population, { scale: "log", limits: [1e+05, 2e+09] }),
x_label("GDP per person"), title("A ramp fixed from 100K to 2B"))On a categorical axis limits is refused, and the refusal names the two ways to get what you meant. A category has no range to lie inside. Choosing which categories appear is filtering your table, and choosing what order they appear in is order().
data(gapminder_2007) + bar * mean + x(continent, limits = c(0, 3)) + y(life)data(gapminder_2007) + bar * mean + x(col.continent, limits = [0, 3]) + y(col.life)data(gapminder_2007) + bar * mean + x(:continent, limits = [0, 3]) +
y(:life)plot(data(gapminder_2007), layer(bar, mean),
x(col.continent, { limits: [0, 3] }), y(col.life))Error:
! gog: `x(continent, limits = …)` — `continent` is text, and a category has no range to lie inside. To choose which categories appear, filter the table before plotting; to change the order they appear in, use `order(continent)`.
gog: nothing was rendered. Fix the above, or set GOG_STRICT=0 to draw anyway.
22.10 The column’s type picks the axis
The time section showed a column of dates choosing its own axis without being asked. A column of text does the same, and this section states the rule behind both. Other tools make you name a scale whenever you want to change one, so you may expect to write scale = "category".
The four scales are not all chosen the same way. You ask for log. You do not ask for time or category. gog reads those off the column: a column of dates gets a calendar axis, and a column of text gets one slot per distinct value.
You may still write them. As with scale = "time" on a date column, writing scale = "category" on a categorical column is allowed and changes nothing. The plot below is identical to the same sentence with the scale left off:
data(gapminder_2007) + bar * mean + x(continent, scale = "category") + y(life) +
title("Writing it changes nothing")(data(gapminder_2007) + bar * mean + x(col.continent, scale = "category") + y(col.life) +
title("Writing it changes nothing"))data(gapminder_2007) + bar * mean + x(:continent, scale = "category") +
y(:life) + title("Writing it changes nothing")plot(data(gapminder_2007), layer(bar, mean),
x(col.continent, { scale: "category" }), y(col.life),
title("Writing it changes nothing"))“Given gapminder 2007: bars derived by mean, x is continent as a category, y is life.”
What you cannot do is use a scale to contradict the column. Ask for a categorical axis on a number and gog refuses:
data(gapminder_2007) + point + x(gdp, scale = "category") + y(life)data(gapminder_2007) + point + x(col.gdp, scale = "category") + y(col.life)data(gapminder_2007) + point + x(:gdp, scale = "category") + y(:life)plot(data(gapminder_2007), point, x(col.gdp, { scale: "category" }),
y(col.life))Error:
! gog: `x(gdp, scale = "category")` — a scale says how a measured column is placed; whether an axis measures at all is the column's type. Removing the scale alone would leave the continuous axis you were trying to escape. Store `gdp` as text or categories before plotting, and drop the scale. To cut the numbers into ranges instead, use `bin`.
gog: nothing was rendered. Fix the above, or set GOG_STRICT=0 to draw anyway.
The reason behind that refusal also explains the others in this chapter. A scale says how a measured column is placed. The column’s type decides whether the axis measures at all. So log on a categorical column is refused, and time on a plain number is refused. A date column cannot be turned back into raw numbers by asking for linear either. None of those refusals offers another scale. Each tells you to change the column or remove the scale, because the column’s type is what the axis reads.
There are two things you might mean by “draw this number as categories”, and there is a different answer for each. For one slot per distinct value, declare the column’s categories: factor() in R and ordered() in the other three bindings, both shown in Data. For ranges, use bin, which cuts the numbers into intervals and is covered in Transforms.
22.11 tick_count: how many ticks the axis aims for
An axis picks about five ticks by default. After limits, the count is the second thing about an axis you can choose. A crowded axis is easier to read with fewer; an axis someone will take numbers from is easier with more. tick_count is written on the binding, beside scale, base and limits, and for the same reason. How many ticks an axis gets is a property of the scale, so it belongs to the sentence that describes the scale rather than to theme(), which controls the page.
(data(gapminder_2007) + point + x(gdp, tick_count = 3) + y(life) +
x_label("GDP per person") + title("A low count: the shape, not the numbers")) |
(data(gapminder_2007) + point + x(gdp, tick_count = 12) + y(life) +
x_label("GDP per person") + title("A high count: for reading values off"))((data(gapminder_2007) + point + x(col.gdp, tick_count = 3) + y(col.life) +
x_label("GDP per person") + title("A low count: the shape, not the numbers")) |
(data(gapminder_2007) + point + x(col.gdp, tick_count = 12) + y(col.life) +
x_label("GDP per person") + title("A high count: for reading values off")))(data(gapminder_2007) + point + x(:gdp, tick_count = 3) + y(:life) +
x_label("GDP per person") +
title("A low count: the shape, not the numbers")) |
(data(gapminder_2007) + point + x(:gdp, tick_count = 12) + y(:life) +
x_label("GDP per person") +
title("A high count: for reading values off"))beside(plot(data(gapminder_2007), point, x(col.gdp, { tick_count: 3 }),
y(col.life), x_label("GDP per person"),
title("A low count: the shape, not the numbers")),
plot(data(gapminder_2007), point, x(col.gdp, { tick_count: 12 }),
y(col.life), x_label("GDP per person"),
title("A high count: for reading values off")))“Given gapminder 2007: points, x is gdp with 3 ticks, y is life, beside the same with 12 ticks.”
The count is a target, not an exact number. The count sets the step between ticks, and the step is then rounded to 1, 2 or 5 times a power of ten. So asking for eight ticks on an axis from 0 to 100 gives a step of 10, and eleven ticks. What you can rely on is the direction: a higher count never gives fewer ticks than a lower one. Every tick is a round number. The count also has an upper limit: an axis draws at most 26 ticks. If the rounded step would give more than 26 ticks, the engine uses a larger round step. The axis is still labeled from one end to the other, and a message names both steps.
Both panels in the plot above cover the same range. That is the rule the count obeys: it changes how densely an axis is labeled, never where the axis stops. Ask for two ticks and the axis does not shrink to the two values it draws.
data(gapminder_asia) + line + x(year, tick_count = 2) + y(life) +
group(country) + style(opacity = 0.4) +
title("Two ticks, and still 1952 to 2007")(data(gapminder_asia) + line + x(col.year, tick_count = 2) + y(col.life) +
group(col.country) + style(opacity = 0.4) +
title("Two ticks, and still 1952 to 2007"))data(gapminder_asia) + line + x(:year, tick_count = 2) + y(:life) +
group(:country) + style(opacity = 0.4) +
title("Two ticks, and still 1952 to 2007")plot(data(gapminder_asia), line, x(col.year, { tick_count: 2 }),
y(col.life), group(col.country), style({ opacity: 0.4 }),
title("Two ticks, and still 1952 to 2007"))A log axis and a date axis take a count too, but their steps are rounded to different values. On a log axis the ticks can be at every power of the base, or skip one or more powers between them. In base 10 they can also be at 1, 2 and 5 times each power. A date axis uses calendar steps, such as a day, a week, a month or a year. The time section describes these steps. On these two axes, the engine picks the step whose number of ticks is closest to your count.
data(gapminder_2007) + point + x(gdp, scale = "log", tick_count = 12) + y(life) +
x_label("GDP per person") + title("Seven ticks: 1, 2 and 5 times each power")(data(gapminder_2007) + point + x(col.gdp, scale = "log", tick_count = 12) + y(col.life) +
x_label("GDP per person") + title("Seven ticks: 1, 2 and 5 times each power"))data(gapminder_2007) + point + x(:gdp, scale = "log", tick_count = 12) +
y(:life) + x_label("GDP per person") +
title("Seven ticks: 1, 2 and 5 times each power")plot(data(gapminder_2007), point,
x(col.gdp, { scale: "log", tick_count: 12 }), y(col.life),
x_label("GDP per person"),
title("Seven ticks: 1, 2 and 5 times each power"))“Given gapminder 2007: points, x is gdp on a log scale with 12 ticks, y is life.”
The sentence asks for twelve ticks, and the axis draws seven. In base 10 no log step is finer than 1, 2 and 5 times each power, so seven is the closest count to twelve. Without a count, this axis reads only 1K and 10K, as at the start of the chapter.
A date axis takes a count too. The plot uses the six_weeks table, shown in the time section:
data(six_weeks) + line + x(day, tick_count = 3) + y(orders) +
title("Three ticks: one every two weeks")(data(six_weeks) + line + x(col.day, tick_count = 3) + y(col.orders) +
title("Three ticks: one every two weeks"))data(six_weeks) + line + x(:day, tick_count = 3) + y(:orders) +
title("Three ticks: one every two weeks")plot(data(six_weeks), line, x(col.day, { tick_count: 3 }), y(col.orders),
title("Three ticks: one every two weeks"))“Given the six weeks table: a line, x is day with 3 ticks, y is orders.”
By default these six weeks get a tick every Monday, six in all. Asked for three, the axis steps two weeks at a time, still on Mondays.
A categorical axis has one place per category, so the data sets the count and you do not. The refusal names the same two fixes the limits refusal named:
data(gapminder_2007) + bar * mean + x(continent, tick_count = 3) + y(life)data(gapminder_2007) + bar * mean + x(col.continent, tick_count = 3) + y(col.life)data(gapminder_2007) + bar * mean + x(:continent, tick_count = 3) +
y(:life)plot(data(gapminder_2007), layer(bar, mean),
x(col.continent, { tick_count: 3 }), y(col.life))Error:
! gog: `x(continent, tick_count = 3)` — `continent` is text, and a categorical axis has one tick per category, so the count is the data's rather than yours. To change which categories appear, filter the table before plotting; to change their order, use `order(continent)`.
gog: nothing was rendered. Fix the above, or set GOG_STRICT=0 to draw anyway.
A legend is not an axis, and it takes no tick count. limits reaches every channel that measures, because every one of them has a domain; tick_count reaches only the three position channels that draw an axis: x, y and z. A color legend names three rows, both ends and the middle, and the scale, linear or log, sets their values, not a count you choose. The middle row is the midpoint explained above: the label for the color painted halfway along the ramp. So gog refuses a tick count on color(), size() or opacity(), the same way it refuses scale and limits on shape(): there is nothing for the count to describe.
22.11.1 Crowding in the plane and in the cube
A flat panel and a cube follow one rule for crowded numbers: no number may be printed over another. A flat continuous axis draws every tick it chose, as long as no two numbers overlap. Where panels are side by side, an end number that would reach past its panel is moved inward. In narrow panels, such as the five panels of a plot faceted by continent (Faceting), the numbers can still overlap. The axis then draws fewer numbers, and the ticks and gridlines of the others are left out too. The numbers it keeps are the ones a larger round step would choose, so 0, 10K, 20K, 30K and 40K become 0, 20K and 40K. A number left out can be read from the numbers on either side, so no message is printed. If you stated a tick_count, a message says how many ticks were drawn. A polar plot, a map, and an axis freed with free = TRUE still draw every number they chose.
The sentence below asks for twelve ticks, but each panel has room for three, and a message says so:
data(gapminder_2007) + point + x(gdp, tick_count = 12) + y(life) |
facet(continent)(data(gapminder_2007) + point + x(col.gdp, tick_count = 12) + y(col.life) |
facet(col.continent))data(gapminder_2007) + point + x(:gdp, tick_count = 12) + y(:life) |
facet(:continent)plot(data(gapminder_2007), point, x(col.gdp, { tick_count: 12 }),
y(col.life), across(col.continent))gog: `x(gdp, tick_count = 12)` asks for about 12 ticks, and the 11 ticks it chose do not fit side by side at this size, so 3 of them are drawn. A smaller `tick_count`, or a wider plot (`theme(width = )`), draws them all.
“Given gapminder 2007: points, x is gdp with 12 ticks, y is life, split into panel columns by continent.”
A cube can also have too little room for its numbers. In a 3-D plot (Space), each axis is longer or shorter depending on the viewing angle. Turning the cube lengthens one axis and shortens another. The cube then draws as many numbers as fit and leaves the rest out. When a count you stated cannot fit, the engine says so rather than quietly drawing fewer.
Names on a categorical axis follow the same rule: no name may be printed over another. The axis tries to name every category, and a name is usually much wider than a number. When names do not fit side by side, a flat axis first turns them 90 degrees, to read upward. A turned name needs room along the axis only for its height, not its width. When even turned names are too close, the flat axis leaves some out, as the cube does. It draws one name in every two or three, each with its tick, and prints a message. The 142 countries of 2007 do not fit, even turned:
data(gapminder_2007) + bar + x(country) + y(life)data(gapminder_2007) + bar + x(col.country) + y(col.life)data(gapminder_2007) + bar + x(:country) + y(:life)plot(data(gapminder_2007), bar, x(col.country), y(col.life))gog: the 142 names of `country` on the x axis do not fit side by side, even turned to read upward: they are 5.1px apart, and a name needs 8.9px. One in every 2 is drawn (71 of 142), with its tick; the rest keep their places, unnamed. To name every one, swap the two axes so `country` runs down the y axis, in a taller plot (`theme(height = 1330)`), or draw fewer rows.
Every bar is still drawn, and a bar without a name keeps its place between two named ones. The message gives two ways to name every country: put country on the y axis in a taller plot, or draw fewer rows.
22.12 percent, short and decimals: writing the numbers
An axis chooses how to write its own numbers, and so does a legend. Most of the time their choice is easy to read. When it is not, three arguments on the binding choose how the numbers are written, beside scale, limits and tick_count. percent = TRUE writes a number as a percent. short = TRUE writes a large number with K for thousands, M for millions, B for billions and T for trillions. decimals sets how many decimal places each number has. A binding without any of the three keeps the axis’s own choice.
A share is the clearest case. proportion gives each bar its share of all the rows, a number between 0 and 1, and the axis writes 0.1, 0.2 and 0.3. As a percent, the same axis writes 10%, 20% and 30%. The transform writes the y column and names it proportion, so the argument goes on y(proportion):
data(gapminder_2007) + bar * proportion + x(continent) +
y(proportion, percent = TRUE) + title("Each continent's share of the countries")(data(gapminder_2007) + bar * proportion + x(col.continent) +
y(col.proportion, percent = True) + title("Each continent's share of the countries"))data(gapminder_2007) + bar * proportion + x(:continent) +
y(:proportion, percent = true) +
title("Each continent's share of the countries")plot(data(gapminder_2007), layer(bar, proportion), x(col.continent),
y(col.proportion, { percent: true }),
title("Each continent's share of the countries"))“Given gapminder 2007: bars derived by proportion, x is continent, y is proportion written as a percent.”
Only the numbers on the axis change. Each bar is as tall as it would be without percent, and limits stays in the column’s own units: limits = c(0, 0.5) means 0 to 0.5, which the axis writes as 0% to 50%.
short matters on an axis whose numbers are large and close together. The two_readings table holds France’s rows for 2002 and 2007:
| country | continent | year | life | population | gdp |
|---|---|---|---|---|---|
| France | Europe | 2002 | 79.590 | 59925035 | 28926.03 |
| France | Europe | 2007 | 80.657 | 61083916 | 30470.02 |
The ticks are half a million apart, and the axis chooses one letter for all of them from that distance. Half a million is less than a million, so the letter is K: 60000K, 60500K and 61000K. With short = TRUE, each tick’s letter comes from the tick’s own size, so the same ticks are 60M, 60.5M and 61M:
data(two_readings) + line + point + x(year) + y(population, short = TRUE) +
title("France: about 60 million people")(data(two_readings) + line + point + x(col.year) + y(col.population, short = True) +
title("France: about 60 million people"))data(two_readings) + line + point + x(:year) +
y(:population, short = true) + title("France: about 60 million people")plot(data(two_readings), line, point, x(col.year),
y(col.population, { short: true }),
title("France: about 60 million people"))“Given the two readings: a line and also points, x is year, y is population written short.”
All three arguments work on a legend as well as on an axis. The color legend of gdp writes 49.4K, 24.8K and 277.6 by default. With decimals = 0 it writes 49K, 25K and 278:
data(gapminder_2007) + point + x(continent) + y(life) + color(gdp, decimals = 0)data(gapminder_2007) + point + x(col.continent) + y(col.life) + color(col.gdp, decimals = 0)data(gapminder_2007) + point + x(:continent) + y(:life) +
color(:gdp, decimals = 0)plot(data(gapminder_2007), point, x(col.continent), y(col.life),
color(col.gdp, { decimals: 0 }))“Given gapminder 2007: points, x is continent, y is life, color by gdp with 0 decimal places.”
On an axis, decimals keeps the letter the axis chose and changes only the number of decimal places, so an axis that writes 2M writes 2.0M with decimals = 1. If the places you ask for cannot tell two ticks apart, the axis draws fewer ticks. With decimals = 0, ticks at 0, 0.5 and 1 would write 0, 0 and 1, so the axis keeps only the whole numbers. When that would leave fewer than two ticks, gog refuses the plot and names the axis’s range, because one tick shows a place but no direction.
percent and short are two ways to write one number, so gog refuses the pair:
data(gapminder_2007) + bar * proportion + x(continent) +
y(proportion, percent = TRUE, short = TRUE)(data(gapminder_2007) + bar * proportion + x(col.continent) +
y(col.proportion, percent = True, short = True))data(gapminder_2007) + bar * proportion + x(:continent) +
y(:proportion, percent = true, short = true)plot(data(gapminder_2007), layer(bar, proportion), x(col.continent),
y(col.proportion, { percent: true, short: true }))Error:
! gog: `y(proportion)` asks for both `short` and `percent`, two ways to write one number. Keep one of them.
gog: nothing was rendered. Fix the above, or set GOG_STRICT=0 to draw anyway.
The three arguments work only where an axis or a legend writes plain numbers. A log axis writes powers of its base, a date axis writes dates, and a map writes degrees, so gog refuses them there and says why. label(), which sets the text a text mark prints, takes the same three arguments, as the text chapter shows.
On a web page, the three arguments also set how a number is written on the card that shows a point’s values, as Pointing at a mark explains. With x(gdp, short = TRUE), the card writes Germany’s gdp as 32.2K rather than 32170.37442. Without any of the three, the card writes the number as it is in your table.
22.13 axis: leaving an axis out
An axis lets a reader decode a position, as a legend lets a reader decode a color. Sometimes the reader does not need it. On a map, a reader finds a place by its shape and its neighbors, not by its longitude and latitude. Write axis = FALSE on the binding, beside scale and limits, and the axis is left out: its tick marks, its numbers and its name. The map below draws world_borders, which holds one row for each point along a coastline or a border:
| lon | lat | country | continent | piece |
|---|---|---|---|---|
| 66.519 | 37.363 | Afghanistan | Asia | p001 |
| 69.196 | 37.151 | Afghanistan | Asia | p001 |
| 70.807 | 38.486 | Afghanistan | Asia | p001 |
| 71.845 | 36.738 | Afghanistan | Asia | p001 |
| 73.260 | 37.495 | Afghanistan | Asia | p001 |
data(world_borders) + path + x(lon, axis = FALSE) + y(lat, axis = FALSE) +
group(piece) + map() + title("The coastlines, without axes")(data(world_borders) + path + x(col.lon, axis = False) + y(col.lat, axis = False) +
group(col.piece) + map() + title("The coastlines, without axes"))data(world_borders) + path + x(:lon, axis = false) +
y(:lat, axis = false) + group(:piece) + map() +
title("The coastlines, without axes")plot(data(world_borders), path, x(col.lon, { axis: false }),
y(col.lat, { axis: false }), group(col.piece), map(),
title("The coastlines, without axes"))“Given the world borders: paths, x is lon without an axis, y is lat without an axis, grouped by piece, in the map space.”
The scale is unchanged, so every point is drawn where it would be with the axes. The panel takes the room the axes would have used. The gridlines stay, and so does the frame: the two lines along the bottom and the left of the panel. Both belong to the whole plot rather than to one axis. theme() removes them with grid = "none" and frame = "none". In Map, three small maps of Ohio leave out both axes, the gridlines and the frame.
axis is written on the three positions, x, y and z, because they are the channels that draw an axis. On any other channel gog refuses it and names the three. The name is part of the axis, so gog refuses x_label() for an axis that is left out, even x_label(""):
data(gapminder_2007) + point + x(gdp, axis = FALSE) + y(life) +
x_label("GDP per person")(data(gapminder_2007) + point + x(col.gdp, axis = False) + y(col.life) +
x_label("GDP per person"))data(gapminder_2007) + point + x(:gdp, axis = false) + y(:life) +
x_label("GDP per person")plot(data(gapminder_2007), point, x(col.gdp, { axis: false }),
y(col.life), x_label("GDP per person"))Error:
! gog: `x_label()` names the x axis, and `x(gdp)` with its `axis` set to false leaves that axis out, its name included. Keep one of them: drop `x_label()`, or set `axis` to true to draw the axis with its name.
gog: nothing was rendered. Fix the above, or set GOG_STRICT=0 to draw anyway.
22.14 In short
A scale changes where a number lands on the axis. It never changes the number itself. limits states the domain: narrowing it removes rows and reports how many, while a brush removes none. tick_count is a target. A crowded continuous axis draws fewer numbers. A crowded categorical axis turns its names, then leaves some out. axis = FALSE leaves an axis out and keeps its scale. percent, short and decimals change only how the numbers are written.
22.15 What comes next
A scale decides how far along a channel a number lands. It does not decide which visual property shows the column. Channels covers that: which property carries a column, and what guide it gets.