Last active
January 7, 2019 03:25
-
-
Save yohm/2983060275a9c60263d03979f7830cf4 to your computer and use it in GitHub Desktop.
d3.jsでforce directed layout
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!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