| 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 |
51 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. Everything in this book up to here is as true in Julia as it is in R.
Of the three languages that are not R, Julia asks for the fewest changes to the sentence you already know. Like R and Python, it lets a package give +, *, | and / new meanings. Like R, it also keeps a column apart from a value in the syntax itself. So the sentence below differs from the R version only by a colon in front of each column:
data(gm) + bar * bin + x(life) | facet(era)data(gm) + bar * bin + x(:life) | facet(:era)The first sections cover how a column is written, how to break a sentence across lines, and how values are written. The next one shows where Julia groups the operators differently from R. Then come the tables Julia accepts, showing a plot, the nine names gog shares with Base, reading from a database, and installing the package.
Every Julia block on this page that shows a plot, a refusal or printed output ran when the page was built, through the same engine every other chapter uses. The other blocks are shown but not run. Each block ran after gapminder_2007 was fetched as The table shows, and after these two lines, which Nine names Julia already uses explains:
using GrammarOfGraphics
using GrammarOfGraphics: count, sum, min, max, range, size, step, stack, map51.1 A column is a symbol
The colon is the main change to the sentence, so this section shows it in a plot. R writes a column as a bare name, Python as col.gdp, and Julia as a symbol, a name written after a colon, such as :gdp:
data(gapminder_2007) + point + x(:gdp, scale = "log") + y(:life) + color(:continent)“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, without its title and 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. A channel takes a column and never a value. Because a symbol and a string look different, both you and the package can see when a value is written where a column belongs.
So a string where a column belongs is refused, and the message says 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 opposite mistake, a column where a value belongs, 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 name with a space or a dot in it is written Symbol("life exp"). A name in another writing system needs no Symbol(): :지역 (region) is an ordinary symbol. Julia accepts letters from any writing system in a name.
51.2 A sentence over several lines
A long sentence can be broken across lines, and Julia asks for nothing extra. Like R, it continues a line that ends in +:
data(gapminder_2007) + point + x(:gdp, scale = "log") + y(:life) +
color(:continent) +
size(:population)“Given gapminder 2007: points, x is gdp on a log scale, y is life, color by continent, size by population.”
What does not work is the opposite: a line that starts with the operator. Julia ends the statement at the line break and reads the next line on its own, so it reports an error about + instead of drawing. To write a sentence that way, put the whole sentence inside parentheses. The Python chapter writes every multi-line sentence this way (Python), and inside parentheses a line may begin with + in Julia too. Values also take Julia’s spelling, as the next section shows.
51.3 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)). Most of it reads the same in Julia once each column is a symbol. The values are what change. R writes several values inside c(), and Julia writes them in a vector, inside square brackets. The table gives the Julia spelling of each kind of value the book uses:
| Kind of value | R | Julia |
|---|---|---|
| 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 |
missing |
| the number e | exp(1) |
exp(1), or ℯ |
| 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(:life, limits = [37.5, 87.5]) in Julia. Write several values in square brackets. A tuple in parentheses also works for limits, but palette() and ordered() do not accept one. A named argument is written as in R, because Julia has named arguments of its own: 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.
51.4 The operators, and their precedence
A sentence can use up to four operators, so it matters which one Julia groups first. That order is called precedence, and Julia’s is not R’s. R and Python group | after +, and Julia groups | together with +, from left to right. That difference could change how a sentence is grouped, so the table compares four expressions in the three languages:
| Written | Julia reads it as | R and Python read it as |
|---|---|---|
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) |
The first three rows are read the same way in all three languages. In the first, Julia groups | together with +, from left to right, so everything left of the | is joined first. In the second and third, * and / group first in every language. Only the last row differs, because it is the only one with a + after the |. When b is a facet and c an atom such as title(), the difference does not change the plot. R joins the atom to the facet first, and Julia joins it to the plot directly. Both draw the same plot as a sentence with the atom written before the facet.
Faceting therefore reads exactly as it does in R:
data(gapminder_2007) + point + x(:gdp, scale = "log") + y(:life) | facet(:continent)“Given gapminder 2007: points, x is gdp on a log scale, y is life, split into panel columns by continent.”
A transform joins its mark first, as in R, because * groups before + in both languages:
data(gapminder_2007) + bar * mean + x(:continent) + y(:life)“Given gapminder 2007: bars derived by mean, x is continent, y is life.”
The last row of the table matters in an arrangement, two plots joined by | or /. The plot on the right of | starts with its own data(), and its other atoms follow with +. So an arrangement written in full has a + after the |. Without parentheses, Julia groups the first plot with that data() alone, and gog refuses an arrangement that it draws in R and Python:
data(gapminder_2007) + point + x(:gdp) + y(:life) |
data(gapminder_2007) + bar * count + x(:continent)gog: `|` places one plot beside another, and the plot beside it holds nothing but its table. Julia groups `|` with `+`, from left to right, so `|` joined that table before `+` could add the rest of its plot. Put each plot in parentheses: `(data(df) + point + ...) | (data(df) + bar + ...)`.
So in an arrangement, put each plot inside its own parentheses, with | as well as with /, which groups before + in all three languages. Each plot is then complete before | joins it, and the arrangement draws as it does in R:
(data(gapminder_2007) + point + x(:gdp) + y(:life)) |
(data(gapminder_2007) + bar * count + x(:continent))“Given gapminder 2007: points, x is gdp, y is life, beside bars derived by count, x is continent.”
The same row decides where a theme() for the whole arrangement goes. Written after an arrangement with no parentheses around it, the theme() goes to the arrangement in Julia, and to the second plot in R and Python. Put parentheses around the arrangement before the theme(), as in ((…) | (…)) + theme(height = 260). Then the theme() goes to the arrangement in all three languages (Parentheses in an arrangement).
51.5 The table
Every sentence starts from a table. The simplest table in Julia is a named tuple of columns, which the language provides, 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)The table holds three people and their heights, written directly inside the call. A Dict of columns works too. So does any object that supports names(table) and table[!, col], as a DataFrame does. The package never imports DataFrames, so if you do not use one, you install nothing extra. A CategoricalArray, from the CategoricalArrays package, is drawn in the order of its declared levels, as R’s factor is.
The other plots use gapminder_2007, one of the book’s shared tables, and gapminder_2007 = gog_table("gapminder_2007") fetches it. gog_table() fetches any of the shared tables by name, as it does in the other three languages (The book’s data).
Julia has both a Date and a DateTime. R and Python each have the same pair of types under their own names. A Date column never gets clock ticks, and a DateTime column can, because it stores a time of day (Scales). The package tells the two apart by type, so nothing is guessed.
In R, data() reads a table’s name from the expression you wrote. In Julia, data() receives only the table itself, so an unnamed table gets a generated name instead: data, then data2. Some messages name the table they are about, such as the warning about missing values, so write data(df, name = "notes") when you want that message to say notes (Data).
A column of lengths of time is refused. A Period from Dates, such as Minute(42), can be shown in minutes, hours or days, and gog does not choose the unit for you. travel holds how many minutes the trip to work took on five days:
using Dates
travel = (day = Date(2024, 5, 6) .+ Day.(0:4),
time = Minute.([42, 38, 55, 40, 47]))gog refuses the plot and names the column, and the message gives the code that makes it a number. That code uses the table’s name, so the sentence below gives the table one. Without it, the message would write data.time, which is not your variable:
data(travel, name = "travel") + line + point + x(:day) + y(:time)gog: column `time` of `travel` holds lengths of time, and gog has no scale for a duration. Make it a number in the unit you mean, such as hours: `Dates.value.(Dates.Second.(travel.time)) ./ 3600`.
Choose the unit you want to read on the axis. Here it is minutes:
travel = merge(travel,
(minutes = Dates.value.(Dates.Second.(travel.time)) ./ 60,))
println(travel.minutes)[42.0, 38.0, 55.0, 40.0, 47.0]
data(travel, name = "travel") + line + point + x(:day) + y(:minutes)“Given the travel table: a line and also points, x is day, y is minutes.”
travel still holds time, and this plot draws, because gog checks only the columns the sentence names.
51.6 Showing the plot
The plots on this page appear under their code. In your own session, where a plot appears depends on where you run it. In a notebook, such as Jupyter or Pluto, the notebook asks the package for a picture, and the plot is drawn under the cell. At the REPL, Julia’s prompt in a terminal, a plot prints as one line of text, such as <gog plot: bar on heights> for the heights plot in The table. To see the picture, assign the plot to a variable such as p, write it to a file with save_svg(p, "plot.svg"), and open that file in a web browser. render_svg(p) returns the same drawing as text.
Julia also exports three functions that R and Python do not. You need them only to build a tool with gog, such as a web server, and never to write a sentence. svg_block(svg) puts the text from render_svg() in centered HTML for a web page. Given the plot too, svg_block(svg, p) adds its 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. JavaScript exports the same three.
51.7 Nine names Julia already uses
Every plot above ran after two lines that its block leaves out, and this section explains them. Nine of the kernel’s words are also names in Base: count, sum, min, max, range, size, step, stack and map. Julia will not choose between two modules that export the same name, and the rule is the language’s own. Writing one of these nine without an import stops with an UndefVarError and a hint, rather than quietly calling the wrong function. The fix is one more import line:
using GrammarOfGraphics
using GrammarOfGraphics: count, sum, min, max, range, size, step, stack, mapWith them, a bare sum is gog’s transform, and Base.sum still adds numbers:
println(sum)
println(Base.sum([1, 2, 3]))<gog sum>
6
bin is not in the import line, because Base.bin is not exported, so there is no second bin to choose between.
The import line treats the nine names alike, but your existing code does not use them equally. map and size appear in ordinary Julia code constantly. After the import, a call such as map(f, xs) or size(A) in that code reaches gog’s word instead, and the error appears on a line you did not change. Write Base.map or Base.size there, and the same prefix works for any of the nine.
R, Python and Julia all have this problem, and each one solves it in its own way. 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.
51.8 Reading from a database
Your table may be stored in a database rather than in your session. query() runs a SQL query on that database, and it takes a DBInterface.jl connection. DBInterface.jl is Julia’s database standard, and SQLite.jl, LibPQ.jl, MySQL.jl and DuckDB.jl all implement it.
using DBInterface, SQLite, GrammarOfGraphics
con = SQLite.DB("sales.db")
query(con, "SELECT status, revenue FROM orders") + bar + x(:status) + y(:revenue)GrammarOfGraphics does not depend on DBInterface. Every dependency the package declares is a Julia standard library, and many readers never write SQL, so none of them should have to install a database package. Instead, the package uses the DBInterface that your driver loads. If no driver is loaded, drawing the plot stops with a message that names DBInterface and the line that loads it.
51.9 Installing gog
The Julia package is named GrammarOfGraphics. The name is longer than the one R and Python use, and the registry is the reason. Julia’s registry checks a new name automatically. The name must be at least five letters long and must start with a capital. A lowercase gog is too short and has no capital.
Every plot is drawn by the engine, a separate program written in Rust, so the Julia package has to be able to find it. Python and JavaScript each publish the engine built for every common platform, and the install picks the one for your computer. R builds it during the install, or installs one already built. Julia fetches it as an artifact, which is how a Julia package distributes a program it needs. Pkg downloads the copy of the engine built for your computer along with the package, so one call to Pkg.add installs everything:
using Pkg
Pkg.add("GrammarOfGraphics")To work from a checkout, a copy of gog’s source repository on your own computer, first build the engine once from the checkout’s top folder (this needs Rust):
cargo build --release -p gog-cliThen run Pkg.develop from the same folder:
using Pkg
Pkg.develop(path = "jl-pkg/GrammarOfGraphics")
using GrammarOfGraphicsThe Julia package looks for the engine in six places. In order: the GOG_CLI_PATH environment variable, a build in the checkout that holds the package, the artifact the install fetched, a copy you placed inside the package yourself, gog-cli on your PATH, and last a build found by searching upward from the working directory. The search stops at the first place that holds the engine. An installed copy is not inside a checkout, so it uses the artifact. Inside a checkout, your own build is searched before the artifact on purpose. The artifact is the engine of the last release, and using it would draw with an old engine while you are changing the engine’s source. 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.
51.10 What comes next
R, Python and Julia write one sentence three ways, and the main difference is how a column is named. JavaScript reads differently, because it cannot give + * | / new meanings, so it spells them as words.