Appendix B — Functions around the grammar

Every word in the kernel belongs to a sentence. point is a mark, x is a channel, bin is a transform, and none of them means anything on its own. They are words, and a word needs a sentence.

The functions in this appendix are not words. Five of them take a finished plot and act on it. The sixth gives you a table that your sentence can name. None of them can be written into a sentence with + or *, and none of them is part of the grammar.

The name alone will not tell you whether a function is a word of the grammar. x_label is a word of the grammar, and its name joins two parts with an underscore, exactly as render_svg does. What separates them is where each one is written. A word of the grammar is written inside a sentence. The six functions below are always written outside one, which is why none of them appears in The kernel card.

What it gives you
render_svg(plot) The plot as text, in one string
save_svg(plot, path) The plot as an SVG file
save_gif(plot, path) A played plot as a GIF file
save_png(plot, path) The plot as a PNG file
save_json(plot, path) The plot as the engine reads it, in a JSON file
gog_table(name) One of this book’s shared tables

Each function has a section below, in the same order as the rows above. The first turns a finished plot into text. The last section lists the buttons under each plot in the web edition. They are neither words nor functions.

B.1 render_svg()

A plot is a specification until the engine draws it. Sometimes you want the drawing itself: to save it as a file, or to put it in a web page of your own. render_svg() draws the plot and gives you the drawing as ordinary text. It takes the plot as its only argument, and it has the same name in all four languages:

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
picture <- render_svg(data(gapminder_2007) + point + x(gdp) + y(life))
cat(substr(picture, 1, 51), "\n")
cat(nchar(picture), "characters\n")
<svg xmlns="http://www.w3.org/2000/svg" width="800" 
14710 characters
picture = render_svg(data(gapminder_2007) + point + x(col.gdp) + y(col.life))
print(picture[:51])
print(len(picture), "characters")
<svg xmlns="http://www.w3.org/2000/svg" width="800"
14710 characters
picture = render_svg(data(gapminder_2007) + point + x(:gdp) + y(:life))
println(first(picture, 51))
println(length(picture), " characters")
<svg xmlns="http://www.w3.org/2000/svg" width="800"
14710 characters
const picture = render_svg(plot(data(gapminder_2007), point, x(col.gdp), y(col.life)));
console.log(picture.slice(0, 51));
console.log(picture.length, "characters");
<svg xmlns="http://www.w3.org/2000/svg" width="800"
14710 characters

The first line of output shows where that text begins. The second line gives its whole length. So every plot in this book is also a piece of text. It starts with <svg and gives the width of the picture. After that it gives the position of every point, every tick and every label.

That text is the whole picture. A notebook displays it, and so does this book. Write it to a file whose name ends in .svg, and any web browser can open that file as a picture. A vector drawing program, such as Inkscape, can edit it too, because the file describes shapes rather than a grid of colored dots.

save_svg() writes that file for you, in all four languages.

B.1.1 What the file carries

An SVG file is text, so anyone who opens it in a text editor can read everything in it. The file carries the drawing and nothing else, but the drawing itself can show private values. If your table holds something private, open the file and read it yourself before you publish.

A plot uses only the columns its sentence names. Columns the sentence never names are absent from the file, and so are their names:

patients <- data.frame(
  patient = c("A. Smith", "B. Jones", "C. Brown", "D. Taylor"),
  email   = c("as@ex.com", "bj@ex.com", "cb@ex.com", "dt@ex.com"),
  weight  = c(61.2, 74.8, 55.4, 68.0)
)

picture <- render_svg(data(patients) + bar * bin + x(weight))
sapply(c("A. Smith", "email", "ex.com", "61.2"), grepl, x = picture, fixed = TRUE)
A. Smith    email   ex.com     61.2 
   FALSE    FALSE    FALSE    FALSE
patients = {
  "patient": ["A. Smith", "B. Jones", "C. Brown", "D. Taylor"],
  "email": ["as@ex.com", "bj@ex.com", "cb@ex.com", "dt@ex.com"],
  "weight": [61.2, 74.8, 55.4, 68.0],
}

picture = render_svg(data(patients) + bar * bin + x(col.weight))
for text in ["A. Smith", "email", "ex.com", "61.2"]:
    print(text + ":", text in picture)
A. Smith: False
email: False
ex.com: False
61.2: False
patients = (
  patient = ["A. Smith", "B. Jones", "C. Brown", "D. Taylor"],
  email = ["as@ex.com", "bj@ex.com", "cb@ex.com", "dt@ex.com"],
  weight = [61.2, 74.8, 55.4, 68.0],
)

picture = render_svg(data(patients) + bar * bin + x(:weight))
for text in ["A. Smith", "email", "ex.com", "61.2"]
    println(text, ": ", occursin(text, picture))
end
A. Smith: false
email: false
ex.com: false
61.2: false
const patients = {
  patient: ["A. Smith", "B. Jones", "C. Brown", "D. Taylor"],
  email: ["as@ex.com", "bj@ex.com", "cb@ex.com", "dt@ex.com"],
  weight: [61.2, 74.8, 55.4, 68],
};

const picture = render_svg(plot(data(patients), layer(bar, bin), x(col.weight)));
for (const text of ["A. Smith", "email", "ex.com", "61.2"]) {
  console.log(text + ":", picture.includes(text));
}
A. Smith: false
email: false
ex.com: false
61.2: false

None of the four searches finds anything. The sentence names one column, so the drawing uses one column. Even the weights are gone: bin cuts the weights into intervals and counts the rows in each, so the bars show only the counts.

The weights are absent because of the transform, not because a plot never carries values. A point with no transform draws one glyph per row, and each glyph is placed from one value:

picture <- render_svg(data(patients) + point + x(weight) + y(patient))
lengths(gregexpr("<circle", picture))
[1] 4
picture = render_svg(data(patients) + point + x(col.weight) + y(col.patient))
print(picture.count("<circle"))
4
picture = render_svg(data(patients) + point + x(:weight) + y(:patient))
println(length(findall("<circle", picture)))
4
const picture = render_svg(plot(data(patients), point, x(col.weight), y(col.patient)));
console.log(picture.split("<circle").length - 1);
4

The four rows are drawn as four circles, and the four patient names are printed along the axis. This is not special to gog: a scatterplot in any plotting tool draws a point for every row you give it.

The weights are not written in the file as numbers. But each circle is placed by its weight, so a reader who measures the drawing can recover every weight.

So the rule is not that the file is safe. It is narrower: what your sentence names is what you publish. That includes a column named only in facet(), whose values are printed as panel labels. A column the sentence never names cannot be recovered from the file, so it stays private.

B.1.2 Plots that carry the table

A saved SVG file carries only the drawing. A web page can carry more, and that includes a page that a notebook or Quarto makes for you. You should know what it carries before you publish it.

Some plots are drawn twice. The engine makes the first drawing. The reader’s browser makes the second, because the reader can move a selection or turn the view. Four kinds of plot do this: one that binds brush(), one in the cube, one on the globe, and a network given an angle. The second drawing needs the numbers, so the web page also holds a copy of the table. That copy is in the page’s code: the screen does not show it, but anyone who opens the page’s code can read it.

One more kind of plot carries the table, although the browser never draws it again. It is a plot that draws each mark at its own row’s values, such as a scatter. Resting the pointer on a mark shows the values of the mark’s row, as Pointing at a mark explains, so the page needs the rows. A plot without brush whose table has more than 20,000 rows carries no copy, and its marks show no values.

Where the page holds a copy, it holds every row of the table, but only the columns the sentence names. As in a saved SVG file, a column the sentence never names is not in the page. The difference is in the values: the page holds each row’s exact numbers, and the file holds only a drawing of them. Here is patients again, with a brush:

data(patients) + point + x(weight) + y(patient) + brush(weight)
data(patients) + point + x(col.weight) + y(col.patient) + brush(col.weight)
data(patients) + point + x(:weight) + y(:patient) + brush(:weight)
plot(data(patients), point, x(col.weight), y(col.patient),
  brush(col.weight))
55 60 65 70 75 D. Taylor C. Brown B. Jones A. Smith Patient Weight

“Given the patients: points, x is weight, y is patient, and let the reader select weight.”

That sentence names a brush and draws one point for each row. Either one is enough for the web edition of this page to carry a copy of the table. The copy has two columns, weight and patient, because the sentence names those two. The email column is not in that copy at all. In the web version, the plot has a show rows button under it, which Selection explains. The table that button opens lists the same two columns.

An SVG file never carries the table, even when its sentence names a brush. render_svg() returns only the picture.

B.1.3 What “byte for byte” means

This book says in several places that the four languages draw the same picture byte for byte. That claim is about the text that render_svg() returns.

A plot looks like a picture on your screen, and that picture is drawn from this text. A byte is the unit a computer stores text in: one English letter, digit or space takes one byte. Two plots are the same when their text is the same, byte for byte. Text that is only similar does not count, even when the two pictures look the same.

That is how gog checks that the four languages draw the same picture. Nearly every sentence in this book is written in R, in Python, in Julia and in JavaScript. Each one that draws is turned into its text, and the text from Python, Julia and JavaScript is compared with R’s, byte for byte. One wrong byte counts as a disagreement, and one disagreement makes the whole check fail.

A test that allowed small differences would accept pictures that are not the same. The pictures are not compared by eye, because a person cannot see a bar that is half a pixel too wide, or a color that is slightly wrong. Comparing the text finds both differences, and it works the same way every time.

B.2 save_svg()

save_svg() draws the plot and writes the drawing to a file. It takes the plot and a path that ends in .svg, and it has the same name in all four languages:

path <- file.path(tempdir(), "life.svg")
save_svg(data(gapminder_2007) + point + x(gdp) + y(life), path)
cat(basename(path), file.size(path), "bytes\n")
life.svg 14710 bytes
import os, tempfile

path = os.path.join(tempfile.gettempdir(), "life.svg")
save_svg(data(gapminder_2007) + point + x(col.gdp) + y(col.life), path)
print(os.path.basename(path), os.path.getsize(path), "bytes")
life.svg 14710 bytes
path = joinpath(tempdir(), "life.svg")
save_svg(data(gapminder_2007) + point + x(:gdp) + y(:life), path)
println(basename(path), " ", filesize(path), " bytes")
life.svg 14710 bytes
import { tmpdir } from "node:os";
import { join, basename } from "node:path";
import { statSync } from "node:fs";

const path = join(tmpdir(), "life.svg");
save_svg(plot(data(gapminder_2007), point, x(col.gdp), y(col.life)), path);
console.log(basename(path), statSync(path).size, "bytes");
life.svg 14710 bytes

The output names the file that was written and its size. The file holds the text that render_svg() returns, byte for byte, so the same plot gives the same file in every language. For a plot p, the call is the same in all four:

R save_svg(p, "life.svg")
Python save_svg(p, "life.svg")
Julia save_svg(p, "life.svg")
JavaScript save_svg(p, "life.svg")

A path that does not end in .svg is refused, and the message repeats the path with the right ending. gog draws the plot before it writes anything. So when gog refuses the plot, a file already at that path stays unchanged.

B.3 save_gif()

A plot that binds play() is a sequence of pictures rather than one. It moves in a browser, because the SVG carries its own timing. A chat message, a slide in a talk and a post on a social site cannot show that movement. save_gif() writes the same sequence as a file that those places do play:

gm_all: first 5 of 1704 rows
country continent year life population gdp
Afghanistan Asia 1952 28.801 8425333 779.4453
Afghanistan Asia 1957 30.332 9240934 820.8530
Afghanistan Asia 1962 31.997 10267083 853.1007
Afghanistan Asia 1967 34.020 11537966 836.1971
Afghanistan Asia 1972 36.088 13079460 739.9811
ring <- file.path(tempdir(), "ring.gif")
save_gif(data(gm_all) + bar * mean + x(continent) + y(life) + polar() +
           play(year),
         ring, scale = 2)
cat(basename(ring), round(file.size(ring) / 1024), "KB\n")
ring.gif 708 KB
import os, tempfile

ring = os.path.join(tempfile.gettempdir(), "ring.gif")
save_gif(data(gm_all) + bar * mean + x(col.continent) + y(col.life) + polar() +
           play(col.year),
         ring, scale=2)
print(os.path.basename(ring), round(os.path.getsize(ring) / 1024), "KB")
ring.gif 708 KB
ring = joinpath(tempdir(), "ring.gif")
save_gif(data(gm_all) + bar * mean + x(:continent) + y(:life) + polar() +
           play(:year),
         ring, scale = 2)
println(basename(ring), " ", round(Int, filesize(ring) / 1024), " KB")
ring.gif 708 KB
import { tmpdir } from "node:os";
import { join, basename } from "node:path";
import { statSync } from "node:fs";

const ring = join(tmpdir(), "ring.gif");
save_gif(plot(data(gm_all), layer(bar, mean), x(col.continent), y(col.life),
              polar(), play(col.year)),
         ring, { scale: 2 });
console.log(basename(ring), Math.round(statSync(ring).size / 1024), "KB");
ring.gif 708 KB

The output names the file that was written and its size. The engine writes the GIF itself, so no other program has to be installed.

scale multiplies the width and the height of the picture. A plot is 800 by 600 pixels unless its theme says otherwise, so scale = 2 writes a GIF of 1600 by 1200 pixels.

Three of the four languages take scale as a named argument. JavaScript has no named arguments, so it takes scale in an object at the end of the call:

R save_gif(p, "ring.gif", scale = 2)
Python save_gif(p, "ring.gif", scale=2)
Julia save_gif(p, "ring.gif", scale = 2)
JavaScript save_gif(p, "ring.gif", { scale: 2 })

A plot with no play() is refused rather than written as a file of one frame. The Play chapter shows that refusal. It also explains why gog writes no video.

B.4 save_png()

An SVG stays sharp at any size, because it is text that is drawn again each time it is shown. Some places accept only a picture made of pixels, such as an older word processor, some slide programs and most chat messages. save_png() saves the plot as a PNG file, which all of them accept. It takes the plot and a path that ends in .png, and it has the same name in all four languages:

path <- file.path(tempdir(), "life.png")
save_png(data(gapminder_2007) + point + x(gdp) + y(life) + color(continent), path)
cat(basename(path), round(file.size(path) / 1024), "KB\n")
life.png 230 KB
import os, tempfile

path = os.path.join(tempfile.gettempdir(), "life.png")
save_png(data(gapminder_2007) + point + x(col.gdp) + y(col.life) + color(col.continent), path)
print(os.path.basename(path), round(os.path.getsize(path) / 1024), "KB")
life.png 230 KB
path = joinpath(tempdir(), "life.png")
save_png(data(gapminder_2007) + point + x(:gdp) + y(:life) + color(:continent), path)
println(basename(path), " ", round(Int, filesize(path) / 1024), " KB")
life.png 230 KB
import { tmpdir } from "node:os";
import { join, basename } from "node:path";
import { statSync } from "node:fs";

const path = join(tmpdir(), "life.png");
save_png(plot(data(gapminder_2007), point, x(col.gdp), y(col.life), color(col.continent)), path);
console.log(basename(path), Math.round(statSync(path).size / 1024), "KB");
life.png 230 KB

The output names the file that was written and its size. As with save_gif(), the engine writes the picture itself, so no other program has to be installed.

A plot is 800 by 600 pixels unless its theme says otherwise. scale multiplies the width and the height, and it is 2 if you leave it out, so the picture above is 1600 by 1200 pixels. That is sharp enough for a document or a slide on most screens. scale = 1 saves the plot at 800 by 600, and scale = 3 at 2400 by 1800. JavaScript takes scale in an object at the end of the call, as it does for save_gif():

R save_png(p, "life.png", scale = 3)
Python save_png(p, "life.png", scale=3)
Julia save_png(p, "life.png", scale = 3)
JavaScript save_png(p, "life.png", { scale: 3 })

A PNG holds one picture, so a plot that plays more than one frame is refused. Saving only its first frame would drop every other frame, and nothing would tell you. The message names save_gif(), which writes every frame:

moving <- data(gm_all) + point + x(gdp) + y(life) + play(year)
save_png(moving, "life.png")
Error:
! gog: a PNG holds one picture, and this plot plays 12 frames of `year`, so every frame but the first would be left out. `save_gif()` writes every frame. To save one frame as a still, keep only that frame's rows in the table.
gog: nothing was rendered. Fix the above, or set GOG_STRICT=0 to draw anyway.
moving = data(gm_all) + point + x(col.gdp) + y(col.life) + play(col.year)
save_png(moving, "life.png")
gog: a PNG holds one picture, and this plot plays 12 frames of `year`, so every frame but the first would be left out. `save_gif()` writes every frame. To save one frame as a still, keep only that frame's rows in the table.
gog: nothing was rendered. Fix the above, or set GOG_STRICT=0 to draw anyway.
moving = data(gm_all) + point + x(:gdp) + y(:life) + play(:year)
save_png(moving, "life.png")
gog: a PNG holds one picture, and this plot plays 12 frames of `year`, so every frame but the first would be left out. `save_gif()` writes every frame. To save one frame as a still, keep only that frame's rows in the table.
gog: nothing was rendered. Fix the above, or set GOG_STRICT=0 to draw anyway.
const moving = plot(data(gm_all), point, x(col.gdp), y(col.life), play(col.year));
save_png(moving, "life.png");
gog: a PNG holds one picture, and this plot plays 12 frames of `year`, so every frame but the first would be left out. `save_gif()` writes every frame. To save one frame as a still, keep only that frame's rows in the table.
gog: nothing was rendered. Fix the above, or set GOG_STRICT=0 to draw anyway.

An arrangement of plots is saved as one picture. As with save_svg(), a path that does not end in .png is refused, and a file already at the path stays unchanged when gog refuses the plot.

A PNG is pixels, so its text becomes blurred when a reader zooms in. Its letters are drawn with the fonts installed on the computer that saves it, so the same plot saved on two computers can give two slightly different pictures. A computer with no fonts at all, such as a bare server, gives a picture with no text, and gog says so when it saves one. For a web page, use the SVG, whose text stays sharp at any zoom.

B.5 save_json()

When a plot is drawn wrong, the person who fixes it has to draw the same plot. Your code alone cannot do that, because it reads a table that only you have.

Every language turns your sentence into one request for the engine. The request holds the sentence and every column the sentence names, and the engine draws the plot from that request alone. save_json() writes the request to a file. The file is in JSON, a plain-text format for structured data, so any text editor can open it. Like save_svg(), it takes the plot and a path, and the path ends in .json.

The code below saves the histogram of patients from What the file carries. Then it searches the file for the same four pieces of text as that section:

path <- file.path(tempdir(), "weights.json")
save_json(data(patients) + bar * bin + x(weight), path)
request <- readLines(path)
sapply(c("A. Smith", "email", "ex.com", "61.2"), grepl, x = request, fixed = TRUE)
A. Smith    email   ex.com     61.2 
   FALSE    FALSE    FALSE     TRUE
import os, tempfile

path = os.path.join(tempfile.gettempdir(), "weights.json")
save_json(data(patients) + bar * bin + x(col.weight), path)
request = open(path).read()
for text in ["A. Smith", "email", "ex.com", "61.2"]:
    print(text + ":", text in request)
A. Smith: False
email: False
ex.com: False
61.2: True
path = joinpath(tempdir(), "weights.json")
save_json(data(patients) + bar * bin + x(:weight), path)
request = read(path, String)
for text in ["A. Smith", "email", "ex.com", "61.2"]
    println(text, ": ", occursin(text, request))
end
A. Smith: false
email: false
ex.com: false
61.2: true
import { tmpdir } from "node:os";
import { join } from "node:path";
import { readFileSync } from "node:fs";

const path = join(tmpdir(), "weights.json");
save_json(plot(data(patients), layer(bar, bin), x(col.weight)), path);
const request = readFileSync(path, "utf8");
for (const text of ["A. Smith", "email", "ex.com", "61.2"]) {
  console.log(text + ":", request.includes(text));
}
A. Smith: false
email: false
ex.com: false
61.2: true

The file leaves out the patient and email columns, their values and their names, because the sentence never names them. But it holds 61.2, the first patient’s weight, which the SVG file did not hold. The SVG file shows only the counts that bin made. The request holds every value of each column the sentence names, because the engine needs those values to draw the plot.

So read the file before you share it. If a column the sentence names holds something private, replace its values in your table with invented ones. Draw the plot again and check that it still shows the problem, then save the file again.

This file is what to attach to a bug report. From the file alone, the engine draws the plot you saw, byte for byte, without your code and without the rest of your table. Unlike save_svg(), save_json() writes the file even when gog refuses the plot, because a refusal you think is wrong is also worth reporting. A path that does not end in .json is refused, and the message repeats the path with the right ending.

In R, the reprex package (Bryan et al., 2024) turns your code into a reproducible example, called a reprex. It has the same aim as this file: give the person who fixes a problem everything they need to draw it again. The difference is in what each one holds. A reprex runs your code in a new, empty session, so your code must first build a few rows of data itself, and you choose which rows. The file from save_json() already holds the data the plot needs, which is why you read it before you share it. All four languages write this file, and the engine draws the same plot from each one.

B.6 gog_table()

Every example in this book starts from a table. The examples draw from a set of shared tables, such as gapminder_2007, and each one is published with the book as a CSV file. gog_table() reads one by its name, so you can run any example without finding and saving the file yourself. Unlike the other four, this function comes before the plot rather than after it:

gapminder_2007 <- gog_table("gapminder_2007")
data(gapminder_2007) + point + x(gdp) + y(life)

No plot appears under that block, because the book does not run it. The call reads the table over the network, and running it here would mean the book could be built only with a network connection.

gog_table() is there so that you can run this book’s examples, not for your own work. When the data is yours, bind it with data() and skip gog_table() entirely.

gog_table() takes a second argument, text. A CSV file records each value but never what kind of value it is. A column of 01, 02, 03 looks like numbers, so it is read as 1, 2, 3, and the leading zeros are lost. To keep such a column as text, list it in text. Labels that look like numbers names the three columns in the book’s tables that need it.

The book’s data explains this function in full. It lists every shared table in the book and shows the call in each of the four languages.

B.7 The buttons under a plot

In the web edition, a line of buttons is under every plot. Each one changes how you look at the plot, or saves what you see, and none changes its sentence. So none of them is a word of the grammar:

Button What it does
the magnifiers, − and + Make the picture smaller or larger
the frame button (four corners around a small square) Returns to the whole picture
the camera Saves what you are looking at as a PNG file, large enough for a slide or a journal figure

A plot that plays adds three buttons to the same line: they pause the sequence and step through its frames (Play). A brushed plot adds a line of controls for its selection (Selection), and a plot in the cube adds a line for its viewing angle (Space). The buttons under every plot introduces this line.