49  JavaScript

The grammar does not change when the language does. One engine reads one specification, so a sentence means the same thing no matter who 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 the operators are why. 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: a + b glues two objects into text, a * b is NaN, 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 the change this chapter starts from, and it is worth reading the two lines side by side first:

data(gm) + bar * bin + x(life) | facet(era)
plot(data(gm), layer(bar, bin), x(col.life), across(col.era))

Every JavaScript block with a plot under it was executed, through the same engine every other chapter uses.

49.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. A named argument such as scale = "log" gathers into an object at the end of the call, which the section on named arguments below sets out in full:

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
plot(data(gapminder_2007), point, x(col.gdp, { scale: 'log' }), y(col.life), color(col.continent))
1K 10K 40 50 60 70 80 Life Gdp Continent Asia Europe Africa Americas Oceania

“Given gapminder 2007: points, x is gdp on a log scale, y is life, color by continent.”

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.

49.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 use here, so the tighter binding becomes nesting:

plot(data(gapminder_2007), layer(bar, mean), x(col.continent), y(col.life))
Asia Europe Africa Americas Oceania 0 20 40 60 80 Life Continent

“Given gapminder 2007: bars derived by mean, x is continent, y is life.”

Nesting is the clearer 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. Just over half the sentences in this book 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' }))
Asia Europe Africa Americas Oceania 0 20 40 Count Continent

“Given gapminder 2007: bars derived by count, x is continent.”

49.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, because facet() means nothing without one of them, so JavaScript writes the phrase as one word:

plot(data(gapminder_2007), point, x(col.gdp, { scale: 'log' }), y(col.life), across(col.continent))
1K 10K 40 50 60 70 80 1K 10K 1K 10K 1K 10K 1K 10K Asia Europe Africa Americas Oceania Life Gdp

“Given gapminder 2007: points, x is gdp on a log scale, y is life, split into panel columns by 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. Writing facet() from memory gets a refusal that names the word to use instead:

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)`.

49.4 Composing two plots: beside() and below()

R’s | means two things, and what is on its right decides which: 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 interpret, so the second meaning gets 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)))
0 20 40 60 Count 0K 20K 40K 40 50 60 70 80 Life Gdp

“Given gapminder 2007: bars derived by bin, x is gdp, above points, x is gdp, y is life.”

The two words come from an older algebra: Henderson’s Functional Geometry (Henderson, 1982) combines pictures with beside and above, and that is the picture language SICP teaches (Composition gives its full name). 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.

49.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 two languages that most need it stated.

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 names the spelling to write, because a string here is a value and a channel takes a column (Setting vs 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, because it holds a space or a dot, takes the bracket form instead: col["life exp"]. Names in other writing systems need no such thing: col.지역 (jiyeok, region) is an ordinary identifier.

49.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'))
256 512 1024 2048 4096 8192 16384 32768 40 50 60 70 80 Doublings of income Life Gdp

“Given gapminder 2007: points, x is gdp on a log scale in base 2, y is life.”

style() is the case where every argument is named, so it is written as one object throughout the book: style({ color: "tomato", size: 3 }).

49.7 The table, and its name

A table is an object of columns, { person: ['Ada', 'Alan', 'Grace'], cm: [160, 172, 181] }, so a first plot needs nothing installed:

plot(data(heights), bar, x(col.person), y(col.cm))
Ada Alan Grace 0 50 100 150 Cm Person

R reads the table’s name off the expression you wrote, and Python off the variable you passed. JavaScript can do neither, and the name matters: a layer resolves its columns against the nearest table by that name (Data). 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.

49.8 A sub-expression is a value

This spelling makes one law easy to see. 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 law is Compositional Invariance, the sixth of the nine laws, and JavaScript keeps it with no help from the binding.

49.9 Reading from a database

Node.js, where JavaScript runs outside a browser, 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, data, 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 built into Node.js, behind a flag from version 22 and without one from 23.4, so that example needs nothing installed. better-sqlite3 has the same shape and works the same way.

49.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 where a language rule changes what a sentence can do rather than how it is spelled, and it costs one await.

49.10 Getting it

A plot is drawn by the engine, which is a compiled Rust binary, so the npm package has to be able to find one.

npm install grammar-of-graphics

That 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 only builds the specification and hands it to 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. Nothing has to open a viewer window and nothing has to become pixels first, which is where JavaScript sits closest to the engine.

To work from a checkout instead, build the engine once and import the package from where it sits:

cargo build --release -p gog-cli
import { plot, data, point, x, y, col, render_svg } from "./js-pkg/gog/src/index.js";

The engine is looked for in four places, in this order: the GOG_CLI_PATH environment variable, the platform package installed beside this one, gog-cli on your PATH, and a target/release build in a surrounding checkout. The second and third are how an installed copy works; GOG_CLI_PATH is how this page was rendered.

49.11 A plot for a web page: html_block()

The string render_svg() returns is the drawing and nothing else. For a web page, html_block() returns the same drawing with everything around it: the line of controls under the picture, and, for some plots, a copy of the engine compiled for the browser, which redraws them. A plot in the cube, a plot on the globe, a turned network, and a plot with a brush all carry that engine. Those are the plots of Space, Globe, Network and Selection.

const cube = plot(data(gapminder_2007), point,
                  x(col.gdp), y(col.life), z(col.population));
console.log(html_block(cube).split("\n")[0]);
<div class="gog-plot" id="gog-00007g442g" style="text-align:center;">

The function returns HTML as a string. Put it in a page, a notebook cell, or a dashboard panel. The picture is written first, so it is on the page before that copy of the engine loads. A page whose security policy forbids WebAssembly, the format that copy ships in, still shows the plot. What it loses is the dragging: the cube and the globe stop turning, and the brush stops moving.

R, Python and Julia never call this function. A notebook in those languages asks the plot object how it would be displayed, and each binding answers: R with repr_html, Python with _repr_html_, Julia with a show method. JavaScript has no such request to answer, so in JavaScript it is a function you call.