43  Julia

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 Julia as it is in R.

Of the four languages, Julia is the one with the least to say for itself, and that is the interesting fact about it. It is the only binding besides R that can spell all four assembly operators, and its symbols keep a column apart from a value the way R’s bare names do. So a sentence differs from the R above it by one colon:

data(gm) + bar * bin + x(life) | facet(era)
data(gm) + bar * bin + x(:life) | facet(:era)

Every Julia block below was executed to draw the plot beside it, through the same engine every other chapter uses.

43.1 A column is a symbol

data(gapminder_2007) + point + x(:gdp, scale = "log") + y(:life) + color(:continent)
1K 10K 40 50 60 70 80 Life Gdp Continent Asia Europe Africa Americas Oceania

That is A first plot with a colon in front of each column. The colon is doing the work R’s bare name does and Python’s col. accessor has to be built for: in this grammar a plain string is how you spell a value, as in style(color = "tomato") or palette("okabe"), and a symbol is visibly not a string. The grammar’s oldest rule, that a channel takes a column and never a value, is therefore in the syntax rather than in a check.

So a string where a column belongs is refused, and told what to write:

data(gapminder_2007) + point + x("gdp") + y(:life)
gog: `x("gdp")` binds a *value*, and a channel takes a *column*. In Julia a column is a symbol, which is what keeps the two apart: `x(:gdp)` maps the column called `gdp`.

The mistake in the other direction is refused the same way, and for the same reason (Setting is not mapping):

data(gapminder_2007) + point + x(:gdp) + y(:life) + style(color = :continent)
gog: `style(color = )` fixes one value for the whole layer, and `:continent` is a column. To *map* it — one value per category, with a legend to decode it — that is a channel: `color(:continent)`.

A column whose name is not a Julia identifier is written Symbol("life exp"). Names in other scripts need no such thing: :지역 (jiyeok, region) is an ordinary symbol, because Julia identifiers are Unicode.

43.2 The operators, and their precedence

Julia’s precedence table is not R’s. R puts | below +; Julia puts it in the addition tier, alongside +. That could have broken every faceted sentence in this book, so it was measured rather than assumed:

Written Julia builds R builds
a + b + c \| d (a + b + c) \| d the same
a \| b / c a \| (b / c) the same
a * b + c (a * b) + c the same
a \| b + c (a \| b) + c a \| (b + c)

Only the last row differs, and it is the shape where something is written after a facet. None of the 500 sentences in this book has it, and R refuses that shape anyway, so every sentence the manual teaches parses identically in both languages. Left associativity is what saves the first row: | sitting beside + rather than below it does not matter when everything to its left was written first.

Faceting therefore reads exactly as it does in R:

data(gapminder_2007) + point + x(:gdp, scale = "log") + y(:life) | facet(:continent)
1K 10K 40 50 60 70 80 1K 10K 1K 10K 1K 10K 1K 10K Asia Europe Africa Americas Oceania Life Gdp

And so does a transform, with * binding tighter than +:

data(gapminder_2007) + bar * mean + x(:continent) + y(:life)
Asia Europe Africa Americas Oceania 0 20 40 60 80 Life Continent

43.3 Ten words need naming twice

This is Julia’s one real wrinkle, and every Julia programmer has met it before. Ten of the fifty-one kernel words are also names in Base (bin, count, sum, min, max, range, size, step, stack, map), and Julia will not silently pick a winner between two modules that export the same name. Using one bare gives an UndefVarError with a hint rather than the wrong function, which is the good outcome, but it does mean a second import line:

println(bar * bin)
println(Base.sum([1, 2, 3]))
<gog bar>
6

The preamble that made that work is the one every snippet on this page runs, and it is worth seeing in full:

using GrammarOfGraphics
using GrammarOfGraphics: bin, count, sum, min, max, range, size, step, stack, map

Base.sum is still there, as the second line of output shows.

The ten are listed as equals, and one is not. map is the one most likely to reach code you already wrote. Base.map is everyday Julia. Base.step and Base.stack are not. A script that called map before loading this package will stop working, and the error will name a line that did not change.

This is the third language to hit the same wall from a different side: R’s range, sum and min belong to base R, Python’s bin, sum and max are builtins, and Julia’s are Base. A grammar keeps its own vocabulary; what changes is how each language asks you to say which one you meant.

43.4 The table

A table is a named tuple of columns, so a first plot needs nothing installed:

data(heights) + bar + x(:person) + y(:cm)
Ada Alan Grace 0 50 100 150 Cm Person

A Dict works too, and so does anything that answers names and [!, col], which is what a DataFrame does. That last one is duck-typed rather than imported, so the binding depends on nothing but Dates.

Julia and R are also the only two languages here that draw a date apart from a timestamp, which is the distinction the engine’s time axis wants: a Date column gets day ticks and a DateTime column keeps its clock, with no inference in between.

R reads a table’s name off the expression you wrote, and Julia cannot, so an unnamed table is given a unique one and data(df, name = "notes") is there when a message should say notes rather than data2 (Bind once).

43.5 Reading from a database

query() takes a DBInterface.jl connection. That is Julia’s database standard, and it covers SQLite.jl, LibPQ.jl, MySQL.jl and DuckDB.jl.

using DBInterface, SQLite, GrammarOfGraphics

con = SQLite.DB("sales.db")
query(con, "SELECT status, revenue FROM orders") + bar + x(:status) + y(:revenue)

One detail is specific to this binding. GrammarOfGraphics does not depend on DBInterface. The package declares one dependency, Dates, and adding a database stack for a feature many readers never use would be a poor trade. So the package looks DBInterface up in your session instead.

The practical consequence is one line: you must write using DBInterface yourself. Without it, query() says so and names the fix rather than failing somewhere further in.

43.6 Getting it

The Julia package is named GrammarOfGraphics. The name is longer than the one R and Python use, and the registry is the reason: Julia asks a package name for at least five letters and capitalized words, so a lowercase gog is not legal there.

Installing takes two commands here, and the engine comes first. A plot is drawn by the engine, which is a compiled Rust binary. The R, Python and JavaScript packages each ship one built for your computer, and the Julia package does not carry one yet, so this is the binding that asks you to install it:

cargo install --git https://github.com/psychometrician/gog gog-cli

That command needs Rust, which rustup installs in one step. It puts gog-cli on your PATH, which is where the package looks. If you started Julia from an editor or a notebook, that session may not see your PATH, so set ENV["GOG_CLI_PATH"] to the binary instead. Then the package:

using Pkg
Pkg.add("GrammarOfGraphics")

The order is advice rather than a rule. The package loads without an engine and looks for one only when it draws, so the other order works as well. Installing the engine first means you never meet the error.

The proper answer is an artifact, which is how Julia normally distributes a compiled binary, and it is owed rather than optional. Until it lands, the examples in this chapter assume you installed the engine yourself.

To work from a checkout instead, add the package from the path where it sits:

using Pkg
Pkg.develop(path = "jl-pkg/GrammarOfGraphics")

using GrammarOfGraphics

The engine is looked for in four places, in this order: the GOG_CLI_PATH environment variable, the copy inside the installed package, gog-cli on your PATH, and a local target/release build. The second place is the empty one today, which is why the third and the fourth matter more in Julia than they do in the other three languages.

render_svg(plot) returns the SVG as a string, and a plot shown in a notebook draws itself, because the binding answers the image/svg+xml MIME type.