Last active
March 14, 2018 20:37
-
-
Save sineline/3a205e292af68bb6c7a695d3852057a6 to your computer and use it in GitHub Desktop.
Testing of v4 force graph with grid - Based in // Credit https://github.com/john-guerra/forceInABox among others
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
{ | |
"nodes": [ | |
{"name": "Myriel", "group": 1}, | |
{"name": "Napoleon", "group": 1}, | |
{"name": "Mlle.Baptistine", "group": 1}, | |
{"name": "Mme.Magloire", "group": 1}, | |
{"name": "CountessdeLo", "group": 1}, | |
{"name": "Geborand", "group": 1}, | |
{"name": "Champtercier", "group": 1}, | |
{"name": "Cravatte", "group": 1}, | |
{"name": "Count", "group": 1}, | |
{"name": "OldMan", "group": 1}, | |
{"name": "Labarre", "group": 2}, | |
{"name": "Valjean", "group": 2}, | |
{"name": "Marguerite", "group": 3}, | |
{"name": "Mme.deR", "group": 2}, | |
{"name": "Isabeau", "group": 2}, | |
{"name": "Gervais", "group": 2}, | |
{"name": "Tholomyes", "group": 3}, | |
{"name": "Listolier", "group": 3}, | |
{"name": "Fameuil", "group": 3}, | |
{"name": "Blacheville", "group": 3}, | |
{"name": "Favourite", "group": 3}, | |
{"name": "Dahlia", "group": 3}, | |
{"name": "Zephine", "group": 3}, | |
{"name": "Fantine", "group": 3}, | |
{"name": "Mme.Thenardier", "group": 4}, | |
{"name": "Thenardier", "group": 4}, | |
{"name": "Cosette", "group": 5}, | |
{"name": "Javert", "group": 4}, | |
{"name": "Fauchelevent", "group": 0}, | |
{"name": "Bamatabois", "group": 2}, | |
{"name": "Perpetue", "group": 3}, | |
{"name": "Simplice", "group": 2}, | |
{"name": "Scaufflaire", "group": 2}, | |
{"name": "Woman1", "group": 2}, | |
{"name": "Judge", "group": 2}, | |
{"name": "Champmathieu", "group": 2}, | |
{"name": "Brevet", "group": 2}, | |
{"name": "Chenildieu", "group": 2}, | |
{"name": "Cochepaille", "group": 2}, | |
{"name": "Pontmercy", "group": 4}, | |
{"name": "Boulatruelle", "group": 6}, | |
{"name": "Eponine", "group": 4}, | |
{"name": "Anzelma", "group": 4}, | |
{"name": "Woman2", "group": 5}, | |
{"name": "MotherInnocent", "group": 0}, | |
{"name": "Gribier", "group": 0}, | |
{"name": "Jondrette", "group": 7}, | |
{"name": "Mme.Burgon", "group": 7}, | |
{"name": "Gavroche", "group": 8}, | |
{"name": "Gillenormand", "group": 5}, | |
{"name": "Magnon", "group": 5}, | |
{"name": "Mlle.Gillenormand", "group": 5}, | |
{"name": "Mme.Pontmercy", "group": 5}, | |
{"name": "Mlle.Vaubois", "group": 5}, | |
{"name": "Lt.Gillenormand", "group": 5}, | |
{"name": "Marius", "group": 8}, | |
{"name": "BaronessT", "group": 5}, | |
{"name": "Mabeuf", "group": 8}, | |
{"name": "Enjolras", "group": 8}, | |
{"name": "Combeferre", "group": 8}, | |
{"name": "Prouvaire", "group": 8}, | |
{"name": "Feuilly", "group": 8}, | |
{"name": "Courfeyrac", "group": 8}, | |
{"name": "Bahorel", "group": 8}, | |
{"name": "Bossuet", "group": 8}, | |
{"name": "Joly", "group": 8}, | |
{"name": "Grantaire", "group": 8}, | |
{"name": "MotherPlutarch", "group": 9}, | |
{"name": "Gueulemer", "group": 4}, | |
{"name": "Babet", "group": 4}, | |
{"name": "Claquesous", "group": 4}, | |
{"name": "Montparnasse", "group": 4}, | |
{"name": "Toussaint", "group": 5}, | |
{"name": "Child1", "group": 10}, | |
{"name": "Child2", "group": 10}, | |
{"name": "Brujon", "group": 4}, | |
{"name": "Mme.Hucheloup", "group": 8} | |
], | |
"links": [ | |
{"source":1,"target":1,"value":1} | |
] | |
} |
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
/* global d3 */ | |
// Credit https://github.com/john-guerra/forceInABox | |
function forceInABox(alpha) { | |
function index(d) { | |
return d.index; | |
} | |
var id = index, | |
nodes, | |
links, //needed for the force version | |
tree, | |
size = [100,100], | |
nodeSize = 1, // The expected node size used for computing the cluster node | |
forceCharge = -2, | |
foci = {}, | |
// oldStart = force.start, | |
linkStrengthIntraCluster = 0.1, | |
linkStrengthInterCluster = 0.01, | |
// oldGravity = force.gravity(), | |
templateNodes = [], | |
offset = [0,0], | |
templateForce, | |
templateNodesSel, | |
groupBy = function (d) { return d.cluster; }, | |
template = "treemap", | |
enableGrouping = true, | |
strength = 0.1; | |
// showingTemplate = false; | |
function force(alpha) { | |
if (!enableGrouping) { | |
return force; | |
} | |
if (template==="force") { | |
//Do the tick of the template force and get the new focis | |
templateForce.tick(); | |
getFocisFromTemplate(); | |
} | |
for (var i = 0, n = nodes.length, node, k = alpha * strength; i < n; ++i) { | |
node = nodes[i]; | |
node.vx += (foci[groupBy(node)].x - node.x) * k; | |
node.vy += (foci[groupBy(node)].y - node.y) * k; | |
} | |
} | |
function initialize() { | |
if (!nodes) return; | |
// var i, | |
// n = nodes.length, | |
// m = links.length, | |
// nodeById = map(nodes, id), | |
// link; | |
if (template==="treemap") { | |
initializeWithTreemap(); | |
} else { | |
initializeWithForce(); | |
} | |
} | |
force.initialize = function(_) { | |
nodes = _; | |
initialize(); | |
}; | |
function getLinkKey(l) { | |
var sourceID = groupBy(l.source), | |
targetID = groupBy(l.target); | |
return sourceID <= targetID ? | |
sourceID + "~" + targetID : | |
targetID + "~" + sourceID; | |
} | |
function computeClustersNodeCounts(nodes) { | |
var clustersCounts = d3.map(); | |
nodes.forEach(function (d) { | |
if (!clustersCounts.has(groupBy(d))) { | |
clustersCounts.set(groupBy(d), 0); | |
} | |
}); | |
nodes.forEach(function (d) { | |
// if (!d.show) { return; } | |
clustersCounts.set(groupBy(d), clustersCounts.get(groupBy(d)) + 1); | |
}); | |
return clustersCounts; | |
} | |
//Returns | |
function computeClustersLinkCounts(links) { | |
var dClusterLinks = d3.map(), | |
clusterLinks = []; | |
links.forEach(function (l) { | |
var key = getLinkKey(l), count; | |
if (dClusterLinks.has(key)) { | |
count = dClusterLinks.get(key); | |
} else { | |
count = 0; | |
} | |
count += 1; | |
dClusterLinks.set(key, count); | |
}); | |
dClusterLinks.entries().forEach(function (d) { | |
var source, target; | |
source = d.key.split("~")[0]; | |
target = d.key.split("~")[1]; | |
clusterLinks.push({ | |
"source":source, | |
"target":target, | |
"count":d.value, | |
}); | |
}); | |
return clusterLinks; | |
} | |
//Returns the metagraph of the clusters | |
function getGroupsGraph() { | |
var gnodes = [], | |
glinks = [], | |
// edges = [], | |
dNodes = d3.map(), | |
// totalSize = 0, | |
clustersList, | |
c, i, size, | |
clustersCounts, | |
clustersLinks; | |
clustersCounts = computeClustersNodeCounts(nodes); | |
clustersLinks = computeClustersLinkCounts(links); | |
//map.keys() is really slow, it's crucial to have it outside the loop | |
clustersList = clustersCounts.keys(); | |
for (i = 0; i< clustersList.length ; i+=1) { | |
c = clustersList[i]; | |
size = clustersCounts.get(c); | |
gnodes.push({id : c, size :size }); | |
dNodes.set(c, i); | |
// totalSize += size; | |
} | |
clustersLinks.forEach(function (l) { | |
glinks.push({ | |
"source":dNodes.get(l.source), | |
"target":dNodes.get(l.target), | |
"count":l.count | |
}); | |
}); | |
return {nodes: gnodes, links: glinks}; | |
} | |
function getGroupsTree() { | |
var children = [], | |
totalSize = 0, | |
clustersList, | |
c, i, size, clustersCounts; | |
clustersCounts = computeClustersNodeCounts(force.nodes()); | |
//map.keys() is really slow, it's crucial to have it outside the loop | |
clustersList = clustersCounts.keys(); | |
for (i = 0; i< clustersList.length ; i+=1) { | |
c = clustersList[i]; | |
size = clustersCounts.get(c); | |
children.push({id : c, size :size }); | |
totalSize += size; | |
} | |
// return {id: "clustersTree", size: totalSize, children : children}; | |
return {id: "clustersTree", children : children}; | |
} | |
function getFocisFromTemplate() { | |
//compute foci | |
foci.none = {x : 0, y : 0}; | |
templateNodes.forEach(function (d) { | |
if (template==="treemap") { | |
foci[d.data.id] = { | |
x : (d.x0 + (d.x1-d.x0) / 2) - offset[0], | |
y : (d.y0 + (d.y1-d.y0) / 2) - offset[1] | |
}; | |
} else { | |
foci[d.id] = {x : d.x - offset[0] , y : d.y - offset[1]}; | |
} | |
}); | |
} | |
function initializeWithTreemap() { | |
var treemap = d3.treemap() | |
.size(force.size()); | |
tree = d3.hierarchy(getGroupsTree()) | |
// .sort(function (p, q) { return d3.ascending(p.size, q.size); }) | |
// .count() | |
.sum(function (d) { return d.size; }) | |
.sort(function(a, b) { | |
return b.height - a.height || b.value - a.value; }) | |
; | |
templateNodes = treemap(tree).leaves(); | |
getFocisFromTemplate(); | |
} | |
function checkLinksAsObjects() { | |
// Check if links come in the format of indexes instead of objects | |
var linkCount = 0; | |
if (nodes.length===0) return; | |
links.forEach(function (link) { | |
var source, target; | |
if (!nodes) return; | |
source = link.source; | |
target = link.target; | |
if (typeof link.source !== "object") source = nodes[link.source]; | |
if (typeof link.target !== "object") target = nodes[link.target]; | |
if (source === undefined || target === undefined) { | |
console.log(link); | |
throw Error("Error setting links, couldn't find nodes for a link (see it on the console)" ); | |
} | |
link.source = source; link.target = target; | |
link.index = linkCount++; | |
}); | |
} | |
function initializeWithForce() { | |
var net; | |
if (nodes && nodes.length>0) { | |
if (groupBy(nodes[0])===undefined) { | |
throw Error("Couldn't find the grouping attribute for the nodes. Make sure to set it up with forceInABox.groupBy('attr') before calling .links()"); | |
} | |
} | |
checkLinksAsObjects(); | |
net = getGroupsGraph(); | |
templateForce = d3.forceSimulation(net.nodes) | |
.force("x", d3.forceX(size[0]/2).strength(0.5)) | |
.force("y", d3.forceY(size[1]/2).strength(0.5)) | |
.force("collide", d3.forceCollide(function (d) { return d.size*nodeSize; })) | |
.force("charge", d3.forceManyBody().strength(function (d) { return forceCharge * d.size; })) | |
.force("links", d3.forceLink(!net.nodes ? net.links :[])) | |
templateNodes = templateForce.nodes(); | |
getFocisFromTemplate(); | |
} | |
function drawTreemap(container) { | |
container.selectAll(".cell").remove(); | |
container.selectAll("cell") | |
.data(templateNodes) | |
.enter().append("svg:rect") | |
.attr("class", "cell") | |
.attr("x", function (d) { return d.x0; }) | |
.attr("y", function (d) { return d.y0; }) | |
.attr("width", function (d) { return d.x1-d.x0; }) | |
.attr("height", function (d) { return d.y1-d.y0; }); | |
} | |
function drawGraph(container) { | |
container.selectAll(".cell").remove(); | |
templateNodesSel = container.selectAll("cell") | |
.data(templateNodes); | |
templateNodesSel | |
.enter().append("svg:circle") | |
.attr("class", "cell") | |
.attr("cx", function (d) { return d.x; }) | |
.attr("cy", function (d) { return d.y; }) | |
.attr("r", function (d) { return d.size*nodeSize; }); | |
} | |
force.drawTemplate = function (container) { | |
// showingTemplate = true; | |
if (template === "treemap") { | |
drawTreemap(container); | |
} else { | |
drawGraph(container); | |
} | |
return force; | |
}; | |
//Backwards compatibility | |
force.drawTreemap = force.drawTemplate; | |
force.deleteTemplate = function (container) { | |
// showingTemplate = false; | |
container.selectAll(".cell").remove(); | |
return force; | |
}; | |
force.template = function (x) { | |
if (!arguments.length) return template; | |
template = x; | |
initialize(); | |
return force; | |
}; | |
force.groupBy = function (x) { | |
if (!arguments.length) return groupBy; | |
if (typeof x === "string") { | |
groupBy = function (d) {return d[x]; }; | |
return force; | |
} | |
groupBy = x; | |
return force; | |
}; | |
force.enableGrouping = function (x) { | |
if (!arguments.length) return enableGrouping; | |
enableGrouping = x; | |
// update(); | |
return force; | |
}; | |
force.strength = function (x) { | |
if (!arguments.length) return strength; | |
strength = x; | |
return force; | |
}; | |
force.getLinkStrength = function (e) { | |
if(enableGrouping) { | |
if (groupBy(e.source) === groupBy(e.target)) { | |
if (typeof(linkStrengthIntraCluster)==="function") { | |
return linkStrengthIntraCluster(e); | |
} else { | |
return linkStrengthIntraCluster; | |
} | |
} else { | |
if (typeof(linkStrengthInterCluster)==="function") { | |
return linkStrengthInterCluster(e); | |
} else { | |
return linkStrengthInterCluster; | |
} | |
} | |
} else { | |
// Not grouping return the intracluster | |
if (typeof(linkStrengthIntraCluster)==="function") { | |
return linkStrengthIntraCluster(e); | |
} else { | |
return linkStrengthIntraCluster; | |
} | |
} | |
}; | |
force.id = function(_) { | |
return arguments.length ? (id = _, force) : id; | |
}; | |
force.size = function(_) { | |
return arguments.length ? (size = _, force) : size; | |
}; | |
force.linkStrengthInterCluster = function(_) { | |
return arguments.length ? (linkStrengthInterCluster = _, force) : linkStrengthInterCluster; | |
}; | |
force.linkStrengthIntraCluster = function(_) { | |
return arguments.length ? (linkStrengthIntraCluster = _, force) : linkStrengthIntraCluster; | |
}; | |
force.nodes = function(_) { | |
return arguments.length ? (nodes = _, force) : nodes; | |
}; | |
force.links = function(_) { | |
if (!arguments.length) | |
return links; | |
if (_ === null) links = []; | |
else links = _; | |
return force; | |
}; | |
force.nodeSize = function(_) { | |
return arguments.length ? (nodeSize = _, force) : nodeSize; | |
}; | |
force.forceCharge = function(_) { | |
return arguments.length ? (forceCharge = _, force) : forceCharge; | |
}; | |
force.offset = function(_) { | |
return arguments.length ? (offset = _, force) : offset; | |
}; | |
return force; | |
} |
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
<html> | |
<head> | |
<style> | |
/* HTML styles */ | |
html{ width: 100%; } | |
body{ | |
width: 100%; | |
margin: 0; padding: 0; | |
display: flex; | |
font-family: sans-serif; font-size: 75%; } | |
.controls { | |
flex-basis: 200px; | |
padding: 0 5px; | |
} | |
.controls .force { | |
background-color:#eee; | |
border-radius: 3px; | |
padding: 5px; | |
margin: 5px 0; | |
} | |
.controls .force p label { margin-right: .5em; font-size: 120%; font-weight: bold;} | |
.controls .force p { margin-top: 0;} | |
.controls .force label { display: inline-block; } | |
.controls input[type="checkbox"] { transform: scale(1.2, 1.2); } | |
.controls input[type="range"] { margin: 0 5% 0.5em 5%; width: 90%; } | |
/* alpha viewer */ | |
.controls .alpha p { margin-bottom: .25em; } | |
.controls .alpha .alpha_bar { height: .5em; border: 1px #777 solid; border-radius: 2px; padding: 1px; display: flex; } | |
.controls .alpha .alpha_bar #alpha_value { background-color: #555; border-radius: 1px; flex-basis: 100% } | |
.controls .alpha .alpha_bar:hover { border-width: 2px; margin:-1px; } | |
.controls .alpha .alpha_bar:active #alpha_value { background-color: #222 } | |
/* SVG styles */ | |
svg { | |
flex-basis: 100%; | |
min-width: 200px; | |
} | |
.links line { | |
stroke: #aaa; | |
} | |
.nodes circle { | |
pointer-events: all; | |
} | |
line { | |
stroke: rgb(212, 212, 212); | |
stroke-width: 1px; | |
shape-rendering: crispEdges; | |
} | |
</style> | |
</head> | |
<body> | |
<div class="controls"> | |
<div class="force alpha"> | |
<p><label>alpha</label> Simulation activity</p> | |
<div class="alpha_bar" onclick="updateAll();"><div id="alpha_value"></div></div> | |
</div> | |
<div class="force"> | |
<p><label>Settings</label> </p> | |
<input id="checkGroupInABox" type="checkbox">Group in a Box</input> | |
<input id="checkshowGrid" type="checkbox">Show Grid</input> | |
<input id="snapGrid" type="checkbox">Snap to Grid</input> | |
<input id="checkShowTreemap" type="checkbox">Show Template</input> | |
<select id="selectTemplate" type="select"> | |
<option value="treemap">Treemap</option> | |
<option value="force">Force</option> | |
</select> | |
</div> | |
<div class="force"> | |
<p><label>Grid Settings</label> </p> | |
<label> | |
R | |
<output id="Resolution_XSliderOutput">25</output> | |
<input type="range" min="0" max="125" value="25" step="0.01" oninput="d3.select('#Resolution_XSliderOutput').text(Number(value)); gridProperties.r=Number(value); updateAll();"> | |
Resolution | |
<output id="Resolution_XSliderOutput">25</output> | |
<input type="range" min="0" max="125" value="25" step="0.01" oninput="d3.select('#Resolution_XSliderOutput').text(Number(value)); gridProperties.resolution=Number(value); updateAll();"> | |
</label> | |
</div> | |
<div class="force"> | |
<p><label>center</label> Shifts the view, so the graph is centered at this location.</p> | |
<label> | |
x | |
<output id="center_XSliderOutput">.5</output> | |
<input type="range" min="0" max="1" value=".5" step="0.01" oninput="d3.select('#center_XSliderOutput').text(Number(value)); forceProperties.center.x=Number(value); updateAll();"> | |
</label> | |
<label> | |
y | |
<output id="center_YSliderOutput">.5</output> | |
<input type="range" min="0" max="1" value=".5" step="0.01" oninput="d3.select('#center_YSliderOutput').text(Number(value)); forceProperties.center.y=Number(value); updateAll();"> | |
</label> | |
</div> | |
<div class="force"> | |
<p><label><input type="checkbox" checked onchange="forceProperties.charge.enabled = this.checked; updateAll();"> charge</label> Attracts (+) or repels (-) nodes to/from each other.</p> | |
<label title="Negative strength repels nodes. Positive strength attracts nodes."> | |
strength | |
<output id="charge_StrengthSliderOutput">-30</output> | |
<input type="range" min="-200" max="50" value="-30" step=".1" oninput="d3.select('#charge_StrengthSliderOutput').text(Number(value)); forceProperties.charge.strength=Number(value); updateAll();"> | |
</label> | |
<label title="Minimum distance where force is applied"> | |
distanceMin | |
<output id="charge_distanceMinSliderOutput">1</output> | |
<input type="range" min="0" max="50" value="1" step=".1" oninput="d3.select('#charge_distanceMinSliderOutput').text(Number(value)); forceProperties.charge.distanceMin=Number(value); updateAll();"> | |
</label> | |
<label title="Maximum distance where force is applied"> | |
distanceMax | |
<output id="charge_distanceMaxSliderOutput">2000</output> | |
<input type="range" min="0" max="2000" value="2000" step=".1" oninput="d3.select('#charge_distanceMaxSliderOutput').text(Number(value)); forceProperties.charge.distanceMax=Number(value); updateAll();"> | |
</label> | |
</div> | |
<div class="force"> | |
<p><label><input type="checkbox" checked onchange="forceProperties.collide.enabled = this.checked; updateAll();"> collide</label> Prevents nodes from overlapping</p> | |
<label> | |
strength | |
<output id="collide_StrengthSliderOutput">.7</output> | |
<input type="range" min="0" max="2" value=".7" step=".1" oninput="d3.select('#collide_StrengthSliderOutput').text(Number(value)); forceProperties.collide.strength=Number(value); updateAll();"> | |
</label> | |
<label title="Size of nodes"> | |
radius | |
<output id="collide_radiusSliderOutput">5</output> | |
<input type="range" min="0" max="100" value="5" step="1" oninput="d3.select('#collide_radiusSliderOutput').text(Number(value)); forceProperties.collide.radius=Number(value); updateAll();"> | |
</label> | |
<label title="Higher values increase rigidity of the nodes (WARNING: high values are computationally expensive)"> | |
iterations | |
<output id="collide_iterationsSliderOutput">1</output> | |
<input type="range" min="1" max="10" value="1" step="1" oninput="d3.select('#collide_iterationsSliderOutput').text(Number(value)); forceProperties.collide.iterations=Number(value); updateAll();"> | |
</label> | |
</div> | |
<div class="force"> | |
<p><label><input type="checkbox" onchange="forceProperties.forceX.enabled = this.checked; updateAll();"> forceX</label> Acts like gravity. Pulls all points towards an X location.</p> | |
<label> | |
strength | |
<output id="forceX_StrengthSliderOutput">.1</output> | |
<input type="range" min="0" max="1" value=".1" step="0.01" oninput="d3.select('#forceX_StrengthSliderOutput').text(Number(value)); forceProperties.forceX.strength=Number(value); updateAll();"> | |
</label> | |
<label title="The X location that the force will push the nodes to (NOTE: This demo multiplies by the svg width)"> | |
x | |
<output id="forceX_XSliderOutput">.5</output> | |
<input type="range" min="0" max="1" value=".5" step="0.01" oninput="d3.select('#forceX_XSliderOutput').text(Number(value)); forceProperties.forceX.x=Number(value); updateAll();"> | |
</label> | |
</div> | |
<div class="force"> | |
<p><label><input type="checkbox" onchange="forceProperties.forceY.enabled = this.checked; updateAll();"> forceY</label> Acts like gravity. Pulls all points towards a Y location.</p> | |
<label> | |
strength | |
<output id="forceY_StrengthSliderOutput">.1</output> | |
<input type="range" min="0" max="1" value=".1" step="0.01" oninput="d3.select('#forceY_StrengthSliderOutput').text(Number(value)); forceProperties.forceY.strength=Number(value); updateAll();"> | |
</label> | |
<label title="The Y location that the force will push the nodes to (NOTE: This demo multiplies by the svg height)"> | |
y | |
<output id="forceY_YSliderOutput">.5</output> | |
<input type="range" min="0" max="1" value=".5" step="0.01" oninput="d3.select('#forceY_YSliderOutput').text(Number(value)); forceProperties.forceY.y=Number(value); updateAll();"> | |
</label> | |
</div> | |
<div class="force"> | |
<p><label><input type="checkbox" checked onchange="forceProperties.link.enabled = this.checked; updateAll();"> link</label> Sets link length</p> | |
<label title="The force will push/pull nodes to make links this long"> | |
distance | |
<output id="link_DistanceSliderOutput">30</output> | |
<input type="range" min="0" max="100" value="30" step="1" oninput="d3.select('#link_DistanceSliderOutput').text(Number(value)); forceProperties.link.distance=Number(value); updateAll();"> | |
</label> | |
<label title="Higher values increase rigidity of the links (WARNING: high values are computationally expensive)"> | |
iterations | |
<output id="link_IterationsSliderOutput">1</output> | |
<input type="range" min="1" max="10" value="1" step="1" oninput="d3.select('#link_IterationsSliderOutput').text(Number(value)); forceProperties.link.iterations=Number(value); updateAll();"> | |
</label> | |
</div> | |
</div> | |
<svg></svg> | |
<script src="//d3js.org/d3.v4.js"></script> | |
<script type="text/javascript" src="./forceInABox.js"></script> | |
<script> | |
var link, node; | |
// the data - an object with nodes and links | |
var graph; | |
var svgm = d3.select("svg"), | |
width = +svgm.node().getBoundingClientRect().width, | |
height = +svgm.node().getBoundingClientRect().height; | |
var svgbg = svgm.append("g"); | |
var svg = svgm.append("g"); | |
var useGroupInABox = true, | |
drawTemplate = true, | |
drawGrid = false, | |
snapGrid = false, | |
template = "force"; | |
d3.select("#checkGroupInABox").property("checked", useGroupInABox); | |
d3.select("#checkshowGrid").property("checked", drawGrid); | |
d3.select("#snapGrid").property("checked", snapGrid); | |
d3.select("#checkShowTreemap").property("checked", drawTemplate); | |
d3.select("#selectTemplate").property("value", template); | |
var force = d3.forceSimulation() | |
.force("charge", d3.forceManyBody()) | |
.force("collide",d3.forceCollide( function(d){return d.r + 8 }).iterations(16) ) | |
.force("x", d3.forceX(width/2).strength(0.05)) | |
.force("y", d3.forceY(height/2).strength(0.05)); | |
var groupingForce = forceInABox(); | |
// set up the simulation and event to update locations after each tick | |
function initializeSimulation() { | |
groupingForce | |
.strength(0.1) // Strength to foci | |
.template(template) // Either treemap or force | |
.groupBy("group") // Node attribute to group | |
.links(graph.links) // The graph links. Must be called after setting the grouping attribute | |
.enableGrouping(useGroupInABox) | |
.nodeSize(5) // How big are the nodes to compute the force template | |
.forceCharge(-20) // Separation between nodes on the force template | |
.size([width, height]); // Size of the chart | |
force | |
.nodes(graph.nodes) | |
.force("group", groupingForce) | |
.force("link", d3.forceLink(graph.links) | |
// .distance(5) | |
.strength(groupingForce.getLinkStrength) | |
); | |
//simulation.nodes(graph.nodes); | |
initializeForces(); | |
force.on("tick", ticked); | |
} | |
gridProperties = { | |
r:1, | |
resolution : 25 | |
} | |
// values for all forces | |
forceProperties = { | |
center: { | |
x: 0.5, | |
y: 0.5 | |
}, | |
charge: { | |
enabled: true, | |
strength: -30, | |
distanceMin: 1, | |
distanceMax: 2000 | |
}, | |
collide: { | |
enabled: true, | |
strength: .7, | |
iterations: 1, | |
radius: 5 | |
}, | |
forceX: { | |
enabled: false, | |
strength: .1, | |
x: .5 | |
}, | |
forceY: { | |
enabled: false, | |
strength: .1, | |
y: .5 | |
}, | |
link: { | |
enabled: true, | |
distance: 30, | |
iterations: 1 | |
} | |
} | |
function round(p, n) { | |
return p % n < n / 2 ? p - (p % n) : p + n - (p % n); | |
} | |
// add forces to the simulation | |
function initializeForces() { | |
// add forces and associate each with a name | |
force | |
.force("link", d3.forceLink()) | |
.force("charge", d3.forceManyBody()) | |
.force("collide", d3.forceCollide()) | |
.force("center", d3.forceCenter()) | |
.force("forceX", d3.forceX()) | |
.force("forceY", d3.forceY()); | |
// apply properties to each of the forces | |
updateForces(); | |
} | |
// apply new force properties | |
function updateForces() { | |
// get each force by name and update the properties | |
force.force("center") | |
.x(width * forceProperties.center.x) | |
.y(height * forceProperties.center.y); | |
force.force("charge") | |
.strength(forceProperties.charge.strength * forceProperties.charge.enabled) | |
.distanceMin(forceProperties.charge.distanceMin) | |
.distanceMax(forceProperties.charge.distanceMax); | |
force.force("collide") | |
.strength(forceProperties.collide.strength * forceProperties.collide.enabled) | |
.radius(forceProperties.collide.radius) | |
.iterations(forceProperties.collide.iterations); | |
force.force("forceX") | |
.strength(forceProperties.forceX.strength * forceProperties.forceX.enabled) | |
.x(width * forceProperties.forceX.x); | |
force.force("forceY") | |
.strength(forceProperties.forceY.strength * forceProperties.forceY.enabled) | |
.y(height * forceProperties.forceY.y); | |
force.force("link") | |
.id(function(d) {return d.id;}) | |
.distance(forceProperties.link.distance) | |
.iterations(forceProperties.link.iterations) | |
.links(forceProperties.link.enabled ? graph.links : []); | |
// updates ignored until this is run | |
// restarts the simulation (important if simulation has already slowed down) | |
force.alpha(1).restart(); | |
} | |
//////////// DISPLAY //////////// | |
// generate the svg objects and force simulation | |
function initializeDisplay() { | |
var color = d3.scaleOrdinal(d3.schemeCategory20); | |
/*var svgm = d3.select("body").append("svg") | |
.attr("width", width) | |
.attr("height", height);*/ | |
if(drawGrid) { | |
svgbg.selectAll('.vertical') | |
.data(d3.range(1, width / gridProperties.resolution)) | |
.enter().append('line') | |
.attr('class', 'vertical') | |
.attr('x1', function (d) { | |
return d * gridProperties.resolution; | |
}) | |
.attr('y1', 0) | |
.attr('x2', function (d) { | |
return d * gridProperties.resolution; | |
}) | |
.attr('y2', height); | |
svgbg.selectAll('.horizontal') | |
.data(d3.range(1, height / gridProperties.resolution)) | |
.enter().append('line') | |
.attr('class', 'horizontal') | |
.attr('x1', 0) | |
.attr('y1', function (d) { | |
return d * gridProperties.resolution; | |
}) | |
.attr('x2', width) | |
.attr('y2', function (d) { | |
return d * gridProperties.resolution; | |
}); | |
} | |
// set the data and properties of link lines | |
link = svg.append("g") | |
.attr("class", "links") | |
.selectAll("line") | |
.data(graph.links) | |
.enter().append("line"); | |
// set the data and properties of node circles | |
node = svg.append("g") | |
.attr("class", "nodes") | |
.selectAll("circle") | |
.data(graph.nodes) | |
.enter().append("circle") | |
.style("fill", function(d) { return color(d.group); }).attr("class", "node") | |
.call(d3.drag() | |
.on("start", dragstarted) | |
.on("drag", dragged) | |
.on("end", dragended)); | |
// node tooltip | |
node.append("svg:text") | |
.text(function(d) { return d.name; }); | |
// visualize the graph | |
updateDisplay(); | |
} | |
// update the display based on the forces (but not positions) | |
function updateDisplay() { | |
svgbg.selectAll('.vertical').remove(); | |
svgbg.selectAll('.horizontal').remove(); | |
if(drawGrid) { | |
svgbg.selectAll('.vertical') | |
.data(d3.range(1, width / gridProperties.resolution)) | |
.enter().append('line') | |
.attr('class', 'vertical') | |
.attr('x1', function (d) { | |
return d * gridProperties.resolution; | |
}) | |
.attr('y1', 0) | |
.attr('x2', function (d) { | |
return d * gridProperties.resolution; | |
}) | |
.attr('y2', height); | |
svgbg.selectAll('.horizontal') | |
.data(d3.range(1, height / gridProperties.resolution)) | |
.enter().append('line') | |
.attr('class', 'horizontal') | |
.attr('x1', 0) | |
.attr('y1', function (d) { | |
return d * gridProperties.resolution; | |
}) | |
.attr('x2', width) | |
.attr('y2', function (d) { | |
return d * gridProperties.resolution; | |
}); | |
} else { | |
} | |
node | |
.attr("r", forceProperties.collide.radius) | |
// .attr("stroke", forceProperties.charge.strength > 0 ? "blue" : "red") | |
.attr("stroke-width", forceProperties.charge.enabled==false ? 0 : Math.abs(forceProperties.charge.strength)/15); | |
link | |
.attr("stroke-width", forceProperties.link.enabled ? 1 : .5) | |
.attr("opacity", forceProperties.link.enabled ? 1 : 0); | |
} | |
// update the display positions after each simulation tick | |
function ticked() { | |
/*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; });*/ | |
if (snapGrid) { | |
node.attr("cx", function (d) { | |
return d.x = round(Math.max(gridProperties.r, Math.min(width - gridProperties.r, d.x)), gridProperties.resolution); | |
}) | |
.attr("cy", function (d) { | |
return d.y = round(Math.max(gridProperties.r, Math.min(width - gridProperties.r, d.y)), gridProperties.resolution); | |
}); | |
d3.select("#alpha_value").style("flex-basis", (force.alpha()*100) + "%"); | |
} else { | |
node.attr("cx", function (d) { | |
return d.x | |
}) | |
.attr("cy", function (d) { | |
return d.y | |
}); | |
d3.select("#alpha_value").style("flex-basis", (force.alpha()*100) + "%"); | |
} | |
} | |
// update size-related forces | |
d3.select(window).on("resize", function(){ | |
width = +svg.node().getBoundingClientRect().width; | |
height = +svg.node().getBoundingClientRect().height; | |
updateForces(); | |
}); | |
// convenience function to update everything (run after UI input) | |
function updateAll() { | |
updateForces(); | |
updateDisplay(); | |
} | |
function dragstarted(d) { | |
d3.select(this).raise().classed("active", true); | |
} | |
function dragged(d) { | |
d3.select(this).select("text") | |
.attr("x", d.x = d3.event.x) | |
.attr("y", d.y = d3.event.y); | |
d3.select(this).select("rect") | |
.attr("x", d.x = d3.event.x) | |
.attr("y", d.y = d3.event.y); | |
} | |
function dragended(d) { | |
d3.select(this).classed("active", false); | |
} | |
(function() { | |
// your page initialization code here | |
// the DOM will be available here | |
d3.json("data.json", function(error, _graph) { | |
if (error) throw error; | |
graph = _graph; | |
initializeDisplay(); | |
initializeSimulation(); | |
}); | |
d3.select("#checkGroupInABox").on("change", function () { | |
force.stop(); | |
useGroupInABox = d3.select("#checkGroupInABox").property("checked"); | |
force | |
.force("group").enableGrouping(useGroupInABox) | |
force.alphaTarget(0.5).restart(); | |
}); | |
d3.select("#snapGrid").on("change", function () { | |
force.stop(); | |
snapGrid = d3.select("#snapGrid").property("checked"); | |
force | |
// .force("link", d3.forceLink(graph.links).distance(50).strength( | |
// function (l) { return !useGroupInABox? 0.7 : | |
// l.source.group!==l.target.group ? 0 : 0.1; | |
// })) | |
.force("group").enableGrouping(useGroupInABox) | |
force.alphaTarget(0.5).restart(); | |
}); | |
d3.select("#checkshowGrid").on("change", function () { | |
drawGrid = d3.select("#checkshowGrid").property("checked"); | |
if(drawGrid) { | |
svgbg.selectAll('.vertical') | |
.data(d3.range(1, width / gridProperties.resolution)) | |
.enter().append('line') | |
.attr('class', 'vertical') | |
.attr('x1', function (d) { | |
return d * gridProperties.resolution; | |
}) | |
.attr('y1', 0) | |
.attr('x2', function (d) { | |
return d * gridProperties.resolution; | |
}) | |
.attr('y2', height); | |
svgbg.selectAll('.horizontal') | |
.data(d3.range(1, height / gridProperties.resolution)) | |
.enter().append('line') | |
.attr('class', 'horizontal') | |
.attr('x1', 0) | |
.attr('y1', function (d) { | |
return d * gridProperties.resolution; | |
}) | |
.attr('x2', width) | |
.attr('y2', function (d) { | |
return d * gridProperties.resolution; | |
}); | |
} | |
else { | |
svgbg.selectAll('.vertical').remove() | |
svgbg.selectAll('.horizontal').remove() | |
} | |
}); | |
d3.select("#selectTemplate").on("change", function () { | |
template = d3.select("#selectTemplate").property("value"); | |
force.stop(); | |
force.force("group").template(template); | |
force.alphaTarget(0.5).restart(); | |
}); | |
d3.select("#checkShowTreemap").on("change", function () { | |
drawTemplate = d3.select("#checkShowTreemap").property("checked"); | |
if (drawTemplate) { | |
force.force("group").drawTemplate(svg); | |
} else { | |
force.force("group").deleteTemplate(svg); | |
} | |
}); | |
})(); | |
</script> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment