Skip to content

Instantly share code, notes, and snippets.

@yohm
Last active January 7, 2019 03:25
Show Gist options
  • Save yohm/2983060275a9c60263d03979f7830cf4 to your computer and use it in GitHub Desktop.
Save yohm/2983060275a9c60263d03979f7830cf4 to your computer and use it in GitHub Desktop.
d3.jsでforce directed layout
<!DOCTYPE html>
<style>
div.tooltip {
position: absolute;
text-align: center;
width: 80px;
height: 18px;
padding: 2px;
font: 14px sans-serif;
background: lightsteelblue;
border: 0px;
border-radius: 8px;
pointer-events: none;
}
</style>
<div>
<textarea id="linklist" rows="10" cols="20" placeholder="link list">
a b 1
b c 1
c a 1
</textarea>
<textarea id="nodelist" rows="10" cols="20" placeholder="node list">
a feature of node a
b feature of node b
c feature of node c
</textarea>
<textarea id="colorlist" rows="10" cols="20" placeholder="color list(id r g b)">
</textarea>
<button onclick="draw();" type="button" id="draw_btn">
draw
</button>
<span id="feature">
</span>
</div>
<div class="slidecontainer" id="sliders">
<input type="range" min="50" max="200" value="200" step="10" class="slider" id="slider_l" onchange="change_length(this.value); document.getElementById('val_l').textContent = this.value;">length : <span id="val_l">200</span><br/>
<input type="range" min="100" max="1000" value="1000" step="100" class="slider" id="slider_r" onchange="change_charge(this.value); document.getElementById('val_r').textContent = this.value;">repulsion : <span id="val_r">1000</span><br/>
</div>
<div class="tooltip" style="opacity: 0"></div>
<svg width="800" height="800"></svg>
<script src="https://d3js.org/d3.v4.min.js"></script>
<script>
const read_link_list = () => {
let s = document.getElementById("linklist").value;
let lines = s.split("\n");
let nodes = {};
let links = [];
for( let l of lines ) {
let a = l.split(" ");
if( a.length != 3 ) { continue; }
if( !(a[0] in nodes) ) { nodes[a[0]] = {id: a[0]}; }
if( !(a[1] in nodes) ) { nodes[a[1]] = {id: a[1]}; }
links.push( {source: nodes[a[0]], target: nodes[a[1]], weight: Number(a[2]) } );
}
{ // read node list
let nlines = document.getElementById("nodelist").value.split("\n");
for(let n of nlines) {
let a = n.split(" ");
if(a[0] in nodes) { nodes[a[0]].features = a.slice(1); }
}
}
let node_colors = {};
{ // read node colors
let nlines = document.getElementById("colorlist").value.split("\n");
for(let n of nlines) {
let a = n.split(" ");
if(a[0] in nodes) { node_colors[a[0]] = [parseInt(a[1]), parseInt(a[2]), parseInt(a[3])]; }
}
}
return [Object.values(nodes), links, node_colors];
}
const svg = d3.select("svg"),
width = +svg.attr("width"),
height = +svg.attr("height"),
color = d3.scaleOrdinal(d3.schemeCategory10);
const tooltip = d3.select(".tooltip");
const feature_span = d3.select("#feature")
let nodes = [],
links = [],
node_colors = {};
const ticked = () => {
node.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; })
link.attr("x1", function(d) { return d.source.x; })
.attr("y1", function(d) { return d.source.y; })
.attr("x2", function(d) { return d.target.x; })
.attr("y2", function(d) { return d.target.y; });
}
const dragstarted = (d) => {
if (!d3.event.active) simulation.alphaTarget(0.3).restart();
d.fx = d.x;
d.fy = d.y;
}
const dragged = (d) => {
d.fx = d3.event.x;
d.fy = d3.event.y;
}
const dragended = (d) => {
if (!d3.event.active) simulation.alphaTarget(0);
d.fx = null;
d.fy = null;
}
const simulation = d3.forceSimulation(nodes)
.force("charge", d3.forceManyBody().strength(-1000))
.force("link", d3.forceLink(links).distance(200))
.force("center", d3.forceCenter(0, 0))
//.alphaTarget(0)
.velocityDecay(0.8)
.on("tick", ticked);
let g = svg.append("g").attr("transform", "translate(" + width / 2 + "," + height / 2 + ")"),
link = g.append("g").attr("stroke", "#000").attr("stroke-width", 1.5).selectAll(".link"),
node = g.append("g").attr("stroke", "#fff").attr("stroke-width", 1.5).selectAll(".node");
const restart = () => {
// Apply the general update pattern to the nodes.
node = node.data(nodes, function(d) { return d.id;});
node.exit().remove();
const get_color = (id) => {
if(id in node_colors) { let c = node_colors[id]; return d3.rgb(c[0],c[1],c[2]); }
else {return color(id); }
}
node = node.enter().append("circle").attr("r", 8).merge(node);
node.attr("fill", function(d) { return get_color(d.id); });
node
.on("mouseover", function(d) {
tooltip.transition().duration(200).style("opacity", .9);
tooltip.html(`id: ${d.id}`)
.style("left", (d3.event.pageX) + "px")
.style("top", (d3.event.pageY - 28) + "px");
feature_span.text(`id: ${d.id}, ${d.features}`)
})
.on("mouseout", function(d) {
tooltip.transition()
.duration(500)
.style("opacity", 0);
})
.call(d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended));
// Apply the general update pattern to the links.
link = link.data(links, function(d) { return d.source.id + "-" + d.target.id; });
link.exit().remove();
link = link.enter().append("line").merge(link);
link.attr("stroke-width", function(d) { return Math.log10(d.weight)+1.0; });
// Update and restart the simulation.
simulation.nodes(nodes);
simulation.force("link").links(links);
simulation.alpha(1).restart();
}
const draw = () => {
let nl = read_link_list();
nodes = nl[0], links = nl[1], node_colors = nl[2];
restart();
}
const change_length = (l) => {
simulation.force("link", d3.forceLink(links).distance(l));
restart();
}
const change_charge = (c) => {
simulation.force("charge", d3.forceManyBody().strength(-c));
restart();
}
draw();
</script>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment