如何将 ObservableHq 简单算法解释为可重用的代码片段?

How to interpret ObservableHq simple algorithms as reusable code snippets?

D3js 解决方案的主要来源是observableHq.com,但似乎不可能(?)通过copy/paste重用算法...是吗?即使检查教程like this,也没有简单的方法(插件更少或程序员的时间消耗!)来检查和重用

示例:我需要一个新的 2020 D3js v5 缩进树可视化算法,并且有一个很好的解决方案:observableHq.com/@d3/indented-tree
下载没有用,因为基于复杂 运行时class...

但是,似乎是一个简单的图表生成器算法,

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

我可以通过简单的人工步骤,将其转换为简单的 HTML,无需复杂的改编,以 <script src="https://d3js.org/d3.v5.min.js"></script> 开头,不以 结尾运行时class使用?


更多细节作为例子

想象一下我为 cited indented-tree algorithm 做的每一步,我无法完善并需要你的帮助:

假设从一个干净的 HTML5 模板开始。例如:

<!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. 准备全局变量,好像是rootnodeSize=17width

  2. 准备数据...JSON数据丑陋./files/e6537420...,我用它的真实名称移动到项目的根目录,flare-2.json

  3. 读取JSON数据的简单class标准D3js方式:d3.json("./flare-2.json").then( data=> console.log(data) );
    必须测试并检查没有CORS错误等

  4. 准备数据作为 root 变量。全部放入 data => {} 块以避免同步问题...
    似乎 root 是基于 function(d3,data) { let i = 0; return d3.hierarchy(data).eachBefore(d => d.index = i++); }.

  5. 复制粘贴 chart = 上面引用的内容,在 root 使用数据初始化之后。

  6. ...


常见问题解答

评论问题和答案:

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

原来的ObservableHq算法很简单,我需要另一种方法,一种简单的方法来重用它,通过copy/paste 和最小的改编。

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

是的,那里没有新闻:没有 "human instruction" 关于如何重用代码...只有 "install it" 和 "install that"。没有关于我上面解释的"copy/paste and minimal adaptations"的说明。

(@nobody) - What you need as answer?

正如我在上面展示的那样,一个简单的人类可读的逐步过程转换......理想情况下,最终结果可以通过测试,证明它可以在例如 JSFiddle 上使用 copy/paste 代码和一些更多的改编行来表明你的观点。

2020 年 11 月编辑

Observable 现在有一个 embed 功能,详情见 this page

原版post

这是将 linked 可观察图表移植到自托管网页的分步过程,通过复制粘贴代码,而无需使用 observable runtime library.

从 HTML 页面和 HTML 页面中引用的 JavaScript 文件开始。假设 Web 服务器是 运行 并且配置合适。

  1. 获取数据。
  • 如果您想使用自己的数据而不是笔记本中使用的数据,请在您的网络服务器上的目录中提供数据文件。
  • 否则,使用每个 data 单元格菜单中的 Download JSON link 下载附加到笔记本的每个输入数据集。

  1. 使用d3-fetch
  2. 加载页面中的每个数据集
d3.json("/path/to/data.json").then(function(data) {
  console.log(data); // [{"Hello": "world"}, …]
});
  1. 获取笔记本中包含变量或函数的每个单元格的内容,然后将其放入上一步的.then函数中。此 notebook visualizer 工具有助于识别相关单元格。

  2. 根据需要调整刚刚复制的函数的语法。例如,以下笔记本单元格:

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

可以转换为:

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

root = getRoot()
  1. 如果笔记本中的某些函数需要,定义一个变量width,并用所需的值初始化它。

  2. 调整 DOM 操作代码,以便将元素附加到 DOM,而不是依赖可观察运行时的隐式执行。

下面截图中的演示:

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>

非常简单的方法是使用他们的运行时嵌入版本。这是在 HTML5 模板中重复使用笔记本的一种非常相似的方法。

您还可以下载运行时和笔记本 js 以托管在您的服务器上。

这里的技巧是使用运行时与 Observable 反应单元对话。

在这个例子中,我使用 d3.json 获取新的 json 数据并重新定义原始 notebook 中的 data 单元格。

<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>

编辑以使用 Severo's project 添加步骤

使用 Severo's Notebook visualizer 您可以了解笔记本的数据流并重写您的独立代码。请记住,从头开始重写可能会变得非常复杂,因为您的代码使用了 Observable 的特性,例如反应性和状态管理。在这种情况下,我建议您按照我上面的回复使用 Observable runtime。

考虑到这一点,让我们看看可视化工具并按照 Severo's intructions

  • 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.

这是按照这些说明转换后的代码

<!--- 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>