Built with blockbuilder.org
<!DOCTYPE html>
<head>
<meta charset="utf-8">
<script src="https://d3js.org/d3.v4.min.js"></script>
<style>
body {margin: 0; position: fixed; top: 0; right: 0; bottom: 0; left: 0;}
#my-table {
border: 1px solid #CCCCCC;
border-radius: 2px;
font-family: Helvetica;
font-size: 12px;
margin: 8px;
overflow-x: auto;
white-space: nowrap;
}
.column {
display: inline-block;
}
.column:first-of-type .cell {
text-align: left;
}
.cell {
padding: 8px;
text-align: right;
}
.cell:first-of-type {
border-bottom: 1px solid #CCCCCC;
border-right: 1px solid #CCCCCC;
}
svg .tick line {stroke: #666;}
svg path.domain {stroke: #666;}
svg text {fill: #666;}
</style>
</head>
<body>
<div id="my-table">
<div class="column">
<div class="cell"># Steps</div>
<div class="cell">Most common failureID</div>
<div class="cell">2nd common failureID</div>
<div class="cell">3rd common failureID</div>
<div class="cell">4th common failureID</div>
</div>
</div>
<script>
const table = document.getElementById('my-table');
const margin = {top: 20, right: 20, bottom: 30, left: 40};
const width = 960 - margin.left - margin.right;
const height = 500 - margin.top - margin.bottom - table.offsetHeight - 2 * 12;
const x = d3.scaleLinear()
.range([0, width]);
const y = d3.scaleLinear()
.range([height, 0]);
const size = d3.scaleLog()
.range([1, 20]);
const colors = d3.scaleQuantize()
.domain([0, 0.48])
.range(["#FFFFFF", "#FDEBE9", "#FEE5DE", "#FBD8D4"]);
const xAxis = d3.axisBottom()
.scale(x);
const yAxis = d3.axisLeft()
.tickFormat(d3.format(".0%"))
.scale(y);
const svg = d3.select("body").append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
const happyIds = [699, 706];
d3.json("data.json", (error, root) => {
if (error) throw error;
const nodeIdToNodes = root.nodes;
const nodeIds = Object.keys(nodeIdToNodes);
const nodes = [];
const depths = {};
nodeIds.forEach(updateChildren);
function updateChildren(nodeId) {
const node = nodeIdToNodes[nodeId];
// NOTE: SOMEHOW IT'S POSSIBLE FOR A HAPPY NODE TO
// HAVE CHILDREN EVEN THOUGH IT'S HAPPY
node.isHappy = Boolean(happyIds.includes(node.eventID))
nodes.push(node);
if (node.children) {
node.children = node.children.map(nodeId => nodeIdToNodes[nodeId]);
}
}
function traverseDF(node, parent, depth = 1) {
node.parent = parent;
if (node.children && !node.isHappy && node.size > 1) {
node.children.forEach(child => traverseDF(child, node, depth + 1));
} else {
depths[depth] = depths[depth] || {happy: 0, sad: 0, failures: {}};
if (node.isHappy) {
depths[depth].happy++;
} else {
depths[depth].sad++;
depths[depth].failures[node.eventID] = depths[depth].failures[node.eventID] || 0;
depths[depth].failures[node.eventID]++;
}
}
}
nodes
.filter(node => node.size || node.childEndCount)
.forEach(node => traverseDF(node, root))
const data = Object.keys(depths)
.map(d => ({
failuresByCount: Object.keys(depths[d].failures)
.map(failureId => ({id: failureId, count: depths[d].failures[failureId]}))
.sort((a, b) => b.count - a.count),
size: depths[d].happy + depths[d].sad,
x: Number(d),
y: depths[d].happy / (depths[d].happy + depths[d].sad)
}))
.filter(d => d.y)
x.domain(data.length > 1 ? d3.extent(data, d => d.x) : [0, data[0].x]).nice();
y.domain(data.length > 1 ? d3.extent(data, d => d.y) : [0, data[0].y]).nice();
size.domain(data.length > 1 ? d3.extent(data, d => d.size) : [0, data[0].size]).nice();
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")")
.call(xAxis)
.append("text")
.attr("class", "label")
.attr("x", width)
.attr("y", -6)
.style("text-anchor", "end")
.text("Number of steps");
svg.append("g")
.attr("class", "y axis")
.call(yAxis)
.append("text")
.attr("class", "label")
.attr("transform", "rotate(-90)")
.attr("y", 6)
.attr("dy", ".71em")
.style("text-anchor", "end")
.text("% Reaching happy path")
svg.selectAll(".dot")
.data(data)
.enter().append("circle")
.attr("class", "dot")
.attr("cx", d => x(d.x))
.attr("cy", d => y(d.y))
.attr("opacity", 0)
.attr("r", 0)
.style("fill", "#1B6DE0")
.transition().delay((d, i) => i * 18)
.attr("r", d => size(d.size))
.attr("opacity", 0.8)
// Apply CSS for table
data.forEach(({failuresByCount, size, x, y}, index) => {
const column = document.createElement('div');
column.className = 'column'
table.appendChild(column);
const row0 = document.createElement('div');
row0.className = 'cell'
row0.innerHTML = x;
column.appendChild(row0);
const row1 = document.createElement('div');
row1.className = 'cell';
const intensity0 = failuresByCount[0].count / size;
row1.style.backgroundColor = colors(intensity0);
row1.innerHTML = failuresByCount[0].id + ` (${Math.round(intensity0 * 100)}%)`;
column.appendChild(row1);
const row2 = document.createElement('div');
row2.className = 'cell';
const intensity1 = failuresByCount[1].count / size;
row2.style.backgroundColor = colors(intensity1);
row2.innerHTML = failuresByCount[1].id + ` (${Math.round(intensity1 * 100)}%)`;
column.appendChild(row2);
const row3 = document.createElement('div');
row3.className = 'cell';
const intensity2 = failuresByCount[2].count / size;
row3.style.backgroundColor = colors(intensity2);
row3.innerHTML = failuresByCount[2].id + ` (${Math.round(intensity2 * 100)}%)`;
column.appendChild(row3);
const row4 = document.createElement('div');
row4.className = 'cell';
const intensity3 = failuresByCount[3].count / size;
row4.style.backgroundColor = colors(intensity3);
row4.innerHTML = failuresByCount[3].id + ` (${Math.round(intensity3 * 100)}%)`;
column.appendChild(row4);
})
})
</script>
</body>