52  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. Everything in this book up to here is as true in JavaScript as it is in R.

The sentence is spelled differently here, and the operators are the reason. Every other binding joins the same words with the same four operators. JavaScript cannot join words that way, because +, *, | and / cannot be given a meaning on objects. For two objects a and b, a + b joins them into one piece of text, a * b is NaN, and a | b is the number 0. There is no version of the language in which data(gm) + point builds a plot.

So JavaScript spells the four operators as six words: plot(), layer(), across(), down(), beside() and below(). That is the change this chapter starts from. Read the two lines together, the R first and the JavaScript under it:

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, a message or a printed line under it was run when the page was built, through the same engine every other chapter uses. The database and install commands are shown, not run. In your own code, import the words you use, such as import { plot, data, point, x, y, col, render_svg } from "grammar-of-graphics". The blocks on this page leave that line out, and also the line that reads the table: const gapminder_2007 = await gog_table("gapminder_2007");.

JavaScript brings in only the names you list, and none of gog’s names is already a name in the language. So, unlike the R, Python and Julia chapters, this one has no section on names the language already uses. If a gog word matches a name your own script uses, such as path beside Node.js’s node:path, import it under another name with as. The sections below take the four operators first. Then come how a column, a named argument and a table are written, a plot for a web page, reading from a database, and installing the package.

52.1 The comma is +

A gog sentence is a list of words, and in JavaScript it is a list of arguments. A comma means nothing on its own. Inside plot(), the commas join the words, exactly as + joins them in R. A named argument is written differently: it goes into an object at the end of the call. The section on named arguments, below, sets that 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.”

Those are the words of A first plot without its title, in the same order, reading the same way. Where you write a word still decides its scope (Encoding scope). A channel written after a mark belongs to that mark, and one written before any mark belongs to the plot. The commas change nothing about that; they are the + signs.

52.2 layer() is *

The commas replaced +. * cannot become a comma, because R groups * before +. That order is called precedence, and it is how bar * bin + x(life) reads as a binned bar rather than a bar plus a bin. JavaScript writes function calls rather than operators here, so there is no precedence to use. Nesting does the same work:

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 clearer than precedence. A reader who does not know that * groups before + has to be told. A reader looking at layer(bar, mean) can see it.

Just over half the sentences in this book need no layer() at all, because they are a flat list of words. Where one is needed, it usually holds two words:

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.”

The two words are a mark and the transform that derives it. The color here is a setting, written as an object inside style().

52.3 across() and down() are | and /

Two operators are left, | and /. With facet() on their right, both split one plot into panels. Faceting in R is plot | facet(era), panels side by side, or plot / facet(era), panels stacked. In R, facet() means nothing on its own: it always follows a | or a /. JavaScript writes that whole 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 the panels instead. Writing both gives a grid: one panel column per value of the column in across(), and one panel row per value of the column in down(). That is what plot | facet(a) / facet(b) does in R.

Because the operator and facet() are one word here, there is no separate facet() to write. Writing facet(), as the other three languages do, is refused, and the message names the two words 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)`.

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

The | you have just replaced had a second job in R. What is on its right decides which job it does: plot | facet(g) splits one plot into panels, and plot_a | plot_b places two plots in one arrangement. JavaScript cannot overload |, so the two meanings never share a symbol. The second one 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 0 10K 20K 30K 40K 50K 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 algebra of pictures published in 1982. Henderson’s Functional Geometry (Henderson, 1982) combines pictures with beside and above. His above(a, b) and gog’s below(a, b) draw the same picture, with a on top and b under it. Composition gives the history. Both words also take an arrangement, the result of an earlier beside() or below(), so one arrangement can hold another. below(top, beside(main, right)) is the marginal plot, exactly as top / (main | right) is in R: a histogram above a scatter, and another to its right. The spelling does not decide what two composed plots share; the engine does. Plots one above the other align their x axes, and plots side by side align their y axes. When both plots put the same column on that aligned axis, and read it the same way, it becomes one axis: one scale, both panels the same length along it, drawn once.

That marginal plot, below(top, beside(main, right)), needs no parentheses beyond the calls themselves. In an arrangement written in R, Python or Julia, this book puts each plot inside its own parentheses. Without them, precedence can group parts of two plots together (Parentheses in an arrangement). JavaScript needs none of those parentheses. Each plot is already one plot() call, and beside() and below() take whole plots and arrangements as their arguments, so the nesting shows the grouping. This book writes a theme() for the whole arrangement as the last argument of beside() or below():

beside(plot(data(gapminder_2007), point, x(col.gdp), y(col.life)), plot(data(gapminder_2007), layer(bar, count), x(col.continent)), theme({ height: 260 }))
0 10K 20K 30K 40K 50K 40 50 60 70 80 Life Gdp Asia Europe Africa Americas Oceania 0 20 40 Count Continent

“Given gapminder 2007: points, x is gdp, y is life, beside bars derived by count, x is continent.”

In R, Python and Julia, a theme() for the whole arrangement follows parentheses around the arrangement, as in ((…) | (…)) + theme(height = 260). In JavaScript, the theme() goes inside the call that makes the arrangement, so no extra parentheses are needed.

52.5 A column is written col.gdp

The operators are the largest change JavaScript makes to a sentence. Three smaller ones are left, and the first is how a column is named. R writes a column as a bare name and Julia as a symbol. Python and JavaScript write it through col, a small object called an accessor: col.gdp names the column gdp. Both languages make that choice 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 both be plain strings, and gog could not tell which one you meant.

So a string where a column belongs is refused, and the message says 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*. In JavaScript, a bare name is a variable, so a column is written with the accessor: `x(col.gdp)` maps the column called `gdp`.

The message names the column spelling, x(col.gdp). A channel takes a column, and a string is a value (Setting vs mapping). The opposite mistake, a column where a value belongs, 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 name with a space or a dot in it is not a JavaScript identifier, the kind of name that can follow a dot. Write it in the bracket form instead: col["life exp"]. A name in another writing system needs no brackets. JavaScript accepts letters from any writing system in an identifier, so col.지역 (region) works.

52.6 A named argument is one trailing object

That was the first of the smaller differences. The second is how a named argument is written. R writes x(gdp, scale = "log"). JavaScript has no syntax for a named argument. You put the named ones into a single object at the end, and 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 }).

52.6.1 How values are written

In the other chapters, a code block that is a sentence has a tab for each language. Code written inside the text has none, so it is written in R, as in x(life, limits = c(37.5, 87.5)). In JavaScript, limits goes into the trailing object, and its value takes JavaScript’s spelling. R writes several values inside c(), and JavaScript writes them in an array, inside square brackets. The table gives the JavaScript spelling of each kind of value the book uses:

Kind of value R JavaScript
true or false TRUE, FALSE true, false
several numbers c(37.5, 87.5) [37.5, 87.5]
several strings, such as colors c("tomato", "steelblue") ["tomato", "steelblue"]
a color for each category c(Asia = "tomato", Europe = "steelblue") { Asia: "tomato", Europe: "steelblue" }
a missing value NA null
the number e exp(1) Math.E
a declared order factor(values, levels = c("Low", "High")) ordered(values, ["Low", "High"])

So x(life, limits = c(37.5, 87.5)) in the text is x(col.life, { limits: [37.5, 87.5] }) in JavaScript, and legend = FALSE is { legend: false }. ordered() declares the order of a column’s categories, as R’s factor() does, and the Data chapter describes it.

52.7 The table, and its name

The third difference is how a table gets its name. A table is an object of columns, or an array of rows, one object per row. Neither needs a package besides gog, and this one is written in place:

const heights = { person: ["Ada", "Alan", "Grace"], cm: [160, 172, 181] };
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 from the expression you wrote, and Python from the variable you passed. JavaScript can do neither. The name matters, because a layer looks up its columns in the nearest table, and it finds that table by name (Data). So an unnamed table gets a generated name instead, such as data or data2, which is enough to tell two tables in one sentence apart. Some messages name the table they are about, such as the warning about missing values. Write data(df, { name: "notes" }) when you want such a message to say notes.

A column of Date values is drawn on a calendar axis. gog reads each Date in local time, the time shown by the clock of the computer that runs your code. So new Date(2024, 0, 1, 12), noon on January 1, is drawn at 12:00. JavaScript’s Date type holds both a date and a date-time, so gog distinguishes the two by their values. If every value in a column is midnight in local time, as new Date(2024, 0, 1) is, gog reads the column as dates and never gives it clock ticks. JavaScript reads the text in new Date("2024-01-01") as midnight in UTC, the time every time zone is counted from. On a computer in New York, gog draws that value at 19:00 on December 31, 2023. On a computer in Seoul it is 09:00 on January 1: the right day, but not midnight, so the column is read as date-times. To get midnight in every time zone, write a time after the date: new Date("2024-01-01T00:00") is midnight in local time.

Three more things have a JavaScript spelling. A missing value is null, undefined or NaN: a row with one in a column the plot maps is left out, and gog says how many rows it left out. ordered(values, levels) declares the order of a column’s categories, which the Data chapter describes. And the book’s own tables come from gog_table(), which fetches them over the network. It returns a Promise, JavaScript’s object for a value that arrives later, so you write await before it, as The book’s data shows.

A column can also be a typed array, such as a Float64Array or an Int32Array, which numeric libraries often return. A BigInt is JavaScript’s type for a whole number of any size, written with a final n, as in 1204n. Some database drivers return one, and gog reads it as a number:

const sales = { year: Int32Array.of(2019, 2020, 2021, 2022, 2023), units: [1204n, 1350n, 1288n, 1523n, 1689n] };
plot(data(sales), line, point, x(col.year), y(col.units))
2019 2020 2021 2022 2023 1200 1300 1400 1500 1600 1700 Units Year

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

JavaScript’s Number type holds every whole number exactly only up to 253, about 9 × 1015. Above that, gog refuses a BigInt that a Number cannot hold exactly, in a column the sentence names, rather than draw it in the wrong place.

52.7.1 A second table

A sentence can also draw from a second table. In JavaScript a later data() is one more argument, and as in every language it applies only to the mark written directly after it (Data). So two marks that read the second table each take their own data(). Here the line reads actuals, and the points and the area both read forecast:

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
plot(data(actuals), x(col.year), y(col.sales),
  line,
  data(forecast), point,
  data(forecast), 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 forecast: an area.”

You might try to give the second table to both marks at once, by putting them in a plot() of their own inside the first one. A plot() cannot be an argument to another plot(), so gog refuses the sentence:

plot(data(actuals), x(col.year), y(col.sales), line, plot(data(forecast), point, area))
gog: a plot cannot be an argument to another plot, so everything inside it would be dropped. Write the atoms in sequence instead, and repeat `data()` before each mark that reads that table: `plot(data(a), line, data(b), point, data(b), area)`. To arrange two plots, use `beside()` or `below()`.

The message gives the sentence above. It is the same mistake as + (data(forecast) + point + area) in R, Python and Julia (What parentheses may group).

52.8 A sub-expression is a value

Writing a sentence as plain function calls makes one law easy to see. A piece of a sentence is an ordinary JavaScript value, so you can give it a name, use it again, and pass it to a function. It means the same thing in every sentence that holds it:

const piled = layer(point, bin, stack);
console.log(String(plot(data(gapminder_2007), piled, x(col.life))));
<gog plot: point on data>

Printing a plot gives one line, naming its mark and its table, here the generated name data. It does not give the picture. piled could go into a second sentence unchanged, and those three words would mean the same thing there. That law is Compositional Invariance, the sixth of the nine laws.

52.9 A plot for a web page: html_block()

Everything so far has been about writing a sentence. The rest of this chapter is about using one: on a web page, with a table from a database, and from an installed package.

render_svg() returns a plot as an SVG string: the drawing and nothing else. For a web page, html_block() returns the same drawing with the buttons under every plot. Some plots also get a copy of the engine compiled for the browser, which redraws the plot as you turn or brush it. Four kinds of plot need it: a plot in the cube, a plot on the globe, a network given a viewing angle, and a plot with a brush. 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-2f77ca5808" style="text-align:center;">

The function returns HTML as a string. Put it in a page, a notebook cell, or a dashboard panel. The drawing comes first in that string, so a reader sees the plot before the copy of the engine has finished loading. That copy is compiled to WebAssembly, and a page whose security policy forbids WebAssembly still shows the plot. What stops working is everything that copy does. The cube, the globe and a network given a viewing angle no longer turn, and a drag on a brushed plot selects nothing. A note under the plot says that the engine could not start. The buttons under every plot still work, because they do not need WebAssembly. On a plot without brush, the card that shows a mark’s values still works too.

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 here it is a function you call.

A script run outside a notebook has nowhere to put that HTML, so show(plot) saves it to a file in your computer’s folder for temporary files. It returns the file’s path. Unlike Python’s plot.show(), it opens no browser.

Three more functions are for building a tool with gog, such as a web server, and never for writing a sentence. svg_block(svg) puts an SVG string in centered HTML that shrinks to fit a web page, without the controls. find_gog_cli() returns the path of the engine the package found (Installing gog). to_wire(table, name) returns the table as gog sends it to the engine, before it becomes JSON; name labels its messages. Julia exports these three as well; R and Python do not.

52.10 Reading from a database

Your table may be stored in a database rather than held in your session. R, Python and Julia each have a database standard, but Node.js, the program that runs JavaScript outside a browser, has none. So query() accepts a connection by the methods it has rather than by its type. Any connection will work if it can prepare a statement and return all its rows:

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. It works without a flag from Node.js 22.13, and from 23.4 in the version 23 series, so on those versions that example needs nothing installed. Node.js prints a warning that the module is experimental, and the plot is drawn anyway. better-sqlite3 has the same shape and works the same way.

52.10.1 Asynchronous drivers

Not every driver, the package that connects to one kind of database, returns its rows that way. render_svg() is synchronous: it returns the SVG rather than a Promise. That is what lets a plot be written inline in an expression. A driver whose query returns a Promise cannot be awaited inside a synchronous function.

So pg and mysql2 do not work with query(), and gog says so rather than letting a Promise reach the engine. Fetch the rows yourself instead. Both drivers return an array of rows, one object per row, and data() takes that array directly:

const { rows } = await con.query("SELECT status, revenue FROM orders");
render_svg(plot(data(rows), bar, x(col.status), y(col.revenue)));

The first line is written for pg. With mysql2 it begins const [rows] = instead. The line after it is the sentence you would write for any table. The driver also decides each column’s type: pg returns a count or a numeric column as text, so cast it in the SQL, as in COUNT(*)::int or revenue::float8. Everywhere else, a rule of JavaScript changes only how a sentence is spelled. Here it changes what a sentence can do: the query runs before the sentence, with one await.

52.11 Installing gog

Every plot is drawn by the engine, a separate program written in Rust, so the npm package has to be able to find it.

npm install grammar-of-graphics

That command works. The package is on npm, and the name is written in full 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 is installed with it. There are five more packages on npm, one per platform, each holding the engine compiled for that platform. The main package lists all five as optional dependencies. The install 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 five cover macOS on Intel and ARM processors, Linux on Intel, AMD and ARM processors, and Windows on Intel and AMD processors. On any other computer the install still succeeds, and the first plot stops with an error that says no engine is published for that computer.

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))));

The package runs in Node.js and not in a browser, because it starts the engine as a separate program. On a server, you send the SVG string as the response, and the page puts it straight into its DOM. Nothing has to open a viewer window, and nothing has to be turned into pixels first. What the engine returns is already the finished picture.

To work from a checkout instead, a copy of gog’s source repository, build the engine once. Then import the package by its path, from the repository’s top folder:

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 the main one, gog-cli on your PATH, and a target/release build in a surrounding checkout. The search stops at the first place that holds the engine, so an installed copy normally stops at the platform package. GOG_CLI_PATH is how this page was rendered. If GOG_CLI_PATH names no file, gog refuses to draw and says which path it was given. It does not move on to the next place, because a mistyped path would then draw with an engine you did not choose.

52.12 What comes next

These four chapters are about spelling, and none of them changes what a sentence means. The afterword closes the book with the ten-day challenge, and the one goal that remains.