Observable is a web-first interactive notebook for data analysis, visualization, and exploration .
It was created by Mike Bostock, the creator of d3.js, which we discussed previously way back in 2014 . At some point, Mike stepped down from d3.js to focus on a new library called d3.express, which he presented in 2017 during the OpenVis conference  and was still in the makes. d3.express eventually got renamed to Observable.
I’ve been experimenting with Observable and in this post I’ll cover some of my learnings while trying to build two visualizations.
One of the benefits of notebooks like Observable is to co-locate code with its output. This makes it much easier to explain code because in addition to markdown comments, we can modify the code and see the results in real-time. This means that a lot of the documentation of APIs available in Observable are notebooks themselves. For these reasons, this post consists of a series of links to notebooks with high-level comments.
What is Observable?
Why Observable – this notebook explains the motivation behind Observable and how it works at a high level:
An Observable notebook consists of reactive cells, each defining a piece of state. Rather than track dependencies between mutable values in your head, the runtime automatically extracts dependencies via static analysis and efficiently re-evaluates cells whenever things change, like a spreadsheet
Five-Minute Introduction – provides a hands-on approach of the many topics, some of which we’ll cover in more detail in this post:
- Cell references
- HTML/DOM display
- Importing from other notebooks
- A cell is composed by a cell name and an expression:
[cell name] = [expression]
It can be simple statements like 1 + 2, or multi-line statements like
- The value of
cell_namecan be used in other cells, but by default is read-only.
- Each cell can only have one
cell_nameassignment. In other words, it can only “export” one variable. It’s possible to cheat by exporting an object. Note that because curly brackets are used to denote code blocks, we need to wrap an object literal with parenthesis:
- Cells can refer to other cells in any part of the code – Observable builds a dependency DAG to figure out the right order. This also means dependencies cannot be circular. How Observable Runs explains this in more details.
- Constructs like async functions (await) and generators (yield) have special behavior in a cell. We’ll expand in the Generators section below.
- Cells can be mutated if declared with a qualifier (mutable). We’ll expand in the Mutability section below.
Introduction to Mutable State – cells are read-only by default but Observable allows changing the value of a cell by declaring it
mutable. When changing the value of the cell elsewhere, the
mutable keyword also must be used.
It’s important to note that the cell is immutable, but if the cell is a reference to a value, say an
Object, then the value can be mutated. This can lead to unexpected behavior, so I created a notebook, Mutating references, to highlight this issue.
Markdown summary – is a great cheat sheet of Markdown syntax in Observable. I find the Markdown syntax in Observable verbose which is not ideal given text is so prevalent in notebooks:
More cumbersome still is typing inline code. Because it uses backticks, It has to be escaped:
To be fair, most code will be typed as cells, so this shouldn’t be too common. It supports LaTeX via KaTeX which is awesome. KaTeX on Observable contains a bunch of examples.
Introduction to Generators explains how generators can be used in Observable. The combination of generators and delays (via promises) is a great mechanism to implement a ticker, which is the base of animation. Here’s a very simple way to define a ticker in Observable (remember that Promises are awaited without extra syntax):
Introduction to Views explains what views are and how to construct one from scratch. I like this recommendation:
If there is some value that you would like the user to control in your notebook, then usually the right way to represent that value is as a view.
Views are thus convenient ways to expose parameters via buttons and knobs such as text inputs, dropdowns and checkboxes.
I ran into a problem when using checkbox with labels in it, like the cell below:
It does not yield true/false as I wanted. This notebook, Checkbox, discusses the problem and offers a solution. This is an example where great but imperfect abstractions make it hard to understand when something doesn’t work. It’s worth learning about how
viewof is implemented behind the scenes.
In one of my experiments I needed to synchronize a view and a ticker. Bostock provides a neat abstraction in his Synchronized Views notebook.
Importing from other notebooks
Introduction to Imports shows how easy it is to import cells from other notebooks.
It also shows that it’s possible to override the value of some of the cells in that notebook:
This is neat, because the
chart cell depends on the
data, and by allowing overriding data, it allows parameterizing
chart. In other words, we import
chart as a function as opposed to a value (the result of
Versioning. When importing a notebook we always import the most recent version, which means it could potentially break if the notebook’s contract changes. The introduction above mentions the possibility of specifying the version when importing but didn’t provide an example on how to do so.
My strategy so far has been to clone a notebook if I’m really concerned about it changing, but that means one has to manually update / re-fork if they want to sync with the upstream changes.
One natural question that crossed my mind is whether I should have written this whole post as an Observable notebook. In the end I decided to stick with an old-school static post for two reasons:
- Durability: Observable has a lot of potential but it’s still mostly experimental and I’m not sure I can rely on it sticking around for a long time.
Notebooks mentioned in the post