48  Julia

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

Of the four languages, Julia is the one that changes least about the sentence you already know. Like R and Python, it gives +, *, | and / a meaning of their own, and like R it keeps a column apart from a value in the syntax: a symbol is not a string. 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 with a plot under it was executed, through the same engine every other chapter uses.

48.1 A column is a symbol

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

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

That is the sentence from A first plot, with a colon in front of each column. In this grammar a plain string spells a value, as in style(color = "tomato") or palette("okabe"). A symbol is visibly not a string, so the colon does the same work as R’s bare name and Python’s col. accessor. 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 vs 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"). A name in another writing system needs no such thing: :지역 (jiyeok, region) is an ordinary symbol, because Julia identifiers are Unicode.

48.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. No sentence in this book has it, and R refuses that shape anyway, so every sentence this book teaches parses identically in both languages. The first row is safe because Julia reads operators of one tier from left to right: everything left of the | is joined before the | is reached.

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

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

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

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

48.3 Nine names Julia already uses

Nine of the kernel’s words are also names in Base: count, sum, min, max, range, size, step, stack and map. Julia will not silently choose between two modules that export the same name, and every Julia programmer has met that rule before. Writing one of them without the import 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

Those two lines open every block on this page:

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

Base.sum is still there, as the second line of output shows. bin is not in the import line above, because Base.bin is not exported and so there is no second bin to choose between.

The import line treats the nine alike, and one of them costs more than the others. map is the one most likely to break 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.

Every language meets this problem, and each one asks you to solve it differently: R’s range, sum and min belong to base R, Python’s bin, sum and max are builtins, and Julia’s belong to Base. A grammar keeps its own vocabulary; what changes is how each language asks you to say which one you meant.

48.4 The table

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

data((person = ["Ada", "Alan", "Grace"], cm = [160.0, 172.0, 181.0]),
     name = "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. DataFrames is never imported, so a reader who does not use one installs nothing for it. gog_table("gapminder_2007") fetches any of the book’s shared tables, as it does in the other three languages.

Julia has both a Date and a DateTime, as R and Python do. A Date column gets day ticks, and a DateTime column keeps its hours and minutes. The engine reads the unit off the type, so nothing is guessed.

R reads a table’s name off the expression you wrote, and Julia cannot. An unnamed table gets a generated name instead, such as data2. Write data(df, name = "notes") when a message should say notes (Data).

48.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. Every dependency the package declares is a Julia standard library, and a database package would be the first that is not, installed for every reader although many never write SQL. 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() names the missing package and the line that fixes it, rather than failing later with a message about something else.

48.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 requires a package name of at least five letters, in capitalized words, so a lowercase gog cannot be registered.

Installing takes two commands here, and the engine comes first. A plot is drawn by the engine, which is a compiled Rust binary. The Python and JavaScript packages carry an engine built for your computer, and the R package carries one on macOS and Windows. The Julia package carries none, 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. cargo install then puts gog-cli on your PATH, which is one of the places 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 message that says the engine is missing.

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 Julia package carries no engine of its own, so the third and the fourth places are the ones that find it.

render_svg(plot) returns the SVG as a string, and a plot shown in a notebook draws itself, because the binding hands the notebook a picture when it asks for one.