🏠 back to Observable

Notebook to Vanilla JavaScript Steps


#1

Hi all, I have my IDE setup (Apache Netbeans) for all kind of projects including D3 vanilla JavaScript. What are the steps to get from a (Observable) JavaScript notebook to a vanilla JavaScript index.html?


#2

Have you looked at the “Downloading and embedding notebooks” tutorial?


#3

Yes, but the extra runtime environment is for me and I think for many others kind of a barrier towards a vanilla JavaScript index.html


#4

The barrier is there because the way notebooks run is rather different from the way a typical “vanilla JS” script runs.

If the code in your notebook is very simple and doesn’t rely much on the reactive functionality of the runtime, you might be able to get away with just copying notebook cells into similarly-named functions (possibly with extra inputs) and adapting the rest of the code (possibly importing the standard library), but this is still going to be a case-by-case sort of process.

One way to check the complexity of this task is to put your notebook in the visualizer and see if you can tell whether copying the cells into functions in a vanilla JS script will lead to something with the same execution order.

If you have a specific notebook in mind I could take a quick look and try to give some suggestions on how you might begin converting it back to vanilla JS.


#5

If you have a specific notebook in mind I could take a quick look and try to give some suggestions on how you might begin converting it back to vanilla JS.

Yes, a good starting point would be my notebook with Mike Bostock’s D3 Circle Packing example.


#6

You’re lucky, this particular example is really simple. In particular, it really only has one main visualization cell, and one data fetching cell and the rest are just helper functions / variables.

Here is a port to a “block”. To get to this, I worked roughly from bottom to top.

First, I added a script tags to index.html which import the d3 and d3-require libraries as well as the Observable standard library:

<script src="https://d3js.org/d3.v5.min.js"></script>
<script src="https://unpkg.com/d3-require@1"></script>
<script src="https://cdn.jsdelivr.net/npm/@observablehq/stdlib"></script>

Next I added an svg element to serve as a target for the visualization:

  <svg id="packSVG" width=932 height=932 viewBox="0,0,932,932"></svg>

Then I added another script tag, and copied each of the notebook cells below data as functions / variables (making sure that the variables are defined in an order that makes sense):

<script>
const color = d3.scaleSequential(d3.interpolateMagma).domain([8, 0]);
const format = d3.format(",d");
const width = 932;
const height = width;
const pack = data => d3.pack()
    .size([width - 2, height - 2])
    .padding(3)
  (d3.hierarchy(data)
    .sum(d => d.size)
    .sort((a, b) => b.value - a.value));
</script>

The cell data fetches a file asynchronously, so we’ll have to wrap the rest of the code inside a promise. Put the following inside the above script tag.

d3.require('@observablehq/flare')
      .then(data => {
// add code here 
});

I then took the code from the main cell and copied it into the function inside then above, so that it will run after the data is successfully fetched.

The only changes I made were to change the main d3 selection from DOM.svg to a selector which picks out the target SVG element we added above, and then to comment out some references to DOM.uid in the standard library which weren’t working. The latter causes the text labels to spill outside the circles. I’ll see if I can figure out what’s going on with that later.

(By the way, it doesn’t look like you’ve changed anything from Mike Bostock’s notebook. Might you possibly have forgotten to share your edits?)


#7

Will this jsfiddle example help?

https://jsfiddle.net/5w906pL1/20/


#8

That was exactly what I was looking for. The link to the Observable Standard Library is gold. Though it looks that it did not work (with DOM.uid). Nevertheless that is now a kind of blueprint for me for other notebooks.

Yes please, let see how the Observable standard library should work in your vanilla JavaScript example.

I only forked it. My aim was to transform a Notebook into a vanilla JavaScript which you did perfectly. Thank you.


#9

Nope. I was looking for Bryan’s solution. But thanks.


#10

OK, I figured out how to import the standard library. I should have just read the docs more carefully.

The key thing to do was to add this line, which creates a new “Library” object and then extracts DOM from it:

    const {DOM} = new observablehq.Library;

Here’s the link again https://blockbuilder.org/bryangingechen/ffd619bb5889d146fe6c5d581d3ea00e

edit: Since stdlib exports (a slightly wrapped version of) d3-require, you can even get rid of the script tag which fetches d3-require above (although the syntax to use require is slightly changed). See the latest version, which includes the following:

  const {DOM, require} = new observablehq.Library;

  require()('@observablehq/flare') // need the extra () because require in the stdlib wraps d3-require in a weird way
    .then(data => {

#11

Many thanks Bryan, that will be my template now.


#12

Glad I was able to help. I still believe that using the runtime will be the more robust solution, particularly when it comes to notebooks with generator cells, those with a lot of interactivity across cells or those which import other notebooks.

Note also that the v1 notebook JS code (what you see when you click “download code” from the menu in the top right, and what is contained with a basic index.html in the download tarball file) is not particularly hard to manipulate in external text editors.


#13

This is pretty fabulous stuff, Bryan, thank you!

Out of curiosity (and just to confirm that I’ve got you), when you say that “using the runtime will be the more robust solution” compared to writing everything in a ‘traditional’ manner, as demonstrated by blockbuilder, do you mean that a more robust solution would be to install Observable’s customized JS runtime (software stack - similar but distinct from node.js) / spin up a server environment that exposes this environment (for those of us with crazy work computers that don’t allow us to install any sort of software) for interpreting the code that is downloadable via a notebook’s ... menu?


#14

Observable’s runtime doesn’t require any server resources, it can be run entirely in the browser. Here’s an exercise that might be enlightening. Try downloading the tarball of any of your favorite notebooks and copying its contents (you really just need the .js file and index.html) into an empty folder.

If you open that index.html file directly in your browser, you’re likely to get CORS errors, so start up a simple HTTP webserver (like the python one described in this post) and then open index.html using that server and it should work.

Note that all this local server is doing is just passing the HTML and JS files to the browser. There’s nothing else necessary on the server side. If you have webhosting of your own, you could also just copy the .js and index.html files into a folder there and see that your notebook works without any fancy server environment.

In fact, you can copy your files to a github “gist” and create a fully-functioning block from an Observable notebook this way as well:


#15

My jaw is on the floor. I can’t start an HTTP webserver on my work computer. I’ve also had to abandon trying to share around to colleagues certain visualizations I was playing with because they required an HTTP server to load (and I didn’t realize it was CORS). Although I’ve discovered hosting via GitHub as a work-around, I hadn’t put all these pieces together until now. I thought that depending on the runtime meant creating some sort of designated server-side environment. Thanks for this clarification! Amazing.


#16

What when I want read from a local file flare.json?


#17

There are multiple things you might mean here.

If you have a single local file that you want to be able to read from a local copy of the notebook, you could use d3.json (replace the require call with this):

d3.json("path/to/file.json").then(data => {

(Note that this will probably give you CORS errors unless you open the page in a local web server)

Alternatively, you might want a button that lets you select a file from your computer like the examples here: https://beta.observablehq.com/@mbostock/reading-local-files

Then you might add something like this to the HTML body:

<input id="inputJSON" type="file" accept="application/json">

and replace the require call with this:

const inputElement = document.querySelector('#inputJSON');
inputElement.addEventListener('change', () => {
  const url = URL.createObjectURL(inputElement.files[0]);
  d3.json(url).then(data => {

(this wraps the original code in an additional function; here’s a link to a fork of the above block with this change).

This earlier post of mine is also related to changing out the flare dataset for another one (in a different notebook) and might be helpful if you’re looking to get the format right.


#18

Thanks Bryan for getting back. Look at the following 3 Vanilla JavaScript variants. I’ve chosen Mike Bostock’s D3 Zoomable Sunburst example for our 3 variants (w/ d3.json(), button, require()):

  1. D3 v5 Zoomable Sunburst w/ d3.json() (not working)
  2. D3 v5 Zoomable Sunburst w/ Button (not working)
  3. D3 v5 Zoomable Sunburst w/ require() (works)

I’ve tried it with a local file flare.json from my computer as well as remote files from gist.githubusercontent.com but all fail.

Actually I can see that in all cases the data is loaded. But those file based versions whether local or remote do not work. When I compare the data from the require() vs. d3.json() variants they look the same. Though there is a tiny difference. Look below:

What is that ?


#19

These flare.json files don’t quite have the right format to be viewed with the code. At the bottom level, the entries look like this:

{"name": "AgglomerativeCluster", "value": 3938},

whereas they should look like this:

{"name": "AgglomerativeCluster", "size": 3938},

To fix this, you can either substitute “size” for “value” in the json files, or substitute “value” for “size” in the JS code.


#20

Arrrrrrrrrrgh. Haven’t seen that one. You are right. That did it. Thanks again.