Let’s make maps with bertin.js in Quarto


Nicolas Lambert


October 14, 2022

1 What is Quarto?

Quarto® is an open-source scientific and technical publishing system built on Pandoc. It allows to create dynamic content with Python, R, Julia, and Observable.

2 Data handling with R

2.1 R cells

Quarto is a multi-language, next generation version of R Markdown from RStudio, with many new features and capabilities. Like R Markdown, Quarto uses Knitr to execute R code, and is therefore able to render most existing Rmd files without modification.

To write R code, you have to put it in a R chunk as below.

# Some stuffs here

2.2 Data import

First, with read.csv, we can load tabular data…

stats <-  read.csv(file = '../data/stats.csv')

…and we perform some statistical analysis. Here, for the example, we just create a new variable gdppc (GDP per capita).

stats$gdppc = round(stats$gdp/stats$pop, digits = 2)

To see se table, we use the package DT.

stats %>% datatable()

Second, thanks to sf, we load geometries (world countries).

geom <- st_read("../data/world.gpkg") 

2.3 Join

Then, we make a join between the attribute data and the base map.

world = merge(geom, stats, by.x = "ISO3", by.y = "id")
world %>% datatable()

2.4 Passing data from R to ojs

The data is ready. Before making maps, we have to make them accessible to the ojs cells. For this we use the ojs_define function in a R chunk. To do this with an objet sf, we must first convert it to geojson, then pass it to ojs. For that,
we sugget to use the geojsonsf package.

ojs_define(data = sf_geojson(world))

If you want to know more about the way to pass variables from R to ojs, you can see this document.

That’s all about R. Now let’s go into Observable to plot this data.

3 Data visualization with ojs

We have left R. We are now in the world of Observable.

3.1 ojs cells

Quarto includes native support for Observable JS, a set of enhancements to vanilla JavaScript created by Mike Bostock (also the author of D3). Observable JS is distinguished by its reactive runtime, which is especially well suited for interactive data exploration and analysis.

To write ojs code, you have to put it in an ojs chunk as below.

// Some stuffs here

Note that with ojs_define, we have passed the variable geo as a string and not actually as an object.

data.substr(1, 300)

The first thing to do here is to transform our string into a real object. To do this, we use the javascript statement JSON.parse.

countries = JSON.parse(data) 

Note that unlike the R universe, here we manipulate always data in JSON (JavaScript Object Notation) format.

3.2 Visualize geojson data

First, if you want to view the attribute data, you can write this line:

Inputs.table(countries.features.map(d => d.properties))

And to visualize geometries, the package geoverview is a good option.

geo = require("geoverview@1.2.1")
geo.view(countries, {width: 750})

Well, we are finally ready to draw thematic maps!

3.3 Load the bertin library

bertin is a JavaScript library for visualizing geospatial data and make thematic maps for the web. To use it in Quarto, you can call it with the require function. You ca, specify the version of the library with @major.minor.patch as below. All the versions of the library are available on npm. The documentation and the code of the library is available here.

bertin = require("bertin@1.2.4")

3.4 First steps with bertin

To display the geometries quickly, you can use the quickdraw function


But if you want to draw a fully customizable map (projection, colors, etc), you must use the following syntax. All explanations are available in the documentation. Many examples are available in this Observable collection.

  params: { projection: "Eckert3" },
  layers: [{ geojson: countries, fill: "#3996c4" }]

In the layer properties, you can also add layers. You can add other geometries or specific layers include in the library using the type parameter.

  params: { projection: "Gilbert", width: 500 },
  layers: [
    { type: "header", text: "The Blue Marble" },
    { geojson: countries, fill: "white", stroke:"none" },
    { type: "graticule" },
    { type: "outline" }

Remember that Observable is a full reactive environment. As a result, you can interactively vary all the elements of the map.

viewof lobes = Inputs.range([6, 30], {label: "Number of lobes", value: 10, step: 1})
viewof color = Inputs.color({label: "Color", value: "#4682b4"})
  params: { projection: `Gingery.lobes(${lobes})`, width: 500, clip: true },
  layers: [
    { type: "header", text: `${lobes} lobes` },
    { geojson: countries, fill: color, stroke:"none" },
    { type: "graticule" },
    { type: "outline" }

3.5 Typologies

Ta map a qualitative data, you can use the type typo instead of a static fill value.

  params: {
    projection: "InterruptedHomolosine",
    clip: true
  layers: [
      type: "layer",
      geojson: countries,
      tooltip: ["$region", "$NAMEen"],
      fill: {
        type: "typo",
        values: "region",
        strokeWidth: 0.3,
        colors: "Tableau10",
        leg_title: `The
        leg_x: 55,
        leg_y: 180
    { type: "graticule" },
    { type: "outline" }

3.6 Bubbles

To draw a map with proportional symbols (absolute quantitative data), you can use the type bubble. This map is interactive. Hover over the bubbles with your mouse.

viewof k = Inputs.range([10, 80], { label: "radius max", step: 1, value: 35 })
viewof dorling = Inputs.toggle({ label: "Dorling Cartogram", value: true })
  params: { projection: "Bertin1953"},
  layers: [
      type: "bubble",
      geojson: countries,
      values: "pop",
      k: k,
      dorling: dorling,
      fill: "#e368c0",
      tooltip: ["$NAMEen", "$pop", "inhabitants"],
      leg_round: 0,
      leg_x: 700,
      leg_y: 400,
      leg_round: -1,
      leg_title: "World Population\nin 2018 (inh.)"
      type: "layer",
      geojson: countries,
      fill: "white",
      fillOpacity: 0.3
    { type: "graticule" },
    { type: "outline" }

3.7 Choropleth

Ta map a relative quantitative data, you can use the type choro instead of a static fill value.

viewof nbreaks = Inputs.range([3, 9], { label: "nbreaks", step: 1, value: 7 })
viewof method = Inputs.select(["jenks", "q6", "quantile", "equal", "msd"], {
  label: "method",
  value: "quantile"
choro = bertin.draw({
  params: { projection: "Eckert3"},
  layers: [
      type: "layer",
      geojson: countries,
      fill: {
        type: "choro",
        values: "gdppc",
        nbreaks: nbreaks,
        method: method,
        colors: "RdYlGn",
        leg_round: -2,
        leg_title: `GDP per inh
(in $)`,
        leg_x: 100,
        leg_y: 200
      tooltip: ["$name", "$gdppc", "(current US$)"]
    { type: "graticule" },
    { type: "outline" }

4 Conclusion

Combining R and Observable javascript allows to take advantage of the strengths of both languages. It allows to combine the statistical analysis possibilities of R and the reactive visualization features of Observable. A win win strategy.

Concerning bertin, many other types of maps are possible with bertin. Spikes, mushroom, discontinuities, squares, flows, dot cartograms, symbols, etc. You can refer to the documentation and to the Observable notebooks to learn how to do it. In the end, once you understand the principle, it is just a matter of filling in the parameters. Or you can simply copy a piece of code, insert your data and modify some parameters.