5  Data

Where does a plot get its data? Every sentence starts by naming a table, and everything after that depends on the name. This chapter covers the two ways to name one, data() and query(), the shape the table must have, and the order of categories that a column can carry with it.

5.1 Binding a table

Most of the time the table is already loaded in your session, and data() names it. Column names after that are bare words: no quotes, no table prefix, no indexing.

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) + point + x(gdp) + y(life)
data(gapminder_2007) + point + x(col.gdp) + y(col.life)
data(gapminder_2007) + point + x(:gdp) + y(:life)
plot(data(gapminder_2007), point, x(col.gdp), y(col.life))
0 10K 20K 30K 40K 50K 40 50 60 70 80 Life Gdp

“Given gapminder 2007: points, x is gdp, y is life.”

gog looks up gdp and life inside the gapminder_2007 table automatically. This is the Bind-Once Law: name the table once, then use columns freely.

gapminder_2007 here is an ordinary table in your session, whatever your language calls one: a data frame in R, a dict of columns or a pandas DataFrame in Python. In Julia it is a named tuple of columns or a DataFrame, and in JavaScript an object of columns or an array of rows. You do not register the table or convert it to a special type first. Part VI, The host language, shows how each language builds one. The R chapter also lists the twenty names that gog and base R both use. For the eleven that are functions, it shows how to call base R’s version, such as base::order().

Writing data() does more than supply the numbers. When you write data(gapminder_2007), gog keeps the name gapminder_2007, not only the numbers inside it. The name matters as soon as a plot has more than one table. Each layer looks up its bare column names in one of those tables, and gog finds that table by its name. Leave data() out and the table arrives with no name, so gog refuses and tells you what to write:

gapminder_2007 + point + x(gdp) + y(life)
Error:
! gog: a plot starts with `data()`, which names the table — columns are bare names and the nearest named table wins, so the name matters. Write `data(gapminder_2007) + point + ...`.

The message above is R’s. gog gives the same advice for a sentence with no data at all, point + x(gdp) with nothing before it. Each language’s package writes this advice with its own way of naming a column.

5.2 The table has to be tidy

Every table in this book has the same shape. Each row is one observation, and each column is one variable. That shape has a name, tidy data (Wickham, 2014), and gog assumes it in every sentence you write.

The reason is the rule that defines a channel in Grammar: a channel maps a column to a visual property, so anything you want to map has to be a column. Suppose you record sales for two regions, and you give each region a column of its own:

wide <- data.frame(year  = 2019:2023,
                   north = c(120, 135, 128, 152, 168),
                   south = c( 90, 101, 140, 133, 155))
wide: all 5 rows
year north south
2019 120 90
2020 135 101
2021 128 140
2022 152 133
2023 168 155

Now ask for one line per region:

data(wide) + line + x(year) + y(sales) + color(region)
data(wide) + line + x(col.year) + y(col.sales) + color(col.region)
data(wide) + line + x(:year) + y(:sales) + color(:region)
plot(data(wide), line, x(col.year), y(col.sales), color(col.region))
Error:
! gog: `y(sales)` refers to a column that is not in the data. Check the spelling of `sales`.
gog: `color(region)` refers to a column that is not in the data. Check the spelling of `region`.
gog: nothing was rendered. Fix the above, or set GOG_STRICT=0 to draw anyway.

The message names two problems: there is no column called sales, and no column called region. It asks you to check the spelling, but the spelling is not the problem. Your two regions are column names, not values in a column. Your sales numbers are in two columns instead of one. The sentence asks for columns the table does not have, so gog draws nothing.

The same numbers, one row per observation, look like this:

long <- data.frame(year   = rep(2019:2023, times = 2),
                   region = rep(c("north", "south"), each = 5),
                   sales  = c(120, 135, 128, 152, 168,
                               90, 101, 140, 133, 155))
long: all 10 rows
year region sales
2019 north 120
2020 north 135
2021 north 128
2022 north 152
2023 north 168
2019 south 90
2020 south 101
2021 south 140
2022 south 133
2023 south 155

Now sales and region are both columns, so gog draws the plot and its legend:

data(long) + line + x(year) + y(sales) + color(region)
data(long) + line + x(col.year) + y(col.sales) + color(col.region)
data(long) + line + x(:year) + y(:sales) + color(:region)
plot(data(long), line, x(col.year), y(col.sales), color(col.region))
2019 2020 2021 2022 2023 100 120 140 160 Sales Year Region north south

“Given the long table: lines, x is year, y is sales, color by region.”

gog never reshapes a table, and no transform turns columns into rows. You do that work before you write the sentence, in the language you are already using. R has tidyr::pivot_longer(). Python has pandas.melt(). Julia has DataFrames.stack(). JavaScript has no standard table library, so you write a short loop instead.

gog is stricter than ggplot2, but not about the shape of the table. ggplot2 does not reshape tables either, and it refuses the wide table above just as gog does. What differs is one thing: ggplot2 lets you calculate a value while you map it, so aes(x = log(gdp)) works there. gog does not allow that, because a channel takes a column name and nothing else. Some of those calculations are already part of the grammar. A log axis is one of them, written x(gdp, scale = "log"). For anything else, add the column to your table first. So a tidy table is not enough on its own. Every column your sentence names has to be in the table already.

This book does not teach you how to tidy data, and it should not, because there is a better book for it. Read R for Data Science by Hadley Wickham, Mine Çetinkaya-Rundel and Garrett Grolemund (Wickham et al., 2023), which is free online at https://r4ds.hadley.nz/. Its chapter on data tidying is the one that matters here. If the shape of your data is not settled yet, read that chapter before you read this one.

5.3 The plot’s table and a layer’s table

A line and the points it passes through come from one table. You should not have to name it twice. data() at the start of the sentence names the plot’s table: it applies to every layer that does not name its own table. The actuals table holds the sales of five years that have passed:

actuals: all 5 rows
year sales
2019 120
2020 135
2021 128
2022 152
2023 168
# Both layers read from "actuals"
data(actuals) + x(year) + y(sales) +
  line +
  point
(data(actuals) + x(col.year) + y(col.sales) +
  line +
  point)
data(actuals) + x(:year) + y(:sales) + line + point
plot(data(actuals), x(col.year), y(col.sales), line, point)
2019 2020 2021 2022 2023 120 130 140 150 160 170 Sales Year

“Given the actuals: x is year, y is sales, a line and also points.”

A forecast continues a history of past values, and the two usually arrive as separate tables. You want both on one pair of axes. forecast holds the three years still to come:

forecast: all 3 rows
year sales
2024 180
2025 195
2026 210

A data() that appears between two marks overrides the table for the layer that follows it:

# line uses "actuals"; point uses "forecast"
data(actuals) + x(year) + y(sales) +
  line +
  data(forecast) + point
(data(actuals) + x(col.year) + y(col.sales) +
  line +
  data(forecast) + point)
data(actuals) + x(:year) + y(:sales) + line + data(forecast) + point
plot(data(actuals), x(col.year), y(col.sales), line, data(forecast),
  point)
2020 2022 2024 2026 120 140 160 180 200 Sales Year

“Given the actuals: x is year, y is sales, a line; then given the forecast: points.”

The rule: the first data() is the plot’s table, and each later data() applies to the mark written directly after it. A mark with no data() of its own reads the plot’s table. The refusal at the start of this chapter holds for the second table too: write + data(forecast), not + forecast.

Two things follow from the rule for a later data(). gog tells you about the first, but not about the second.

A data() at the end of a sentence has no mark after it, so its table would never be drawn. gog refuses the sentence rather than leave that table out of the plot without telling you:

data(actuals) + x(year) + y(sales) +
  line +
  point +
  data(forecast)
(data(actuals) + x(col.year) + y(col.sales) +
  line +
  point +
  data(forecast))
data(actuals) + x(:year) + y(:sales) + line + point + data(forecast)
plot(data(actuals), x(col.year), y(col.sales), line, point,
  data(forecast))
Error:
! gog: `data(forecast)` ends the sentence, so no mark comes after it to read its table. A later `data()` gives its table to the mark written directly after it: write that mark next, as in `+ data(forecast) + point`, or remove `data(forecast)`.

The message names the table and gives two ways to fix the sentence: write a mark after the data(), or remove the data().

Second, a later data() applies only to the one mark written directly after it. The mark after that one reads the plot’s table again:

# line reads actuals. point reads forecast. area reads actuals again.
data(actuals) + x(year) + y(sales) +
  line +
  data(forecast) + point +
  area
(data(actuals) + x(col.year) + y(col.sales) +
  line +
  data(forecast) + point +
  area)
data(actuals) + x(:year) + y(:sales) + line + data(forecast) + point +
  area
plot(data(actuals), x(col.year), y(col.sales), line, data(forecast),
  point, area)
2020 2022 2024 2026 0 100 200 Sales Year

“Given the actuals: x is year, y is sales, a line; then given the forecast: points; then given the actuals again: an area.”

The two tables cover different years, which makes the change easy to see. actuals runs from 2019 to 2023, and forecast from 2024 to 2026. The area and the line are both on the left. The points are alone on the right. To draw two marks from the second table, write data() before each of them.

The one-mark rule fits what a second table is usually for. The axes come from the first table, and a later table adds an annotation: a threshold, a band, a note, a forecast. An annotation is one mark, so a data() that reaches one mark is enough. In this book, every table added after the first one is an annotation of this kind.

The sentence above is correct, and it still reads badly. area is written on the line of code just below data(forecast), so it looks like part of the forecast. Only the rule tells you that area draws from actuals. The same sentence is easier to read when each mark is written next to the table it draws from. Drawn again, it is the same picture:

data(actuals) + x(year) + y(sales) +
  line + area +
  data(forecast) + point
(data(actuals) + x(col.year) + y(col.sales) +
  line + area +
  data(forecast) + point)
data(actuals) + x(:year) + y(:sales) + line + area + data(forecast) +
  point
plot(data(actuals), x(col.year), y(col.sales), line, area, data(forecast),
  point)
2020 2022 2024 2026 0 100 200 Sales Year

“Given the actuals: x is year, y is sales, a line and also an area; then given the forecast: points.”

This is style, not grammar. Both spellings are legal, and both draw the same plot. The order of area and point has changed, and here that changes nothing. The two tables share no year, so neither mark is drawn over the other. gog will never refuse either spelling, because legal is not the same as clear (Law 8). Two sentences can mean the same thing and still be very different to read. Which one you write is your choice.

So group the marks by the table they read. Keep each extra data() on the same line as the mark it applies to, and keep the marks that read the plot’s table together. Every line is then one table and the marks that read it, and no mark is written under a table it does not read. The rest of this book follows that layout.

The first data() is written differently. It applies to every mark below it, so it takes a line of its own. A later data() applies to one mark only, so it shares that mark’s line of code. Alone on a line, it would seem to apply to every mark below it. That is why, in the code above, data(actuals) has a line of its own and data(forecast) shares its line with point.

5.4 When the second table names its columns differently

The plot above was the easy case: actuals and forecast both call their columns year and sales, so one pair of positions written at the front served both layers.

Plots often carry a note at a chosen point, and the note comes from a small table of its own. That table’s column names rarely match the first table’s. The milestones table marks two points on the income axis:

milestones: all 2 rows
at value note
10000 72 less steep from here
40000 80 almost flat here

Its columns are at, value and note. None of those names appear in gapminder_2007, whose columns are gdp and life. A layer may name its own columns, and the axis does not change:

data(gapminder_2007) + point + x(gdp) + y(life) +
  data(milestones) + text + x(at) + y(value) + label(note) +
  style(size = 20, color = "red")
(data(gapminder_2007) + point + x(col.gdp) + y(col.life) +
  data(milestones) + text + x(col.at) + y(col.value) + label(col.note) +
  style(size = 20, color = "red"))
data(gapminder_2007) + point + x(:gdp) + y(:life) + data(milestones) +
  text + x(:at) + y(:value) + label(:note) +
  style(size = 20, color = "red")
plot(data(gapminder_2007), point, x(col.gdp), y(col.life),
  data(milestones), text, x(col.at), y(col.value), label(col.note),
  style({ size: 20, color: "red" }))
less steep from here almost flat here 0 10K 20K 30K 40K 50K 40 50 60 70 80 Life Gdp

“Given gapminder 2007: points, x is gdp, y is life; then given the milestones: text, x is at, y is value, label by note.”

There is still one x axis and one y axis, one scale each, one set of ticks. Only the names differ, because the two tables were written by different people for different reasons.

What the two tables share is the measurement: an axis stands for one measurement. at holds income and value holds years of life, the same two quantities gdp and life hold. A table whose numbers mean something else does not belong on these axes, whatever its columns are called. If you put sales figures on an income axis, gog draws the plot without an error, but nothing it shows is true. Matching names are not the test. Matching meanings are.

The same test says when a second table does not belong in the plot at all. A layer is drawn on the axes that come from the first table, so its values must measure what those axes measure. Values that measure something else need their own axes, and a second pair of axes means a second plot. Put that plot beside the first one instead. Composition shows how, with | and /.

A layer that names no position of its own reads the axis that is already there. That is why the earlier plot worked with the positions written once at the front.

5.5 A table in a database

data() is the first of the two ways to name a table. This section shows the second, for a table kept in a database.

Not every table fits on one computer. Most of the data used at work is kept in a database. A warehouse, a database built to keep all of a company’s records, can hold billions of rows. gog draws from those tables with one word, query(), written where data() would be. It sends your SQL and receives the rows the database returns.

Nothing else in the sentence changes:

data(orders)                          + bar + x(status)
query(connection, "SELECT ...")       + bar + x(status)

The SQL is written only inside query(), as a string; nothing after it is SQL. After it, the columns are still bare names. The marks, channels, transforms and operators are the ones you have already learned. You are not writing a second language for databases. You are naming the rows a different way.

Here is a real database. The code below builds a small one in memory, so it needs no server and no password:

con <- DBI::dbConnect(RSQLite::SQLite(), ":memory:")
DBI::dbWriteTable(con, "countries", gapminder_2007)

Now the sentence. The connection is the only new part:

query(con, "SELECT gdp, life, continent FROM countries WHERE gdp > 5000") +
  point + x(gdp) + y(life) + color(continent)
10K 20K 30K 40K 50K 50 60 70 80 Life Gdp Continent Europe Africa Americas Oceania Asia

“Given the query: points, x is gdp, y is life, color by continent.”

The plot is an ordinary scatterplot. gog never saw the database. It received a table of rows, as it does for every other sentence in this book. The database chose which rows to send. gog drew what arrived.

A query and a loaded table produce the same picture, byte for byte, when they hold the same rows. The tests of all four packages check exactly that. So query() is never a different way of drawing; it is a different way of finding the rows to draw.

5.5.1 Three things to know

You open the connection yourself. gog never opens one, and it never stores a password. You connect with whatever your language already uses, and hand the open connection to query(). That also means gog adds no database driver to your install. Someone who never writes SQL never installs one.

The query runs when the plot is drawn, not when the sentence is written. Writing query(con, "...") asks the database nothing. The sentence is a specification, exactly like every other sentence in this book, and the engine draws a plot from it later. gog sends your SQL exactly as you wrote it.

Every language reaches a different set of databases. The word is the same in all four. What differs is the connection you hand it, because each language has its own database convention. Your language’s chapter says which: R, Python, Julia, JavaScript.

5.5.2 How much data comes back

query() brings every row the query returns to the computer running gog. The database does the selecting. Your machine does the drawing. Know that before you run this against a large table.

For most tables it does not matter. A few thousand rows arrive quickly, so write the simple query and stop there. Selecting the columns you need and letting gog do the rest is the normal case, and the example above is exactly that.

As a rough guide, the number of rows starts to matter at about a hundred thousand. Above that, moving the rows costs more time than drawing them. A warehouse table can hold billions, and SELECT * FROM that_table would try to bring all of them to you.

The way to avoid it is already familiar to anyone who works with a warehouse: summarize in SQL, then draw the summary. This works because a plot is nearly always a summary anyway. A bar chart of a billion orders shows only a few bars, and a database can produce those bars far more cheaply than your laptop can.

query(con, "SELECT continent, COUNT(*) AS countries
            FROM countries GROUP BY continent") +
  bar + x(continent) + y(countries)
Africa Americas Asia Europe Oceania 0 20 40 Countries Continent

“Given the query: bars, x is continent, y is countries.”

The database did the counting and sent back five rows. Without the count, SELECT continent FROM countries would have sent all 142. The bars are the same ones bar * count would draw from the loaded table.

5.5.3 Two directions, and which one suits you

If you write the summary in SQL, you may ask whether the whole plot could be written in SQL too. There is another way to give SQL users a grammar of graphics. Posit’s ggsql (Posit Software, PBC, 2026) adds visual clauses to SQL itself: you write VISUALIZE, DRAW and SCALE inside the query, and the database returns a picture.

It has two strengths. Its clauses read like SQL, so someone who writes SQL all day stays in the language they already think in, and no other language, such as R or Python, is needed at all. And it runs the whole pipeline in the database, one query per layer, so a bar chart of ten billion rows fetches only the bar heights. On that second point ggsql does something gog does not. gog brings the rows to you, so on a large table you write the summary in SQL yourself, as the count above did.

If you write only SQL, ggsql is probably the better choice. gog would first ask you to learn R, Python, Julia or JavaScript, which is a real cost for someone who uses none of them.

gog is built in the opposite direction. The grammar stays in one place and the data comes to it, so one sentence draws a loaded table, a table in Postgres, and a table in a warehouse, in any of four languages. The sentence stays the same in every language and for every kind of table.

You work The smaller cost
Only in SQL ggsql: no other language to install, and the syntax is the one you use
Across several languages gog: one grammar covers all of them, and the database too

5.6 Declaring the order of categories

Wherever the table comes from, some of its categories have an order that a column of plain text does not record. Low, Medium and High have an order every reader knows. A plot that draws them in any other order makes the reader sort them. A column of plain text has no order of its own, so gog draws its categories in the order the rows happen to arrive:

severity <- data.frame(
  level = c("High", "Low", "Medium"),
  count = c(30.0, 10.0, 20.0)
)

data(severity) + bar + x(level) + y(count) +
  title("Text column: the order the rows came in")
severity = {
  "level": ["High", "Low", "Medium"],
  "count": [30.0, 10.0, 20.0],
}
(data(severity) + bar + x(col.level) + y(col.count) +
  title("Text column: the order the rows came in"))
severity = (
  level = ["High", "Low", "Medium"],
  count = [30, 10, 20],
)
data(severity) + bar + x(:level) + y(:count) +
  title("Text column: the order the rows came in")
const severity = {
  level: ["High", "Low", "Medium"],
  count: [30, 10, 20],
};
plot(data(severity), bar, x(col.level), y(col.count),
  title("Text column: the order the rows came in"))
High Low Medium 0 10 20 30 Text column: the order the rows came in Count Level

That order is almost never the one you mean. Declare the order you mean, where you build the table. In R the declaration is a factor. Python, Julia and JavaScript have no factor type in the language itself, so gog supplies the declaration, under one name: ordered(). It takes the column’s values and the order its categories go in, and everything after it reads the same in all four languages:

severity <- data.frame(
  level = factor(c("High", "Low", "Medium"), levels = c("Low", "Medium", "High")),
  count = c(30.0, 10.0, 20.0)
)

data(severity) + bar + x(level) + y(count) +
  title("Declared: the order you asked for")
severity = {
  "level": ordered(["High", "Low", "Medium"], ["Low", "Medium", "High"]),
  "count": [30.0, 10.0, 20.0],
}
(data(severity) + bar + x(col.level) + y(col.count) +
  title("Declared: the order you asked for"))
severity = (
  level = ordered(["High", "Low", "Medium"], ["Low", "Medium", "High"]),
  count = [30, 10, 20],
)
data(severity) + bar + x(:level) + y(:count) +
  title("Declared: the order you asked for")
const severity = {
  level: ordered(["High", "Low", "Medium"], ["Low", "Medium", "High"]),
  count: [30, 10, 20],
};
plot(data(severity), bar, x(col.level), y(col.count),
  title("Declared: the order you asked for"))
Low Medium High 0 10 20 30 Declared: the order you asked for Count Level

The order goes with the column, so every part of the plot agrees: the axis, the color assignment, and the legend all read in the same order. The next plot is there only to show the legend. Its x and y are both count, so the points form a diagonal line.

data(severity) + point + x(count) + y(count) + color(level) +
  title("The legend follows the same order")
(data(severity) + point + x(col.count) + y(col.count) + color(col.level) +
  title("The legend follows the same order"))
data(severity) + point + x(:count) + y(:count) + color(:level) +
  title("The legend follows the same order")
plot(data(severity), point, x(col.count), y(col.count), color(col.level),
  title("The legend follows the same order"))
10 15 20 25 30 10 15 20 25 30 The legend follows the same order Count Count Level Low Medium High

The declared order stays even when gog does the counting for you. The first of the eight tickets below is High, but the axis of bar * count still shows Low, Medium, High. count makes one row per level, and the levels belong to the column, so the axis keeps their order, not the order of the rows:

tickets <- data.frame(
  level = factor(
    c("High", "Low", "High", "Medium", "High", "Low", "Medium", "High"),
    levels = c("Low", "Medium", "High")
  )
)

data(tickets) + bar * count + x(level) +
  title("bar * count: the levels still order the axis")
tickets = {"level": ordered(["High", "Low", "High", "Medium", "High", "Low", "Medium", "High"], ["Low", "Medium", "High"])}
(data(tickets) + bar * count + x(col.level) +
  title("bar * count: the levels still order the axis"))
tickets = (level = ordered(["High", "Low", "High", "Medium", "High", "Low", "Medium", "High"], ["Low", "Medium", "High"]),)
data(tickets) + bar * count + x(:level) +
  title("bar * count: the levels still order the axis")
const tickets = { level: ordered(["High", "Low", "High", "Medium", "High", "Low", "Medium", "High"], ["Low", "Medium", "High"]) };
plot(data(tickets), layer(bar, count), x(col.level),
  title("bar * count: the levels still order the axis"))
Low Medium High 0 1 2 3 4 bar * count: the levels still order the axis Count Level

“Given the tickets: bars derived by count, x is level.”

In R, the list of levels can mean two things. factor(x, levels = ...) says only which order the categories appear in. Adding ordered = TRUE says more: that Low really is less than High. gog draws the categories in the listed order either way. Most people list levels only to set the order on the page, and a plot needs nothing more than that.

In Python, a pandas table can carry the same declaration as a Categorical column, and in Julia a CategoricalArray can; gog reads its categories, in their order, as the levels. ordered() works without either package, so you can declare an order in plain lists.

Two rules apply:

  • Levels set the order. The data sets which categories appear. A level with no rows in the data gets no bar and no slot on the axis, because an empty labeled gap is harder to read than a shorter axis. A value in the data that the levels do not mention is not dropped. It is drawn after the listed ones. R is the exception, because of a rule in R itself. There, factor() turns a value you did not list into a missing value before gog sees it, so the row is dropped. In R, list every value you want drawn.
  • order() in the plot wins. The levels came with the table, so they apply to every plot drawn from it. order() is written in this one plot, so it applies here, and the more specific instruction wins. Name another column in order(), one that holds numbers, and the categories are ordered by its values instead. That is how you sort by value:
data(severity) + bar + x(level) + y(count) + order(count, desc = TRUE) +
  title("order() overrides the levels")
(data(severity) + bar + x(col.level) + y(col.count) + order(col.count, desc = True) +
  title("order() overrides the levels"))
data(severity) + bar + x(:level) + y(:count) +
  order(:count, desc = true) + title("order() overrides the levels")
plot(data(severity), bar, x(col.level), y(col.count),
  order(col.count, { desc: true }), title("order() overrides the levels"))
High Medium Low 0 10 20 30 order() overrides the levels Count Level

“Given the severity table: bars, x is level, y is count, ordered by count, largest first.”

order() can also name the categorical column itself, as in order(level). That asks for the column’s own order. For a column of plain text, that order is alphabetical. For a column with declared levels, it is the declared order. desc = TRUE reverses whichever applies. So a declared order cannot be turned alphabetical from inside the plot. To change it, change the levels where the table was built, in the language you are already using. The declaration belongs to the table, and so does the change.

5.7 Missing values

A declared order is something you choose. Missing values are something you do not choose. Real data has gaps, and survey tables are usually missing many values. A row with no value in a column the plot maps cannot be placed, so gog drops it and says how many, rather than changing the count without telling you:

patchy <- data.frame(
  weight = c(3.1, 4.2, NA, 5.8, 6.0, NA, 4.9, 5.1, 3.7, 5.5),
  batch  = c("A", "A", "B", "B", NA, "B", "A", "B", "A", "B")
)
data(patchy) + bar * bin + x(weight) +
  x_label("Weight") + title("Two rows have no weight")
patchy = {
  "weight": [3.1, 4.2, None, 5.8, 6.0, None, 4.9, 5.1, 3.7, 5.5],
  "batch": ["A", "A", "B", "B", None, "B", "A", "B", "A", "B"],
}
(data(patchy) + bar * bin + x(col.weight) +
  x_label("Weight") + title("Two rows have no weight"))
patchy = (
  weight = [3.1, 4.2, missing, 5.8, 6, missing, 4.9, 5.1, 3.7, 5.5],
  batch = ["A", "A", "B", "B", missing, "B", "A", "B", "A", "B"],
)
data(patchy) + bar * bin + x(:weight) + x_label("Weight") +
  title("Two rows have no weight")
const patchy = {
  weight: [3.1, 4.2, null, 5.8, 6, null, 4.9, 5.1, 3.7, 5.5],
  batch: ["A", "A", "B", "B", null, "B", "A", "B", "A", "B"],
};
plot(data(patchy), layer(bar, bin), x(col.weight), x_label("Weight"),
  title("Two rows have no weight"))
gog: dropped 2 rows of `patchy` with a missing value in `weight` — a row with no value in a column the plot maps cannot be placed, and it is left out of every layer drawn from `patchy`. To keep those rows in a layer that does not map `weight`, give that layer a table of its own.
3.5 4.0 4.5 5.0 5.5 0 1 2 3 Two rows have no weight Count Weight

“Given the patchy table: bars derived by bin, x is weight.”

Only the columns the plot maps decide which rows are dropped. This plot maps weight, so the message names weight, and the two rows with no weight are left out. The table also has one row with no batch. The plot never reads batch, so that row stays in the histogram. Bind batch to color, and the same row is dropped as well, because it now has no color to take. The message then names both columns:

data(patchy) + bar * bin + x(weight) + color(batch) +
  x_label("Weight") + title("One more column mapped, one more row dropped")
(data(patchy) + bar * bin + x(col.weight) + color(col.batch) +
  x_label("Weight") + title("One more column mapped, one more row dropped"))
data(patchy) + bar * bin + x(:weight) + color(:batch) +
  x_label("Weight") +
  title("One more column mapped, one more row dropped")
plot(data(patchy), layer(bar, bin), x(col.weight), color(col.batch),
  x_label("Weight"),
  title("One more column mapped, one more row dropped"))
gog: dropped 3 rows of `patchy` with a missing value in `batch` or `weight` — a row with no value in a column the plot maps cannot be placed, and it is left out of every layer drawn from `patchy`. To keep those rows in a layer that does not map any of them, give that layer a table of its own.
3.5 4.0 4.5 5.0 5.5 0.0 0.5 1.0 1.5 2.0 One more column mapped, one more row dropped Count Weight Batch A B

“Given the patchy table: bars derived by bin, x is weight, color by batch.”

So a missing value in a column the plot never reads changes nothing. A missing value in a column the plot maps always drops the row, and the message says which column.

A mark that joins its rows into one stroke breaks where a row was dropped for a missing measurement. A line, an area and a step outline break where y is missing, and a path breaks where x or y is missing. They do not join the rows on either side of the gap, because nothing was measured between them. A row with no x has no place along a line, so the line goes on without it. monthly has no value for June:

monthly <- data.frame(
  month  = 1:12,
  visits = c(42, 45, 51, 58, 63, NA, 71, 69, 60, 52, 47, 44)
)
data(monthly) + line + point + x(month) + y(visits) +
  title("June was not recorded")
gog: dropped 1 row of `monthly` with a missing value in `visits` — a row with no value in a column the plot maps cannot be placed, and it is left out of every layer drawn from `monthly`. To keep those rows in a layer that does not map `visits`, give that layer a table of its own.
2 4 6 8 10 12 50 60 70 June was not recorded Visits Month

“Given the monthly table: a line and also points, x is month, y is visits.”

The line stops at May and starts again at July, and the message still says that one row was dropped. A row that is missing some other mapped value, such as the column that gives its color, has no line to break, so it is only dropped.

An infinite value, such as the one a computer returns for the logarithm of 0, is not a missing value, so gog does not drop its row. No scale can place it either, so gog refuses the plot when a column the plot maps holds one. If you want such a row left out, write a missing value in its place, the way your language writes one: NA in R, None in Python, missing in Julia and null in JavaScript. The R chapter shows the message in Two kinds of value gog refuses.

5.8 Special characters and other writing systems

The people who built a table chose its names, and a plot has to print them unchanged. Category names, titles and axis labels you write are drawn exactly as you wrote them. A plot is drawn as an SVG file, which is a picture written as text. Three characters have a special meaning inside one: &, < and >. gog writes them in the form the file needs, so they print as themselves, and a name like R&D needs no special handling. The plot below puts all three into its names and its title:

special <- data.frame(
  firm  = c("R&D", "Sales & Ops", "<10% unit"),
  spend = c(10, 25, 15)
)
data(special) + bar + x(firm) + y(spend) + title("Q3 <spend> & forecast")
special = {
  "firm": ["R&D", "Sales & Ops", "<10% unit"],
  "spend": [10, 25, 15],
}
data(special) + bar + x(col.firm) + y(col.spend) + title("Q3 <spend> & forecast")
special = (
  firm = ["R&D", "Sales & Ops", "<10% unit"],
  spend = [10, 25, 15],
)
data(special) + bar + x(:firm) + y(:spend) +
  title("Q3 <spend> & forecast")
const special = {
  firm: ["R&D", "Sales & Ops", "<10% unit"],
  spend: [10, 25, 15],
};
plot(data(special), bar, x(col.firm), y(col.spend),
  title("Q3 <spend> & forecast"))
R&D Sales & Ops <10% unit 0 10 20 Q3 <spend> & forecast Spend Firm

A name in a table can also hold a control character, an invisible character such as U+0001, which is Unicode character number 1. An SVG file cannot hold one, so gog draws U+FFFD, the replacement character, in its place. The file then still opens.

Many tables are not written in Latin letters at all. gog reads their column names as written, with no renaming first. A name in another writing system can be a bare column name as well as a label. The table below is in Korean. Its name, 지역별, means by region. Its two columns are 지역 (region) and 값 (value). The three rows are the cities 서울 (Seoul), 부산 (Busan) and 대구 (Daegu). The plot title, 한국 지역별 값, means value by region, Korea.

지역별 <- data.frame(
  지역 = c("서울", "부산", "대구"),
  값   = c(10, 25, 15)
)
data(지역별) + bar + x(지역) + y(값) + title("한국 지역별 값")
지역별 = {
  "지역": ["서울", "부산", "대구"],
  "값": [10, 25, 15],
}
data(지역별) + bar + x(col.지역) + y(col.값) + title("한국 지역별 값")
지역별 = (
  지역 = ["서울", "부산", "대구"],
  값 = [10, 25, 15],
)
data(지역별) + bar + x(:지역) + y(:값) + title("한국 지역별 값")
const 지역별 = {
  지역: ["서울", "부산", "대구"],
  값: [10, 25, 15],
};
plot(data(지역별), bar, x(col.지역), y(col.값), title("한국 지역별 값"))
서울 부산 대구 0 10 20 한국 지역별 값 값 지역

A label’s width is measured from its characters, and a Korean or Chinese character is about twice as wide as a Latin one, so these labels get the margin they need and no more.

5.9 In short

The first data() names the plot’s table; a later data() applies to the mark after it. gog refuses a sentence that names columns the table does not have; it never reshapes the table. query() is written where data() would be. A missing value drops its row only when the plot maps that column. A declared order goes with the column onto the axis and the legend; order() changes it for one plot, and the levels change it for every plot.

5.10 What comes next

This chapter says what gog needs from your own table, and nothing about the tables in this book. The book’s data names them, says where they come from, and shows how to read one in your own language.