This randomly-generated tree is positioned using a force simulation.
xxxxxxxxxx
<meta charset="utf-8">
<canvas width="960" height="960"></canvas>
<script src="https://d3js.org/d3.v4.min.js"></script>
<script>
function getData (data) {
let nested = d3.nest().key(d => d["Session #"]).entries(data)
let root = {};
let parent;
let maxLevel = d3.max(nested, d => d.values.length) - 1;
for (let i = 0; i < maxLevel; i++) {
let levelActions = getLevelActions(i);
if (i === 0) {
root.actionText = levelActions[0].actionText;
root.children = [];
} else {
levelActions.forEach(d => {
if (parent) {
let _parent = parent.filter(x => x.actionText === d.parent
&& x.sessions.indexOf(d.sessions[0]) > -1);
if (_parent.length) {
if (!_parent[0].children)
_parent[0].children = [];
_parent[0].children.push(d)
}
} else {
root.children.push(d)
}
})
parent = levelActions;
}
}
function getLevelActions(level) {
let actions = [];
for (let j = 0; j < nested.length; j++) {
let values = nested[j].values;
if (!values[level]) continue;
let actionText = values[level]["User Action"];
let action = actions.filter(x => x.actionText == actionText);
let products = [];
if (level > 0) {
products = [
values[level - 1].Prod1,
values[level - 1].Prod2,
values[level - 1].Prod3
]
}
if (action.length == 0) {
actions.push({
actionText: actionText,
products: products,
level: level,
sessions: [+nested[j].key],
parent: level == 0 ? null : values[level - 1]["User Action"]
});
} else {
if (level > 0) {
action = action.filter(x => x.products[0] == values[level - 1].Prod1 &&
x.products[1] == values[level - 1].Prod2 &&
x.products[2] == values[level - 1].Prod3)
if (action.length == 0) {
actions.push({
actionText: actionText,
products: products,
level: level,
sessions: [+nested[j].key],
parent: values[level - 1]["User Action"]
});
} else {
action[0].sessions.push(+nested[j].key)
}
}
}
}
return actions;
}
return root
}
d3.csv('data.csv', function (data) {
var dt = getData(data);
var tree = data => {
const root = d3.hierarchy(data);
root.dx = 20;
root.dy = 960 / (root.height + 1);
return d3.tree().nodeSize([root.dx, root.dy])(root);
}
const root = tree(dt);
let x0 = Infinity;
let x1 = -x0;
let maxDepth = 0;
root.each(d => {
if (d.x > x1) x1 = d.x;
if (d.x < x0) x0 = d.x;
if (d.depth > maxDepth) maxDepth = d.depth
});
var nodes = root.descendants().reverse();
var links = root.links();
var simulation = d3.forceSimulation(nodes)
.force("charge", d3.forceManyBody())
.force("link", d3.forceLink(links).distance(20).strength(1))
.force("x", d3.forceX())
.force("y", d3.forceY())
.on("tick", ticked);
var canvas = document.querySelector("canvas"),
context = canvas.getContext("2d"),
width = canvas.width,
height = canvas.height;
d3.select(canvas)
.attr('height', height)
.call(d3.drag()
.container(canvas)
.subject(dragsubject)
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended));
function ticked() {
context.clearRect(0, 0, width, height);
context.save();
context.translate(width / 2, height / 2);
context.beginPath();
links.forEach(drawLink);
context.strokeStyle = "#aaa";
context.stroke();
context.beginPath();
nodes.forEach(drawNode);
context.fill();
context.strokeStyle = "#fff";
context.stroke();
context.restore();
}
function dragsubject() {
return simulation.find(d3.event.x - width / 2, d3.event.y - height / 2);
}
function dragstarted() {
if (!d3.event.active) simulation.alphaTarget(0.3).restart();
d3.event.subject.fx = d3.event.subject.x;
d3.event.subject.fy = d3.event.subject.y;
}
function dragged() {
d3.event.subject.fx = d3.event.x;
d3.event.subject.fy = d3.event.y;
}
function dragended() {
if (!d3.event.active) simulation.alphaTarget(0);
d3.event.subject.fx = null;
d3.event.subject.fy = null;
}
function drawLink(d) {
context.moveTo(d.source.x, d.source.y);
context.lineTo(d.target.x, d.target.y);
}
function drawNode(d) {
context.moveTo(d.x + 3, d.y);
context.arc(d.x, d.y, 3, 0, 2 * Math.PI);
}
})
</script>
https://d3js.org/d3.v4.min.js