🏠 back to Observable

How do I work with the D3 Sankey Example?

I want to make a copy of Mike Bostock’s Sankey diagram.

I’m having trouble replicating it. I don’t know what language the code is written in. I run into this error when I try the code with javascript: Uncaught SyntaxError: Unexpected identifier.

Here’s a fiddle with a replica version of the sankey.

I tried to go through line-by-line and turn it into something more similar to Javascript I’ve seen elsewhere. I got stuck trying to replicate this part of the code.

sankey = {
  const sankey = d3.sankey()
      .nodeWidth(15)
      .nodePadding(10)
      .extent([[1, 1], [width - 1, height - 5]]);
  return ({nodes, links}) => sankey({
    nodes: nodes.map(d => Object.assign({}, d)),
    links: links.map(d => Object.assign({}, d))
  });
}

What is the way to get the Sankey diagram working?

1 Like

There’s another version here and I can see the javascript underneath. I’m concerned that it’s from 2012 and possibly not up to the standards of the current version of D3 Sankey.

Hi!

Code in Observable notebooks is indeed written in JavaScript, but in general, you can’t just copy and paste the code into an ordinary script. Notebooks are executed using the Observable “runtime”. See the tutorials “How Observable Runs” and “Downloading and Embedding Notebooks” for some more info. As explained on the latter page, the easiest and most general way to get a copy of a notebook running on another page is to click the menu in the top right and choose the “Download tarball” link, and then put those files on a webserver.

See also this similar question on the forums a few weeks ago.

As with the notebook in the other thread, I thought it might be interesting / educational for me to try to get the notebook code working outside the Observable runtime environment in your fiddle so I took a crack at it. Here’s the updated fiddle; sorry about the lack of indentation:

https://jsfiddle.net/s3opcnm8/

Here are some brief notes on how I did the conversion:

  • In the HTML file, I fixed the script tags and then put in a container SVG element
  • In the JS file, I moved all of the code out of “cell blocks” (i.e. the stuff like the code you quoted in your first post) since that’s Observable notebook syntax:
    • the return statements became variable assignments to e.g. color and sankey
    • this required renaming some of the variables that were previously cell-scoped, see e.g. _color and _sankey and sankey,
  • Next, I reordered the JS code so that nothing was referencing anything before it was defined
  • Since that the chart depends on an external data file which has to be loaded, I wrapped most of the D3 code in a “.then” call to d3-json (since that function returns a Promise)
  • I then adapted the code depending on DOM.uid since that depends on the Observable standard library.
  • The final step was to deal with the fact that the “path” objects defined by the “link” selection in the SVG depend on the variable “edgeColor”, which comes from a “select” input.
    • I copied the HTML for the select input from the notebook into the HTML file of the fiddle
    • I wrapped the code that depended on “edgeColor” in an update function
    • I wrote an “onchange” handler for the select input to update “edgeColor” and call update

As you can see, it’s a lot of steps, so I will reiterate my recommendation that for “copying” general notebooks you use the technique described in the Downloading and Embedding tutorial.

3 Likes

Doooooooooooooooooooooope.

Thank you. I’ll let you know if I publish something from the Sankey diagrams.

This is so annoying. D3 has its examples either on bl.ocks.org, where they’re using outdated versions of D3 making them pretty much unusable, or on observable, with its own special code you can’t really use for your own projects unless you rewrite them top to bottom.
Argh!

Is there a better way to convert observable code to plain JS by now?

If you are struggling with a specific problem, feel free to share a gist or fiddle of your adaption and we’ll try to assist as best as we can. Please remember that you can find the actual documentation in the project’s repository, which is also the first link in the above notebook.

Examples on Observable are not meant to provide copy-pastable code that can be treated as a blackbox. They communicate concepts and allow for rapid prototyping, which would be a lot more difficult (and unfocused) in a vanilla JS implementation.

2 Likes

@bgchen already outlined how you can adapt the code while keeping the interactivity. If we only want to port the static chart, we can simplify the steps a bit.

Boilerplate

First, our template:

<!DOCTYPE html>
<script src="https://cdn.jsdelivr.net/npm/d3-require@1"></script>
<script>
(async()=>{

// code goes here

})()
</script>

This gives us require, although we need to access it through window.d3.require().

Next, the license. This part is especially important if you plan to republish the code or any derivative version of it:

/*
Copyright 2018–2020 Observable, Inc.
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.

THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/

We may also want to reference the notebook version:

/* Ported from https://observablehq.com/@d3/sankey-diagram@278 */

Basics

With that out of the way, it’s time to port the actual code. Like Bryan already mentioned, the order matters. Because Observable resolves cell dependencies automatically, you’ll often find the final results at the top. A good approach is therefore to port all cells starting from the bottom:

  const d3 = await window.d3.require("d3@6", "d3-sankey@0.12");
  const width = 954;
  const height = 600;

Next, the viewofs. Since we’re removing the interactivity, we might as well just port their default values:

  const align = "justify";
  const edgeColor = "path";

The next rule of thumb: Whenever you see a cell with curly braces and a return statement:

someName = {
  return 1 + 2;
}

wrap it in an immediately invoked function expression:

const someName = (()=>{
  return 1 + 2;
})();

That way you don’t have to restructure the inner code. Let’s see that applied to color:

  const color = (()=>{
    const color = d3.scaleOrdinal(d3.schemeCategory10);
    return d => color(d.category === undefined ? d.name : d.category);
  })();

FileAttachment

In data we meet our first major obstacle, FileAttachment. To keep things simple, we download the attached CSV file and inline its contents by adding another <script> tag:

<!-- Data: Department of Energy & Climate Change via Tom Counsell -->
<script id="data" type="text/csv">source,target,value
Agricultural 'waste',Bio-conversion,124.729
Bio-conversion,Liquid,0.597
...
Wave,Electricity grid,19.013
Wind,Electricity grid,289.366
</script>

Note that the opening tag must immediately be followed by the data. No line break, no whitespace.

With the data inlined, we remove the reference to FileAttachment and just grab the data through standard DOM methods. Our data variable becomes:

  const data = (()=>{
    const links = d3.csvParse(document.getElementById('data').textContent, d3.autoType);
    const nodes = Array.from(new Set(links.flatMap(l => [l.source, l.target])), name => ({name, category: name.replace(/ .*/, "")}));
    return {nodes, links, units: "TWh"};
  })();

The format and sankey cells are left as an exercise to the reader.

DOM.uid

Finally, there is the chart cell. We can move its code over almost 1:1, with one exception, a call to DOM.uid:

        .attr("id", d => (d.uid = DOM.uid("link")).id)

This short statement produces a lot of magic, so we need to make multiple adjustments. The first change sets the ID, as before:

        .attr("id", (d, i) => (d.uid = `link-${i}`))

When a DOM.uid() value gets converted to a string, it “magically” turns into url(#the-uid). We don’t have that luxury anymore, so our next step is to look for the line:

            : edgeColor === "path" ? d.uid 

and change it to

            : edgeColor === "path" ? `url(#${d.uid})`

Wrapping up

We conclude our exercise by appending the chart to the DOM:

  document.body.appendChild(chart);

Your final code should look similar to this gist (see it rendered here).

3 Likes

Ah! I didn’t include the license in my JSFiddle, and unfortunately it’s too late to edit my post now. Let’s pretend the link there is this one instead: https://jsfiddle.net/sjtav809/

1 Like