Where is the line you want to compare every value against? Most marks read two positions. rule reads one, and draws a line there. Along the other axis, the line crosses the whole panel. That gives two familiar plots: a reference line at a threshold, and a rug of ticks showing where the observations are.
Most plotting libraries draw these two plots with three marks: a vertical reference line, a horizontal one, and a rug. gog has one. A vertical and a horizontal reference line differ only in which axis holds the position. gog works that out from the columns, as it does for a bar.
18.1 A line at a threshold
How many countries have a life expectancy above 70? On a scatter the reader has to find 70 on the axis and judge each point against it. With a line at 70, each point is compared with the line instead of with the axis. Here is the 2007 gapminder scatter, income against life expectancy, with lines drawn at three life-expectancy thresholds:
data(gapminder_2007) + point +x(gdp) +y(life) +data(life_bands) + rule +x_label("GDP per person") +y_label("Life expectancy") +title("Three thresholds, one table")
(data(gapminder_2007) + point + x(col.gdp) + y(col.life) + data(life_bands) + rule + x_label("GDP per person") + y_label("Life expectancy") + title("Three thresholds, one table"))
data(gapminder_2007) + point +x(:gdp) +y(:life) +data(life_bands) + rule +x_label("GDP per person") +y_label("Life expectancy") +title("Three thresholds, one table")
plot(data(gapminder_2007), point,x(col.gdp),y(col.life),data(life_bands), rule,x_label("GDP per person"),y_label("Life expectancy"),title("Three thresholds, one table"))
“Given gapminder 2007: points, x is gdp, y is life, and also rules from the life bands.”
The sentence names rule once, and the plot draws three lines. That is the mark’s most useful property: its position is a column, not a number. life_bands holds three values, one per line. Add a fourth row and a fourth line appears with no change to the plot’s sentence at all.
Every channel works this way: color(band) names a column, not a color. That is why there is no rule(70). A number typed into the plot would be one value for the whole plot. A column is data, so it can hold a different threshold for each panel of a faceted plot, and it follows the scale and the space like every other column.
A rule takes no transform, so it cannot compute a reference for you. What it refuses shows that refusal. Which reference you need decides which mark you write:
Mark
The reference it draws
rule
a line at each value in a column you supply
line * mean
the means joined into a profile
ribbon * bounds
a band between a low column and a high one
zone * bounds
a block across the panel, between the same two
For a line at the overall mean, compute the mean yourself and give it to rule as a column.
18.2 Horizontal or vertical
Nothing in the read-aloud sentence under that plot said “horizontal”. The mark took the direction from the columns instead. life_bands has a life column and no gdp column, and the plot’s y is life. One axis answers and the other does not. The rule marks a value on the axis that answers, and spans the axis that does not.
A threshold on income needs a vertical line, and there is no second mark for it. Give the rule a table that holds gdp instead, and the same word draws the line vertically:
data(gapminder_2007) + point +x(gdp) +y(life) +data(gdp_threshold) + rule +style(color ="firebrick", pattern ="dashed") +x_label("GDP per person") +y_label("Life expectancy") +title("The same mark, the other axis")
(data(gapminder_2007) + point + x(col.gdp) + y(col.life) + data(gdp_threshold) + rule + style(color ="firebrick", pattern ="dashed") + x_label("GDP per person") + y_label("Life expectancy") + title("The same mark, the other axis"))
data(gapminder_2007) + point +x(:gdp) +y(:life) +data(gdp_threshold) + rule +style(color ="firebrick", pattern ="dashed") +x_label("GDP per person") +y_label("Life expectancy") +title("The same mark, the other axis")
plot(data(gapminder_2007), point,x(col.gdp),y(col.life),data(gdp_threshold), rule,style({ color:"firebrick",pattern:"dashed" }),x_label("GDP per person"),y_label("Life expectancy"),title("The same mark, the other axis"))
gdp_threshold holds gdp, the plot’s x, so the line is vertical. gog draws both directions with one rule. ggplot2 needs two marks for them, geom_vline() and geom_hline(), and matplotlib needs axvline() and axhline().
18.3 A rug of ticks
A scatter shows where each country is on two axes at once. A second question is only about how the countries spread along the income axis. A rug tick reads one position too. It marks one value and extends a short distance into the panel from its edge. That is a threshold line, only shorter. One setting makes the line short, so a rug needs no second mark:
data(gapminder_2007) + point +x(gdp) +y(life) +style(opacity =0.45) + rule +x(gdp) +style(reach ="edge") +x_label("GDP per person") +y_label("Life expectancy") +title("Where the countries are on the income axis")
(data(gapminder_2007) + point + x(col.gdp) + y(col.life) + style(opacity =0.45) + rule + x(col.gdp) + style(reach ="edge") + x_label("GDP per person") + y_label("Life expectancy") + title("Where the countries are on the income axis"))
data(gapminder_2007) + point +x(:gdp) +y(:life) +style(opacity =0.45) + rule +x(:gdp) +style(reach ="edge") +x_label("GDP per person") +y_label("Life expectancy") +title("Where the countries are on the income axis")
plot(data(gapminder_2007), point,x(col.gdp),y(col.life),style({ opacity:0.45 }), rule,x(col.gdp),style({ reach:"edge" }),x_label("GDP per person"),y_label("Life expectancy"),title("Where the countries are on the income axis"))
“Given gapminder 2007: points, x is gdp, y is life, and also rules, x is gdp, with reach edge.”
The rule names x(gdp) even though the points already named it. This layer has no table of its own, so it reads the scatter’s, which holds both gdp and life. Both axes answer, so the rule has to say which one it is on. The life_bands table holds only life, which is why those rules named no position.
Look along the bottom. Each tick is one country, drawn at its own income. Below $5,000 the ticks merge into one solid block: 66 of the 142 countries are in the first tenth of the income axis. The scatter holds the same countries but spreads them up the other axis, so the crowding is harder to see.
In general a rug puts the one-dimensional distribution of one column in the margin of a plot that already shows something else. You read the spread of gdp and still see the scatter, with no second plot whose axis you would have to match.
Everything else about the mark is unchanged. style(reach = ) takes one of two words: panel, the default, and edge. You do not give it a length. A tick’s length comes from the panel: a small share of it, held between 4 and 14 pixels so that it stays visible in a small facet. A number in place of edge would be a pixel count, and it would mean something different in every facet and at every plot size.
18.3.1 The other axis
The rug so far has been along the bottom, on income. Life expectancy has a distribution of its own, and the overlapping points hide it the same way. A rug is not only for the horizontal axis. The position you name on the layer decides which axis it is on. Write y(life) instead of x(gdp) and the ticks run up the left edge. Write both, as two layers, and the scatter has a rug along the bottom and another up the left at once:
data(gapminder_2007) + point +x(gdp) +y(life) +style(opacity =0.45) + rule +x(gdp) +style(reach ="edge") + rule +y(life) +style(reach ="edge") +x_label("GDP per person") +y_label("Life expectancy") +title("Both margins, from two layers")
(data(gapminder_2007) + point + x(col.gdp) + y(col.life) + style(opacity =0.45) + rule + x(col.gdp) + style(reach ="edge") + rule + y(col.life) + style(reach ="edge") + x_label("GDP per person") + y_label("Life expectancy") + title("Both margins, from two layers"))
data(gapminder_2007) + point +x(:gdp) +y(:life) +style(opacity =0.45) + rule +x(:gdp) +style(reach ="edge") + rule +y(:life) +style(reach ="edge") +x_label("GDP per person") +y_label("Life expectancy") +title("Both margins, from two layers")
plot(data(gapminder_2007), point,x(col.gdp),y(col.life),style({ opacity:0.45 }), rule,x(col.gdp),style({ reach:"edge" }), rule,y(col.life),style({ reach:"edge" }),x_label("GDP per person"),y_label("Life expectancy"),title("Both margins, from two layers"))
Each rug names one axis, and both read the scatter’s table. A single rule layer that names both is refused, because a rule draws one line per row, placed by one column. The message suggests the two layers above, as What it refuses shows.
The left edge shows a different distribution from the bottom edge. The ticks are dense above 70, thinnest between 60 and 70, and a little denser again from 60 down to 40. Of the 142 countries, 83 are above 70 and 43 below 60, with only 16 between. This is a distribution with two groups and a thin stretch between them, not a single group. The bottom rug cannot show this, because its ticks are placed by income. In the scatter it is harder to see, because the same countries are spread over two axes.
18.4 Coloring the lines
The three threshold lines in the first plot are all drawn alike, and nothing says what each one stands for. Give each line its own color instead, and the legend prints the band’s name beside that color. A rule draws one line per row of its table, so color gives each row its own color. life_bands has a band column naming each threshold, and that is the column to use:
data(gapminder_2007) + point +x(gdp) +y(life) +style(color ="lightgray") +data(life_bands) + rule +color(band) +x_label("GDP per person") +y_label("Life expectancy") +title("One line per band, named in the legend")
(data(gapminder_2007) + point + x(col.gdp) + y(col.life) + style(color ="lightgray") + data(life_bands) + rule + color(col.band) + x_label("GDP per person") + y_label("Life expectancy") + title("One line per band, named in the legend"))
data(gapminder_2007) + point +x(:gdp) +y(:life) +style(color ="lightgray") +data(life_bands) + rule +color(:band) +x_label("GDP per person") +y_label("Life expectancy") +title("One line per band, named in the legend")
plot(data(gapminder_2007), point,x(col.gdp),y(col.life),style({ color:"lightgray" }),data(life_bands), rule,color(col.band),x_label("GDP per person"),y_label("Life expectancy"),title("One line per band, named in the legend"))
“Given gapminder 2007: points, x is gdp, y is life, and also rules from the life bands, colored by band.”
A line colored by a number changes shade along its length. A rule is one segment in one shade, and a single shade on a thin line is too hard to read a number from. So color on a rule takes categories, not a continuous ramp.
18.5 Thickening a rule
A thin line marks a threshold, but it is not the first thing a reader sees. Sometimes the thresholds are the reason the plot exists. Then the reader should find those lines first, before reading the data around them. style(size = ) sets a rule’s width, and a wide, faint rule reads as a highlighted band:
data(gapminder_2007) + point +x(:gdp) +y(:life) +style(opacity =0.55) +data(life_bands) + rule +style(color ="seagreen", size =26, opacity =0.2) +x_label("GDP per person") +y_label("Life expectancy") +title("A thick rule: emphasis, measured in pixels")
plot(data(gapminder_2007), point,x(col.gdp),y(col.life),style({ opacity:0.55 }),data(life_bands), rule,style({ color:"seagreen",size:26,opacity:0.2 }),x_label("GDP per person"),y_label("Life expectancy"),title("A thick rule: emphasis, measured in pixels"))
“Given gapminder 2007: points, x is gdp, y is life, and also rules from the life bands, colored seagreen, with size 26 and opacity 0.20.”
That is a good way to mark a value. The width is measured in pixels, not in data. Each wide rule in that plot covers a little over two years of life expectancy, only because this axis runs from 40 to 80. Facet the plot, resize it, or change the range of the data, and the same size = 26 covers a different number of years while looking identical. Nothing is wrong with that as long as the width is decoration. It is wrong as soon as the width is supposed to mean something.
18.5.1 Thickness as data
Sometimes the thickness is itself a measurement: “the healthy range is 65 to 75”, not “look here”. Then the two edges are data, and you need a mark whose edges are positions. That mark is ribbon. You give it a low column and a high column. healthy_band has two rows, one at the lowest income and one at the highest. The band therefore runs from the poorest country to the richest:
data(healthy_band) + ribbon *bounds(lo, hi) +style(color ="seagreen", opacity =0.18) +data(gapminder_2007) + point +x(gdp) +y(life) +data(life_bands) + rule +style(color ="seagreen", pattern ="dashed") +x_label("GDP per person") +y_label("Life expectancy") +title("A band whose edges are 65 and 75, and rules on top")
(data(healthy_band) + ribbon * bounds(col.lo, col.hi) + style(color ="seagreen", opacity =0.18) + data(gapminder_2007) + point + x(col.gdp) + y(col.life) + data(life_bands) + rule + style(color ="seagreen", pattern ="dashed") + x_label("GDP per person") + y_label("Life expectancy") + title("A band whose edges are 65 and 75, and rules on top"))
data(healthy_band) + ribbon *bounds(:lo, :hi) +style(color ="seagreen", opacity =0.18) +data(gapminder_2007) + point +x(:gdp) +y(:life) +data(life_bands) + rule +style(color ="seagreen", pattern ="dashed") +x_label("GDP per person") +y_label("Life expectancy") +title("A band whose edges are 65 and 75, and rules on top")
plot(data(healthy_band),layer(ribbon,bounds(col.lo, col.hi)),style({ color:"seagreen",opacity:0.18 }),data(gapminder_2007), point,x(col.gdp),y(col.life),data(life_bands), rule,style({ color:"seagreen",pattern:"dashed" }),x_label("GDP per person"),y_label("Life expectancy"),title("A band whose edges are 65 and 75, and rules on top"))
“Given the healthy band table: a ribbon from lo to hi; then given gapminder 2007: points, x is gdp, y is life, and also rules from the life bands.”
Now 65 and 75 are positions. They rescale with the axis and keep the same values in every facet. On a log axis they are still drawn at 65 and 75, not a fixed number of pixels apart. The band is written first, and layers are drawn in the order they are written, so the points are drawn over it.
Give that same table two different gdp values instead of the data’s range, and the band becomes a box bounded on both axes. That is how you shade a corner of a scatter, or one period of a time series.
Neither the band nor the box extends to the edge of the panel. A ribbon is bounded by its data, so it stops exactly at the numbers you gave it. Give it wider values and the scale grows to fit them, so a ribbon never touches the edge. For a rectangle whose sides come from the data on one axis and from the panel on the other, use zone. A zone takes a pair of values on one axis and spans the other, as a rule takes one value and spans the other.
18.6 In polar: the ring and the spoke
Wind comes from a compass direction, so its speed can be drawn on a circle, as a radar chart. A target speed on that chart is a threshold like any other. In polar one of the two axes is bent into a circle. The question is what shape the line takes then. The mark does one thing: it draws a line at one position and spans the whole of the other axis. In polar that is unchanged. A polar plot has two axes, the angle and the radius. A rule that spans the angle is a ring, and one that spans the radius is a spoke: a straight line from the center to the rim, the outer edge of the circle. The Polar chapter explains how the circle itself is built.
winds is the table that The book’s data introduced. speed_target holds one target speed:
winds: first 5 of 264 rows
direction
bearing
speed
season
N
19.612260
10.1
Winter
N
357.049499
11.0
Winter
N
349.709737
13.8
Winter
N
2.602499
7.4
Winter
N
338.210715
3.7
Summer
speed_target: its one row
speed
15
The angular axis runs all the way around the circle, and one full circle of it is called the turn. A rule that spans the whole turn closes into a ring. Here it marks a target speed on the radar chart of mean wind speed by direction, and speed is on the radial axis, the one that runs from the center outward:
data(winds) + line * mean +x(direction) +y(speed) +polar() +data(speed_target) + rule +style(color ="firebrick", pattern ="dashed") +title("A target speed, drawn as a ring")
(data(winds) + line * mean + x(col.direction) + y(col.speed) + polar() + data(speed_target) + rule + style(color ="firebrick", pattern ="dashed") + title("A target speed, drawn as a ring"))
data(winds) + line * mean +x(:direction) +y(:speed) +polar() +data(speed_target) + rule +style(color ="firebrick", pattern ="dashed") +title("A target speed, drawn as a ring")
plot(data(winds),layer(line, mean),x(col.direction),y(col.speed),polar(),data(speed_target), rule,style({ color:"firebrick",pattern:"dashed" }),title("A target speed, drawn as a ring"))
“Given the winds: a line derived by mean, x is direction, y is speed, in polar, and also rules from the speed target.”
A rule that spans the whole radius draws a spoke. Give the rule a table holding direction instead of speed, and the line is drawn at that compass point:
data(winds) + line * mean +x(direction) +y(speed) +polar() +data(data.frame(direction ="E")) + rule +style(color ="firebrick", pattern ="dashed") +title("One compass point, center to rim")
(data(winds) + line * mean + x(col.direction) + y(col.speed) + polar() + data({"direction": ["E"]}, name='data.frame(direction = "E")') + rule + style(color ="firebrick", pattern ="dashed") + title("One compass point, center to rim"))
data(winds) + line * mean +x(:direction) +y(:speed) +polar() +data((direction = ["E"],)) + rule +style(color ="firebrick", pattern ="dashed") +title("One compass point, center to rim")
plot(data(winds),layer(line, mean),x(col.direction),y(col.speed),polar(),data({ direction: ["E"] }), rule,style({ color:"firebrick",pattern:"dashed" }),title("One compass point, center to rim"))
“Given the winds: a line derived by mean, x is direction, y is speed, in polar; then given one row with direction E: a rule.”
Neither shape is a special case in the sentence you write. rule is still one mark in polar because nothing about it changed. It is written the same way, it takes its axis from its column, and it means the same thing in every space. A rule that needed a special case in polar would not be one mark. It would be three: a flat line, a ring, and a spoke.
18.7 What you can set
A threshold line has to look different from the data it crosses, and no column in the data says how it should look. You say it with a setting, and each mark accepts its own list. These are a rule’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
style(reach = )
panel, edge
Four of those are a stroke’s ordinary controls, and one is the mark’s own. style(reach = ) sets how far the rule reaches across the panel. The value edge shortens it to a tick. The other four behave here as on line or path:
“Given gapminder 2007: points, x is gdp, y is life, colored lightgray; then given one row with life 50: a rule colored firebrick, with size 4; then given one row with life 60: a rule colored seagreen, with pattern dashed; then given one row with life 70: a rule colored steelblue, with pattern dotted and size 2.5; then given one row with life 80: a rule colored black, with size 6 and opacity 0.25.”
The pattern values are the five dashes, solid among them, because a rule is a stroke. style(pattern = "hatch") is refused, and the message lists the dashes instead. A hatch is a fill’s texture. bar, area, box and ribbon take it, and so does zone, the next chapter. The setting has one name, and each mark draws it the way its shape allows. A stroke takes a dash, and a fill takes a hatch.
What a rule refuses follows from the same geometry. There is no shape, because a stroke has no glyph to choose, and there is no border. What it refuses shows the border refused.
The first sentence you are likely to try is a bare rule on the plot’s own table. That table holds both position columns, so both axes answer. Nothing in the sentence says which axis is meant, so gog refuses rather than choosing one:
Error:
! gog: `rule` marks a value on one axis and spans the other, but this table has a column for both — `x(gdp)` and `y(life)` — so there is nothing to say which axis is meant. Say it on the layer: `rule + x(gdp)` places every line by `gdp`, `rule + y(life)` by `life`. Giving the rule its own table holding just the one column works too.
gog: nothing was rendered. Fix the above, or set GOG_STRICT=0 to draw anyway.
That refusal is why every rug in this chapter names its axis, rule + x(gdp) or rule + y(life). A threshold with a table of its own needs no position. That table holds only one of the two columns, so the axis is taken from it. A bar also takes its direction from its columns, and gog refuses a bar with categories on both axes. The rule had a column for both axes. The bar has the reverse problem: neither axis holds a number, so the bar has nothing to measure, and gog will not guess.
The opposite, one layer that names both positions, is refused too. A rule draws one line per row, placed by one column, so only one of the two columns could be drawn:
Error:
! gog: `rule` draws one line per row, placed by one column, and this layer names two — `x(gdp)` and `y(life)` — so only one of them could be drawn. For lines on both axes, write two layers, one per axis: `rule + x(gdp) + rule + y(life)`.
gog: nothing was rendered. Fix the above, or set GOG_STRICT=0 to draw anyway.
The message suggests two layers, one per axis, as in the plot under The other axis.
A rule takes no transform either. It is placed by one column and spans the whole panel on the other axis. A statistic such as mean summarizes one axis for each value of the other, and a rule reads only one position:
Error:
! gog: `rule` is placed by one column and spans the other axis, so it has no measure for `mean` to compute and nowhere to put the answer. Compute the value where your data lives and give the rule a table of the results — one row per line — which is what a rule's position always is: a column.
gog: nothing was rendered. Fix the above, or set GOG_STRICT=0 to draw anyway.
The message restates what a rule is. To draw a line at the mean, compute the mean yourself and supply it as a column. A rule’s position always comes from a column.
A rule has no border either. A border is the outline of a filled shape, and a rule is a line with no inside to outline:
Error:
! gog: a `rule` is drawn with a stroke, not a filled shape — it has no separate border. `style(color = )` sets its color and `style(size = )` its width.
gog: nothing was rendered. Fix the above, or set GOG_STRICT=0 to draw anyway.
So style(color = ) is a rule’s color and style(size = ) its width. That is the same answer line, step and path give, because it is the same geometry.
18.9 In short
A rule’s position is a column and never a number, so three thresholds are three rows. With its column on x the line is vertical, and on y horizontal. style(reach = "edge") makes it a rug tick, and in polar it is a ring or a spoke. A rule on the plot’s own table must name exactly one axis, or gog refuses it. It takes no transform: compute the value yourself and give the rule that column.
18.10 What comes next
A rule marks one value. Its width is measured in pixels rather than in data. Zone is the region between two values, a rectangle across the panel rather than a line.