9  Line

How did one value change over time? line sorts the rows by x, from smallest to largest, and draws one stroke through their (x, y) points in that order, the x order. The stroke is straight pieces laid end to end, each joining one row’s point to the next, with nothing smoothed between them.

9.1 Basic usage

How did life expectancy change in five Asian countries? Most examples in this chapter start from the smallest sentence that draws a line: a source, the mark, and its two positions.

gapminder_asia: first 5 of 60 rows
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)
data(gapminder_asia) + line + x(col.year) + y(col.life)
data(gapminder_asia) + line + x(:year) + y(:life)
plot(data(gapminder_asia), line, x(col.year), y(col.life))
gog: up to 5 rows of `line` share one value of `x(year)`, and nothing splits them, so they are joined in x order and the stroke zigzags inside that value. If you have several series, add `color(<field>)` or `group(<field>)`; for one value per `x`, summarize them with `line * mean`.
1960 1980 2000 40 50 60 70 80 Life Year

“Given gapminder Asia: a line, x is year, y is life.”

The stroke looks wrong, but gog drew exactly what the sentence asks for. All five countries have a row for each year, so the one stroke passes through five values at every year. The vertical segments are where it joins the five countries inside one year, before it continues to the next year. That is exactly what a line does when nothing splits the rows: it draws one series, a single stroke in x order. The message printed above the plot is gog’s warning, and it names what to add: a color or a group. Multiple series, two sections below, draws the same table as five lines, one per country.

9.2 Automatic sorting by x

Rows rarely arrive in the order a line needs: a file is sorted by name, or by nothing at all. line sorts its own vertices, the points it joins, by x, so the rows may arrive in any order. The table below is deliberately shuffled, and the line still runs from left to right:

scrambled: all 5 rows
year life
1992 68
1962 52
2002 71
1972 58
1982 63
data(scrambled) + line + x(year) + y(life)
data(scrambled) + line + x(col.year) + y(col.life)
data(scrambled) + line + x(:year) + y(:life)
plot(data(scrambled), line, x(col.year), y(col.life))
1960 1970 1980 1990 2000 55 60 65 70 Life Year

That sorting is part of what the mark means. It is not an extra step that the engine performs for you. A line says this value changed along this axis, and an axis has one direction, so there is only one order the vertices could be joined in.

path is the mark that does the opposite. It joins the rows in the order your table holds them, which is what draws a route or a loop. So choosing between the two is choosing whether the order of your rows carries meaning. With line the row order cannot carry meaning; with path the row order sets the whole shape.

9.3 Multiple series

The chapter’s first plot drew one stroke through all five countries, and gog asked for a color or a group. Here is the same table with color(country) added, and the single stroke splits into a line per country. color also tells the reader which line belongs to which country.

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))
1960 1980 2000 40 50 60 70 80 Life Year Country China India Indonesia Japan Korea, Rep.

“Given gapminder Asia: lines, x is year, y is life, color by country.”

Each unique value in country becomes a separate line with its own color, and a color legend is generated automatically. group makes the same split without a color or a legend. Use it when the lines matter as a whole and their names do not, as in the Many groups recipe.

9.4 Layering a line over other marks

A line shows the trend and hides where the measurements were. A dot at each row shows them again, which is why line and point are so often combined. Write color before the marks and it belongs to the plot, so both layers use it:

data(gapminder_asia) + x(year) + y(life) + color(country) +
  line + point
(data(gapminder_asia) + x(col.year) + y(col.life) + color(col.country) +
  line + point)
data(gapminder_asia) + x(:year) + y(:life) + color(:country) + line +
  point
plot(data(gapminder_asia), x(col.year), y(col.life), color(col.country),
  line, point)
1960 1980 2000 40 50 60 70 80 Life Year Country China India Indonesia Japan Korea, Rep.

“Given gapminder Asia: x is year, y is life, color by country, lines and also points.”

Both the lines and the points share the same color per country.

9.4.1 Giving each layer its own appearance

You do not have to map a column to color at all. Sometimes you want one color for the lines and another for the points, chosen by you rather than read from the data. That is a setting, written style(color = ), and no legend appears. group(country) still keeps one line per country: it splits without coloring. style() attaches to the nearest preceding mark and stops there, so each layer can be styled independently:

data(gapminder_asia) + x(year) + y(life) +
  line + group(country) + style(color = "steelblue") +
  point + style(color = "firebrick", size = 3) +
  title("Blue line, red points: styled per layer")
(data(gapminder_asia) + x(col.year) + y(col.life) +
  line + group(col.country) + style(color = "steelblue") +
  point + style(color = "firebrick", size = 3) +
  title("Blue line, red points: styled per layer"))
data(gapminder_asia) + x(:year) + y(:life) + line + group(:country) +
  style(color = "steelblue") + point +
  style(color = "firebrick", size = 3) +
  title("Blue line, red points: styled per layer")
plot(data(gapminder_asia), x(col.year), y(col.life), line,
  group(col.country), style({ color: "steelblue" }), point,
  style({ color: "firebrick", size: 3 }),
  title("Blue line, red points: styled per layer"))
1960 1980 2000 40 50 60 70 80 Blue line, red points: styled per layer Life Year

“Given gapminder Asia: x is year, y is life, lines grouped by country, colored steelblue, and also points, colored firebrick, with size 3.”

A channel can belong to the whole plot or to one layer. Written before any mark, it belongs to the plot and reaches every layer, as color(country) did above. Written after a mark, it belongs to that mark alone, like style(), so each layer can map a different column. The whole rule, for every atom, is one table in Encoding scope:

data(gapminder_asia) + x(year) + y(life) +
  line  + color(country) +
  point + size(population) +
  title("Color on the lines, size on the points")
(data(gapminder_asia) + x(col.year) + y(col.life) +
  line  + color(col.country) +
  point + size(col.population) +
  title("Color on the lines, size on the points"))
data(gapminder_asia) + x(:year) + y(:life) + line + color(:country) +
  point + size(:population) +
  title("Color on the lines, size on the points")
plot(data(gapminder_asia), x(col.year), y(col.life), line,
  color(col.country), point, size(col.population),
  title("Color on the lines, size on the points"))
1960 1980 2000 40 50 60 70 80 Color on the lines, size on the points Life Year Country China India Indonesia Japan Korea, Rep. Population 20.9M 669.8M 1.3B

“Given gapminder Asia: x is year, y is life, lines colored by country, and also points sized by population.”

That works because a channel written after a mark belongs only to that mark. If it also applied to the earlier line, the line would get a size for each row, which it cannot take, and gog would refuse the plot. See Encoding scope.

9.5 A category on the domain: the profile

In Point, swapping the two bindings turned a strip plot on its side. A line answers the same swap differently. A line’s two axes are not interchangeable. x is the domain, the axis the line is read along. y is the measure, the quantity it traces.

A bar draws horizontally when you swap its bindings, because its categorical column says which axis holds the slots. A line usually has no such column, because its two columns are both numbers, such as year and life. So gog does not read the direction from the columns: the domain is always x. The quarterly table, despite its name, holds one sales figure for each year. With the years on x, the line is the plot you expect:

quarterly: first 5 of 19 rows
year sales
2005 95
2006 104
2007 118
2008 112
2009 88
data(quarterly) + line + x(year) + y(sales) +
  title("Years on x: sales read along the years")
(data(quarterly) + line + x(col.year) + y(col.sales) +
  title("Years on x: sales read along the years"))
data(quarterly) + line + x(:year) + y(:sales) +
  title("Years on x: sales read along the years")
plot(data(quarterly), line, x(col.year), y(col.sales),
  title("Years on x: sales read along the years"))
2005 2010 2015 2020 100 120 140 160 180 Years on x: sales read along the years Sales Year

“Given quarterly: a line, x is year, y is sales.”

Sales climbed for most of the years. They fell in 2008 and 2009, and again in 2020, and rose again after each fall. Swap the two bindings and the line still sorts along x, which is now sales. The stroke starts at 2009, the year with the lowest sales, and puts 2020 between 2012 and 2013, so the years are scrambled:

data(quarterly) + line + x(sales) + y(year) +
  title("The bindings swapped: the line still sorts along x")
(data(quarterly) + line + x(col.sales) + y(col.year) +
  title("The bindings swapped: the line still sorts along x"))
data(quarterly) + line + x(:sales) + y(:year) +
  title("The bindings swapped: the line still sorts along x")
plot(data(quarterly), line, x(col.sales), y(col.year),
  title("The bindings swapped: the line still sorts along x"))
100 120 140 160 180 2005 2010 2015 2020 The bindings swapped: the line still sorts along x Year Sales

“Given quarterly: a line, x is sales, y is year.”

The zigzag seems to show a value that rose and fell many times. It does not. Each segment joins a year to the year with the next higher sales, not to the next year, so the rises and falls come from the sorting. Only the overall slope is real, and only because sales were mostly higher in later years.

Put a category on y instead and gog refuses it, as What it refuses shows. So the swap has two outcomes. When both columns are numbers, gog accepts it, and the line sorts along the new x and draws a different picture. When one column is a category, gog refuses the swap, because that column must stay on x.

Sometimes the domain has to run up the page, as depth does in a plot of ocean temperature, or as the years do here. Then write path instead. A path joins the rows in the order the table holds them, so a table sorted along the domain draws the vertical series the line could not:

data(quarterly) + path + x(sales) + y(year) +
  title("The same rows, joined in table order")
(data(quarterly) + path + x(col.sales) + y(col.year) +
  title("The same rows, joined in table order"))
data(quarterly) + path + x(:sales) + y(:year) +
  title("The same rows, joined in table order")
plot(data(quarterly), path, x(col.sales), y(col.year),
  title("The same rows, joined in table order"))
100 120 140 160 180 2005 2010 2015 2020 The same rows, joined in table order Year Sales

“Given quarterly: a path, x is sales, y is year.”

The domain takes a category as readily as a number, and connecting one summary per category is the profile plot:

gapminder_2007: first 5 of 142 rows
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) + line * mean + point * mean + x(continent) + y(life) +
  y_label("Mean life expectancy") +
  title("line * mean + x(continent): one value per category, joined")
(data(gapminder_2007) + line * mean + point * mean + x(col.continent) + y(col.life) +
  y_label("Mean life expectancy") +
  title("line * mean + x(continent): one value per category, joined"))
data(gapminder_2007) + line * mean + point * mean + x(:continent) +
  y(:life) + y_label("Mean life expectancy") +
  title("line * mean + x(continent): one value per category, joined")
plot(data(gapminder_2007), layer(line, mean), layer(point, mean),
  x(col.continent), y(col.life), y_label("Mean life expectancy"),
  title("line * mean + x(continent): one value per category, joined"))
Asia Europe Africa Americas Oceania 60 70 80 line * mean + x(continent): one value per category, joined Mean life expectancy Continent

“Given gapminder 2007: a line derived by mean and also points derived by mean, x is continent, y is life.”

The mean transform is what makes the profile readable. Without it, a line connects all 142 rows across five category slots:

data(gapminder_2007) + line + x(continent) + y(life) +
  title("Without the transform: 142 rows across five slots")
(data(gapminder_2007) + line + x(col.continent) + y(col.life) +
  title("Without the transform: 142 rows across five slots"))
data(gapminder_2007) + line + x(:continent) + y(:life) +
  title("Without the transform: 142 rows across five slots")
plot(data(gapminder_2007), line, x(col.continent), y(col.life),
  title("Without the transform: 142 rows across five slots"))
Asia Europe Africa Americas Oceania 40 50 60 70 80 Without the transform: 142 rows across five slots Life Continent

“Given gapminder 2007: a line, x is continent, y is life.”

No reader can follow that. Inside each slot the stroke runs up and down through every country, and between slots it joins the last country of one slot to the first country of the next. gog prints a warning in your session when a line has more than five rows and nothing splits or summarizes them. A single series, such as the quarterly line, gets the same message; there one stroke is what you want. mean leaves exactly one point per category, so the stroke joins five summaries instead of 142 rows.

Does the connection mean anything? That depends on your data. Nothing was measured between Africa and the Americas, and the segment is still drawn there. If you read only the vertices, the segments help you compare the five heights. If you read the segments themselves, they claim a change that never happened. The grammar draws what you asked for. You decide whether your data supports the connection. When it does not, keep the mean and change the mark to bar. A bar is in its own slot and claims nothing between one slot and the next. An ordered category, such as a grade, a clothing size or a survey scale, is the case that suits a line, because it has a direction for the line to follow. Two years stored as text are such a category. One line for each country, from its value in the first year to its value in the second, is the slope chart (Comparing categories).

Continents have no order, but a straight axis must still put one of them first and another last. A circle has no first or last place. Drop the point layer and add polar(), and the same profile bends into a circle. That is the radar chart, and it needs no new atom:

data(gapminder_2007) + line * mean + x(continent) + y(life) + polar() +
  title("line * mean + polar(): the radar")
(data(gapminder_2007) + line * mean + x(col.continent) + y(col.life) + polar() +
  title("line * mean + polar(): the radar"))
data(gapminder_2007) + line * mean + x(:continent) + y(:life) + polar() +
  title("line * mean + polar(): the radar")
plot(data(gapminder_2007), layer(line, mean), x(col.continent),
  y(col.life), polar(), title("line * mean + polar(): the radar"))
Asia Europe Africa Americas Oceania 60 70 80 line * mean + polar(): the radar Life Continent

“Given gapminder 2007: a line derived by mean, x is continent, y is life, in polar.”

Look at the segment from Oceania back to Asia. On the flat profile the line has two ends, because a straight axis has two ends. In the circle the five categories are spread around the whole ring, so the last one is beside the first and the line closes. No atom was added to close the ring. A categorical angular axis wraps, and the mark follows the space it is drawn in. The Polar chapter covers this in full.

9.6 Smoothed trend line

People in richer countries tend to live longer. The relationship between gdp and life is positive: as one rises, the other tends to rise as well. Sometimes the direction of a relationship is all a reader needs to know. Combine line with the smooth transform to draw a trend line fitted through all the rows instead of the rows themselves:

data(gapminder_2007) + x(gdp) + y(life) +
  point + color(continent) +
  line * smooth
(data(gapminder_2007) + x(col.gdp) + y(col.life) +
  point + color(col.continent) +
  line * smooth)
data(gapminder_2007) + x(:gdp) + y(:life) + point + color(:continent) +
  line * smooth
plot(data(gapminder_2007), x(col.gdp), y(col.life), point,
  color(col.continent), layer(line, smooth))
0 10K 20K 30K 40K 50K 40 50 60 70 80 Life Gdp Continent Asia Europe Africa Americas Oceania

“Given gapminder 2007: x is gdp, y is life, points colored by continent, and also a line derived by smooth.”

The point layer keeps every country visible, and smooth reduces the same rows to one fitted curve. Only the line layer carries the transform, so the points are untouched.

smooth is not the line’s own transform. Four other marks take it too, and each draws the same fitted curve in its own way:

Mark derived by smooth What it draws
point * smooth one dot at each fitted value
area * smooth the curve filled to the baseline
bar * smooth one thin bar at each fitted value
step * smooth the curve as a staircase
line * smooth the fitted curve above

9.7 A running total

A line can show how a quantity adds up over time. The cumulative transform adds each year’s value to the years before it, so the line passes through the total so far. The quarterly table holds one year of sales in each row:

data(quarterly) + line * cumulative + x(year) + y(sales) +
  title("Sales so far, since 2005")
(data(quarterly) + line * cumulative + x(col.year) + y(col.sales) +
  title("Sales so far, since 2005"))
data(quarterly) + line * cumulative + x(:year) + y(:sales) +
  title("Sales so far, since 2005")
plot(data(quarterly), layer(line, cumulative), x(col.year), y(col.sales),
  title("Sales so far, since 2005"))
2005 2010 2015 2020 0 1K 2K Sales so far, since 2005 Cumulative Year

“Given quarterly: a line derived by cumulative, x is year, y is sales.”

The line ends at 2,595, the sales of all 19 years. It always rises, because the sales of every year are positive. The rise from one year to the next is the sales of the later year. So the flattest piece is the one that ends at 2009, when sales fell to 88. With color or group, each line adds up its own rows.

9.8 Two boundaries from a pair

Sometimes the answer at each x is two values rather than one: a lowest and a highest, or the spread around a mean. Most transforms give a line one value per x. A pair transform gives it two, a low and a high. A line then draws both boundaries as two separate lines, not one zigzag joining them. A ribbon takes the same pair and fills the band between the two boundaries; a line draws only the boundaries. There are six pair transforms: range, confidence, deviation, smooth_band, straight_band and bounds. Here are the lowest and the highest life expectancy among the five countries in each year, drawn as two lines:

data(gapminder_asia) + line * range + x(year) + y(life) +
  y_label("Life expectancy") +
  title("The lowest and highest life expectancy, as two lines")
(data(gapminder_asia) + line * range + x(col.year) + y(col.life) +
  y_label("Life expectancy") +
  title("The lowest and highest life expectancy, as two lines"))
data(gapminder_asia) + line * range + x(:year) + y(:life) +
  y_label("Life expectancy") +
  title("The lowest and highest life expectancy, as two lines")
plot(data(gapminder_asia), layer(line, range), x(col.year), y(col.life),
  y_label("Life expectancy"),
  title("The lowest and highest life expectancy, as two lines"))
1960 1980 2000 40 50 60 70 80 The lowest and highest life expectancy, as two lines Life expectancy Year

“Given gapminder Asia: a line derived by range, x is year, y is life.”

Add style(pattern = "dashed") for the dashed band often used in published figures. Ribbon shows the same pair filled and then unfilled.

9.9 Stroke width and opacity

You may want thicker lines for a slide, or fainter ones where many lines cross. Each line is drawn as a single stroke from its first point to its last. So size and opacity are not channels here: one stroke has one width and one opacity, and no row can change them. That one stroke still has a width and an opacity, and style() sets them:

data(gapminder_asia) + line + x(year) + y(life) + group(country) +
  style(size = 4, opacity = 0.45) +
  title("One width, one opacity, for the whole layer")
(data(gapminder_asia) + line + x(col.year) + y(col.life) + group(col.country) +
  style(size = 4, opacity = 0.45) +
  title("One width, one opacity, for the whole layer"))
data(gapminder_asia) + line + x(:year) + y(:life) + group(:country) +
  style(size = 4, opacity = 0.45) +
  title("One width, one opacity, for the whole layer")
plot(data(gapminder_asia), line, x(col.year), y(col.life),
  group(col.country), style({ size: 4, opacity: 0.45 }),
  title("One width, one opacity, for the whole layer"))
1960 1980 2000 40 50 60 70 80 One width, one opacity, for the whole layer Life Year

“Given gapminder Asia: lines, x is year, y is life, grouped by country, with size 4 and opacity 0.45.”

Two questions look alike and are not the same: “Does this mark have this property?” and “Can a column be mapped here?” For a line’s width, the first answer is yes and the second is no. Setting vs mapping uses a line as its clearest case.

Setting one pale color on a layer is the usual way to make another layer easier to see:

data(gapminder_asia) + x(year) + y(life) +
  line + group(country) + style(color = "lightgray") +
  line * smooth + style(color = "firebrick", size = 3) +
  title("Every country in gray, the trend in red")
(data(gapminder_asia) + x(col.year) + y(col.life) +
  line + group(col.country) + style(color = "lightgray") +
  line * smooth + style(color = "firebrick", size = 3) +
  title("Every country in gray, the trend in red"))
data(gapminder_asia) + x(:year) + y(:life) + line + group(:country) +
  style(color = "lightgray") + line * smooth +
  style(color = "firebrick", size = 3) +
  title("Every country in gray, the trend in red")
plot(data(gapminder_asia), x(col.year), y(col.life), line,
  group(col.country), style({ color: "lightgray" }), layer(line, smooth),
  style({ color: "firebrick", size: 3 }),
  title("Every country in gray, the trend in red"))
1960 1980 2000 40 50 60 70 80 Every country in gray, the trend in red Life Year

Each layer here carries its own style(). Unlike a channel, style() cannot belong to the whole plot: written before any mark, it is an error, because a plot has no appearance of its own to style.

9.10 Each country and its continent’s mean or median

The plot above draws five countries and one trend. The gm_all table holds 142 countries, measured every five years from 1952 to 2007. In one panel, 142 gray lines are too many to read. One trend through all of them would also hide how the continents differ. So draw one panel for each continent, with its countries in gray and their mean as one blue line.

The sentence below has two line layers. The first draws one gray line for each country, as above. The second is line * mean. In each year, mean averages the values of the countries, so it leaves one value per year, and the line joins those values. | facet(continent) splits the whole plot into one panel per continent. Every layer is split the same way, so the blue line in each panel is the mean of that continent’s countries. Faceting explains the operator in full.

gm_all: first 5 of 1704 rows
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_all) + x(year) + y(life) +
  line + group(country) + style(color = "lightgray") +
  line * mean + style(color = "steelblue", size = 3) +
  y_label("Life expectancy") +
  title("Every country, and the continent's mean") | facet(continent)
(data(gm_all) + x(col.year) + y(col.life) +
  line + group(col.country) + style(color = "lightgray") +
  line * mean + style(color = "steelblue", size = 3) +
  y_label("Life expectancy") +
  title("Every country, and the continent's mean") | facet(col.continent))
data(gm_all) + x(:year) + y(:life) + line + group(:country) +
  style(color = "lightgray") + line * mean +
  style(color = "steelblue", size = 3) + y_label("Life expectancy") +
  title("Every country, and the continent's mean") | facet(:continent)
plot(data(gm_all), x(col.year), y(col.life), line, group(col.country),
  style({ color: "lightgray" }), layer(line, mean),
  style({ color: "steelblue", size: 3 }), y_label("Life expectancy"),
  title("Every country, and the continent's mean"), across(col.continent))
1960 1980 2000 40 60 80 1960 1980 2000 1960 1980 2000 1960 1980 2000 1960 1980 2000 Asia Europe Africa Americas Oceania Every country, and the continent's mean Life expectancy Year

“Given all the gapminder years: x is year, y is life, lines grouped by country, and also a line derived by mean, split into panel columns by continent.”

The blue line is the continent’s mean, and the gray lines show how far each country was from it. Every continent’s mean rose, but Africa’s stopped rising for fifteen years. It rose from 39 years in 1952 to 53 in 1987, and in 2002 it was still 53. In 22 of Africa’s 52 countries, life expectancy was lower in 2002 than in 1987. In Zimbabwe it fell from 62 years to 40. The table has only two countries in Oceania, Australia and New Zealand, so the blue line almost covers their two gray lines.

Income is a second way to compare the continents. The same two layers can draw GDP per person, with two changes to the sentence. The first change is the scale. GDP per person runs from a few hundred dollars to more than a hundred thousand, so the next plot draws it on a log scale. On this scale each labeled tick is ten times the one below it, so the poorer countries are not crowded together at the bottom.

The second change is the summary. gog computes mean in dollars, and only then places the result on the log axis (The measured value keeps its own units). In dollars, one very rich country can raise the mean far above most of the others. In 1952, Kuwait’s GDP per person was 108,382 dollars, and Asia’s mean was 5,195. Sorted from poorest to richest, the middle one of Asia’s 33 countries had 1,207, less than a quarter of the mean. In the Asia panel, a mean line would therefore run above most of the gray lines. The median is that middle value, and sorting the countries gives the same order in dollars as in logarithms. So line * median has half of the gray lines above it and half below, on a log axis too:

data(gm_all) + x(year) + y(gdp, scale = "log") +
  line + group(country) + style(color = "lightgray") +
  line * median + style(color = "steelblue", size = 3) +
  y_label("GDP per person") +
  title("GDP per person on five continents") | facet(continent)
(data(gm_all) + x(col.year) + y(col.gdp, scale = "log") +
  line + group(col.country) + style(color = "lightgray") +
  line * median + style(color = "steelblue", size = 3) +
  y_label("GDP per person") +
  title("GDP per person on five continents") | facet(col.continent))
data(gm_all) + x(:year) + y(:gdp, scale = "log") + line +
  group(:country) + style(color = "lightgray") + line * median +
  style(color = "steelblue", size = 3) + y_label("GDP per person") +
  title("GDP per person on five continents") | facet(:continent)
plot(data(gm_all), x(col.year), y(col.gdp, { scale: "log" }), line,
  group(col.country), style({ color: "lightgray" }), layer(line, median),
  style({ color: "steelblue", size: 3 }), y_label("GDP per person"),
  title("GDP per person on five continents"), across(col.continent))
1960 1980 2000 1K 10K 100K 1960 1980 2000 1960 1980 2000 1960 1980 2000 1960 1980 2000 Asia Europe Africa Americas Oceania GDP per person on five continents GDP per person Year

“Given all the gapminder years: x is year, y is gdp on a log scale, lines grouped by country, and also a line derived by median, split into panel columns by continent.”

The medians grew at very different rates. Europe’s rose from 5,142 dollars in 1952 to 28,054 in 2007, more than five times its 1952 value. Africa’s rose from 987 dollars to 1,452, less than one and a half times its 1952 value. In 2002 it was lower than in 1972.

9.11 What you can set

A setting changes how the stroke looks without reading a column, and each mark takes its own. These are a line’s settings, 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, dotdash, longdash

A line is a stroke, so size is its width and pattern its dash. A setting gives the whole layer one value. A channel reads a column instead, so what is drawn follows the data.

Two of the four above are also channels. color(country) draws one color per country. A mapped pattern dashes the series instead. pattern gives a stroke five values, solid among them, shown here in the order gog assigns them:

dashes <- data.frame(
  dash  = c("solid", "solid", "dashed", "dashed", "dotted", "dotted",
            "dotdash", "dotdash", "longdash", "longdash"),
  at    = c(1, 2, 1, 2, 1, 2, 1, 2, 1, 2),
  level = c(5, 5, 4, 4, 3, 3, 2, 2, 1, 1)
)
data(dashes) + line + x(at) + y(level) + pattern(dash) +
  style(size = 2) + x_label("") + y_label("") + title("The five dashes")
dashes = {
  "dash": ["solid", "solid", "dashed", "dashed", "dotted", "dotted",
            "dotdash", "dotdash", "longdash", "longdash"],
  "at": [1, 2, 1, 2, 1, 2, 1, 2, 1, 2],
  "level": [5, 5, 4, 4, 3, 3, 2, 2, 1, 1],
}
(data(dashes) + line + x(col.at) + y(col.level) + pattern(col.dash) +
  style(size = 2) + x_label("") + y_label("") + title("The five dashes"))
dashes = (
  dash = ["solid", "solid", "dashed", "dashed", "dotted", "dotted", "dotdash", "dotdash", "longdash", "longdash"],
  at = [1, 2, 1, 2, 1, 2, 1, 2, 1, 2],
  level = [5, 5, 4, 4, 3, 3, 2, 2, 1, 1],
)
data(dashes) + line + x(:at) + y(:level) + pattern(:dash) +
  style(size = 2) + x_label("") + y_label("") + title("The five dashes")
const dashes = {
  dash: ["solid", "solid", "dashed", "dashed", "dotted", "dotted", "dotdash", "dotdash", "longdash", "longdash"],
  at: [1, 2, 1, 2, 1, 2, 1, 2, 1, 2],
  level: [5, 5, 4, 4, 3, 3, 2, 2, 1, 1],
};
plot(data(dashes), line, x(col.at), y(col.level), pattern(col.dash),
  style({ size: 2 }), x_label(""), y_label(""), title("The five dashes"))
1.0 1.2 1.4 1.6 1.8 2.0 1 2 3 4 5 The five dashes Dash solid dashed dotted dotdash longdash

“Given the dashes table: lines, x is at, y is level, pattern by dash, with size 2.”

A sixth series gets the first of the five again, and gog says so: see pattern. opacity and size can only be set, because a width and an opacity belong to the whole stroke, so no row can change them. color is different. It is the only one that can vary along a single stroke, because a color belongs to a place on the stroke rather than to the whole stroke. What it refuses ends with that case, a line colored by a continuous column.

Set size and pattern together, and every series takes the same width and the same dash:

data(gapminder_asia) + line + x(year) + y(life) + color(country) +
  style(size = 3, pattern = "dashed") +
  y_label("Life expectancy") + title("One width and one dash, for every series")
(data(gapminder_asia) + line + x(col.year) + y(col.life) + color(col.country) +
  style(size = 3, pattern = "dashed") +
  y_label("Life expectancy") + title("One width and one dash, for every series"))
data(gapminder_asia) + line + x(:year) + y(:life) + color(:country) +
  style(size = 3, pattern = "dashed") + y_label("Life expectancy") +
  title("One width and one dash, for every series")
plot(data(gapminder_asia), line, x(col.year), y(col.life),
  color(col.country), style({ size: 3, pattern: "dashed" }),
  y_label("Life expectancy"),
  title("One width and one dash, for every series"))
1960 1980 2000 40 50 60 70 80 One width and one dash, for every series Life expectancy Year Country China India Indonesia Japan Korea, Rep.

“Given gapminder Asia: lines, x is year, y is life, color by country, with size 3 and pattern dashed.”

Each property takes its value from one source: a column, or you. Here color reads a column, and size and pattern are set, so no property is given a value twice. Ask for both on one property and gog refuses, rather than dropping one in silence. Setting vs mapping shows the refusal.

A transform’s own arguments are neither a mapping nor a setting. They change what is computed, not how it is drawn. line * density(adjust = 1.5) widens the bandwidth, so the curve is smoother than the automatic one. Transforms lists what each one takes.

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.

9.12 What it refuses

A dash is the only texture a stroke can carry. A hatch fills an area, and a line has no area to fill.

data(gapminder_asia) + line + x(year) + y(life) + style(pattern = "hatch")
data(gapminder_asia) + line + x(col.year) + y(col.life) + style(pattern = "hatch")
data(gapminder_asia) + line + x(:year) + y(:life) +
  style(pattern = "hatch")
plot(data(gapminder_asia), line, x(col.year), y(col.life),
  style({ pattern: "hatch" }))
Error:
! gog: `style(pattern = "hatch")` is not a stroke pattern. Use "solid" (the default), "dashed", "dotted", "dotdash", "longdash".
gog: nothing was rendered. Fix the above, or set GOG_STRICT=0 to draw anyway.

The refusal lists the five values pattern does take on a stroke. Two more refusals follow from earlier sections. The first is a categorical column on y, refused because y carries the quantity a line traces, and category names are not a quantity. A category on the domain showed where that rule comes from:

data(gapminder_2007) + line * mean + x(life) + y(continent)
data(gapminder_2007) + line * mean + x(col.life) + y(col.continent)
data(gapminder_2007) + line * mean + x(:life) + y(:continent)
plot(data(gapminder_2007), layer(line, mean), x(col.life),
  y(col.continent))
Error:
! gog: `y(continent)` maps a categorical (text) column, but `y` on `line` needs a continuous (numeric) column. On these marks `x` is the domain and `y` the measure, and a category is not a quantity to measure: a mean of category names is not a number, and a region has no categorical baseline to close on. Put the category on `x` instead — `line * mean + x(<category>) + y(<number>)` is the profile plot, and `area * mean` fills it. Unlike `bar`/`box`/`interval`, these marks do not read their orientation off the bindings, because their two axes do not have the same role.
gog: nothing was rendered. Fix the above, or set GOG_STRICT=0 to draw anyway.

The second refusal was described under Stroke width and opacity. A mapped size or opacity is refused, because one stroke has one width and one opacity, and no row can vary them.

color is the exception, because a color belongs to a place on the stroke rather than to the whole stroke. So a continuous column mapped to color changes the color along one line, and it gets a ramp legend rather than one color per series:

data(gapminder_asia) + line + x(year) + y(life) + group(country) +
  color(gdp, scale = "log") + palette("viridis") +
  y_label("Life expectancy") + title("Each country's line, colored by its income")
(data(gapminder_asia) + line + x(col.year) + y(col.life) + group(col.country) +
  color(col.gdp, scale = "log") + palette("viridis") +
  y_label("Life expectancy") + title("Each country's line, colored by its income"))
data(gapminder_asia) + line + x(:year) + y(:life) + group(:country) +
  color(:gdp, scale = "log") + palette("viridis") +
  y_label("Life expectancy") +
  title("Each country's line, colored by its income")
plot(data(gapminder_asia), line, x(col.year), y(col.life),
  group(col.country), color(col.gdp, { scale: "log" }),
  palette("viridis"), y_label("Life expectancy"),
  title("Each country's line, colored by its income"))
1960 1980 2000 40 50 60 70 80 Each country's line, colored by its income Life expectancy Year Gdp 31.7K 3560.4 400.4

“Given gapminder Asia: lines, x is year, y is life, grouped by country, color by gdp on a log scale, with the viridis palette.”

palette() chooses the colors a mapping uses. "viridis" is a ramp, and the named palettes and what each is for are in Palettes.

Read one country’s line from left to right and you see two variables. The height is how long people live, and the color is how rich the country was then. group still splits the mark into one line per country; color carries the second variable, income. A categorical color splits and colors at the same time, which is what Multiple series showed above.

One last refusal concerns the series themselves. A line needs at least two positions in each series, because a stroke runs from one position to the next. The profile in A category on the domain joins five means. Drop its point layer and color the line by continent:

data(gapminder_2007) + line * mean + x(continent) + y(life) + color(continent)
data(gapminder_2007) + line * mean + x(col.continent) + y(col.life) + color(col.continent)
data(gapminder_2007) + line * mean + x(:continent) + y(:life) +
  color(:continent)
plot(data(gapminder_2007), layer(line, mean), x(col.continent),
  y(col.life), color(col.continent))
Error:
! gog: `line` draws each series through at least two positions, and split by `color(continent)`, every series has one, so nothing would be drawn. Use `point * mean`, which draws each position on its own, or remove a split so each series has more rows.
gog: nothing was rendered. Fix the above, or set GOG_STRICT=0 to draw anyway.

Each continent is now a series that holds one mean, so nothing would be drawn, and gog refuses the sentence. A split from color, group, pattern, facet or play is refused in the same way, when it leaves one position in every series and no other layer draws. When only some series hold one position, gog draws the others and prints a message naming the ones it left out. Add point with the line’s transform, such as point * mean, and those positions are drawn as well, as in Layering a line over other marks.

9.13 In short

A categorical color, a group or a mapped pattern splits the rows into one line per category. Each line needs at least two positions. Without a split, one stroke joins every row. A line always sorts along x, so row order means nothing to it. Swap the two bindings and the line re-sorts: you get a different plot, not the same one on its side. Width and opacity belong to the whole stroke, and only color varies along it.

9.14 What comes next

You read a line by comparing its values with each other, so its axis need not reach zero. Bar measures from zero instead, so one bar’s length has a meaning by itself.