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?
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>
Prepare global variables, seems root
, nodeSize=17
, and width
Prepare data... JSON data is at the ugly ./files/e6537420...
, I moved to project's root with it's real name, flare-2.json
.
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.
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++); }
.
Copy-paste chart =
cited above, after root
inicialization with data.
...
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.
Observable now has an embed
feature, details in this page.
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.
Download JSON
link from each data
cell's menu.d3-fetch
d3.json("/path/to/data.json").then(function(data) {
console.log(data); // [{"Hello": "world"}, …]
});
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.
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()
If needed by some function from the notebook, define a variable width
, and initialize it with the desired value.
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>
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
- 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>
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With