| 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 |
17 Path
How did income and life expectancy change together in each country, year by year? path strokes the rows in the order the table gives them. That is all of it, and it is the one thing line does not do. A line sorts its vertices by x first, because a line draws a function: one y for each x, read left to right along a domain. A path draws a route. It visits row 1, then row 2, and it may turn back, cross itself, or return to where it started.
Everything the two marks share, they share exactly: same stroke, same color and group splits, same style(size = ), same style(pattern = ) dash. One question separates them: does the order of the rows mean something? Because a path keeps that order, it can carry three things a line cannot in general: an arrowhead, a spiral, and the third dimension. All three follow from the row order, so none of them is a special case.
17.1 The connected scatterplot
Two measures that change over time can be drawn as two time series, one above the other. The reader is then left to match the years by eye. One plane can hold both, if the order of the rows carries the time. Here are five Asian countries, each measured twelve times between 1952 and 2007. Put income on one axis and life expectancy on the other, and connect each country’s measurements in the order they were taken:
data(gapminder_asia) + path + x(gdp) + y(life) + color(country) +
x_label("GDP per person") + y_label("Life expectancy") +
title("Fifty-five years, five countries, one plane")(data(gapminder_asia) + path + x(col.gdp) + y(col.life) + color(col.country) +
x_label("GDP per person") + y_label("Life expectancy") +
title("Fifty-five years, five countries, one plane"))data(gapminder_asia) + path + x(:gdp) + y(:life) + color(:country) +
x_label("GDP per person") + y_label("Life expectancy") +
title("Fifty-five years, five countries, one plane")plot(data(gapminder_asia), path, x(col.gdp), y(col.life),
color(col.country), x_label("GDP per person"),
y_label("Life expectancy"),
title("Fifty-five years, five countries, one plane"))“Given gapminder Asia: paths, x is gdp, y is life, color by country.”
Each route is one country’s history. Time is nowhere on the plot, and yet the whole plot is about time: it is the order the points are joined in. Japan climbs steeply, then turns right and flattens: past that corner, income keeps rising and life expectancy barely moves. Korea’s route bends in the same place two decades later. China’s route goes almost straight up at very low income. That shape belongs to a country that got healthier long before it got rich, and Indonesia’s route bends the same way. This is a connected scatterplot, and no other arrangement of these atoms draws it.
17.2 Why line cannot draw it
line is the mark most readers would write first, because it is the familiar way to join rows with a stroke. It is worth seeing what line does with this table. Change one word and the same sentence draws the wrong picture:
data(gapminder_asia) + line + x(gdp) + y(life) + color(country) +
x_label("GDP per person") + y_label("Life expectancy") +
title("the same data, sorted by x")(data(gapminder_asia) + line + x(col.gdp) + y(col.life) + color(col.country) +
x_label("GDP per person") + y_label("Life expectancy") +
title("the same data, sorted by x"))data(gapminder_asia) + line + x(:gdp) + y(:life) + color(:country) +
x_label("GDP per person") + y_label("Life expectancy") +
title("the same data, sorted by x")plot(data(gapminder_asia), line, x(col.gdp), y(col.life),
color(col.country), x_label("GDP per person"),
y_label("Life expectancy"), title("the same data, sorted by x"))“Given gapminder Asia: lines, x is gdp, y is life, color by country.”
Look at China’s route. Its income did not rise steadily: it fell and recovered, so the year order and the income order are different sequences, and line draws the income one. Every point is in the right place, and every route between them is wrong. That is not a bug in line; it is what a line is. A function has one value per x, so sorting changes nothing. For a time series read along a date axis, the sort puts the rows in date order.
The two marks answer different questions, so a specification that names the wrong one gets a plausible picture rather than an error. The rule is this. If the reader should follow the marks in a particular order, that order has to come from the table. Only path reads it there.
17.3 Which end is the last row?
A route has a direction, and the plot above does not show it. Japan’s route could be read from either end. So path takes a setting no other mark can:
data(gapminder_asia) + path + x(gdp) + y(life) + color(country) +
style(arrow = "end") +
x_label("GDP per person") + y_label("Life expectancy") +
title("style(arrow = \"end\"): the head sits at 2007")(data(gapminder_asia) + path + x(col.gdp) + y(col.life) + color(col.country) +
style(arrow = "end") +
x_label("GDP per person") + y_label("Life expectancy") +
title("style(arrow = \"end\"): the head sits at 2007"))data(gapminder_asia) + path + x(:gdp) + y(:life) + color(:country) +
style(arrow = "end") + x_label("GDP per person") +
y_label("Life expectancy") +
title("style(arrow = \"end\"): the head sits at 2007")plot(data(gapminder_asia), path, x(col.gdp), y(col.life),
color(col.country), style({ arrow: "end" }), x_label("GDP per person"),
y_label("Life expectancy"),
title("style(arrow = \"end\"): the head sits at 2007"))“Given gapminder Asia: paths, x is gdp, y is life, color by country, with arrow end.”
Now each route points at its last row. arrow takes "end", "start", or "both": a head at the last row, at the first row, or at both ends.
It belongs to path alone, and every other mark refuses it. A line’s last vertex is wherever the domain happens to end, so a head there marks the sort rather than anything the data did. Only a path has an end that comes from the data. What it refuses shows a line refusing an arrowhead.
17.4 An arrow is a layer
A note placed in empty space has to say which point it is about. An arrow from the words to the point says it. The Text chapter drew the note; path draws the arrow beside it. A note is a one-row table; the arrow that points from the note to the thing is a two-row table, one row for each end:
| 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 |
| gdp | life |
|---|---|
| 20000 | 45.5 |
| 13100 | 50.2 |
| gdp | life | what |
|---|---|---|
| 20500 | 44.8 | Botswana |
data(gapminder_2007) + point + x(gdp) + y(life) +
data(botswana_arrow) + path + style(arrow = "end", color = "#c0392b") +
data(botswana_label) + text + label(what) +
title("an annotation, in the grammar you already have")(data(gapminder_2007) + point + x(col.gdp) + y(col.life) +
data(botswana_arrow) + path + style(arrow = "end", color = "#c0392b") +
data(botswana_label) + text + label(col.what) +
title("an annotation, in the grammar you already have"))data(gapminder_2007) + point + x(:gdp) + y(:life) + data(botswana_arrow) +
path + style(arrow = "end", color = "#c0392b") + data(botswana_label) +
text + label(:what) +
title("an annotation, in the grammar you already have")plot(data(gapminder_2007), point, x(col.gdp), y(col.life),
data(botswana_arrow), path, style({ arrow: "end", color: "#c0392b" }),
data(botswana_label), text, label(col.what),
title("an annotation, in the grammar you already have"))“Given gapminder 2007: points, x is gdp, y is life, and also a path from the seg table and text from the note table, label by what.”
This grammar has no annotate() function, and it needs none. An annotation is a layer over a small table, and the marks it uses are the marks everything else uses: text for the words, path for the arrow, point for a marker. What the table holds is data, so it is scaled like data, which is why the arrow’s ends are written in the data’s own units rather than in pixels.
17.5 Two transforms, and the rest refused
A summary such as mean composes with line. Writing the same sentence with path is a natural next step. The other stroke and band marks compose with many transforms. path composes with two, density and cluster, and every summary is refused. What it refuses shows one.
A summary replaces the rows. mean reduces each country to one number, and what is left to connect is one point per country, in whatever order the summary produced them. That is a line, written the long way around. The order a path is does not survive being summarized, and that one fact empties almost the whole row this mark has on the Combinations grid.
density is the one exception, and it does not break the rule above. A summary reduces each group to one value. A density estimated over the plane groups nothing and reduces nothing. It produces vertices in the order they were traced, which is the one thing a path is for.
17.6 The contour: path * density
Where do the flowers cluster, and how tightly? A scatter of 150 flowers shows two groups, and points that share a position hide their count. A contour draws the shape of the cloud without drawing the points. Bind both positions and compose density. gog estimates how thick the cloud of points is everywhere on the plane, then traces the rings where it is equally thick:
| sepal_length | sepal_width | petal_length | species |
|---|---|---|---|
| 5.1 | 3.5 | 1.4 | setosa |
| 4.9 | 3.0 | 1.4 | setosa |
| 4.7 | 3.2 | 1.3 | setosa |
| 4.6 | 3.1 | 1.5 | setosa |
| 5.0 | 3.6 | 1.4 | setosa |
data(iris_flowers) + path * density +
x(sepal_length) + y(petal_length) +
title("where the flowers cluster")(data(iris_flowers) + path * density +
x(col.sepal_length) + y(col.petal_length) +
title("where the flowers cluster"))data(iris_flowers) + path * density + x(:sepal_length) +
y(:petal_length) + title("where the flowers cluster")plot(data(iris_flowers), layer(path, density), x(col.sepal_length),
y(col.petal_length), title("where the flowers cluster"))“Given the iris flowers: paths derived by density, x is sepal length, y is petal length.”
Nothing new was added to the grammar to draw that. One rule decides this, and the same rule turns zone * bin into a heatmap. The mark decides how many axes a transform cuts, and you never state it. A bar leaves an axis free to measure the density along, so bar * density spreads it along one axis and draws a curve. A path is a route through two columns and treats them equally, so it has no axis left over. density therefore cuts both. There is no contour atom for the same reason there is no pie() (Polar makes that argument): the sentence already exists.
Read the outermost ring, then the ones inside it. The outermost is a single ring around both clusters, and the next level inward splits into two separate rings. So a level alone cannot tell gog where one stroke ends. Each ring is its own stroke, and the level only decides its color, so the two inner rings are never joined by a stroke across the gap between them.
The color is the level, and nothing had to bind it, exactly as bar * bin binds y for you. Say it out loud if you prefer, and change how many rings are traced:
data(iris_flowers) + path * density(levels = 10) +
x(sepal_length) + y(petal_length) + color(level) +
title("ten levels, named out loud")(data(iris_flowers) + path * density(levels = 10) +
x(col.sepal_length) + y(col.petal_length) + color(col.level) +
title("ten levels, named out loud"))data(iris_flowers) + path * density(levels = 10) + x(:sepal_length) +
y(:petal_length) + color(:level) + title("ten levels, named out loud")plot(data(iris_flowers), layer(path, density({ levels: 10 })),
x(col.sepal_length), y(col.petal_length), color(col.level),
title("ten levels, named out loud"))“Given the iris flowers: paths derived by density at 10 levels, x is sepal length, y is petal length, color by level.”
density is one transform read three ways. A line draws a curve, a zone paints the whole field, and a path strokes the level sets that levels cuts the field into. Each parameter belongs to one reading and is refused in the other two. levels cuts the field, the estimated thickness everywhere on the plane, into that many steps. A one-dimensional density is a single curve, and a curve has no field to cut.
bandwidth is refused for the mirror reason: it needs one axis, and a contour has two. It is a width in the data’s own units, and a contour spreads over two columns that measure different things. Here the two agree, since sepal length and petal length are both in centimeters, but two axes usually do not. One number cannot be a width in both. What it refuses shows both refusals.
adjust is a multiplier rather than a length, so it means the same thing on either axis. It is the one parameter all three readings accept:
data(iris_flowers) + path * density(adjust = 1.6) +
x(sepal_length) + y(petal_length) +
title("the same cloud, smoothed harder")(data(iris_flowers) + path * density(adjust = 1.6) +
x(col.sepal_length) + y(col.petal_length) +
title("the same cloud, smoothed harder"))data(iris_flowers) + path * density(adjust = 1.6) + x(:sepal_length) +
y(:petal_length) + title("the same cloud, smoothed harder")plot(data(iris_flowers), layer(path, density({ adjust: 1.6 })),
x(col.sepal_length), y(col.petal_length),
title("the same cloud, smoothed harder"))“Given the iris flowers: paths derived by density adjusted by 1.6, x is sepal length, y is petal length.”
To estimate one field per category, split the rows with group. The level is already on color, which is why the split has to be group:
petals <- iris_flowers[iris_flowers$species != "setosa", ]
data(petals) + path * density(levels = 4) +
x(sepal_length) + y(petal_length) + group(species) +
title("two species, estimated separately")“Given the petals: paths derived by density at 4 levels, x is sepal length, y is petal length, grouped by species.”
A zone has no measure axis either, so it reads density in two dimensions too, and it fills what this mark strokes. Give both marks the same levels and you get the same curves twice: once as strokes, once as the edges of filled bands. That pair is the plot usually meant by a filled density. That plot belongs to the mark that fills it, zone * density, and the zone chapter covers the color ramp it uses. One field, two marks. It is the same split as bar * bin and line * bin: the mark chooses the geometry, and the transform stays the same.
17.7 In polar, a route is a spiral
Some routes turn: a bearing that swings around the compass, or a value that cycles through the hours while another grows. On a straight axis the end of one turn is far from the start of the next, and on a circle they meet. path bends into polar like every other mark. Give it an angle and a radius that both grow, and the route winds outward:
| a | r |
|---|---|
| 0.0000000 | 0.1000000 |
| 0.0696629 | 0.1438202 |
| 0.1393258 | 0.1876404 |
| 0.2089888 | 0.2314607 |
| 0.2786517 | 0.2752809 |
data(spiral) + path + x(a) + y(r) + polar() +
title("a path in polar")(data(spiral) + path + x(col.a) + y(col.r) + polar() +
title("a path in polar"))data(spiral) + path + x(:a) + y(:r) + polar() + title("a path in polar")plot(data(spiral), path, x(col.a), y(col.r), polar(),
title("a path in polar"))“Given the spiral: a path, x is a, y is r, in polar.”
There is one thing path does not copy from line: the segment that closes a radar chart (Polar draws one). A categorical line in polar joins its last vertex back to its first, because the categories fill the whole turn, and a gap there would leave one sector blank. A path does not read the angular axis as a set of categories. Its last vertex is where the data stopped, so closing the route would add a segment nobody asked for. If a route does return to its start, say so by repeating the first row.
17.8 In space, the route is unchanged
A glider’s route has three positions, and a plane holds two of them. Drawn flat, two circling gliders are two rings, one inside the other, and the climb is lost. The thermals table holds two gliders circling the same thermal, a rising column of warm air, sampled as they climb:
| 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 |
Bind z and the route runs through the cube instead of across the page. Of the marks that join their rows into one stroke, path is the only one that takes the third position. The reason is the row order this whole chapter is about:
data(thermals) + path + x(east) + y(north) + z(altitude) + color(glider) +
title("Two gliders, one thermal")(data(thermals) + path + x(col.east) + y(col.north) + z(col.altitude) + color(col.glider) +
title("Two gliders, one thermal"))data(thermals) + path + x(:east) + y(:north) + z(:altitude) +
color(:glider) + title("Two gliders, one thermal")plot(data(thermals), path, x(col.east), y(col.north), z(col.altitude),
color(col.glider), title("Two gliders, one thermal"))“Given the thermals: paths, x is east, y is north, z is altitude, color by glider.”
An order belongs to no axis, so it survives the third dimension untouched: the route is the same route however the scene is turned. A line sorts by x, and the cube has no left to right. There x is one of three equal positions. At some viewing angles it runs straight into the page and becomes depth, which would leave a line sorted by an axis the reader cannot see. So the sort that makes a line a line cannot follow it into the cube. line, step, area and ribbon refuse z rather than half-drawing it, as What it refuses shows.
Look again at the two routes in the plot above. Where they cross, whichever is nearer to the eye covers the other, and which one that is changes as they climb. A glyph sits at one distance from the eye and can be sorted whole. A stroke running through the cube has a far end and a near end, so the unit that gets sorted is the segment. The segments of both routes are sorted together. If each stroke were sorted as a whole, one glider would sit entirely in front of the other: two coils that never meet, rather than two that thread through each other.
Cutting the route into segments is also what lets color carry a measure along it. Bind the altitude to the color as well as to the height, and the climb is said twice: once by where the route is, once by what color it is:
data(thermals) + path + x(east) + y(north) + z(altitude) +
color(altitude) + group(glider) + palette("viridis") +
space(turn = -25, tilt = 20) +
title("Altitude, said twice")(data(thermals) + path + x(col.east) + y(col.north) + z(col.altitude) +
color(col.altitude) + group(col.glider) + palette("viridis") +
space(turn = -25, tilt = 20) +
title("Altitude, said twice"))data(thermals) + path + x(:east) + y(:north) + z(:altitude) +
color(:altitude) + group(:glider) + palette("viridis") +
space(turn = -25, tilt = 20) + title("Altitude, said twice")plot(data(thermals), path, x(col.east), y(col.north), z(col.altitude),
color(col.altitude), group(col.glider), palette("viridis"),
space({ turn: -25, tilt: 20 }), title("Altitude, said twice"))“Given the thermals: paths, x is east, y is north, z is altitude, color by altitude, grouped by glider, with the viridis palette.”
palette("viridis") picks the ramp; group(glider) keeps the two routes apart. Drop that last atom and the plot is wrong. A category on color splits the mark into one stroke per group, but a measure has no categories to split on. Without group the two gliders would be joined into a single route, and a straight line would run from where one glider’s rows stop to where the other’s begin. Color answers what the route was carrying; group answers which route it is. Two questions, two atoms.
A dash survives the segmentation too. Each segment is drawn as its own piece, and a dash would normally restart at the start of each piece:
data(thermals) + path + x(east) + y(north) + z(altitude) + color(glider) +
style(pattern = "dashed") +
space(turn = -25, tilt = 20)(data(thermals) + path + x(col.east) + y(col.north) + z(col.altitude) + color(col.glider) +
style(pattern = "dashed") +
space(turn = -25, tilt = 20))data(thermals) + path + x(:east) + y(:north) + z(:altitude) +
color(:glider) + style(pattern = "dashed") +
space(turn = -25, tilt = 20)plot(data(thermals), path, x(col.east), y(col.north), z(col.altitude),
color(col.glider), style({ pattern: "dashed" }),
space({ turn: -25, tilt: 20 }))“Given the thermals: paths, x is east, y is north, z is altitude, color by glider, with pattern dashed.”
The dashes run along the route rather than resetting at every sample. The dash belongs to the route, not to the segments the depth sort needs. An arrowhead is sorted the same way, at the depth of its own tip. A head that sits deep in the cube is covered by whatever passes in front of it.
Space owns the third dimension and the viewing angle.
17.9 What a path is for
The connected scatterplot is the best known use, but the mark is more general than that. Any picture whose reading order is the table’s order is a path. A trajectory through two measurements over time is one. So is a route through space, and a cycle that returns to where it began. So is a hysteresis loop, which is a cycle whose forward and return paths differ, and so is an arrow. The test is always the same question. Does the order of the rows mean something? If it does, the mark is path; if the reader should read left to right, it is line.
17.10 What you can set
A route drawn as a plain stroke leaves the reader two questions. Which way does it run, and is it solid or dashed? Neither answer is in a column, so both are set rather than mapped, and each mark takes its own settings. These are a path’s, with the values each accepts:
| Setting | Value |
|---|---|
style(color = ) |
any CSS color name or hex |
style(opacity = ) |
0 to 1 |
style(size = ) |
pixels |
style(pattern = ) |
solid, dashed, dotted |
style(arrow = ) |
end, start, both |
arrow belongs to path alone, and geometry decides that rather than taste. A head marks a direction, and a path is the only mark that has one. line, step and area sort their vertices by x, so their last point is wherever the domain ends rather than where the data stopped.
A route can carry the heads, a width and a dash together:
data(gapminder_asia) + path + x(gdp) + y(life) + color(country) +
style(arrow = "both", size = 2.5, pattern = "dotted") +
x_label("GDP per person") + y_label("Life expectancy") +
title("Both ends marked, on a dotted route")(data(gapminder_asia) + path + x(col.gdp) + y(col.life) + color(col.country) +
style(arrow = "both", size = 2.5, pattern = "dotted") +
x_label("GDP per person") + y_label("Life expectancy") +
title("Both ends marked, on a dotted route"))data(gapminder_asia) + path + x(:gdp) + y(:life) + color(:country) +
style(arrow = "both", size = 2.5, pattern = "dotted") +
x_label("GDP per person") + y_label("Life expectancy") +
title("Both ends marked, on a dotted route")plot(data(gapminder_asia), path, x(col.gdp), y(col.life),
color(col.country),
style({ arrow: "both", size: 2.5, pattern: "dotted" }),
x_label("GDP per person"), y_label("Life expectancy"),
title("Both ends marked, on a dotted route"))“Given gapminder Asia: paths, x is gdp, y is life, color by country, with arrow both, size 2.5 and pattern dotted.”
It takes a value rather than a true-or-false flag, because "both" is an ordinary thing to ask for, and a flag would have needed a second setting to say it. The heads draw solid on a dotted path. The dash is the route’s texture, and a head cut into dashes no longer reads as an arrow.
The transform whose parameters this chapter covers is density, and levels and adjust are above. cluster takes its own, in Cluster. Everything else that varies is either mapped or set.
The grid of every mark and every setting shows which other marks share this list. What a mark maps rather than sets is its row on the companion grid.
17.11 What it refuses
You might want the route to thicken as income rises, so that the stroke itself carries a third measure. A path is one stroke, and a stroke has one width. There is no answer to how thick the route is at a single point, so width cannot vary along it:
data(gapminder_asia) + path + x(gdp) + y(life) + size(gdp)data(gapminder_asia) + path + x(col.gdp) + y(col.life) + size(col.gdp)data(gapminder_asia) + path + x(:gdp) + y(:life) + size(:gdp)plot(data(gapminder_asia), path, x(col.gdp), y(col.life), size(col.gdp))Error:
! gog: `size` cannot be bound to `path` — a path has no size feature. Remove the `gdp` mapping from `size`, or use a mark that has one.
gog: nothing was rendered. Fix the above, or set GOG_STRICT=0 to draw anyway.
Set it instead and every segment takes the same width, which is the Setting vs mapping distinction again.
A summary is the second refusal. It replaces the very rows whose order is the path, as Two transforms, and the rest refused explained:
data(gapminder_asia) + path * mean + x(country) + y(life)data(gapminder_asia) + path * mean + x(col.country) + y(col.life)data(gapminder_asia) + path * mean + x(:country) + y(:life)plot(data(gapminder_asia), layer(path, mean), x(col.country), y(col.life))Error:
! gog: `path` strokes the rows in the order the table gives them, and `mean` replaces those rows with one summary per key — after which the order is the keys' and the path is a `line`. Use `line * mean`, which sorts by `x` and is the mark a statistic is drawn on.
gog: `path * mean` reduces a column within each cell, but nothing says which column — `path` measures by `color`, and no `color()` is bound. Name it: `path * mean + x(<a>) + y(<b>) + color(<column>)`. To count the rows in each cell instead of reducing a column, `count` needs no such binding: `path * count + x(<a>) + y(<b>)`.
gog: nothing was rendered. Fix the above, or set GOG_STRICT=0 to draw anyway.
density(bandwidth = ) is the third. It is a width in one column’s own units, and a contour spreads over two columns that measure different quantities, so one number cannot be a width in both:
data(iris_flowers) + path * density(bandwidth = 0.5) +
x(sepal_length) + y(petal_length)(data(iris_flowers) + path * density(bandwidth = 0.5) +
x(col.sepal_length) + y(col.petal_length))data(iris_flowers) + path * density(bandwidth = 0.5) + x(:sepal_length) +
y(:petal_length)plot(data(iris_flowers), layer(path, density({ bandwidth: 0.5 })),
x(col.sepal_length), y(col.petal_length))Error:
! gog: `density(bandwidth = )` is a width in one column's own units, and `path * density` spreads over *two* columns measuring different quantities — one number cannot be a width in both. Use `density(adjust = )`, which scales the automatic bandwidth on each axis by the same dimensionless factor.
gog: nothing was rendered. Fix the above, or set GOG_STRICT=0 to draw anyway.
The last three are sentences line refuses, and each one says why path is a separate mark. An arrowhead is the first. A line’s last vertex is wherever the domain ends, so a head there would mark the sort rather than the data:
data(gapminder_asia) + line + x(year) + y(life) + style(arrow = "end")data(gapminder_asia) + line + x(col.year) + y(col.life) + style(arrow = "end")data(gapminder_asia) + line + x(:year) + y(:life) + style(arrow = "end")plot(data(gapminder_asia), line, x(col.year), y(col.life),
style({ arrow: "end" }))Error:
! gog: `style(arrow = )` is a `path` setting, and a `line` cannot carry it — a line sorts its vertices by `x`, so its last point is wherever the domain ends rather than where the data stopped — a head there would point at the sort. Use `path`, which strokes the rows in the data's own order and so has an end the data chose.
gog: nothing was rendered. Fix the above, or set GOG_STRICT=0 to draw anyway.
density(levels = ) is the second. It cuts the estimated field into steps, and a one-dimensional density is a single curve with no field to cut:
data(iris_flowers) + line * density(levels = 8) + x(sepal_length)data(iris_flowers) + line * density(levels = 8) + x(col.sepal_length)data(iris_flowers) + line * density(levels = 8) + x(:sepal_length)plot(data(iris_flowers), layer(line, density({ levels: 8 })),
x(col.sepal_length))Error:
! gog: `density(levels = )` cuts a field into levels, and a `line` estimates *one curve* — a curve has no levels to cut. Drop it for a density curve, or read the density over both axes: `path * density(levels = )` traces the contours, `zone * density(levels = )` fills the bands between them.
gog: nothing was rendered. Fix the above, or set GOG_STRICT=0 to draw anyway.
z is the third. A line sorts by x, and the cube has no left to right, so that sort cannot follow it in, as In space, the route is unchanged explained:
data(thermals) + line + x(east) + y(north) + z(altitude)data(thermals) + line + x(col.east) + y(col.north) + z(col.altitude)data(thermals) + line + x(:east) + y(:north) + z(:altitude)plot(data(thermals), line, x(col.east), y(col.north), z(col.altitude))Error:
! gog: `line` reads a *domain* left to right — it sorts by `x` and draws one value for each — and a cube has no left to right: `x` is one of three equal positions, and at some viewing angles it runs into the page and becomes depth. A `line` in space would be sorted by an axis the reader cannot see, so this is refused rather than drawn. For a route through three dimensions use `path`, which is `line` with that sort removed: `path + x(<a>) + y(<b>) + z(altitude)`.
gog: nothing was rendered. Fix the above, or set GOG_STRICT=0 to draw anyway.
All three point at one fact. A line’s shape comes from the sort, and a path’s comes from the rows.