Published
Edited
Mar 21, 2021
Insert cell
Insert cell
`please use this data for the observable visualization:
https://www.ifixit.com/api/2.0/wikis/CATEGORY?display=hierarchy&includeStubs&pretty
(I suppose the last section with "display_titles": can be ignored for now)

API information can be found here:
https://www.ifixit.com/api/2.0/doc/Wikis#

The visualization should build on the radial tidy tree:
https://observablehq.com/@d3/radial-tidy-tree?collection=@d3/d3-hierarchy

with options for colorization and zoom like in the zoomable sunburst example:
https://observablehq.com/@d3/zoomable-sunburst?collection=@d3/d3-hierarchy

and display up to 6 levels of depth and (in brackets) the number of children behind each title/branch.
`
Insert cell
chart = {
const root = tree(data);
const svg = d3.create("svg");
const tidyTree = reusableTidyTree();
svg.datum(root).call(tidyTree)
// const zoomBehaviours = d3.zoom()
// .scaleExtent([0.05, 3])
// .on('zoom', () => d3.selectAll('g').attr('transform', d3.event.transform));

// svg.call(zoomBehaviours);
// .attr("viewBox", autoBox)
return svg.node();
}
Insert cell
depth = 2;
Insert cell
// Follow the D3 reusable pattern
reusableTidyTree = function () {
// Function to be called when the data is updated
let updateData;
// The data currently bound to the visualization
let datum;
// Memoize the data for each descendant of the tree. The is for easy Lookup O(1)
let dataIndexingMap = {}
// The stack used to keep track of the visited nodes. Node is pushed to stack when zooming in, and popped when zooming out.
let dataKeyStack = [];

function getChildrenUpToDepth(depth, node) {
if (depth === 0 || node === null) {
return null;
}
if (depth === 1) {
return {
name: node.data.name,
data: node.data,
depth: node.depth,
children: null, childCount: node.children ? node.children.length : 0
}
}

return {
name: node.data.name,
data: node.data,
depth: node.depth,
children: node.children ? node.children.map(c => {
return getChildrenUpToDepth(depth - 1, c);
}) : null
}
}
function getDatumFromData(d) {
const datum = d3.hierarchy({
name: d.data.name,
data: d.data,
depth: d.depth,
children: d.children ? d.children.map(c => {
return getChildrenUpToDepth(depth, c);
}) : null

});
return tree(datum)
}

function getKey(e) {
return e.data.name + '-' + e.data.index + '-' + e.depth
}

function indexData(d) {
const allData = d.descendants();
allData.forEach(e => {
dataIndexingMap[getKey(e)] = e

});
}

function tidyTree(selection) {

const linksG = selection.append("g")
.attr("fill", "none")
.attr("stroke", "#555")
.attr("stroke-opacity", 0.4)
.attr("stroke-width", 0.2);

const circleG = selection.append("g");

const textG = selection.append("g")
.attr("font-family", "sans-serif")
.attr("font-size", 10)
.attr("stroke-linejoin", "round")
.attr("stroke-width", 3);

indexData(selection.datum());
const key = getKey(selection.datum());
datum = getDatumFromData(dataIndexingMap[key]);
dataKeyStack.push(key);

let updateLinks = function () {
const links = datum.links().filter(d => d.source.depth <= datum.depth + depth && d.target.depth <= datum.depth + depth);;
linksG.selectAll("path")
.data(links)
.join("path")
.attr("d", d3.linkRadial()
.angle(d => d.x)
.radius(d => d.y));
}
let nodeClick = function (d) {
d3.event.stopPropagation();
if (d.parent === null || !d.data.data.children) {
return
}

const key = getKey(d.data);
datum = getDatumFromData(dataIndexingMap[key]);
dataKeyStack.push(key);
updateData();

}

let updateCircles = function () {
const descendants = datum.descendants().filter(d => d.depth <= datum.depth + depth);
circleG.selectAll("circle")
.data(descendants)
.join("circle")
.on('click', nodeClick)
.style('cursor', d => (d.parent === null || !d.data.data.children) ? "" : 'pointer')
.attr("transform", d => `
rotate(${d.x * 180 / Math.PI - 90})
translate(${d.y},0)
`)
.attr("fill", d => d.children ? "#555" : "#999")
.attr("r", 1);
}


let updateTexts = function () {
const descendants = datum.descendants().filter(d => d.depth <= datum.depth + depth);
textG.selectAll("text").remove();
textG.selectAll("text")
.data(descendants)
.join("text")
.on('click', nodeClick)
.attr("transform", d => `
rotate(${d.x * 180 / Math.PI - 90})
translate(${d.y},0)
rotate(${d.x >= Math.PI ? 180 : 0})
`)
.attr("dy", "0.31em")
.attr("x", d => d.x < Math.PI === !d.children ? 6 : -6)
.attr("text-anchor", d => d.x < Math.PI === !d.children ? "start" : "end")
.style('font-size', 2)
.style('fill', d=> d.data.data.children ? 'black' : 'tomato')
.style('cursor', d => (d.parent === null || !d.data.data.children) ? "" : 'pointer')
.html(d => {
return d.data.name + (d.data.childCount > 0 ? `<tspan fill='green' font-weight='bold'> (${d.data.childCount})</tpsan>` : '');
})
.clone(true).lower()
.attr("stroke", "white");
}

selection.on('click', () => {
if (dataKeyStack.length > 1) {
dataKeyStack.pop();
const key = dataKeyStack[dataKeyStack.length - 1];
datum = getDatumFromData(dataIndexingMap[key]);
updateData();
}
})

updateData = function () {
updateCircles();
updateLinks();
updateTexts();
};
updateData();

}

return tidyTree;
}
Insert cell
function autoBox() {
document.body.appendChild(this);
const {x, y, width, height} = this.getBBox();
document.body.removeChild(this);
return [x, y, width, height];
}
Insert cell

function transform(root) {
if (root === null)
return null;
if(root.display_titles)
delete root['display_titles']

return Object.keys(root).map((k, index )=> {
const children = transform(root[k]);
return {
name: k,
index,
[children === null ? "value" : "children"] : children === null ? 1 : children
}
})
}
Insert cell
tree(data).descendants()
Insert cell
data = d3.hierarchy(transform(rawData)[0])
.sort((a, b) => d3.ascending(a.data.name, b.data.name))
Insert cell
transform(rawData)[0]
Insert cell
rawData = (await fetch('https://www.ifixit.com/api/2.0/wikis/CATEGORY?display=hierarchy&includeStubs&pretty')).json()
Insert cell
transform(rawData)[0]
Insert cell
tree = d3.tree()
.size([2 * Math.PI, radius/4])
.separation((a, b) => (a.parent == b.parent ? 1 : 1)/ a.depth)
Insert cell
width = 1024
Insert cell
radius = width / 2
Insert cell
d3 = require("d3@5")
Insert cell
import {zoomAndPanSvg} from '@ehouais/zoom-and-pan'
Insert cell
zoomAndPanSvg
Insert cell
// zoomAndPanSvg(svg`${chart}`, [width, (width * 2) / 3]);
Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more