Getting started with Observable

Ændra Rininsland
Journocoders
Published in
11 min readDec 6, 2018

This was written for the December 2018 Journocoders London meetup event. Like journalism and code? Come join us!

Notebooks are all the rage these days. On one hand, Jupyter has really energised the Python data and machine learning communities (Go have a play with Google Colaboratory if you’ve never done so at some point; it’s super fun), while Observable has brought notebook-format reactive programming to the web.

I particularly like Observable because it’s very web-native and allows the creation of moderately complex webapps for data visualisation. Not only can you share the same sweet, sweet D3 visualisations you used to with bl.ocks and BlockBuilder, but you can also document the steps you took and even provide interfaces for it, all without ever having initialised a Git repo.

We’re going to build a snowflake in Canvas using Observable, and make it totally configurable via sliders. In doing so I’ll briefly explain how Observable works, where a few of the weird bits are, and how to avoid some of the footguns I’ve run into. I’m also very opinionated about things and so I apologise for that in advance.

Also if you just want to go play with the finished product, it’s over at:

https://beta.observablehq.com/@aendrew/fancy-snowflake-generator-for-journocoders-december-2018

Okay, let’s go.

Getting started

First off, go to https://www.observablehq.com/ and sign in using GitHub. If you don’t have a GitHub account, click “Try the Scratchpad”, which is the exact same interface. You’ll need a GitHub account if you want to save your work, however.

1: Preview area 2: Editor area 3: “Run” cell 4: Re-order cell 5: Add new cell above/below 6: Collapse/sticky

Here’s what the interface looks like if you don’t login (if you create a new notebook after logging in it’ll look similar). This is one cell; by using the “add new cell” buttons (5), you’ll create a new empty cell either above or below the existing one.

  1. Preview area: When you run a code snippet, the output (if any) will show up here. This is where you’ll see any HTML output.
  2. Editor area: You write all your code in this part of the cell. It will tell you if you did something wrong.
  3. Run cell: You hit this to run the code in the editor area; it doesn’t evaluate without you doing this. The keyboard shortcut is “shift+enter”.
  4. Re-order cell: Cells don’t run in the order they’re displayed; the output of each cell is a global constant. This means you can re-order them however you want without worrying about breaking anything.
  5. New cell: Click one of the (+) icons to add a new cell above or below the current one.
  6. Pin/collapse cell; cell menu: When you focus another cell, all the other cells collapse. Click the pin icon to keep the current cell open even if you focus another cell (it’s really annoying having to always re-open other cells if you’re working between a few functions). There’s also a triple-dot menu that gives you the option to delete the current cell. You can also collapse the current cell by clicking its left-hand margin.

That doesn’t let you do too much by itself; to get much out of Observable, you’re gonna have to write some JavaScript (and also some HTML and/or CSS, probably). Let’s make something.

Make a fancy snowflake generator with Canvas and Observable

For this we’re going to create a parameterised canvas renderer that draws six “branches” in a loop to form a snowflake. Inspired by Mike Bostock’s Function Plot notebook and tweet thread, we’re going to let people play with it without having to write any code via various input elements.

To start with, create a new cell (or edit the first existing one) to describe your project using the Markdown formatting language. To write markdown, add something like this to your cell:

md`# Awesome Journocoders Snowflake Generator!!!`

The first top-level markdown headline will become your project’s name when you save it.

Next, add a new cell by clicking one of the (+) buttons. In order to use D3, we need to use require() to add it to our project:

d3 = require("d3")

Hey! Fellow JavaScript nerds! Okay, here’s where things go totally off the rails for you!

  1. In Observable, outside of a closure, you don’t need the var/const /let keyword. Top-level cell assignment like that creates a constant (unless you put the mutable keyword in front of the variable name. It’s kinda weird).
  2. This is not NodeJS. You cannot require() any package on npm; only things that expose their modules via UMD or AMD will work. This is easily my biggest bugbear about Observable, for what it’s worth. Effectively, if you can include the module from unpkg.com via CDN in a webpage, it’ll work on Observable. Usually. Sometimes.
  3. There’s also an ES6-style import syntax but that only works with Observable notebooks and differs a bit? 🤷🏼‍

We’re not actually going to use D3 at all because honestly it’d just be more code and wouldn’t be much more readable in this context. But it’s quite likely you’ll want to use D3 for something with Observable, so knowing how to get it into your notebook is helpful.

Next import Jeremy Ashkenas’ fantastic inputs notebook which lets us use fancy sliders for controlling our stuff. Yay! Create a new cell and add the following:

import {slider} from "@jashkenas/inputs"

Hit shift + enter. Cool, we can use the functions defined in that notebook now! To do so, we use the slider() function exposed by @jashkenas/inputs . Create a new cell and add the following:

viewof height = slider({
min: 200,
max: 1000,
step: 1,
value: 600,
title: "Plot height"
})

Hit shift + enter again. This will render a native HTML number slider using the values we’ve provided.

We’ll use this to control how tall our final output is. Notice the viewof keyword — this tells Observable to track the value of this variable and re-render everything if it changes.

We’re going to actually render the snowflake now, using Canvas. It’s worth noting I’ve taken much of this from Robert Lysik’s excellent JavaScript Snowflake tutorial, which discusses Canvas a lot more in-depth than I’m about to do here now. If this piques your interest, I highly recommend reading it.

First, create a new cell with the following:

chart = {
const context = DOM.context2d(width, height);
// Drawing code will go here! return context.canvas;
}

This will create a 2D context object and render it to page as a <canvas> element. If you’ve never worked with Canvas before, imagine it like a computer-controlled MSPaint, where you have a grid of pixels and can use various tools to change them different colours. We set the size of our canvas to the height value we defined via slider (defaulting to 600), and the natural width of our page, which Observable helpfully provides to us as the constant width.

First thing we’re going to do is add a pretty background. Update your code to look like this:

chart = {
const context = DOM.context2d(width, height);
context.fillStyle = "#162D50";
context.fillRect(0, 0, width, height);

return context.canvas;
}

This tells the drawing context (the place where the pixels are rendered) to use a dark blue as the fill colour, and then draw a filled rectangle from coordinates (0,0) (the upper left corner of the drawing area) all the way down to (~838,600) (I say “approximately” 838 pixels because Observable calculates this depending on your screen resolution). If you hit shift + enter you’ll now have a blue rectangle!

Ain’t much to write home about.

Given we need to now draw some lines, it’s probably worth setting up all our our line styles now, however, we’re going to want the width of our lines to be configurable, so it’s time for a new slider. Create a new cell above your Canvas area (it doesn’t matter where but I tend to like to put user interface stuff near the top of the notebook) and add the following:

viewof branchWidth = slider({
min: 1,
max: 10,
step: 1,
value: 2,
title: "Line thickness"
})

Splendid! Going back to your “chart” function, update it to look like this:

chart = {
const context = DOM.context2d(width, height);
context.fillStyle = "#162D50";
context.fillRect(0, 0, width, height);
context.lineCap = 'round';
context.strokeStyle = "#FFFFFF";
context.lineWidth = branchWidth;

return context.canvas;
}

This also makes the edges of the lines a bit rounded and sets their colour to white.

The next thing we’re going to do is reorient the canvas grid so it’s easier to work with. When we drew the background, I mentioned the top left corner is coordinate (0,0) and the bottom right corner is ([width], [height]). Because we want our snowflake to be right in the middle of our canvas element, we’re going to set the grid origin to halfway between those two extremes. Add the following line to your code, which I’ve bolded:

chart = {
const context = DOM.context2d(width, height);
context.fillStyle = "#162D50";
context.fillRect(0, 0, width, height);
context.lineCap = 'round';
context.strokeStyle = "#FFFFFF";
context.lineWidth = branchWidth;

context.translate(width / 2, height / 2);

return context.canvas;
}

We’re now working from the middle of the canvas space. Time to actually draw some lines!

We’re going to create a two nested loops, the outer loop for drawing each “branch” of the snowflake, and the inner loop for drawing all the sub-branches, what I’ve referred to a “sepals”.

chart = {
const context = DOM.context2d(width, height);
context.fillStyle = "#162D50";
context.fillRect(0, 0, width, height);
context.lineCap = 'round';
context.strokeStyle = "#FFFFFF";
context.lineWidth = branchWidth;

context.translate(width / 2, height / 2);
for (let i = 0; i < 6; i++) {
context.save();

for (let j = 1; j <= branchCount; j++) {
drawSegment(context, sepalLength);
}
drawSegment(context, 0); context.restore();
context.rotate(Math.PI/3);
}


return context.canvas;
}

Let’s start with the outer loop. We’re going to iterate six times, creating six variants of the same branch. In each iteration:

  1. We save the context’s current attributes, which are things like rotation, colours, et cetera. We can change these and draw things or whatever, then restore back to whatever the settings were when we saved them.
  2. We iterate however many times our (forthcoming; we’ll create a slider in a second) branchCount variable is set to, which will determine how many sub-branches each branch has. We do this via a function called drawSegment(), which we’ll define in a moment. We then call that again after the loop to get the final section, the tip of each branch.
  3. As mentioned, once we’re done with this branch, we restore the settings so the next branch can start back from the middle like the last one did.
  4. We rotate the canvas 60º before starting again. We get that in radians by dividing Pi by 3.

Observable is probably screaming at you about undefined variables so let’s go create those now. Create a new cell for each of these, I won’t linger on any of them much because adding more sliders is pretty dull at this point:

viewof branchCount = slider({
min: 0,
max: 20,
step: 1,
value: 5,
title: "Sub-branch count"
})

This is the number of sub-branches. Yawn.

viewof sectionLength = slider({
min: 1,
max: 100,
step: 1,
value: 40,
title: "Length of each section"
})

We iterate over this number to determine how long each branch is; effectively the length of each branch is the number of sub-branches (plus one, for the tip) times the length of each section.

viewof sepalLength = slider({
min: 1,
max: 200,
step: 1,
value: 100,
title: "Length of each sepal"
})

Lastly we have how long each sepal’s little tip-y bits is.

Aw, heck, what’s one more slider? We’re going to need this in a second anyway. This will effect the angle each little sepal tip-y bit will be at.

viewof sepalAngle = slider({
min: 0.2,
max: 1,
step: .01,
value: 0.8,
title: "Sepal angle"
})

Cool. Time for some actual drawing code!

Next create a new cell and populate it with the following:

function drawSegment(context, branchLength) {
context.beginPath();
context.moveTo(0,0);
context.lineTo(sectionLength, 0);
context.stroke();
context.translate(sectionLength, 0);
if (branchLength > 0) {
drawBranch(context, branchLength, 1 * sepalAngle);
drawBranch(context, branchLength, -1 * sepalAngle);
}
}

Wow, did we really get this far without having drawn any line code yet? Okay, this function is the real meat and potatoes of all this so I’m going to go pretty in-depth:

  1. We pass in the drawing context as an argument, as well as the length of the sepals.
  2. We tell the drawing context to start a path! 🎉
  3. We tell the drawing context to move the pen to the origin, (0, 0), which, again, we’ve set to the centre of the canvas object.
  4. We draw a line from the centre, to the right, the number of pixels we set for each section via the sectionLength slider.
  5. We give the line definition by giving it a stroke.
  6. We translate the drawing context origin to the current position of the pen on the element.
  7. If we have a length for the sepals/lil’ tip-y bits/whatever we’re calling them at this point, we draw those. If it’s the final section, we don’t want sepals so won’t draw them.
  8. If we do want sepals, though (and rest assured we do because it doesn’t look anything like a snowflake without them!), we call another new function, drawBranch(), pass the context to it as well, in addition to a positive sepal angle value if it’s going to the right, or a negative sepal angle value if it’s going to the left. Again, the sepalAngle variable is one of the ones we’re populating via the sliders.

We are so close to being done. To finish, we need to write the drawBranch() function, which I provide below:

function drawBranch(context, branchLength, direction) {
context.save();
context.rotate(direction * Math.PI/3);
context.moveTo(0,0);
context.lineTo(branchLength, 0);
context.stroke();
context.restore();
}

Bit more line drawing code!

  1. First we save the canvas context so we can return to it for the other sepal.
  2. We rotate the canvas ±60º, depending whether we’re drawing the right or left side of the branch.
  3. We move the pen to our new origin, which we previously set to the end of the current segment.
  4. We draw a line to the right.
  5. We stroke that!
  6. We restore the origin and such to how it was.

Save that cell and you’ve completed your first interactive Observable notebook!

The only thing left to do is publish it. Hit the big blue button in the upper right corner and you’re on you’re way, provided you logged in with GitHub and everything.

Have a play with some of the settings, it’s really easy to get a lot of different shapes. Better yet, you can right-click the canvas element and hit “Save As…” to save a copy of anything you make:

Make a particularly cool one? Tweet or toot it to me!

Next steps

  • Can you use a D3 scale and another input element to let users change the background colour?
  • Can you use D3 colour interpolators to make the snowflake go all Party Parrot?
  • Can you render multiple snowflakes, and possibly animate them?

Ændrew Rininsland is a senior developer on the Interactive Graphics team at the Financial Times, and a co-organiser of both Journocoders London and the London D3.js meetup. He’s pretty much everywhere as @aendrew.

--

--

Ændra Rininsland
Journocoders

Newsroom developer with Interactive Graphics at @ft. she/her or ze/hir. Rather queer. Opinions are mine, not employers. I'm hackers.town/@aendra on Mastodon.