|
<!DOCTYPE html> |
|
<meta charset="utf-8"> |
|
<style> |
|
.links line { |
|
stroke: #d8d8d8; |
|
stroke-opacity: 0.6; |
|
stroke-width: 2; |
|
stroke-dasharray: 10, 6; |
|
} |
|
|
|
.nodes circle { |
|
fill: #00ceb9; |
|
stroke: white ; |
|
stroke-width: 5px; |
|
} |
|
|
|
.label__node { |
|
font-family: sans-serif; |
|
font-size: 19px; |
|
stroke: #dadada; |
|
opacity: .4; |
|
background: white; |
|
font-weight: bold; |
|
} |
|
|
|
.label__error { |
|
font-family: sans-serif; |
|
font-size: 14px; |
|
fill: white; |
|
font-weight: bold; |
|
} |
|
|
|
svg { |
|
border: 1px solid #d8d8d8; |
|
} |
|
</style> |
|
|
|
<svg width="1000" height="400"></svg> |
|
|
|
<script src="https://d3js.org/d3.v4.min.js"></script> |
|
|
|
<script> |
|
const svg = d3.select("svg"); |
|
const width = +svg.attr("width"); |
|
const height = +svg.attr("height"); |
|
const animate = true; |
|
const nodeRadius = 23; |
|
|
|
const data = { |
|
nodes: [ |
|
{"name": "Gordon", "sex": "M"}, |
|
{"name": "Sylvester", "sex": "M"}, |
|
{"name": "Lillian", "sex": "F"}, |
|
{"name": "Mary", "sex": "F"}, |
|
{"name": "Helen", "sex": "F"}, |
|
{"name": "Jamie", "sex": "M"}, |
|
{"name": "Jessie", "sex": "F"}, |
|
{"name": "Ashton", "sex": "M"}, |
|
{"name": "Duncan", "sex": "M"}, |
|
{"name": "Evette", "sex": "F"} |
|
], |
|
links: [ |
|
{"source": "Sylvester", "target": "Lillian", "failures":"10" }, |
|
{"source": "Gordon", "target": "Lillian", "failures":"1290" }, |
|
{"source": "Lillian", "target": "Mary", "failures":"0"}, |
|
{"source": "Mary", "target": "Helen", "failures":"0"}, |
|
{"source": "Helen", "target": "Jessie", "failures":"1.8k"}, |
|
{"source": "Sylvester", "target": "Helen", "failures":"0"}, |
|
{"source": "Helen", "target": "Jamie", "failures":"0"}, |
|
{"source": "Jamie", "target": "Jessie", "failures":"2.1k"}, |
|
{"source": "Ashton", "target": "Jessie", "failures":"0"}, |
|
{"source": "Ashton", "target": "Jamie", "failures":"0"}, |
|
{"source": "Gordon", "target": "Jessie", "failures":"0"}, |
|
{"source": "Jessie", "target": "Duncan", "failures":"0"}, |
|
{"source": "Jessie", "target": "Evette", "failures":"0"} |
|
] |
|
}; |
|
|
|
|
|
// Set up the simulation |
|
const strengthX = -0.08; |
|
const strengthY = 0.017; |
|
const simulation = d3.forceSimulation().nodes(data.nodes); |
|
const forceX = d3.forceX(width/2).strength(strengthX); |
|
const forceY = d3.forceY(height/2).strength(strengthY); |
|
const chargeForce = d3.forceManyBody().strength(-1751); |
|
const centerForce = d3.forceCenter(width / 2, height / 2); |
|
const linkForce = d3.forceLink(data.links).id(d => d.name).distance(180).strength(1.656); |
|
|
|
//const widthScale = d3.scaleLinear.domain([24,]) |
|
|
|
// Add positioning forces to the simulation |
|
simulation |
|
.force('xAxis', forceX) |
|
.force('yAxis', forceY) |
|
.force("charge_force", chargeForce) |
|
.force("center_force", centerForce) |
|
.force("links", linkForce); |
|
|
|
//draw lines for the links |
|
const link = svg.append("g") |
|
.attr("class", "links") |
|
.selectAll("line") |
|
.data(data.links) |
|
.enter().append("line"); |
|
|
|
//draw circles for the nodes |
|
const node = svg.append("g") |
|
.attr("class", "nodes") |
|
.selectAll("g") |
|
.data(data.nodes) |
|
.enter().append("g"); |
|
|
|
const circles = node |
|
.append("circle") |
|
.attr("r", nodeRadius); |
|
|
|
const labels = node.append("text") |
|
.text(d => d.name) |
|
.attr("class", "label__node") |
|
.attr('x', d => 0 - nodeRadius - 4 - d.name.length/2) |
|
.attr('y', 0 - nodeRadius - 8); |
|
|
|
const statusBox = svg.append("g") |
|
.attr("class", "boxes") |
|
.selectAll("g") |
|
.data(data.links) |
|
.enter().append("g"); |
|
|
|
statusBox.append("rect") |
|
.attr("x", -2).attr("y", -23) |
|
.attr("rx", 5).attr("ry", 5) |
|
.attr("width", d => { |
|
return d.failures.length + 30 + Math.log(d.failures.length) * 13; |
|
}) |
|
.attr("height", 30) |
|
.style("fill", d => { |
|
return d.failures === '0' ? '#e3e3e3' : '#f9507f'; |
|
}) |
|
.attr("class", "status-box"); |
|
|
|
statusBox.append("text") |
|
.text(d => d.failures) |
|
.attr("x", d => 9 + (Math.log(d.failures.length) * 0.2)) |
|
.attr("y", -3) |
|
.attr("class", "label__error") |
|
|
|
if (!animate) { |
|
simulation.stop(); |
|
const min = Math.log(simulation.alphaMin()); |
|
const decay = Math.log(1 - simulation.alphaDecay()); |
|
const numTicks = Math.ceil(min / decay); |
|
|
|
// Run the simulation enough times to position nodes |
|
for (let i = 0; i < numTicks; ++i) { |
|
simulation.tick(); |
|
}; |
|
// Position the nodes |
|
tickActions() |
|
} else { |
|
simulation.on("tick", tickActions ); |
|
} |
|
|
|
function tickActions() { |
|
// constrains the nodes to be within a box |
|
node.attr("transform", d => { |
|
const x = Math.max(nodeRadius, Math.min(width, d.x)); |
|
const y = Math.max(nodeRadius, Math.min(height, d.y)); |
|
return "translate(" + x + "," + y + ")"; |
|
}); |
|
|
|
link |
|
.attr("x1", d => d.source.x) |
|
.attr("y1", d => d.source.y) |
|
.attr("x2", d => d.target.x) |
|
.attr("y2", d => d.target.y); |
|
|
|
|
|
statusBox.attr("transform", d => { |
|
const x = (d.target.x + d.source.x) / 2; |
|
const y = (d.target.y + d.source.y) / 2; |
|
return "translate(" + x + "," + y + ")"; |
|
}); |
|
|
|
} |
|
|
|
|
|
</script> |