Last active
March 15, 2018 08:55
-
-
Save sineline/c424542996d6b65a4a9d07606c2b844d to your computer and use it in GitHub Desktop.
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": "Sustainable Development", | |
"group": "Motivated by", | |
"paths": ["Responding to change vs following the plan"] | |
}, | |
{ | |
"name": "Keep it simple", | |
"group": "Motivated by", | |
"paths": ["Less documentation, more action”,“Responding to change vs following the plan"] | |
}, | |
{ | |
"name": "Face to face conversations", | |
"group": "Motivated by", | |
"paths": ["Individual interactions over processes and tools"] | |
}, | |
{ | |
"name": "Work together", | |
"group": "Motivated by", | |
"paths": ["Individual interactions over processes and tools”,“Customer collab. Vs Contract negotiation"] | |
}, | |
{ | |
"name": "Reflect and adjust", | |
"group": "Motivated by", | |
"paths": ["Less documentation, more action"] | |
}, | |
{ | |
"name": "self/organizing teams", | |
"group": "Motivated by", | |
"paths": ["Individual interactions over processes and tools"] | |
}, | |
{ | |
"name": "consumer satisfaction", | |
"group": "Motivated by", | |
"paths": ["Customer collab. Vs Contract negotiation"] | |
}, | |
{ | |
"name": "Digital transformation", | |
"group": "Motivated by", | |
"paths": ["Less documentation, more action”,“Responding to change vs following the plan”,“We love"] | |
}, | |
{ | |
"name": "Challenge status quo", | |
"group": "Motivated by", | |
"paths": ["Responding to change vs following the plan”,“We love"] | |
}, | |
{ | |
"name": "Delivering the best in class digital technological solutions", | |
"group": "Motivated by", | |
"paths": ["Less documentation, more action”,“Responding to change vs following the plan"] | |
}, | |
{ | |
"name": "Innovating ", | |
"group": "Motivated by", | |
"paths": ["Responding to change vs following the plan”,“We love"] | |
}, | |
{ | |
"name": "Fail fast and learn from mistakes", | |
"group": "Motivated by", | |
"paths": ["Less documentation, more action"] | |
}, | |
{ | |
"name": "Better work in future for nestle", | |
"group": "Motivated by", | |
"paths": ["We love"] | |
}, | |
{ | |
"name": "Respond to change", | |
"group": "Motivated by", | |
"paths": ["Responding to change vs following the plan"] | |
}, | |
{ | |
"name": "diverse", | |
"group": "We are", | |
"paths": ["Individual interactions over processes and tools"] | |
}, | |
{ | |
"name": "Learn from colleagues", | |
"group": "We are", | |
"paths": ["Individual interactions over processes and tools"] | |
}, | |
{ | |
"name": "Openminded", | |
"group": "We are", | |
"paths": ["Individual interactions over processes and tools”,“Less documentation, more action”,“Responding to change vs following the plan"] | |
}, | |
{ | |
"name": "curious", | |
"group": "We are", | |
"paths": ["Individual interactions over processes and tools"] | |
}, | |
{ | |
"name": "Knowledge hungry", | |
"group": "We are", | |
"paths": ["Individual interactions over processes and tools”,“Responding to change vs following the plan"] | |
}, | |
{ | |
"name": "Multicultural", | |
"group": "We are", | |
"paths": ["Individual interactions over processes and tools”,“We love"] | |
}, | |
{ | |
"name": "Passionate", | |
"group": "We are", | |
"paths": ["Individual interactions over processes and tools”,“Responding to change vs following the plan"] | |
}, | |
{ | |
"name": "Trustworthy", | |
"group": "We are", | |
"paths": ["Individual interactions over processes and tools”,“Customer collab. Vs Contract negotiation”,“Responding to change vs following the plan"] | |
}, | |
{ | |
"name": "Supportive", | |
"group": "We are", | |
"paths": ["Individual interactions over processes and tools”,“Less documentation, more action”,“Customer collab. Vs Contract negotiation”,“Responding to change vs following the plan"] | |
}, | |
{ | |
"name": "Standing for our values", | |
"group": "We believe", | |
"paths": ["We love"] | |
}, | |
{ | |
"name": "Everyone has a voice", | |
"group": "We believe", | |
"paths": ["Individual interactions over processes and tools"] | |
}, | |
{ | |
"name": "Best version of ourselves", | |
"group": "We believe", | |
"paths": ["Individual interactions over processes and tools”,“Responding to change vs following the plan"] | |
}, | |
{ | |
"name": "Make a difference", | |
"group": "We believe", | |
"paths": ["Responding to change vs following the plan"] | |
}, | |
{ | |
"name": "Technology", | |
"group": "We love", | |
"paths": ["Less documentation, more action”,“Customer collab. Vs Contract negotiation”,“We love"] | |
}, | |
{ | |
"name": "PingPong", | |
"group": "We love", | |
"paths": ["We love"] | |
}, | |
{ | |
"name": "Loving chocolate and coffee", | |
"group": "We love", | |
"paths": ["We love"] | |
} | |
], | |
"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 */ | |
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; | |
} | |
body { | |
margin:0; | |
position:fixed; | |
top:0; | |
right:0; | |
bottom:0; | |
left:0; | |
} | |
.links line { | |
stroke: #aaa; | |
} | |
.nodes circle { | |
pointer-events: all; | |
} | |
line { | |
stroke: rgb(212, 212, 212); | |
stroke-width: 1px; | |
shape-rendering: crispEdges; | |
} | |
path { | |
fill-opacity: .1; | |
stroke-opacity: 1; | |
} | |
</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>Groups bubbles</label> </p> | |
<div id="scaleFactorSettings"> | |
<input id="checkGroupBubbles" type="checkbox">Show grouping bubbles</input> | |
<p>Scale of the groups: <span id='scaleFactorLabel'>1.2</span></p> | |
<input type="range" min="1" max="3" value="1.2" step=".1" | |
oninput="scaleFactor = value; d3.select('#scaleFactorLabel').text(scaleFactor); updateGroups()"> | |
</div> | |
<div id="curveSettings"> | |
<p>Type of curve: <span id='curveLabel'>curveCatmullRomClosed</span></p> | |
</div> | |
</div> | |
<div class="force"> | |
<p><label>Voronoi</label> </p> | |
<div id="scaleFactorSettings"> | |
<input id="checkVoronoi" type="checkbox">Show Voronoi</input> | |
</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 svgbg2 = svgm.append("g"); | |
var svgbg3 = svgm.append("g"); | |
var svg = svgm.append("g"); | |
var valueline = d3.line() | |
.x(function(d) { return d[0]; }) | |
.y(function(d) { return d[1]; }) | |
.curve(d3.curveCatmullRomClosed), | |
paths, | |
text, | |
groups, | |
groupIds, | |
scaleFactor = 1.2, | |
polygon, | |
centroid, | |
node, | |
link, | |
curveTypes = ['curveBasisClosed', 'curveCardinalClosed', 'curveCatmullRomClosed', 'curveLinearClosed']; | |
var useGroupInABox = true, | |
drawTemplate = true, | |
checkGroupBubbles = true, | |
voronoi = true, | |
drawGrid = false, | |
snapGrid = false, | |
template = "force"; | |
d3.select("#checkGroupInABox").property("checked", useGroupInABox); | |
d3.select("#checkshowGrid").property("checked", drawGrid); | |
d3.select("#checkVoronoi").property("checked", voronoi); | |
d3.select("#checkGroupBubbles").property("checked", checkGroupBubbles); | |
d3.select("#snapGrid").property("checked", snapGrid); | |
d3.select("#checkShowTreemap").property("checked", drawTemplate); | |
d3.select("#selectTemplate").property("value", template); | |
var voronoi ; | |
var polygons; | |
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(); | |
var options; | |
var color = d3.scaleOrdinal(d3.schemeCategory20); | |
var simulation = d3.forceSimulation() | |
.force("link", d3.forceLink().id(function(d) { return d.id; })) | |
.force("charge", d3.forceManyBody()) | |
.force("center", d3.forceCenter(width / 2, height / 2)); | |
// 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) | |
.on("tick", ticked); | |
simulation.force("link", d3.forceLink(graph.links)); | |
paths | |
.transition() | |
.duration(2000) | |
.attr('opacity', 1); | |
// add interaction to the groups | |
groups.selectAll('.path_placeholder') | |
.call(d3.drag() | |
.on('start', group_dragstarted) | |
.on('drag', group_dragged) | |
.on('end', group_dragended) | |
); | |
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() { | |
voronoi = d3.voronoi().extent([[0,0], [width, height]]); | |
// create selector for curve types | |
var select = d3.select('#curveSettings') | |
.append('select') | |
.attr('class','select') | |
.on('change', function() { | |
var val = d3.select('select').property('value'); | |
d3.select('#curveLabel').text(val); | |
valueline.curve(d3[val]); | |
updateGroups(); | |
}); | |
options = select | |
.selectAll('option') | |
.data(curveTypes).enter() | |
.append('option') | |
.text(function (d) { return d; }); | |
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; | |
}); | |
} | |
groups = svgbg2.append('g').attr('class', 'groups'); | |
groupIds = d3.set(graph.nodes.map(function(n) { | |
return n.group; | |
})) | |
.values() | |
.map( function(groupId) { | |
return { | |
groupId : groupId, | |
count : graph.nodes.filter(function(n) { return n.group == groupId; }).length | |
}; | |
}) | |
.filter( function(group) { return group.count > 2;}) | |
.map( function(group) { return group.groupId; }); | |
paths = groups.selectAll('.path_placeholder') | |
.data(groupIds, function(d) { return +d; }) | |
.enter() | |
.append('g') | |
.attr('class', 'path_placeholder') | |
.append('path') | |
.attr('stroke', function(d) { return color(d); }) | |
.attr('fill', function(d) { return color(d); }) | |
.attr('opacity', 0); | |
// 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)); | |
polygons = svgbg3.append("g") | |
.attr("class", "polygons") | |
.selectAll("polygon") | |
.data(graph.nodes) | |
.enter().append("polygon") | |
.style("fill", function(d) { return color(d.group); }) | |
.style("fill-opacity", .2) | |
.style("stroke", "black") | |
.call(d3.drag() | |
.on("start", dragstarted) | |
.on("drag", dragged) | |
.on("end", dragended)); | |
/*simulation.force("link") | |
.links(graph.links);*/ | |
// 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; | |
}); | |
} | |
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 (graph.nodes[0].x) { | |
var polygonShapes = voronoi(graph.nodes.map(function(d) {return [d.x, d.y]})).polygons() | |
polygons.attr("points", function(d, i) {return polygonShapes[i]}) | |
} | |
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); | |
}); | |
text.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 | |
}); | |
text.attr("cx", function (d) { | |
return d.x | |
}) | |
.attr("cy", function (d) { | |
return d.y | |
}); | |
d3.select("#alpha_value").style("flex-basis", (force.alpha()*100) + "%"); | |
} | |
updateGroups(); | |
} | |
// 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(); | |
updateGroups(); | |
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); | |
d.fx = d3.event.x; | |
d.fy = d3.event.y; | |
} | |
function dragended(d) { | |
d3.select(this).classed("active", false); | |
} | |
// select nodes of the group, retrieve its positions | |
// and return the convex hull of the specified points | |
// (3 points as minimum, otherwise returns null) | |
var polygonGenerator = function(groupId) { | |
var node_coords = node | |
.filter(function(d) { return d.group == groupId; }) | |
.data() | |
.map(function(d) { return [d.x, d.y]; }); | |
return d3.polygonHull(node_coords); | |
}; | |
function updateGroups() { | |
groupIds.forEach(function(groupId) { | |
var path = paths.filter(function(d) { return d == groupId;}) | |
.attr('transform', 'scale(1) translate(0,0)') | |
.attr('d', function(d) { | |
polygon = polygonGenerator(d); | |
centroid = d3.polygonCentroid(polygon); | |
// to scale the shape properly around its points: | |
// move the 'g' element to the centroid point, translate | |
// all the path around the center of the 'g' and then | |
// we can scale the 'g' element properly | |
return valueline( | |
polygon.map(function(point) { | |
return [ point[0] - centroid[0], point[1] - centroid[1] ]; | |
}) | |
); | |
}); | |
d3.select(path.node().parentNode).attr('transform', 'translate(' + centroid[0] + ',' + (centroid[1]) + ') scale(' + scaleFactor + ')'); | |
}); | |
} | |
// drag groups | |
function group_dragstarted(groupId) { | |
if (!d3.event.active) force.alphaTarget(0.3).restart(); | |
d3.select(this).select('path').style('stroke-width', 3); | |
} | |
function group_dragged(groupId) { | |
node | |
.filter(function(d) { return d.group == groupId; }) | |
.each(function(d) { | |
d.x += d3.event.dx; | |
d.y += d3.event.dy; | |
}) | |
} | |
function group_dragended(groupId) { | |
if (!d3.event.active) force.alphaTarget(0.3).restart(); | |
d3.select(this).select('path').style('stroke-width', 1); | |
} | |
(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("group").enableGrouping(useGroupInABox) | |
force.alphaTarget(0.5).restart(); | |
}); | |
d3.select("#checkGroupBubbles").on("change", function () { | |
force.stop(); | |
checkGroupBubbles = d3.select("#snapGrid").property("checked"); | |
d3.select("#checkGroupBubbles").property("checked") ? svgbg2.style("display","") : svgbg2.style("display","none"); | |
updateAll(); | |
}); | |
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