Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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.

like image 481
Peter Krauss Avatar asked Mar 20 '20 10:03

Peter Krauss


2 Answers

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>
like image 156
Mehdi Avatar answered Oct 14 '22 05:10

Mehdi


The very simple way would be using their runtime embed version. Here is a very similar way to reuse the notebook in a HTML5 template.

You can also download the runtime and the notebook js to host on your server.

The trick here is to use the runtime to talk to Observable reactive cells.

In this example I'm using d3.json to fetch new json data and redefine the data cell from the original notebook.

<div id="observablehq-e970adfb"></div>
<script src="https://d3js.org/d3.v5.min.js"></script>
<script type="module">

//Import Observable Runtime

import {Runtime, Inspector} from "https://cdn.jsdelivr.net/npm/@observablehq/runtime@4/dist/runtime.js";
import define from "https://api.observablehq.com/@d3/indented-tree.js?v=3";
const inspect = Inspector.into("#observablehq-e970adfb");

// Notebook instance
const notebook =(new Runtime).module(define, name => (name === "chart") && inspect());


// Using D3.json to load new Json Data

d3.json("https://gist.githubusercontent.com/radames/9018398d6e63bcaae86a0bf125dc6973/raw/33f19a49e1123a36e172cfc7483f0a444caf6ae3/newdata.json").then((newdata) =>{
  
  // When data is loaded you can use notebook to redefine a cell
  // In this case the data cell, where in the notebook it's using a FileAtachent
  // Here you can redefine with any structure hierarchy structure like
  
  notebook.redefine("data", newdata);
})


</script>

Editing to add steps using Severo's project

Using Severo's Notebook visualizer you can understand your notebook's dataflow and rewrite your standalone code. Keep in mind that rewriting from scratch might become very complicated as your code uses Observable features such as reactivity and state management. In that case I recommend you to use Observable runtime following my response above.

Now with that in mind, let's look at the visualizer and follow Severo's intructions

enter image description here

  • Green cells correspond to external code imported into the notebook: library imported with require (e.g. d3 = require("d3@5")): you typically will install it in your project with npm install, and
    then import it as an ES module imported notebook (e.g. import { radio } from "@jashkenas/inputs"): you will have to repeat the same process in
    this notebook, examining its own dependency graph.
  • Gray cells are anonymous (non-named) cells and will generally not be migrated. They often contain explanation texts, and no other cell can depend on them, so they shouldn't break the code if
    removed. But, be careful: if your main chart cell is not named, you
    will still want to copy its code.
  • Black cells are the actual notebook code written by the user, and you will want to copy it to your project.
  • Purple cells are the toughest ones. They correspond to features of Observable that will typically be used a lot by a notebook writer (see the Standard Library), and their migration to a standalone application can be the hardest part of the rewrite from scratch, particularly mutable and viewof cells, since they manage an internal state.

Here is the code converted following these instructions

<!--- Green Cells / Imports --->
<script src="https://d3js.org/d3.v5.min.js"></script>

<!--- Char Container --->

<div class="chart"></div>
<script>
  // Run main function
  main();

  // async main so we can run our code like Observable cell by cell
  async function main() {
    // as in Observable each cell runs as an async function
    // so here you can await the output to continue
    const data = await d3.json("https://gist.githubusercontent.com/radames/9018398d6e63bcaae86a0bf125dc6973/raw/33f19a49e1123a36e172cfc7483f0a444caf6ae3/newdata.json");

    // run complex code as inline await / async
    const root = await (async() => {
      let i = 0;
      return d3.hierarchy(data).eachBefore(d => d.index = i++);
    })()

    // easy constant
    const nodeSize = 17;

    // easy constant function
    const format = d3.format(",");

    // easy constant
    const 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
      }
    ];
    // on Observable width is reactive, here we have to do it manually
    const width = window.innerHTML;

    window.addEventListener('resize', updateWidth);

    function updateWidth() {
      // update your chart on resize event
    }
    // inline function gets chart svg node
    const chart = (() => {
      const nodes = root.descendants();

      const svg = d3.create("svg")
        .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));
      }

      return svg.node();
    })()

    // select element container append chart
    const container = document.querySelector(".chart")
    container.appendChild(chart);

  }
</script>
like image 24
calmar Avatar answered Oct 14 '22 06:10

calmar