Search code examples
d3.jscode-reuseobservablehq

How to interpret ObservableHq simple algorithms as reusable code snippets?


The main source of D3js solutions is observableHq.com, but seems impossible (?) to reuse algorithms by copy/paste... Is it? Even checking tutorials like this, there are no simple way (with less plugins or programmer's time-consumtion!) to check and reuse.

Example: I need a fresh 2020 D3js v5 algorithm for indented-tree visualization, and there are a good solution: observableHq.com/@d3/indented-tree.
The download is not useful because is based on complex Runtime class...

But, seems a simple chart-builder algorithm,

chart = {  // the indented-tree algorithm
  const nodes = root.descendants();
  const svg = d3.create("svg")// ...
  // ...
  return svg.node();
}

Can I, by simple human step-by-step, convert it in a simple HTML, with no complex adaptations, that starts with <script src="https://d3js.org/d3.v5.min.js"></script> and ends with no Runtime class use?


More details as example

Imagining my step-by-step for the cited indented-tree algorithm, that I can't finesh and need your help:

Suppose to start with a clean HTML5 template. For example:

<!DOCTYPE html>
<head>
    <meta charset="utf-8">
    <title>Indented Tree</title>
    <script src="https://d3js.org/d3.v5.min.js"></script>
    <script>
    function onLOAD() {
        console.log("Hello onLoad!")
        // all copy-paste and adaptations HERE.
        console.log("!Bye!")
    } // \onLOAD
    </script>
</head>
<body onload="onLOAD()">
  <script>
    console.log("Hello!")
    // global INITIALIZATIONS HERE.
  </script>
</body>
</html>
  1. Prepare global variables, seems root, nodeSize=17, and width

  2. Prepare data... JSON data is at the ugly ./files/e6537420..., I moved to project's root with it's real name, flare-2.json.

  3. Simple and classical D3js way to read JSON data: d3.json("./flare-2.json").then( data=> console.log(data) );
    Must test and check no CORS error, etc.

  4. Prepare data as root variable. All into the data => {} block to avoid sync problems...
    Seems that root is based in function(d3,data) { let i = 0; return d3.hierarchy(data).eachBefore(d => d.index = i++); }.

  5. Copy-paste chart = cited above, after root inicialization with data.

  6. ...


FAQ

On-comments questions, and answers:

@Mehdi   -   Could you explain what the problem is with including the D3 script tag and using Runtime library in the code?

When the original ObservableHq algorithm is simple, I need another way, a simple way to reuse it, by copy/paste and minimal adaptations.

@Mehdi   -   Did you read the Downloading and embedding notebooks tutorial?

Yes, no news there: no "human instruction" about how to reuse code... Only "install it" and "install that". No instructions about "copy/paste and minimal adaptations" that I explained above.

(@nobody) - What you need as answer?

As I show above, a simple human-readable step-by-step procedure to convert... Ideally the final result can by tested, a proof that it works at, for example, JSFiddle, with the copy/paste code and some more adaptation lines to show your point.


Solution

  • November 2020 edit

    Observable now has an embed feature, details in this page.

    Original post

    Here is a step-by-step process to port the linked observable chart into a self-hosted web page, by copy-pasting the code, and without having to use the observable runtime library.

    Starting from an HTML page and a JavaScript file referenced in the HTML page. Assuming a web server is running and configured as suitable.

    1. Get the data.
    • In case you want to use your own data instead of the one used in the notebook, make the data file(s) available in a directory on your web server.
    • otherwise, download each input dataset attached to the notebook, using the Download JSON link from each data cell's menu.

    screenshot of an observable notebook cell menu

    1. Load each dataset in the page using d3-fetch
    d3.json("/path/to/data.json").then(function(data) {
      console.log(data); // [{"Hello": "world"}, …]
    });
    
    1. Get the content of each cell containing a variable or a function in the notebook, and put then inside the.then function from previous step. This notebook visualizer tool can be helpful to identify the relevant cells.

    2. Adapt the syntax of the functions just copied as suitable. For example, the following notebook cell:

    root = { let i = 0; return d3.hierarchy(data).eachBefore(d => d.index = i++); }
    

    could be transformed to:

    function getRoot(){
       let i = 0;
        return d3.hierarchy(data).eachBefore(d => d.index = i++);
    }
    
    root = getRoot()
    
    1. If needed by some function from the notebook, define a variable width, and initialize it with the desired value.

    2. adapt the DOM manipulation code in order to append elements to the DOM, rather than relying on the implicit execution by observable runtime.

    Demo in the snipped below:

    d3.json("https://rawcdn.githack.com/d3/d3-hierarchy/46f9e8bf1a5a55e94c40158c23025f405adf0be5/test/data/flare.json").then(function(data) {
    
      const width = 800
        , nodeSize = 17
        , format = d3.format(",")
        , getRoot = function(){
           let i = 0;
            return d3.hierarchy(data).eachBefore(d => d.index = i++);
        }
        , columns = [
          {
            label: "Size", 
            value: d => d.value, 
            format, 
            x: 280
          },
          {
            label: "Count", 
            value: d => d.children ? 0 : 1, 
            format: (value, d) => d.children ? format(value) : "-", 
            x: 340
          }
        ]
        , root = getRoot()
        , chart = function() {
          const nodes = root.descendants();
    
          const svg = d3.select('#chart')
              .attr("viewBox", [-nodeSize / 2, -nodeSize * 3 / 2, width, (nodes.length + 1) * nodeSize])
              .attr("font-family", "sans-serif")
              .attr("font-size", 10)
              .style("overflow", "visible");
    
    
      const link = svg.append("g")
          .attr("fill", "none")
          .attr("stroke", "#999")
        .selectAll("path")
        .data(root.links())
        .join("path")
          .attr("d", d => `
            M${d.source.depth * nodeSize},${d.source.index * nodeSize}
            V${d.target.index * nodeSize}
            h${nodeSize}
          `);
    
          const node = svg.append("g")
            .selectAll("g")
            .data(nodes)
            .join("g")
              .attr("transform", d => `translate(0,${d.index * nodeSize})`);
    
          node.append("circle")
              .attr("cx", d => d.depth * nodeSize)
              .attr("r", 2.5)
              .attr("fill", d => d.children ? null : "#999");
    
          node.append("text")
              .attr("dy", "0.32em")
              .attr("x", d => d.depth * nodeSize + 6)
              .text(d => d.data.name);
    
          node.append("title")
              .text(d => d.ancestors().reverse().map(d => d.data.name).join("/"));
    
          for (const {label, value, format, x} of columns) {
            svg.append("text")
                .attr("dy", "0.32em")
                .attr("y", -nodeSize)
                .attr("x", x)
                .attr("text-anchor", "end")
                .attr("font-weight", "bold")
                .text(label);
    
            node.append("text")
                .attr("dy", "0.32em")
                .attr("x", x)
                .attr("text-anchor", "end")
                .attr("fill", d => d.children ? null : "#555")
              .data(root.copy().sum(value).descendants())
                .text(d => format(d.value, d));
          }
    
      }
    
      chart()
        
    }).catch(function(err) {
      console.log('error processing data', err)
    })
    <script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.8.0/d3.min.js"></script>
    <svg id = 'chart'></svg>