Published
Edited
Mar 30, 2019
1 fork
Insert cell
Insert cell
Insert cell
mutable treeData = $.get("https://api.myjson.com/bins/ai6xu", function(data, textStatus, jqXHR) {});
Insert cell
chart = {

// Create the SVG
// As you an see, the svg is initialized with height = dx.
// This will be updated later when the rest of the nodes in the tree are entered.
const svg =
d3
.create("svg")
.attr("width", width)
.attr("height", dx)
.style("font", "1rem sans-serif")
.on("click", handleClickOnCanvas)
// 2.1 Create a container for all the nodes in the graph
const gNode =
svg
.append("g")
.attr('id', 'nodes')
.attr("cursor", "pointer");
// 2.2 Create a container for all the links in the graph
const gLink =
svg
.append("g")
.attr('id', 'links')
.attr("fill", "none")
.attr("stroke", "#555")
.attr("stroke-opacity", 0.4)
.attr("stroke-width", 1.5);
// 3. Fill in the nodes and links with the hierarchy data
update(svg);

// 4. Register other event handlers
d3.select('body')
.on("keydown", function(e) {
console.log(`keydown: ${d3.event.keyCode}`);
// Check to see if a node is being edited
let nodeIsBeingEdited = gNode.select('g.node-editing').size()
if(d3.event.keyCode == 9) {
console.log("tab - append child to selected node");
appendChildToSelectedNode(svg);
} else if(d3.event.keyCode == 13 && !nodeIsBeingEdited) {
console.log("enter - add sibling to selected node");
addSiblingToSelectedNode(svg);
} else if(d3.event.keyCode == 8 && !nodeIsBeingEdited) {
console.log("delete - remove selected node");
removeSelectedNode(svg);
} else if(d3.event.keyCode == 27) {
console.log("esc - deselect node");
handleKeypressEsc(svg);
}
});
return svg.node();
}
Insert cell
update = (svg) => {
// d3.hierarchy object is a data structure that represents a hierarchy
// It has a number of functions defined on it for retrieving things like
// ancestor, descendant, and leaf nodes, and for computing the path between nodes
// const root = d3.hierarchy(treeData);
const root = d3.hierarchy(treeData);
root.x0 = dy / 2;
root.y0 = 0;
// console.log(root)
// console.log(root.descendants())
root.descendants().forEach((d, i) => {
// console.log(d)
// console.log(i)
d.id = d.data.id;
d._children = d.children;
});
const duration = 100
const nodes = root.descendants().reverse();
const links = root.links();

// Compute the new tree layout.
tree(root);

let left = root;
let right = root;
root.eachBefore(node => {
if (node.x < left.x) left = node;
if (node.x > right.x) right = node;
});

const height = right.x - left.x + margin.top + margin.bottom;

const transition =
svg
.transition()
.duration(duration)
.attr("height", height)
.attr("viewBox", [-margin.left, left.x - margin.top, width, height])
.tween("resize", window.ResizeObserver ? null : () => () => svg.dispatch("toggle"));

// Update the nodes
const existingNodeContainers = svg.select('#nodes').selectAll("g").data(nodes, d => d.id);

// Enter any new nodes at the parent's previous position.
// Create new node containers that each contains a circle and a text label
const newNodeContainers = existingNodeContainers.enter().append("g")
.attr('id', (d,i) => `${d.id}`)
.attr('class', 'node')
.attr("transform", d => `translate(${root.y0},${root.x0})`)
.attr("fill-opacity", 0)
.attr("stroke-opacity", 0)
newNodeContainers.append("circle")
.attr("r", 2)
.attr("fill", d => d._children ? "#555" : "#999");

newNodeContainers.append('foreignObject')
.attr('x', -50)
.attr('y', -35)
.attr('width', 80)
.attr('height', 40)
.append("xhtml:p")
.text(d => d.data.name)
existingNodeContainers.merge(newNodeContainers)
.on("click", handleClickOnNode)
// Transition nodes to their new position.
// Increase opacity from 0 to 1 during transition
const nodeUpdate = existingNodeContainers.merge(newNodeContainers).transition(transition)
.attr("transform", d => `translate(${d.y},${d.x})`)
.attr("fill-opacity", 1)
.attr("stroke-opacity", 1);

// Transition exiting nodes to the parent's new position.
// Reduce opacity from 1 to 0 during transition
const nodeExit = existingNodeContainers.exit().transition(transition).remove()
.attr("transform", d => `translate(${root.y},${root.x})`)
.attr("fill-opacity", 0)
.attr("stroke-opacity", 0);

// Update the links…
const existingLinkPaths = svg.select('#links').selectAll("path").data(links, d => d.target.id);

// Enter any new links at the parent's previous position.
const newLinkPaths = existingLinkPaths.enter().append("path")
.attr("d", d => {
const o = {x: root.x0, y: root.y0};
return diagonal({source: o, target: o});
});

// Transition links to their new position.
existingLinkPaths.merge(newLinkPaths).transition(transition)
.attr("d", diagonal);

// Transition exiting nodes to the parent's new position.
existingLinkPaths.exit().transition(transition).remove()
.attr("d", d => {
const o = {x: root.x, y: root.y};
return diagonal({source: o, target: o});
});

// Stash the old positions for transition.
root.eachBefore(d => {
d.x0 = d.x;
d.y0 = d.y;
});
}
Insert cell
removeSelectedNode = (svg) => {
let idOfSelectedNode = svg
.selectAll('g.node')
.filter(".node-selected")
.attr('id')
// console.log(idOfSelectedNode)
let parentNodes = [treeData]
let nodeFound = false;
let parent
while (parentNodes.length != 0) {
let allNextLevelParents = []
for (let node of parentNodes) {
if (node.children) {
allNextLevelParents = allNextLevelParents.concat(node.children)
if (node.children.map(child => child.id).includes(idOfSelectedNode)) {
nodeFound = true
parent = node
}
}
}
if (nodeFound) break;
else {
parentNodes = allNextLevelParents
}
}
parent.children = parent.children.filter(child => child.id !== idOfSelectedNode)
parent.children.length === 0 && delete parent.children
update(svg)
updateJSONOnServer();
}
Insert cell
appendChildToSelectedNode = (svg) => {
let idOfSelectedNode = svg
.selectAll('g.node')
.filter(".node-selected")
.attr('id')
// console.log(idOfSelectedNode)
let nodeInTree = [treeData]
let nodeFound = false;
let parent
while (nodeInTree.length != 0) {
let allCurrentLevelChildren = []
for (let node of nodeInTree) {
if (node.children) {
allCurrentLevelChildren = allCurrentLevelChildren.concat(node.children)
}
if (node.id === idOfSelectedNode) {
nodeFound = true
parent = node
}
}
if (nodeFound) break;
else {
nodeInTree = allCurrentLevelChildren
}
}
let child = {
name: '',
id: ID()
}
if (parent.children) parent.children.push(child);
else parent.children = [child];
update(svg)
updateJSONOnServer();
}
Insert cell
addSiblingToSelectedNode = (svg) => {
let idOfSelectedNode = svg
.selectAll('g.node')
.filter(".node-selected")
.attr('id')
// console.log(idOfSelectedNode)
let parentNodes = [treeData]
let nodeFound = false;
let parent
while (parentNodes.length != 0) {
let allNextLevelParents = []
for (let node of parentNodes) {
if (node.children) {
allNextLevelParents = allNextLevelParents.concat(node.children)
if (node.children.map(child => child.id).includes(idOfSelectedNode)) {
nodeFound = true
parent = node
}
}
}
if (nodeFound) break;
else {
parentNodes = allNextLevelParents
}
}
let child = {
name: '',
id: ID()
}
parent.children.push(child);
update(svg)
updateJSONOnServer();
}
Insert cell
handleKeypressEsc = (svg) => {
svg
.selectAll('g.node')
.filter(".node-selected")
.each(deselectNode);
}
Insert cell
handleClickOnNode = (d,i,nodes) => {
const currentlySelectedNode =
d3.selectAll(nodes)
.filter('.node-selected')
const clickedNodeIndex = i
const clickedNode = nodes[clickedNodeIndex]
const clickedNodeID = d3.select(clickedNode).attr('id')
const otherNodes =
d3.selectAll(nodes)
.filter((d,i) => i!== clickedNodeIndex)
if (currentlySelectedNode.size() > 0 && currentlySelectedNode.attr('id') === clickedNodeID) {
console.log('going into editing mode!')
d3.select(clickedNode)
.call(editNode)
} else {

d3.select(clickedNode)
.call(selectNode)

// If not already selected, mark as selected
otherNodes
.each(deselectNode)
}

// d.children = d.children ? null : d._children;
// update(d);
// Prevent triggering clickOnCanvas handler
// https://stackoverflow.com/questions/22941796/attaching-onclick-event-to-d3-chart-background
d3.event.stopPropagation();
}
Insert cell
editNode = (node) => {
node
.classed('node-editing', true)
.select('foreignObject')
.select('p')
.style('background-color', '#ddd')
console.log(`${node.attr('id')} is being edited`)
}
Insert cell
selectNode = (node) => {
node
.classed('node-selected', true)
.select('foreignObject')
.select('p')
.attr('contenteditable', 'true')
.style('background-color', '#ddd')
console.log(`${node.attr('id')} selected`)
}
Insert cell
deselectNode = (d,i,nodes) => {
let idOfSelectedNode =
d3.select(nodes[i])
.attr('id')
let newValue =
d3.select(nodes[i])
.select('foreignObject')
.select('p')
.html()
d3.select(nodes[i])
.classed('node-editing', false)
.classed('node-selected', false)
.select('foreignObject')
.select('p')
.attr('contenteditable', 'false')
.style('background-color', null)
updateNodeValue(idOfSelectedNode, newValue);
}
Insert cell
updateNodeValue = (idOfSelectedNode, newValue) => {
let nodeInTree = [treeData]
let nodeFound = false;
let parent
while (nodeInTree.length != 0) {
let allCurrentLevelChildren = []
for (let node of nodeInTree) {
if (node.children) {
allCurrentLevelChildren = allCurrentLevelChildren.concat(node.children)
}
if (node.id === idOfSelectedNode) {
nodeFound = true
parent = node
}
}
if (nodeFound) break;
else {
nodeInTree = allCurrentLevelChildren
}
}

parent.name = newValue;
updateJSONOnServer();
}
Insert cell
updateJSONOnServer = () => {
$.ajax({
url:"https://api.myjson.com/bins/ai6xu",
type:"PUT",
data:JSON.stringify(treeData),
contentType:"application/json; charset=utf-8",
dataType:"json",
success: function(data, textStatus, jqXHR){

}
});
}
Insert cell
handleClickOnCanvas = (d,i,nodes) => {
// console.log(nodes[i])
d3.select(nodes[i])
.selectAll('g.node')
.filter(".node-selected")
.each(deselectNode);
}
Insert cell
diagonal = d3.linkHorizontal().x(d => d.y).y(d => d.x)
Insert cell
tree = d3.tree().nodeSize([dx, dy])
Insert cell
dx = 40
Insert cell
dy = width / 4
Insert cell
margin = ({top: 40, right: 120, bottom: 40, left: 80})
Insert cell
//https://gist.github.com/gordonbrander/2230317
ID = () => '_' + Math.random().toString(36).substr(2, 9);
Insert cell
Insert cell
Insert cell
// mutable treeData =
// ({
// "id": ID(),
// "name": "Coffee",
// "children": [
// {
// "id": ID(),
// "name": "Tastes",
// "children": [
// {
// "id": ID(),
// "name": "Sour",
// },
// {
// "id": ID(),
// "name": "Bitter",
// }
// ]
// },
// {
// "id": ID(),
// "name": "Aromas",
// }
// ]
// })
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