| 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 |
One word makes the plot move
You have seen this chart. A cloud of bubbles, one per country, income along the bottom and life expectancy up the side. It plays through half a century, and the whole cloud climbs and moves right as the world gets richer and lives longer.
Hans Rosling showed it to a lecture hall in 2006 and then to about fifteen million people. It is probably the best known moving chart there is.
So how do you make one?
The table, and how to get it
Everything below reads one table: gm_all, the gapminder data, 142 countries recorded every five years from 1952 to 2007.
You do not have to download it. It is published beside gog’s manual, and every one of the four packages can fetch it by name:
gm <- gog_table("gm_all")gm = gog_table("gm_all")gm = gog_table("gm_all")const gm = await gog_table("gm_all");That is the same call in all four, and it needs no file, no reader and no argument beyond the name. There are 39 tables published this way, so every plot in this post is one you can run in the next minute.
Six columns. year is the one this post is about.
The plot, still
Start with one year and no motion. This is 2007 on its own, income against life expectancy, each bubble sized by population and colored by continent:
gm_2007 <- gm[gm$year == 2007, ]
data(gm_2007) + point + x(gdp, scale = "log") + y(life) + size(population) +
color(continent)Read it aloud: given gapminder 2007, points, x is gdp on a log scale, y is life, size by population, color by continent.
That is Rosling’s chart with the time taken out of it.
The plot, moving
Put the time back by naming the column that holds it:
data(gm) + point + x(gdp, scale = "log") + y(life) + size(population) +
color(continent) + play(year)(data(gm) + point + x(col.gdp, scale = "log") + y(col.life) + size(col.population) +
color(col.continent) + play(col.year))data(gm) + point + x(:gdp, scale = "log") + y(:life) + size(:population) +
color(:continent) + play(:year)plot(data(gm), point, x(col.gdp, { scale: "log" }), y(col.life),
size(col.population), color(col.continent), play(col.year))Twelve frames, 1952 to 2007, and the strip above the panel names the one you are looking at.
You do not have to watch it go past. A plot that plays carries three boxes a still one does not: pause in the middle, and a step to either side of it. Stepping pauses as well, so you can hold one year still and read it. Rest on any of the three and it says what it does.
Compare the two sentences. The table is now every year instead of one, and play(year) has been added at the end. Nothing else moved. No renderer was chosen, no frame rate was set, no second function wrapped the plot, and no file was written out and read back.
What play actually is
It is a channel, and that is the whole of the design.
gog has ten channels. x and y and z are positions. color, size, shape, pattern and opacity are appearances. group splits a series. And play is time. They are written the same way, they combine with everything, and none of them is special.
So the shortest true description of play is that it is faceting read in time. A facet splits the rows by a column’s values and lays the pieces across the page. play splits them exactly the same way and lays them out one after another.
Here are four years as panels:
gm_four <- gm[gm$year %in% c(1952, 1972, 1992, 2007), ]
gm_four$period <- factor(gm_four$year)
data(gm_four) + point + x(gdp, scale = "log") + y(life) + color(continent) |
facet(period)And the same four years as frames:
data(gm_four) + point + x(gdp, scale = "log") + y(life) + color(continent) +
play(period)(data(gm_four) + point + x(col.gdp, scale = "log") + y(col.life) + color(col.continent) +
play(col.period))data(gm_four) + point + x(:gdp, scale = "log") + y(:life) +
color(:continent) + play(:period)plot(data(gm_four), point, x(col.gdp, { scale: "log" }), y(col.life),
color(col.continent), play(col.period))Identical rows, one operator different. The grid is better for comparing two moments carefully, because both are in front of you and your eye can go back and forth. The sequence is better for seeing movement, because a bubble that shifts left is one you watch move rather than one you have to find twice.
Neither is a different plot. They are one plot read two ways.
Why the frames can be trusted
Every scale, the color map and every legend are fitted across the whole sequence at once, never per frame.
That sounds like an implementation detail. It is the property that makes the whole thing worth watching, and the easiest way to see why is to imagine its absence. If each frame fitted its own axis, the poorest countries of 1952 and the poorest of 2007 would both sit against the left edge. A world whose incomes tripled would draw as a world standing still. The only thing actually moving would be the axis, silently, with nothing on the page to say so.
Because the frames share their scales, the opposite reads correctly. The cloud genuinely moves right and up, and a country that does not move is a country that did not.
The colors are shared for a second reason. You follow a bubble by its color, so a palette that changed partway through would take away the one thing you are following. Every continent keeps its color in every frame, including frames where it has no rows at all.
The same word on other plots
play is a channel, so it goes wherever a channel goes. Nothing below is a new feature, and none of it needed a line of code added to the library.
A bar chart counting the countries that had passed sixty years of life expectancy:
gm_past60 <- gm[gm$life > 60, ]
data(gm_past60) + bar * count + x(continent) + play(year)Asia climbs from four to thirty. Africa opens with none and ends with twelve. Europe starts at twenty-three and then runs out of countries to add.
A histogram of life expectancy, walking right across half a century:
data(gm) + bar * bin + x(life) + play(year)data(gm) + bar * bin + x(col.life) + play(col.year)data(gm) + bar * bin + x(:life) + play(:year)plot(data(gm), layer(bar, bin), x(col.life), play(col.year))The bars keep one width through the whole sequence, which is the only way that walk is legible. One set of intervals is cut from every frame’s rows at once, and each frame then counts only its own rows into them.
And a cube, which you can turn while it plays:
data(gm) + point + x(gdp, scale = "log") + y(life) + z(population) +
color(continent) + play(year)(data(gm) + point + x(col.gdp, scale = "log") + y(col.life) + z(col.population) +
color(col.continent) + play(col.year))data(gm) + point + x(:gdp, scale = "log") + y(:life) + z(:population) +
color(:continent) + play(:year)plot(data(gm), point, x(col.gdp, { scale: "log" }), y(col.life),
z(col.population), color(col.continent), play(col.year))Drag it to another angle. The angle survives the frames, so you can find a view and then watch the years pass from it.
Four plots, four kinds of picture, and play(year) written the same way in all of them. That is what a channel buys. The word you learn on a scatterplot is the word that works on the histogram and in the cube, and there is no second thing to read on the way.
When a plot should not move
This is the part a feature tour leaves out, and it is the more useful half.
Animation is the choice most people want to make, and it is often the wrong one. A facet costs a reader one glance. A sequence asks them to watch, and keeps them for as long as the loop runs. If it then says nothing, that time was spent for nothing, and a reader can usually tell.
A short sequence is worth watching only when its frames hold the same things in different states. Then you can follow one of them across the frames. The four years above are that case: the same countries at four dates, and the cloud climbs. Four frames holding four different sets of things are not that case. Nothing moves between them, and the reader waits to be shown what a grid would have shown at once.
There is also a column that looks playable and is not. A facet lays its panels out in space, and you read space in any order you like. A sequence lays its pieces out in time, and time arrives in one order only. So a sequence claims something a grid never claims: this frame comes after that one.
Bind a column with no order, and the plot shows movement the data does not have:
data(gm_2007) + point + x(gdp, scale = "log") + y(life) + color(continent) +
play(continent)(data(gm_2007) + point + x(col.gdp, scale = "log") + y(col.life) + color(col.continent) +
play(col.continent))data(gm_2007) + point + x(:gdp, scale = "log") + y(:life) +
color(:continent) + play(:continent)plot(data(gm_2007), point, x(col.gdp, { scale: "log" }), y(col.life),
color(col.continent), play(col.continent))gog: `play(continent)` has no stated order, so each frame is a snapshot of one `continent` and the frames run in the order the rows arrive: Asia, Europe, Africa, Americas, Oceania. A sequence claims that one frame comes after another. Set the column's factor levels where the data lives, or use `facet(continent)`, which claims no order.
Watch the strip. Asia, Europe, Africa, Americas, Oceania. That order is not alphabetical and it is not geographic. It is the order the continents first appear in the table, because the rows are sorted by country and Afghanistan comes before Albania. The sequence is showing you how a file was sorted, and none of the jumps mean anything.
gog says so above the plot, and then draws it anyway. Nothing about the sentence is malformed, so the sequence is yours to keep if you meant it. The message names the order it had to choose and the two answers to it: declare the column’s levels, or facet instead.
That is the rule the whole project runs on. A plot is refused for not being well formed, never for being a poor idea. Which picture says more about your data is your judgment, and the engine’s job is to tell you what it had to assume.
Taking the sequence with you
A page can play. A slide deck and a journal cannot, so the same sentence writes a file:
save_gif(data(gm) + point + x(gdp, scale = "log") + y(life) +
size(population) + color(continent) + play(year),
"rosling.gif")Same sentence, wrapped in one call, and the frames it wrote are the frames you watched.
What this is not
gog is version 0.1.0, and that number is accurate. It is young. There are things mature libraries draw that it cannot, and the manual keeps a dated list of exactly what those are, in both directions.
In R, the established answer to this problem is gganimate, and it is good work. It gives ggplot2 a family of transitions, easing between frames, and shadows that leave a trail behind a moving point. gog has none of those yet. What it has instead is one arrangement. The word for time sits in the same list as the word for color, so there is no second package and no second vocabulary.
What you can do
Run the plots above
Every one reads gog_table("gm_all"), so there is nothing to download. Three of the four packages carry the drawing engine inside them, built for your platform.
# R
install.packages("gog", repos = c("https://psychometrician.r-universe.dev",
"https://cloud.r-project.org"))# Python
pip install gog# Julia
using Pkg; Pkg.add("GrammarOfGraphics")# JavaScript
npm install grammar-of-graphicsRead the chapter
The manual has 58 chapters, and every plot in them is drawn by the engine as the page builds. The Play chapter covers speed, a layer that stands still while another moves, and the sentences play refuses.
Tell us where it is wrong
Draw data you know well here, and draw the same thing in a tool you already trust. Put the two pictures side by side and say where they differ. That is the most useful thing anyone can do for this project, because the checks compare the four packages against each other and cannot prove that all four are right.
Issues are open. The source is on GitHub, Apache 2.0.
Be agog. Use gog.