plot(data(gapminder_2007), point, x(col.gdp, { scale: 'log' }), y(col.life), color(col.continent))44 JavaScript
The grammar does not change when the language does. One engine reads one specification, so a sentence means what it means whoever wrote it, and everything in this book up to here is as true in JavaScript as it is in R.
The sentence, though, is spelled differently here, and this is the one chapter in the book where that is so. Every other binding writes the same words joined by the same four operators. JavaScript cannot join them, because +, *, | and / cannot be given a meaning on objects: whatever you put on either side, a + b is a number and a | b is a bitwise integer. There is no version of the language in which data(gm) + point builds a plot.
So JavaScript spells the four operators as four words. That is all that changes, and it is worth reading the two lines side by side before anything else:
data(gm) + bar * bin + x(life) | facet(era)plot(data(gm), layer(bar, bin), x(col.life), across(col.era))Every JavaScript block below was executed to draw the plot beside it, through the same engine every other chapter uses.
44.1 The comma is +
A gog sentence is a list of words, and in JavaScript it is a list of arguments. plot() is what gives the comma its meaning:
That is A first plot exactly: the same words, in the same order, reading the same way. Position still decides scope (Encoding scope), so a channel written after a mark belongs to that mark and one written before belongs to the plot. The commas change nothing about that; they are the + signs.
44.2 layer() is *
In R, * binds tighter than +, which is how bar * bin + x(life) reads as a binned bar rather than a bar plus a bin. JavaScript has no precedence to lean on, so the tighter binding becomes nesting:
plot(data(gapminder_2007), layer(bar, mean), x(col.continent), y(col.life))Nesting is arguably the better of the two. A reader who does not already know that * binds tighter than + has to be told; a reader looking at layer(bar, mean) can see it.
Most sentences need no layer() at all. Of the 500 sentences in this book, 264 are a flat list of words, and the ones that do use it usually hold two:
plot(data(gapminder_2007), layer(bar, count), x(col.continent), style({ color: 'steelblue' }))44.3 across() and down() are | and /
Faceting in R is plot | facet(era), panels side by side, or plot / facet(era), panels stacked. The | and the facet() were always one phrase (neither means anything without the other), so JavaScript writes the phrase as one word:
plot(data(gapminder_2007), point, x(col.gdp, { scale: 'log' }), y(col.life), across(col.continent))down() stacks them instead, and writing both crosses the two into a grid, which is what plot | facet(a) / facet(b) does in R.
Because the pair is one word here, there is no separate facet() to write. Typing one from memory says where it went:
plot(data(gapminder_2007), point, x(col.gdp), y(col.life), facet(col.continent))gog: JavaScript has no `facet()`, because it has no `|` or `/` to join it with — the pair is one word here. Panels side by side: `across(col.continent)`. Panels stacked: `down(col.continent)`.
44.4 Composing two plots: beside() and below()
R’s | means two things, told apart by what is on its right: plot | facet(g) splits one plot into panels, plot_a | plot_b arranges two plots on a page. With no operator to overload there is nothing to tell apart, so the second reading is its own word: beside(), and below() for /:
below(plot(data(gapminder_2007), layer(bar, bin), x(col.gdp), theme({ height: 130 })), plot(data(gapminder_2007), point, x(col.gdp), y(col.life)))The two words are not new coinages: Henderson’s Functional Geometry (Henderson, 1982) built its algebra of pictures out of beside and above, and that is the picture language SICP teaches. Both take pages as well as plots, so below(top, beside(main, right)) is the marginal plot, exactly as top / (main | right) is in R. What the composed plots share is decided by the engine and not by the spelling: the same column on the same axis in two of them is one axis. See Composition.
44.5 A column is written col.gdp
R writes a column as a bare name, Julia as a symbol, Python and JavaScript through a small accessor. The two languages with no bare names give the same answer, and they give it for the same reason: in this grammar a plain string is how you spell a value, as in style({ color: "tomato" }), title("…"), palette("okabe"). Without the accessor a column and a value would look identical, and the grammar’s oldest rule (a channel takes a column, never a value) would be invisible in the one place it most needs saying.
So a string where a column belongs is refused, and told what to write:
plot(data(gapminder_2007), point, x('gdp'), y(col.life))gog: `x("gdp")` binds a *value*, and a channel takes a *column*. JavaScript has no bare names, so a column is written with the accessor: `x(col.gdp)` maps the column called `gdp`.
The message offers both spellings because both are real: one maps a column and earns a legend, the other sets one value and earns none (Setting is not mapping). The mistake in the other direction is refused the same way:
plot(data(gapminder_2007), point, x(col.gdp), y(col.life), style({ color: col.continent }))gog: `style({ color: … })` fixes one value for the whole layer, and `col.continent` is a column. To *map* it — one value per category, with a legend to decode it — that is a channel: `color(col.continent)`.
A column whose name is not a JavaScript identifier takes the bracket form instead, col["life exp"], which is also how you reach a name with a space or a dot in it. Names in other scripts need no such thing: col.지역 (jiyeok, region) is a perfectly ordinary identifier.
44.6 A named argument is one trailing object
R writes x(gdp, scale = "log"). JavaScript has no syntax for a named argument, so the named ones gather into a single object at the end while the positional ones stay where they are:
plot(data(gapminder_2007), point, x(col.gdp, { scale: 'log', base: 2 }), y(col.life), title('Doublings of income'))style() is the case where every argument is named, so it is written as one object throughout the book: style({ color: "tomato", size: 3 }).
44.7 The table, and what is lost
A table is an object of columns, so a first plot needs nothing installed:
plot(data(heights), bar, x(col.person), y(col.cm))R reads the table’s name off the expression you wrote, and Python off the frame you called from. JavaScript can do neither, and the name matters: a layer resolves its columns against the nearest table by that name (Bind once). So an unnamed table is given a unique one, which is enough for two tables in one sentence to stay apart, and data(df, { name: "notes" }) is there for when a message should say notes rather than data2.
44.8 A sub-expression is a value
One thing this spelling gains over R’s. A piece of a sentence is an ordinary JavaScript value, so it can be named, reused, and passed around, and it means the same thing everywhere it lands:
const piled = layer(point, bin, stack);
console.log(String(plot(data(gapminder_2007), piled, x(col.life))));<gog plot: point on data>
That is the sixth of the nine laws holding in a language that had to be talked into the other eight.
44.9 Reading from a database
Node has no database standard, so query() accepts a connection by the shape of its methods rather than by its type. A connection that can prepare a statement and return all its rows works:
import { DatabaseSync } from "node:sqlite";
import { plot, query, bar, x, y, col, render_svg } from "grammar-of-graphics";
const con = new DatabaseSync("sales.db");
render_svg(plot(query(con, "SELECT status, revenue FROM orders"),
bar, x(col.status), y(col.revenue)));node:sqlite is included with Node from version 22, so that example needs nothing installed. better-sqlite3 has the same shape and works the same way.
44.9.1 Asynchronous drivers are refused
render_svg() is synchronous. It returns the SVG rather than a Promise, which is what lets a plot be written inline in an expression. A driver whose query returns a Promise cannot be awaited inside it.
So pg and mysql2 do not work with query(), and gog says so rather than letting a Promise reach the engine. The direction it gives is the fix: fetch the rows yourself, then bind them as an ordinary table.
const { rows } = await con.query("SELECT status, revenue FROM orders");
render_svg(plot(data(rows), bar, x(col.status), y(col.revenue)));That works with every driver, asynchronous ones included. data() takes the rows an async driver returns, and the sentence after it is unchanged. This is the only place in the four bindings where a language’s own shape reaches into the grammar, and it costs one await.
44.10 Getting it
npm install grammar-of-graphicsThat command works today. The package is on npm, and the name is spelled out because npm already had a package called gog. The registry also forbids capital letters in a new name, so this is Julia’s GrammarOfGraphics written the way npm writes names.
The engine arrives with it. There are five more packages on npm, one per platform, each holding the engine compiled for that platform, and the main package lists all five as optional dependencies. Installing takes the one that matches your computer and skips the other four. So there is nothing to put on your PATH, nothing to build, and no Rust toolchain to install.
The binding itself is a thin layer over that engine, and it depends on nothing else:
import { plot, data, point, x, y, col, render_svg } from "grammar-of-graphics";
const gm = { gdp: [1000, 2000, 3000], life: [60, 70, 80] };
console.log(render_svg(plot(data(gm), point, x(col.gdp), y(col.life))));render_svg() returns the SVG as a string, which in a browser goes straight into the DOM and on a server goes straight into a response. There is no viewer and no raster step, which is the one place JavaScript fits the engine better than any other language does.