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, the shape the table must have, and the declarations that travel with its columns.

5.1 Binding a table

Most of the time the table is already loaded and you know what you want to ask. The only work left is the sentence. data() takes a table you already hold in your session. 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))
0K 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, a named tuple of columns or a DataFrame in Julia, an object of columns in JavaScript. No registration step, no special wrapping required. Each chapter in the bindings part shows its own. The R chapter also lists the ten names that loading gog masks in R, with the qualified form of each.

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, because each layer looks up its bare column names in the nearest table by name. Leave data() out and the table arrives with no name, so the plot 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 + ...`.

gog gives the same advice for a sentence with no data at all, point + x(gdp) with nothing before it. Each binding words the advice for its own spelling of a column, so this refusal is shown in R alone.

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. Your two regions are column names, and a name is not a column. Your sales numbers sit 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 is a column and region is a column, so the sentence draws the plot and earns the 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 here, but not about the shape. ggplot2 does not reshape tables either, and it refuses the wide table above just as gog does. The real difference is narrower. 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 tidy is not quite 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 Plot-level vs. layer-level data

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 expression is the plot-level default: it applies to all layers that don’t declare their own table.

# Both layers read from "gapminder_2007"
data(gapminder_2007) + x(gdp) + y(life) +
  line +
  point
(data(gapminder_2007) + x(col.gdp) + y(col.life) +
  line +
  point)
data(gapminder_2007) + x(:gdp) + y(:life) + line + point
plot(data(gapminder_2007), x(col.gdp), y(col.life), line, point)
0K 10K 20K 30K 40K 50K 40 50 60 70 80 Life Gdp

“Given gapminder 2007: x is gdp, y is life, 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. actuals holds the sales of five years that have passed, and forecast holds the three years still to come:

actuals: all 5 rows
year sales
2019 120
2020 135
2021 128
2022 152
2023 168
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 that rule, and both are easy to miss.

A data() at the end of a sentence applies to nothing, because no mark comes after it. The plot is the same as it would be without that line:

# `forecast` is never drawn, because no mark comes after it
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))
2019 2020 2021 2022 2023 120 130 140 150 160 170 Sales Year

A data() also stops after that one mark. The mark after it reads the plot-level 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 both sit on the left. The points sit alone on the right.

That rule fits what a second table is usually for. The first table builds the frame, and a later one annotates it: a threshold, a band, a note, a forecast. An annotation is one mark, so a data() that reaches one mark is enough. Every second table in this book is one of those.

To draw two marks from the second table, write data() before each of them.

The specification above is correct, and it still reads badly. area sits on the line below data(forecast), so it looks like part of the forecast. Only the rule says otherwise. The same plot reads better with the marks grouped by the table they read. 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. That difference is yours to control.

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-level table together. Every line is then one table and the marks that read it, and no mark sits under a table it does not read. The rest of this book follows that layout.

The first data() is the one exception. It applies to every mark below it, so it takes a line of its own. A later data() applies to one mark only. On a line of its own, it would look like it applies to every mark below it, as the first one does. That is why data(actuals) stands alone above 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 income takes off
40000 80 the long plateau

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" }))
income takes off the long plateau 0K 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 is shared is the measurement, and that is what an axis is. 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, however its columns are spelled: put sales figures on an income axis and the plot is drawn correctly and reads as nonsense. 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 the first table set, so its values must measure what those axes measure. Values that measure something else need axes of their own, and axes of their own means a plot of its own. 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

Not every table fits on one computer. Most working data sits in a database, and a warehouse (a database built to hold a company’s whole history) can hold billions of rows. gog reaches those tables with one word, query, and it stands exactly where data stands.

Nothing else in the sentence changes:

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

The SQL stays inside that one word; 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 scatter plot, because the grammar did an ordinary thing. 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. Every binding tests 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

The connection is yours. 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. A reader 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 it later. The delay is not only neatness; it makes something else possible. A specification the engine can read before the database runs is one where the engine could rewrite the question, and ask for a summary instead of the rows. Counting is a job a database does well, and the design allows for that later.

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.

It starts to matter somewhere around a hundred thousand rows. Past that, moving the rows costs more 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.”

Five rows crossed the connection, not 142. The plot is the same one you would get by counting locally.

5.5.3 Two directions, and which one suits you

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 host language is installed 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 SQL is where you work, ggsql is probably the better fit. 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. One syntax, in as many languages and places as we can reach.

You work The lower cost is
Only in SQL ggsql: nothing 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

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 is rarely what you want here. 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, so gog supplies the declaration itself, 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.

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 order holds even when gog does the counting for you. These tickets arrive High first, but bar * count still reads Low, Medium, High. A summary carries the declared order onto its axis. It does not return to the order the rows arrived in:

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 gog reads its categories, in their order, as the levels. ordered() is for a plain dictionary of lists, so that you can declare an order without pandas installed.

Two rules worth knowing:

  • 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.
  • 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 a different column in order(), and the categories follow that column’s 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

Real data has gaps, and survey tables are usually missing many values. A row with no value where the plot needs one 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, so it is left out (the same as other plotting tools drop NA).
4 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`, `weight` — a row with no value in a column the plot maps cannot be placed, so it is left out (the same as other plotting tools drop NA).
4 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.

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 are drawn exactly as you wrote them. Three characters have a special meaning inside an SVG file: &, < 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

Many tables are not written in Latin letters at all. Their column names should be drawn as they are, with no renaming first. A name in another writing system is also drawn as written, and it can be a bare column name as well as a label. The table below is in Korean. Its name, 지역별 (jiyeokbyeol), means by region. Its two columns are 지역 (jiyeok), region, and 값 (gap), value. The three rows are the cities 서울 (Seoul), 부산 (Busan) and 대구 (Daegu). The plot title 한국 지역별 값 (Hanguk jiyeokbyeol gap) reads 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.

That is everything a sentence needs from its table. The next chapter shows where this book’s own tables come from, and the mark chapters follow it.