| 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 |
38 Play
How did the whole picture change from year to year? play cuts the plot into frames and shows them in sequence. The shortest way to say what play does is that it is faceting read in time. A facet splits the rows by a column’s distinct values and lays the pieces out across the page; play splits them exactly the same way and lays them out in time, one after another.
data(gm_all) + point + x(gdp, scale = "log") + y(life) + color(continent) +
play(year)(data(gm_all) + point + x(col.gdp, scale = "log") + y(col.life) + color(col.continent) +
play(col.year))data(gm_all) + point + x(:gdp, scale = "log") + y(:life) +
color(:continent) + play(:year)plot(data(gm_all), point, x(col.gdp, { scale: "log" }), y(col.life),
color(col.continent), play(col.year))“Given all the gapminder years: points, x is gdp on a log scale, y is life, color by continent, played by year.”
Twelve frames, one per year in the data, running from 1952 to 2007. The strip above the panel names the one you are looking at.
38.1 The same sentence, in space
A sequence is not the only way to show the years. The choice is easier to make with the same rows drawn as panels. Everything play does, facet does too, and swapping one operator for the other is the whole difference:
gm_four <- gm_all[gm_all$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)“Given four of the gapminder years: points, x is gdp on a log scale, y is life, color by continent, split into panel columns by period.”
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))Four panels, then four frames, over identical rows. 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 point that shifts left is a point you watch move rather than one you have to find twice. Neither is a different plot; they are one plot read two ways, which is why they are one grammar with one column bound differently.
On paper only the first frame is printed: in the PDF this page shows 1952 with its strip, while the web edition plays. That is why the faceted version is here beside it: the grid is what a reader holding the printed book sees instead of the motion.
38.2 The column’s order
play and facet split the rows the same way, so you might expect the same columns to suit both. The same columns do not suit both. The test is not the column’s type. It is whether the column has an order.
A facet lays its panels out in space, and you read space in any order you like. Your eye goes to any panel you choose, then comes back. 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:
| 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 |
data(gapminder_2007) + point + x(gdp, scale = "log") + y(life) +
color(continent) + play(continent)(data(gapminder_2007) + point + x(col.gdp, scale = "log") + y(col.life) +
color(col.continent) + play(col.continent))data(gapminder_2007) + point + x(:gdp, scale = "log") + y(:life) +
color(:continent) + play(:continent)plot(data(gapminder_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. The points jump between frames, and none of the jumps mean anything.
gog says so above the plot, and it 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. Give the column its levels and the frames follow them. Bind facet(continent) instead and the panels claim no order at all.
This is the rainbow-ramp mistake (Channels) in reverse. There the data had an order the hues could not carry; here the frames carry an order the data does not have, so a reader looks for a pattern that was never there. Written as | facet(continent), the five groups sit side by side and claim nothing about sequence.
Ordered does not mean numeric. A year is ordered, and so is a date. So is a text column whose levels you declared: months in calendar order, or a rating from low to high. gog runs the frames in the order the levels declare, the same way it runs an axis (Data). A categorical column with no declared levels takes the order its values first appear in, which is the trap above.
How many values there are is a separate question, and the next section answers it. Twelve frames and a hundred and seventy frames are both worth animating, as long as each one says something new after the one before it. A long sequence costs the reader time, not meaning, and speed is there for that.
38.3 Too many panels for one page
Whether a column has an order decides whether it can be played. How many values it has decides something else: whether a facet of it is still readable. A facet has to fit every piece on one page. A sequence does not.
Nothing limits how many pieces a sequence shows, because it shows them one at a time.
Having an order is not on its own a reason to play a column. A short sequence should move only when its frames hold the same things in different states. Then a reader can follow one of them across the frames. The four frames 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. The reader waits, and then has to remember what a facet would have shown at once. This is worth saying plainly, because animation is the choice most people want to make.
The limit is the point where panels stop being readable. They share one page, so every panel added makes every panel smaller. wrap folds a long line into a rectangle, which makes each panel wider and shorter (Faceting). The rectangle is still one page.
Here are the two gliders from the cube, cut into one panel per second instead:
thermal_moments <- thermals
thermal_moments$moment <- factor(thermal_moments$second)
data(thermal_moments) + point + x(east) + y(north) + color(glider) |
facet(moment, wrap = 13)“Given the thermal moments: points, x is east, y is north, color by glider, split into panel columns by moment, wrapped at 13.”
170 panels, already folded into a rectangle. The shapes are still there: two dots in every cell, one for each glider. Their positions are not readable. The tick labels have printed over one another. A dot cannot be read against them, and its position is all this plot measures. A reader can see that something changes, and cannot see what.
The same rows advanced in time instead, one frame per second of flight (speed is explained under How fast, below):
| east | north | altitude | glider | second |
|---|---|---|---|---|
| 330.0000 | 0.00000 | 900.0000 | Alpha | 0 |
| 327.5939 | 39.77710 | 907.0082 | Alpha | 2 |
| 320.4108 | 78.97417 | 914.0163 | Alpha | 4 |
| 308.5554 | 117.01961 | 921.0245 | Alpha | 6 |
| 292.2005 | 153.35865 | 928.0327 | Alpha | 8 |
data(thermals) + point + x(east) + y(north) + color(glider) +
play(second, speed = 3)(data(thermals) + point + x(col.east) + y(col.north) + color(col.glider) +
play(col.second, speed = 3))data(thermals) + point + x(:east) + y(:north) + color(:glider) +
play(:second, speed = 3)plot(data(thermals), point, x(col.east), y(col.north), color(col.glider),
play(col.second, { speed: 3 }))gog: `play(second)` cuts 170 frames, so the animation loops every 45 seconds. Run it faster with `play(second, speed = 6)`, or bind a coarser column.
“Given the thermals: points, x is east, y is north, color by glider, played by second at speed 3.”
One panel at full size, one pair of axes, and the two gliders circling through all 170 frames.
gog drew both of those sentences. It refuses neither, and it never will. A plot is refused for not being well formed, never for being a poor idea (Design laws). Which of the two says more about gliders is your judgment, not the engine’s. What the engine does instead is name the one cost it can measure. The message above counts the frames and reports how long one loop takes. It offers two answers: speed, or a column with fewer values.
That judgment is worth making before anyone else sees the plot. A facet that was the wrong choice costs a reader one glance. A sequence asks a reader 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.
The choice is easiest to see with a large number. Imagine a year of hourly temperatures for New York, drawn as one curve for each day. As a facet that is 365 panels, and no fold leaves a curve large enough to read. As a sequence it is 365 frames, and a reader watches a year of weather pass. Below about ten pieces the panels usually read better, and Faceting shows ten of them wrapped and readable. Well above that the panels stop being readable, as the 170 above were, and a sequence is the picture that still works.
Holding more does not make a sequence the better picture. What it loses is what a facet does best: two pieces side by side, where a reader compares them instead of remembering them.
38.5 A layer with no frames
play is a channel, so it obeys the ordinary scope rule: written before any mark it reaches every layer, and written after one it binds that layer alone. A layer that never names it is drawn whole in every frame, which is how a fixed reference sits behind something that moves.
| life | band |
|---|---|
| 60 | Low |
| 70 | Middle |
| 80 | High |
data(gm_all) + point + x(gdp, scale = "log") + y(life) + play(year) +
data(life_bands) + rule + y(life) + style(color = "#c0392b")(data(gm_all) + point + x(col.gdp, scale = "log") + y(col.life) + play(col.year) +
data(life_bands) + rule + y(col.life) + style(color = "#c0392b"))data(gm_all) + point + x(:gdp, scale = "log") + y(:life) + play(:year) +
data(life_bands) + rule + y(:life) + style(color = "#c0392b")plot(data(gm_all), point, x(col.gdp, { scale: "log" }), y(col.life),
play(col.year), data(life_bands), rule, y(col.life),
style({ color: "#c0392b" }))“Given all the gapminder years: points, x is gdp on a log scale, y is life, played by year; then given the life bands: rules, y is life.”
The three lines hold still while the cloud climbs past them. Nothing was added to the grammar to allow that: the reference layer simply never mentioned year, and a layer with no play binding has no frames to be cut into.
The same scope rule works in the cube and across panels together. thermal_marks holds one row per glider per instant, saying where that glider was at that moment:
| east | north | altitude | glider | instant |
|---|---|---|---|---|
| 330.0000 | 0.0000 | 900.0000 | Alpha | 0 |
| 117.0196 | 308.5554 | 970.0817 | Alpha | 20 |
| -247.0085 | 218.8305 | 1040.1634 | Alpha | 40 |
| -292.2005 | -153.3586 | 1110.2450 | Alpha | 60 |
| 39.7771 | -327.5939 | 1180.3267 | Alpha | 80 |
The route is the whole flight, drawn once; the marker is where the glider actually is:
data(thermals) + path + x(east) + y(north) + z(altitude) + color(glider) +
data(thermal_marks) + point + play(instant, speed = 3) | facet(glider)(data(thermals) + path + x(col.east) + y(col.north) + z(col.altitude) + color(col.glider) +
data(thermal_marks) + point + play(col.instant, speed = 3) | facet(col.glider))data(thermals) + path + x(:east) + y(:north) + z(:altitude) +
color(:glider) + data(thermal_marks) + point +
play(:instant, speed = 3) | facet(:glider)plot(data(thermals), path, x(col.east), y(col.north), z(col.altitude),
color(col.glider), data(thermal_marks), point,
play(col.instant, { speed: 3 }), across(col.glider))“Given the thermals: paths, x is east, y is north, z is altitude, color by glider; then given the thermal marks: points, played by instant at speed 3, split into panel columns by glider.”
play is bound to the second layer because it is written after point, and a channel written after a mark reaches that mark alone. So the coil stands still in both panels while a dot climbs it, and you can see the glider with the tighter coil climbing faster than the wider one, which is the whole reason the two tables are drawn together.
One detail of the second table is worth knowing, because it decides how many frames the plot has rather than what it means. thermal_marks calls its time column instant where thermals calls the same measurement second. The frames are gathered from every table that carries the played column, not only from the layer that bound play, on the same rule that gives the plot one scale rather than one per layer. Had the marker also called its column second, the backdrop’s 170 samples would have set the frame count and the sequence would have run 170 frames instead of 17.
38.6 How fast
A slow loop makes a reader wait for a pattern they have already seen. speed is a multiple of the normal pace, written on the binding beside scale and limits, because it belongs to play and to nothing else:
data(gm_all) + point + x(gdp, scale = "log") + y(life) + color(continent) +
play(year, speed = 3)(data(gm_all) + point + x(col.gdp, scale = "log") + y(col.life) + color(col.continent) +
play(col.year, speed = 3))data(gm_all) + point + x(:gdp, scale = "log") + y(:life) +
color(:continent) + play(:year, speed = 3)plot(data(gm_all), point, x(col.gdp, { scale: "log" }), y(col.life),
color(col.continent), play(col.year, { speed: 3 }))These are the same twelve frames the chapter opened with, running three times as fast. speed = 0.5 would run them at half pace. Long sequences are the reason it exists. Past about thirty frames, gog reports how many seconds the loop will take. The length of a loop is a default you did not choose and cannot see.
38.7 The column that cannot do both jobs
One sentence is worth knowing, because it looks so reasonable. Bind the same column to x and to play, on a mark that reads a function along x, and every frame then holds a single position:
| 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) +
play(year, speed = 3)(data(gapminder_asia) + line + x(col.year) + y(col.life) + color(col.country) +
play(col.year, speed = 3))data(gapminder_asia) + line + x(:year) + y(:life) + color(:country) +
play(:year, speed = 3)plot(data(gapminder_asia), line, x(col.year), y(col.life),
color(col.country), play(col.year, { speed: 3 }))Error:
! gog: `line` reads a function along `x`, but `year` both cuts the plot into frames and supplies `x` — so every frame holds a single `year`, and a line needs at least two positions to read between. Animate a different column — `play` is what the frames advance through, and `x(year)` is what the line draws along. Or use `point`, which places each row on its own and so needs no domain.
gog: nothing was rendered. Fix the above, or set GOG_STRICT=0 to draw anyway.
A line needs at least two positions to read between. If each frame is one year, there is no domain left in it, and what you get is axes and a legend around an empty panel. The refusal matters because the alternative is silent: a frame strip counting up over an empty panel, with nothing to say why.
The same is true of the same column split across the page rather than the clock, | facet(year), and for the same reason; the engine refuses both, because panels and frames are one split asked twice. Marks that place each row on its own (point, bar, box) have no domain to lose, so they draw in every frame:
data(gapminder_asia) + point + x(year) + y(life) + color(country) + play(year)data(gapminder_asia) + point + x(col.year) + y(col.life) + color(col.country) + play(col.year)data(gapminder_asia) + point + x(:year) + y(:life) + color(:country) +
play(:year)plot(data(gapminder_asia), point, x(col.year), y(col.life),
color(col.country), play(col.year))38.8 Crossed with a facet
Do all five continents climb at the same pace? Panels split the page and frames split the clock, so the two compose and every panel advances together:
data(gm_all) + point + x(gdp, scale = "log") + y(life) + play(year) |
facet(continent)(data(gm_all) + point + x(col.gdp, scale = "log") + y(col.life) + play(col.year) |
facet(col.continent))data(gm_all) + point + x(:gdp, scale = "log") + y(:life) + play(:year) |
facet(:continent)plot(data(gm_all), point, x(col.gdp, { scale: "log" }), y(col.life),
play(col.year), across(col.continent))Five continents, twelve frames, one shared pair of axes. Each panel is its own continent, and the strip above them all names the year they share.
38.9 Frames in the cube
The gapminder cloud has a third measurement, population, and the whole cloud moves through the years. Nothing about play belongs to the plane. A frame is a subset of the rows, and the third position is a way of looking at whatever rows you have, so the two compose, and neither needed anything added for the other:
data(gm_all) + point + x(gdp, scale = "log") + y(life) + z(population) +
color(continent) + play(year)(data(gm_all) + point + x(col.gdp, scale = "log") + y(col.life) + z(col.population) +
color(col.continent) + play(col.year))data(gm_all) + point + x(:gdp, scale = "log") + y(:life) +
z(:population) + color(:continent) + play(:year)plot(data(gm_all), point, x(col.gdp, { scale: "log" }), y(col.life),
z(col.population), color(col.continent), play(col.year))“Given all the gapminder years: points, x is gdp on a log scale, y is life, z is population, color by continent, played by year.”
That one is worth reading slowly, because it is the sentence this kernel was designed around. Three of the kernel’s ideas were designed to compose before any of them was drawn: the third position, the sequence, and the selection. The sentence above spends two of them at once. Selection adds the third to this very sentence, so all three stand in one line.
The cube is fitted once across the whole sequence, exactly as a flat plot’s axes are, and it matters more in a sequence than in a still picture. A box that resized itself per frame would swing its own corners around a cloud that had not moved, and every apparent motion would belong to the guide rather than to the countries. Fitted once, the height axis has room for 2007’s China from the first frame onward, which is why 1952 opens with the cloud pressed low and only China and India standing well clear of the floor.
A transform inside the cube is the same transform. The floor is cut once from every frame’s rows and each frame then counts only its own into the cells, which is this chapter’s shared-cut rule read with a second axis present:
data(gm_all) + bar * bin(12) + x(life) + y(continent) + color(continent) +
palette("okabe") + space() + play(year)(data(gm_all) + bar * bin(12) + x(col.life) + y(col.continent) + color(col.continent) +
palette("okabe") + space() + play(col.year))data(gm_all) + bar * bin(12) + x(:life) + y(:continent) +
color(:continent) + palette("okabe") + space() + play(:year)plot(data(gm_all), layer(bar, bin(12)), x(col.life), y(col.continent),
color(col.continent), palette("okabe"), space(), play(col.year))“Given all the gapminder years: bars derived by bin into 12, x is life, y is continent, color by continent, with the okabe palette, in the cube, played by year.”
Follow one continent’s row of bars rather than the whole floor. Each pile slides toward longer lives as the sequence runs, and it slides against cells that never move, so what you are watching is the countries changing which cell they fall in. color(continent) names the same column the y axis does, and it is there so that following one row is possible at all. A floor axis in a cube runs diagonally into the picture. Picking a row off it by eye means tracing a receding edge while five rows of bars block the view. The legend names the row at once; the axis makes you trace it.
The palette is a choice the cube makes for you, and it is the argument the okabe set was built on, reached a second way. A solid in space is shaded per face, so one category is already three tones before any two categories are compared, and a palette whose hues sit close together loses the difference that shading has already reduced. Okabe and Ito’s eight are spaced for readers who cannot separate some hues at all, which turns out to be the same requirement as separating them after a third of the light has been taken out.
What the frames do not touch is the viewing angle. space(turn = , tilt = ) is a property of the space rather than of the data, so one angle serves the whole sequence and every frame is seen from the same place. That is a design choice, not a gap: a viewpoint that swung while the sequence played would be motion that no column asked for, and a reader would then have to subtract it from the motion that does carry one. play moves the data past a fixed viewpoint.
You can still turn the plot yourself while it runs, and that is a different thing. The design choice above is about a viewpoint that moves on its own, with no column behind it. A reader who drags knows which motion is theirs, and can stop it. This is worth more here than anywhere else in the book. Something is moving inside a solid shape. The only way to see what the near edge hides is to look around it.
Panels and frames still compose in the cube, and the plot below shows it. They are one split asked twice, so crossing them costs nothing: the panels divide the page, the frames divide the clock, and each panel projects the cube its own rectangle has room for.
data(gm_all) + point + x(gdp, scale = "log") + y(life) + z(population) +
color(continent) + play(year) | facet(continent, wrap = 3)(data(gm_all) + point + x(col.gdp, scale = "log") + y(col.life) + z(col.population) +
color(col.continent) + play(col.year) | facet(col.continent, wrap = 3))data(gm_all) + point + x(:gdp, scale = "log") + y(:life) +
z(:population) + color(:continent) + play(:year) |
facet(:continent, wrap = 3)plot(data(gm_all), point, x(col.gdp, { scale: "log" }), y(col.life),
z(col.population), color(col.continent), play(col.year),
across(col.continent, { wrap: 3 }))Five cubes, twelve frames, and one strip above them all naming the year, because a frame is the whole plot, while a panel is one piece of it. That is four of the kernel’s ideas in one sentence: the third position, the panels, the frames, and a scale shared across every one of them so the cubes stay comparable while they run.
And the sheet moves too. ripples describes a wave tank tapped in one corner, computed on a grid at twelve instants, in a shallow tank and a deep one.
| across | along | height | tank | instant |
|---|---|---|---|---|
| 20 | 20 | 3.547 | Shallow | 1 |
| 40 | 20 | 5.845 | Shallow | 1 |
| 60 | 20 | 4.353 | Shallow | 1 |
| 80 | 20 | -0.178 | Shallow | 1 |
| 100 | 20 | -4.265 | Shallow | 1 |
One row is one grid node at one instant in one tank, so instant counts frames from 1 to 12 rather than measuring a time, and height is a signed height in millimeters. Depth sets how fast water carries a wave, so the same tapping makes crests 140 mm apart in the shallow tank and 220 mm apart in the deep one:
data(ripples) + surface + x(across) + y(along) + z(height) +
color(height) + palette("plasma") + play(instant, speed = 4) | facet(tank)(data(ripples) + surface + x(col.across) + y(col.along) + z(col.height) +
color(col.height) + palette("plasma") + play(col.instant, speed = 4) | facet(col.tank))data(ripples) + surface + x(:across) + y(:along) + z(:height) +
color(:height) + palette("plasma") + play(:instant, speed = 4) |
facet(:tank)plot(data(ripples), surface, x(col.across), y(col.along), z(col.height),
color(col.height), palette("plasma"), play(col.instant, { speed: 4 }),
across(col.tank))“Given the ripples: a surface, x is across, y is along, z is height, color by height, with the plasma palette, played by instant at speed 4, split into panel columns by tank.”
Watch one arc leave the corner and cross the floor. It is the same tapping in both panels and it is drawn against one shared height scale, so the difference you see is a wavelength and not a scaling. speed = 4 matters here: at the normal pace twelve frames take nearly ten seconds, which is a slideshow of a wave rather than a wave.
The height is said twice, once as the third position and once as the color, and that is deliberate rather than a redundancy. A sheet seen from one angle hides part of itself: the far corner is foreshortened and the near crests stand in front of the troughs behind them, so a shape that is plain at the front is guesswork at the back. Bright crest bands survive the projection where the height alone does not, and the wavelength this plot exists to compare is the spacing of those bands. palette("plasma") runs dark blue through magenta and orange to yellow; Channels has the rest, including two diverging ramps whose neutral color would sit at the still-water line if the sign of the height were the point.
38.10 Frames in polar
Every picture in the polar chapter held one moment, and a rose can grow across the years too. The next coordinate space works the same way, and for the same reason. polar() bends the panel, the frames choose the rows, and neither has to know about the other:
data(gm_all) + bar * mean + x(continent) + y(life) + polar() + play(year)data(gm_all) + bar * mean + x(col.continent) + y(col.life) + polar() + play(col.year)data(gm_all) + bar * mean + x(:continent) + y(:life) + polar() +
play(:year)plot(data(gm_all), layer(bar, mean), x(col.continent), y(col.life),
polar(), play(col.year))“Given all the gapminder years: bars derived by mean, x is continent, y is life, in polar, played by year.”
A rose of mean life expectancy, one petal per continent, opening across half a century. The radial axis is fitted over the whole sequence exactly as a flat one is, which is why the ring at 80 is already drawn in 1952 with nothing reaching it: the petals grow inside a circle that does not resize under them. Asia’s is the one to watch, from 46 years to 71. Africa’s climbs steadily until the late 1980s and then stops for fifteen years, which is a pause the average reports rather than smooths away.
The pie is where that shared axis says something surprising, and it is worth following, because it is the flat rule bent rather than a rule of its own:
data(gm_all) + bar * sum * stack + y(population) + color(continent) + polar() +
play(year)(data(gm_all) + bar * sum * stack + y(col.population) + color(col.continent) + polar() +
play(col.year))data(gm_all) + bar * sum * stack + y(:population) + color(:continent) +
polar() + play(:year)plot(data(gm_all), layer(bar, sum, stack), y(col.population),
color(col.continent), polar(), play(col.year))“Given all the gapminder years: bars derived by sum and stack, y is population, color by continent, in polar, played by year.”
The circle does not close until the last frame. A pie’s turn is its measure axis, that axis is fitted across the sequence like every other, and 1952’s world population is a little under two fifths of 2007’s, so 1952 sweeps a little under two fifths of the turn. The identical sentence without polar() draws a stacked column that stops well short of the top of its axis, and nobody finds that surprising. A pie that does not close is that stacked bar bent.
If the question is each year’s shares rather than each year’s total, the sentence can say so, and then every frame closes:
data(gm_all) + bar * sum * stack(share = TRUE) + y(population) + color(continent) +
polar() + play(year)(data(gm_all) + bar * sum * stack(share = True) + y(col.population) + color(col.continent) +
polar() + play(col.year))data(gm_all) + bar * sum * stack(share = true) + y(:population) +
color(:continent) + polar() + play(:year)plot(data(gm_all), layer(bar, sum, stack({ share: true })),
y(col.population), color(col.continent), polar(), play(col.year))“Given all the gapminder years: bars derived by sum and stack as shares, y is population, color by continent, in polar, played by year.”
The turn is one whole in every frame now, and what moves is how it divides: Asia widens a little from 58% to 61%, Europe falls from 17% to 9%, and Africa grows by half, from 10% to 15%. Two sentences, one atom apart, answering two different questions about the same column. Choosing between them is the reader’s job and the grammar’s business is to keep both sayable.
38.11 Taking the sequence with you
Every plot on this page moves because the SVG carries its own timing. A browser reads that timing. Most other places do not. A message to a friend, a slide in a talk, a post on a social site: each of them shows the first frame and stops.
save_gif() writes the same sequence as a file that those places will play:
wave <- file.path(tempdir(), "wave.gif")
save_gif(data(ripples) + surface + x(across) + y(along) + z(height) +
color(height) + palette("plasma") + play(instant, speed = 4) |
facet(tank),
wave, scale = 2)
cat(basename(wave), round(file.size(wave) / 1024), "KB\n")wave.gif 742 KB
Nothing has to be installed first. The engine converts and encodes the file itself, so the same line works on every machine that can draw the plot at all.
scale multiplies the canvas. A plot is 800 by 600 pixels unless its theme says otherwise, which is small for a post, so scale = 2 doubles both numbers. The pace is kept without being restated: this sequence was written speed = 4, and each frame holds for a fifth of a second in the file as it does on the page.
The file cannot disagree with the plot beside it, and the reason is worth one paragraph, because it is the reason Design laws gives for having one renderer. The frames are not drawn again one at a time. The plot is drawn once, with every scale, the color map and each legend fitted across the whole sequence, and what you see at any time is that single drawing with one frame left showing. So there is no second set of decisions that could come out different.
Two sentences are refused. A plot with no play() has no frames to write, and saying so is more useful than writing a file of one frame:
still <- data(gapminder_2007) + point + x(gdp) + y(life)
save_gif(still, file.path(tempdir(), "still.gif"))Error:
! gog: this plot does not play, so it has no moments to write. Bind a column with an order to `play` — `play(year)` — or save the still picture it already is.
gog: nothing was rendered. Fix the above, or set GOG_STRICT=0 to draw anyway.
And the path has to end in .gif, because the name of a file is a claim about what is inside it:
moving <- data(gm_all) + point + x(gdp) + y(life) + play(year)
save_gif(moving, "wave.png")Error:
! gog: `save_gif()` writes a GIF, so the path ends in `.gif` — `save_gif(p, "wave.gif")`.
One thing changes when the file is written, and it is worth knowing before you send the file rather than after. A GIF is pixels. The text stops being text, so it no longer sharpens when a reader zooms in, and the colors are fitted to a palette of 256. The SVG remains the plot itself. The GIF is a copy of it made for somewhere that cannot read the original.
38.11.1 Why a GIF and not a video
A video file would be smaller and sharper than a GIF, and a few places accept video and nothing else. gog writes no video, and the reason is about what a package can carry rather than about what a plot is.
Making a video needs a video encoder. The one nearly everybody uses is more than ten times the size of this whole engine, once for every platform a package supports, and its license does not allow it to travel inside a package licensed the way this one is. The other route is to require the reader to install it separately. That sounds harmless until you count the readers who cannot. A reader who installs a prebuilt package rather than building from source gets whatever the package carries, so a tool that cannot fit inside it never reaches them at all. The feature would work for some readers and refuse for others, which is a worse answer than not having it.
A GIF asks for nothing. The engine writes it on every machine that can draw the plot, in every one of the four languages. When the file looks soft, raise scale. When you genuinely need a video, converting a GIF into one is easy, and free converters online do it in one step.
38.12 What it refuses
Having crossed frames with panels, you may try the same column on both. One column names the panels or names the frames, and not both. If it were drawn, one panel’s rows would fall in one frame and every other panel would be empty:
data(gm_four) + point + x(gdp) + y(life) + play(period) | facet(period)data(gm_four) + point + x(col.gdp) + y(col.life) + play(col.period) | facet(col.period)data(gm_four) + point + x(:gdp) + y(:life) + play(:period) |
facet(:period)plot(data(gm_four), point, x(col.gdp), y(col.life), play(col.period),
across(col.period))Error:
! gog: `period` cannot both name the frames and name the panels — every panel but one would be empty in every frame. Split the page by one column and the time by another, or drop `| facet(period)` and let `play(period)` carry it.
gog: nothing was rendered. Fix the above, or set GOG_STRICT=0 to draw anyway.
A speed that is not a pace is refused at the binding that carries it:
data(gm_all) + point + x(gdp) + y(life) + play(year, speed = 0)data(gm_all) + point + x(col.gdp) + y(col.life) + play(col.year, speed = 0)data(gm_all) + point + x(:gdp) + y(:life) + play(:year, speed = 0)plot(data(gm_all), point, x(col.gdp), y(col.life),
play(col.year, { speed: 0 }))Error:
! gog: `speed = 0` — a speed is a multiple of the normal pace, so it has to be above zero. `speed = 2` is twice as fast, `speed = 0.5` half.
speed belongs to play and to nothing else, because nothing else in the grammar takes time to draw: a color is drawn all at once, so there is no duration for a multiplier to scale. A refusal for that mistake is not shown here, and the reason is worth saying. None of the four packages gives any other channel a speed argument at all, so color(continent, speed = 2) fails in R (or in Python, or Julia, or JavaScript) before a specification is ever built. The engine still checks it, because a specification can also be written by hand, but you will never see that message from a sentence: the language you are writing in refuses first, which is the earlier and better place for it.
38.13 What the page adds
A played plot on a web page carries three more buttons. They sit on the same line as the four every plot has, a little to the right of them, and they control the clock:
| Control | What it does |
|---|---|
| the left arrow | Goes back one frame, and stops the clock |
| the pause | Stops the clock. It becomes a play arrow, which starts it again |
| the right arrow | Goes forward one frame, and stops the clock |
Stepping stops the clock, and it has to. A running clock would move past the frame you just asked for, in less than a second. So the arrows stop it, and the middle button is how you start it again.
The two ends are connected. Go back from the first frame and you arrive at the last. The sequence already runs that way: it loops, so the last frame is followed by the first, every time. Buttons that stopped at the ends would disagree with the sequence they control.
Nothing displays the frame you are on, because the strip above the panel already names it. That strip is the sequence’s guide, drawn on every played plot, the way an axis is drawn for x.
None of the three is in the sentence, and that is the rule rather than an omission. Every frame is already in the file: the whole sequence is drawn once, and a clock decides which frame you see at each moment. Stepping moves that clock. It chooses no rows, hides no rows, and changes nothing the sentence said. So it is like turning a cube (Space), which changes how you look and never what is drawn. Neither one needs a word in the grammar.
Stop the clock and the camera photographs the frame you stopped on. That is the camera’s own rule holding rather than a rule about animation: it saves what you are looking at, the same way it saves how far you have zoomed in.
On paper, and in the PDF edition, a played plot is the first frame with its strip. The buttons are the page’s, so a printed sequence has none, and none is missing there.
38.14 Cuts between frames
A sequence cuts. Frames do not glide into one another: a point in 1957 and a point in 1962 are two complete pictures shown in turn, not one point eased between two positions. Easing needs a key, something that says Afghanistan in one frame is the same country as Afghanistan in the next. The grammar has no such word. group looks like the word until you notice that a point does not take it, and the point everyone pictures gliding is exactly that: a point. So the difference is a word the vocabulary does not have, not a picture the engine cannot draw.