| year | sales |
|---|---|
| 2019 | 120 |
| 2020 | 135 |
| 2021 | 128 |
| 2022 | 152 |
| 2023 | 168 |
43 Over time
How did a measurement change over time? Time is not a chart type in this grammar. It is a column like any other, and you decide where it goes. Most recipes here put it on x, which gives the familiar time series. One gives it to play instead, to show where earthquakes happened, one week at a time. The last section asks for one panel per year, which gog refuses.
43.1 How did it change?
The actuals table holds one sales figure per year. The question is whether sales rose or fell. A line with year on x answers it:
data(actuals) + line + x(year) + y(sales)data(actuals) + line + x(col.year) + y(col.sales)data(actuals) + line + x(:year) + y(:sales)plot(data(actuals), line, x(col.year), y(col.sales))“Given the actuals: a line, x is year, y is sales.”
Sales rose over the five years, from 120 to 168, and fell only once, in 2021. A line sorts the rows by x and joins each row’s point to the next. With years on x, the stroke runs from the earliest year to the latest. Owning chapter: Line.
One atom away. If the question is how large sales were in each year, not whether they rose, swap line for area. An area fills from zero up to each value, so the height of the fill is the amount itself. Two variables draws that plot on this table.
43.2 How did several things change?
The recipe above needed only one line, because actuals holds one row per year. The gapminder_asia table holds five countries, each with one row for the same twelve years, 1952 to 2007. One line reads every row, so without color the five countries join into a single stroke. A categorical color does two jobs: it splits the rows into one line per category, and it gives each line its own color:
| 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) + line + x(year) + y(life) + color(country)data(gapminder_asia) + line + x(col.year) + y(col.life) + color(col.country)data(gapminder_asia) + line + x(:year) + y(:life) + color(:country)plot(data(gapminder_asia), line, x(col.year), y(col.life),
color(col.country))“Given gapminder Asia: lines, x is year, y is life, color by country.”
Five countries, five lines, and a legend that names them. Life expectancy rose in all five countries from 1952 to 2007. Only China’s line ever falls, from 50.5 years in 1957 to 44.5 in 1962. Owning chapters: Line, Channels.
One atom away. When the lines must be kept apart but their names do not matter, swap color for group. It splits the rows in the same way, draws every line in one color, and adds no legend. Many groups draws that plot.
43.3 How did many things change?
The recipe above drew five countries, and each of the five lines can be followed on its own. Thirty-three lines cannot: the lines cross each other, and no reader can match 33 legend colors to 33 lines. The gm_asia table holds the 33 countries of Asia in the gapminder data, each with one row for the same twelve years. In the gapminder data, Asia includes Western Asia, so Iraq, Israel and Saudi Arabia are among the 33. A heatmap gives each country a row and each year a column, and colors each cell by life expectancy:
| country | continent | year | life | population | gdp |
|---|---|---|---|---|---|
| Afghanistan | Asia | 1952 | 28.801 | 8425333 | 779.4453 |
| Afghanistan | Asia | 1957 | 30.332 | 9240934 | 820.8530 |
| Afghanistan | Asia | 1962 | 31.997 | 10267083 | 853.1007 |
| Afghanistan | Asia | 1967 | 34.020 | 11537966 | 836.1971 |
| Afghanistan | Asia | 1972 | 36.088 | 13079460 | 739.9811 |
data(gm_asia) + zone * bin(width = 5) * mean + x(year) + y(country) +
color(life) + order(life, desc = TRUE) + palette("cividis") +
style(border_color = "white") + theme(legend = "top") +
title("Life expectancy in Asia, 1952 to 2007")(data(gm_asia) + zone * bin(width = 5) * mean + x(col.year) + y(col.country) +
color(col.life) + order(col.life, desc = True) + palette("cividis") +
style(border_color = "white") + theme(legend = "top") +
title("Life expectancy in Asia, 1952 to 2007"))data(gm_asia) + zone * bin(width = 5) * mean + x(:year) + y(:country) +
color(:life) + order(:life, desc = true) + palette("cividis") +
style(border_color = "white") + theme(legend = "top") +
title("Life expectancy in Asia, 1952 to 2007")plot(data(gm_asia), layer(zone, bin({ width: 5 }), mean), x(col.year),
y(col.country), color(col.life), order(col.life, { desc: true }),
palette("cividis"), style({ border_color: "white" }),
theme({ legend: "top" }),
title("Life expectancy in Asia, 1952 to 2007"))“Given the gm Asia table: zones derived by bin at width 5 and mean, x is year, y is country, color by life, ordered by life, largest first, with the cividis palette.”
A zone needs sides, and a year is a number, which has no width. bin(width = 5) gives the years their sides. It cuts the axis into cells five years wide, from 1950 to 2010, and each cell holds one of the table’s twelve years. mean then colors each cell by life. A cell holds one row, so its mean is that row’s own value. Leave out either atom and gog refuses the sentence, as Zone shows.
order(life, desc = TRUE) sorts the countries by their mean life expectancy over all twelve years, with the longest lives at the top. palette("cividis") runs from dark blue for short lives to yellow for long ones. style(border_color = "white") draws a thin line around each cell, and theme(legend = "top") puts the legend above the panel.
Read across a row for one country’s change, and down a column to compare the countries in one year. Sorted this way, the colors run from dark at the bottom left (Afghanistan in 1952) to light at the top right (Japan in 2007). Life expectancy was higher in 2007 than in 1952 in every country, and Oman gained the most, from 38 years to 76. Life expectancy fell by more than five years from one column to the next in only three countries. Each fall is a cell much darker than the cell to its left. In China, it fell from 50.5 years in 1957 to 44.5 in 1962. In Cambodia, it fell twice: from 45.4 years in 1967 to 40.3 in 1972, then to 31.2 in 1977. In Iraq, it fell from 65.0 years in 1987 to 59.5 in 1992, and it was still below 65 in 2007. Owning chapters: Zone, Channels.
One atom away. Leave out order(), and the countries keep the table’s order, which is alphabetical. Each cell keeps its color, but dark rows and light rows are now mixed together. The pattern from dark at the bottom left to light at the top right is gone.
43.4 What about a date column?
The recipes above put a plain number on x. The six_weeks table holds one row for each of 42 days, with the date, the number of orders, the weekday and the week. Its day column holds calendar dates rather than plain numbers, so you get a calendar axis without asking for one. Each tick falls at the start of a day, a week, a month or a year, and a week starts on a Monday:
| 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 |
data(six_weeks) + line + x(day) + y(orders)data(six_weeks) + line + x(col.day) + y(col.orders)data(six_weeks) + line + x(:day) + y(:orders)plot(data(six_weeks), line, x(col.day), y(col.orders))Orders climb from 20 to 33 in the first week, fall to 13 on March 22 and 23, and end at 34 on April 11. The table starts on a Friday, March 1, so the first tick is on the first Monday, March 4. After that there is a tick every Monday.
You never have to write scale = "time", because the column’s type already says that it holds dates. A longer span of dates gets a longer step between ticks: days, then weeks, then months, then years. A column of date-times also stores the time of day, so over a span of hours its ticks are clock times, such as 06:00. The midnight tick shows its date instead of 00:00. Scales owns the calendar’s rules.
43.5 What happened, and what comes next?
The sales line in the first recipe stops at 2023, and the forecast table holds the three years after it. Both tables are drawn on one plot, and a different mark for each shows which values were measured and which are forecast. The first data() names the table that every mark reads, unless a mark is given another. A later data() gives its table to the one mark written directly after it:
| year | sales |
|---|---|
| 2024 | 180 |
| 2025 | 195 |
| 2026 | 210 |
data(actuals) + x(year) + y(sales) +
line +
data(forecast) + point(data(actuals) + x(col.year) + y(col.sales) +
line +
data(forecast) + point)data(actuals) + x(:year) + y(:sales) + line + data(forecast) + pointplot(data(actuals), x(col.year), y(col.sales), line, data(forecast),
point)“Given the actuals: x is year, y is sales, a line; then given the forecast: points.”
The line stops at 2023 and the points start at 2024, because the line reads only actuals and the points read only forecast. x and y are written once, before both marks, so both marks use them. That works because forecast has the same two columns, year and sales. Data owns plots that read more than one table.
43.6 Where did it happen, week by week?
The recipes above follow one measurement along x. Some questions are about places instead: where did something happen, and when? The quakes_2011 table holds every earthquake of magnitude 5 or more that the US Geological Survey recorded in 2011. That was the year of the magnitude 9.1 earthquake off the coast of Tohoku, in Japan, on March 11.
The table is shaped for a sequence of weekly frames. week holds the first day of each seven-day week, counted from January 1, so every week here starts on a Saturday. A frame draws only its own rows, so an earthquake with a single row would appear for one week and then vanish. Each earthquake therefore has four rows. The first is in the week it struck, with age “this week”. The other three repeat it in each of the next three weeks, with age “1 week ago”, “2 weeks ago” and “3 weeks ago”. The land comes from world_borders, drawn as in the Globe chapter:
| week | lon | lat | magnitude | age |
|---|---|---|---|---|
| 2011-01-01 | 143.17 | 27.25 | 5.00 | this week |
| 2011-01-01 | -63.14 | -26.80 | 7.00 | this week |
| 2011-01-01 | -63.22 | -26.85 | 5.78 | this week |
| 2011-01-01 | 121.56 | -49.16 | 5.00 | this week |
| 2011-01-01 | 97.92 | 24.65 | 5.00 | this week |
| 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) + zone + x(lon) + y(lat) + group(country) +
style(color = "#1c2636") +
data(quakes_2011) + point + x(lon) + y(lat) + size(magnitude) + color(age) +
palette(c("gold", "darkorange", "orangered", "darkred")) + play(week, speed = 3) +
globe(turn = 150, tilt = 15) + theme(background = "#050a14", grid = "none")(data(world_borders) + zone + x(col.lon) + y(col.lat) + group(col.country) +
style(color = "#1c2636") +
data(quakes_2011) + point + x(col.lon) + y(col.lat) + size(col.magnitude) + color(col.age) +
palette(["gold", "darkorange", "orangered", "darkred"]) + play(col.week, speed = 3) +
globe(turn = 150, tilt = 15) + theme(background = "#050a14", grid = "none"))data(world_borders) + zone + x(:lon) + y(:lat) + group(:country) +
style(color = "#1c2636") + data(quakes_2011) + point + x(:lon) +
y(:lat) + size(:magnitude) + color(:age) +
palette(["gold", "darkorange", "orangered", "darkred"]) +
play(:week, speed = 3) + globe(turn = 150, tilt = 15) +
theme(background = "#050a14", grid = "none")plot(data(world_borders), zone, x(col.lon), y(col.lat),
group(col.country), style({ color: "#1c2636" }), data(quakes_2011),
point, x(col.lon), y(col.lat), size(col.magnitude), color(col.age),
palette(["gold", "darkorange", "orangered", "darkred"]),
play(col.week, { speed: 3 }), globe({ turn: 150, tilt: 15 }),
theme({ background: "#050a14", grid: "none" }))“Given the world borders: zones, x is lon, y is lat, grouped by country; then given quakes 2011: points, x is lon, y is lat, sized by magnitude, colored by age, with a palette of gold, darkorange, orangered and darkred, played by week at speed 3, on the globe.”
Fifty-three frames: 52 full weeks, and a last one that holds only December 31. A typical week adds about 40 new earthquakes, most of them along the edges of the Pacific. The frame for the week of March 5 adds 335, and 303 of them lie off the coast of Japan. Twenty-six of them came in the two days before the magnitude 9.1 earthquake. The rest are the magnitude 9.1 earthquake itself and 276 aftershocks, the smaller earthquakes that follow a large one, in the 18 hours that remained in the week. Over the next three frames that cluster turns from gold to dark red. Meanwhile new aftershocks keep arriving off Japan: 201 in the following week, then 52, then 23.
The palette runs from gold to dark red in the declared order of age, so an earthquake starts bright and darkens as it ages. The four rows were in the table before gog read it, because play cannot keep a row visible for several frames by itself. The same preparation works for any event that should stay visible for several frames. Repeat its row once for each of those frames, and add a column that says how old each copy is. Owning chapters: Play, Globe.
One atom away. Swap globe(turn = 150, tilt = 15) for map(), and the whole earth shows at once, flattened. The earthquakes on the far side of this globe, in the Americas and along the middle of the Atlantic, then come into view. Map owns the flat version.
43.7 What this section refuses
The recipes so far put time on x or gave it to play. Suppose instead you want one scatter of GDP per person and life expectancy for each year, with the years side by side. One panel per year is what faceting does. A facet column names its panels, so it must be categorical, but year holds numbers. gog refuses the sentence, and the message names two fixes:
data(gapminder_asia) + point + x(gdp) + y(life) | facet(year)data(gapminder_asia) + point + x(col.gdp) + y(col.life) | facet(col.year)data(gapminder_asia) + point + x(:gdp) + y(:life) | facet(:year)plot(data(gapminder_asia), point, x(col.gdp), y(col.life),
across(col.year))Error:
! gog: `facet(year)` splits on a number column, but a facet variable names the panels, so it must be a category column. Store `year` as text or categories before plotting, or cut it into named groups first.
gog: nothing was rendered. Fix the above, or set GOG_STRICT=0 to draw anyway.
The first fix turns year into text. The gm_eras table already holds each year as text, in the column era. Many groups draws a plot split by era, with one row of panels for each of the two years.