Skip to content

Instantly share code, notes, and snippets.

@dianaow
Last active June 11, 2019 13:31
Show Gist options
  • Save dianaow/8b41a8fb5d744ed1401043ac663bd87f to your computer and use it in GitHub Desktop.
Save dianaow/8b41a8fb5d744ed1401043ac663bd87f to your computer and use it in GitHub Desktop.
D3 Forces Playground
license: mit

I wanted to have some practice with d3 force and got obsessed and too carried away...

For better viewing on a wider screen, open in a new tab!

Built with blockbuilder.org

var distribute = function () {
///////////////////////////////////////////////////////////////////////////
///////////////////////////////// Globals /////////////////////////////////
///////////////////////////////////////////////////////////////////////////
var simulation, circles, g, DURATION, DELAY, nodes, distributed, scattered
var canvasDim = { width: screen.width*0.9, height: screen.height*0.9}
var margin = {top: 50, right: 50, bottom: 50, left: 50}
var width = canvasDim.width - margin.left - margin.right;
var height = canvasDim.height - margin.top - margin.bottom;
var simulation = d3.forceSimulation()
var modal = d3.select(".modal-content2")
var modalDiv = modal.append('div')
.attr('id', "div-content2")
.attr('width', width)
.attr('height', height)
.attr('float', 'left')
///////////////////////////////////////////////////////////////////////////
///////////////////////////// Create scales ///////////////////////////////
///////////////////////////////////////////////////////////////////////////
var color = ["#E47D06", "#DB0131", "#AF0158", "#7F378D", "#3465A8", "#0AA174", "#7EB852"]
var category = ["1", "2", "3", "4", "5", "6", "7"]
var colorScale = d3.scaleOrdinal()
.domain(category)
.range(color)
///////////////////////////////////////////////////////////////////////////
///////////////////////////// Create sliders //////////////////////////////
///////////////////////////////////////////////////////////////////////////
// Step slide from 1000 to 2000 for transition duration in steps of 100
var sliderStepDuration = d3.sliderBottom()
.min(1000)
.max(2000)
.width(260)
.tickFormat(d3.format(''))
.ticks(4)
.step(500)
.default(2000)
.on('onchange', val => {
d3.select('p#value-step-duration').text(d3.format('.2%')(val))
});
// Step slide from 5 to 50 for transition duration in steps of 5
var sliderStepDelay = d3.sliderBottom()
.min(5)
.max(50)
.width(260)
.tickFormat(d3.format(''))
.ticks(10)
.step(5)
.default(10)
.on('onchange', val => {
d3.select('p#value-step-delay').text(d3.format('.2%')(val));
});
var gStep1 = d3.select('div#slider-step1')
.append('svg')
.attr('width', 300)
.attr('height', 90)
.append('g')
.attr('transform', 'translate(20, 20)')
var gStep2 = d3.select('div#slider-step2')
.append('svg')
.attr('width', 300)
.attr('height', 90)
.append('g')
.attr('transform', 'translate(20, 20)')
gStep1.call(sliderStepDuration)
gStep2.call(sliderStepDelay)
///////////////////////////////////////////////////////////////////////////
/////////////////////////////////// CORE //////////////////////////////////
///////////////////////////////////////////////////////////////////////////
return {
clear : function () {
modal.select("svg").remove()
},
run : function () {
//////////////////// Set up and initiate containers ///////////////////////
var svg = modalDiv.append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
g = svg.append("g")
.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')')
if(screen.width > 420){
svg.append('rect')
.attr("width", width*0.8)
.attr("height", height*0.95)
.attr('fill', 'none')
.style('stroke-width', '2px')
.style('rx', '6px')
.style('stroke', 'black')
.attr('x', width/4)
.attr('y', 10)
}
///////////////// Run animation sequence based on chosen parameters ///////////////////
initAllData()
getParameters()
run_scatter()
function initAllData() {
nodes = getData()
distributed = distributedData(nodes) // run this function first to get IDs of nodes
scattered = scatteredData(distributed) // modify x-y coordinates only
}
function getParameters() {
DURATION = sliderStepDuration.value() // get current slider value
DELAY = sliderStepDelay.value()
d3.select('p#value-step-duration').text(d3.format('')(DURATION));
d3.select('p#value-step-delay').text(d3.format('')(DELAY));
TRANSITION_TYPE = d3.select("input[name='transition_type']:checked").property('value');
d3.select('.run_cluster_sorted').on("click", function () { run_cluster_sorted() });
d3.select('.run_cluster_bubbles').on("click", function () { run_cluster_bubbles() });
d3.select('.reset_button').on("click", function () { reset() });
d3.select('.submit_button').on("click", function () { to_distributed() });
//console.log(TRANSITION_TYPE, DURATION, DELAY)
}
function run_scatter() {
update(scattered, 'transition_by_default', DURATION, '') // transition nodes to new positions
scatter(scattered) // run force simulation to prevent collision of nodes
distributed.forEach((d,i) => {
d.length = getPathLength(scattered, d)
})
}
function run_cluster_sorted() {
cluster_sorted(scattered) // modify x-y coordinates
distributed.forEach((d,i) => {
d.length = getPathLength(scattered, d) // recalculate shortest distance between clustered and distributed layout for each node
})
}
function run_cluster_bubbles() {
cluster_bubbles(scattered) // modify x-y coordinates
distributed.forEach((d,i) => {
d.length = getPathLength(scattered, d)
})
}
function to_distributed() {
getParameters()
simulation.stop()
update(distributed, TRANSITION_TYPE, DURATION, DELAY)
}
function reset() {
simulation.stop()
initAllData()
run_scatter()
}
}
}
///////////////////////////////////////////////////////////////////////////
///////////////////////// Find length of trajectory ///////////////////////
///////////////////////////////////////////////////////////////////////////
function getPathLength(other, d) {
var d2 = other.find(b=>b.id==d.id)
var len = Math.sqrt((Math.pow(d.x-d2.x,2))+(Math.pow(d.y-d2.y,2)))
return len
}
///////////////////////////////////////////////////////////////////////////
//////////////////////////// Data Processing //////////////////////////////
///////////////////////////////////////////////////////////////////////////
function getData() {
var nodes = []
d3.range(1,201).map((d,i) => {
var rand = Math.round(randn_bm(1, 8, 1))
nodes.push({
'band': rand
})
})
return nodes
}
///////////////////////////////////////////////////////////////////////////
//////////////////////////////// Scatter plot /////////////////////////////
///////////////////////////////////////////////////////////////////////////
function scatteredData(data) {
var xScale = d3.scaleLinear()
.range([width/4, width])
var yScale = d3.scaleLinear()
.range([0, height*(1/2)])
// modify x-y coordinates of nodes to form a scattered distribution
var nodes = data.map(function(d, i) {
return {
id: d.id,
x: xScale(+Math.random()),
y: yScale(+Math.random()),
color: d.color,
band: d.band,
radius: d.radius
}
})
let xMax = d3.max(nodes, d=> +d.x) * 1.1
let xMin = d3.min(nodes, d=> + d.x) - xMax/15
let yMax = d3.max(nodes, d=> +d.y) * 1.1
let yMin = d3.min(nodes, d=> + d.y) - yMax/15
xScale.domain([xMin, xMax])
yScale.domain([yMin, yMax])
return nodes
}
function scatter(data) {
var buffer = screen.width < 420 ? 0.5 : 3
simulation = simulation.nodes(data, d=>d.id)
.force('charge', d3.forceManyBody().strength(-20))
.force("collide", d3.forceCollide(function(d,i) { return d.radius + buffer}))
.force("cluster", forceCluster())
.force("x", d3.forceX(function (d) { return d.x }))
.force("y", d3.forceY(function (d) { return d.y }))
simulation.force("cluster").strength(0)
simulation.on('tick', ticked);
simulation.alpha(0.02).restart()
function ticked() {
circles
.attr('cx', (d) => d.x)
.attr('cy', (d) => d.y)
}
}
///////////////////////////////////////////////////////////////////////////
//////////////////////////////// Cluster plot /////////////////////////////
///////////////////////////////////////////////////////////////////////////
// 1) Nodes are clustered in the middle, but not sorted
function cluster_default() {
simulation.stop();
simulation
.force('x', d3.forceX(width/2))
.force('y', d3.forceY(height/2))
simulation.alpha(0.5).restart()
}
// 2) Nodes are clustered in the middle, and sorted horizontally
function cluster_sorted(nodes) {
var xScaleCluster = d3.scaleBand()
.range([width*(1/4), width*(3/4)])
.domain(category)
simulation.stop();
nodes.forEach(d=>{
d.x = xScaleCluster(d.band)
})
var responsiveCharge = screen.width < 420 ? -5 : -20 // modify the force charge based on screen size
simulation.nodes(nodes)
.force('charge', d3.forceManyBody().strength(responsiveCharge))
.force("cluster", forceCluster())
//.force('center', d3.forceCenter(width/2, height/2))
//.force('x', d3.forceX(function (d) { return xScaleCluster(d.band) }).strength(0.3))
.force('x', d3.forceX((width/4) + (width*(3/4))/2))
.force('y', d3.forceY(height/2-100))
simulation.force("cluster").strength(0)
simulation.velocityDecay(0.1).alpha(0.5).restart()
}
// 3) Nodes are clustered in the middle, but clusters pulled separately from other clusters based on their centroid
//https://bl.ocks.org/mbostock/7881887
function cluster_bubbles() {
simulation.stop();
var responsiveCharge = screen.width < 420 ? -10 : -45
simulation
.force('charge', d3.forceManyBody().strength(responsiveCharge))
.force("cluster", forceCluster())
.force('x', d3.forceX((width/4) + (width*(3/4))/2))
.force('y', d3.forceY(height/2-100))
simulation.force("cluster").strength(0.8)
simulation.velocityDecay(0.5).alpha(0.5).restart()
}
function forceCluster() {
var strength = 0.8;
let nodes;
function force(alpha) {
const centroids = d3.rollup(nodes, centroid, d => d.band);
const l = alpha * strength;
for (const d of nodes) {
const {x: cx, y: cy} = centroids.get(d.band);
d.vx -= (d.x - cx) * l;
d.vy -= (d.y - cy) * l;
}
}
force.initialize = _ => nodes = _;
force.strength = function(_) {
return arguments.length ? (strength = +_, force) : strength;
};
return force;
}
function centroid(nodes) {
let x = 0;
let y = 0;
let z = 0;
for (const d of nodes) {
let k = d.radius ** 2;
x += d.x * k;
y += d.y * k;
z += k;
}
return {x: x / z, y: y / z};
}
// Drag functions used for interactivity
function dragstarted(d) {
if (!d3.event.active) simulation.alphaTarget(0.3).restart();
d.fx = d.x;
d.fy = d.y;
}
function dragged(d) {
d.fx = d3.event.x;
d.fy = d3.event.y;
}
function dragended(d) {
if (!d3.event.active) simulation.alphaTarget(0);
d.fx = null;
d.fy = null;
}
///////////////////////////////////////////////////////////////////////////
///////////////////////////// Distribution plot ///////////////////////////
///////////////////////////////////////////////////////////////////////////
function distributedData(nodes) {
var nodeRadius = screen.width < 420 ? 4 : 8
var tilesPerRow = 8
var tileSize = nodeRadius * 1.5
var barWidth = screen.width < 420 ? 50 : (screen.width <= 1024 ? 150 : 200)
// find count of nodes within each category
var counts = nodes.reduce((p, c) => {
var name = c.band; // key property
if (!p.hasOwnProperty(name)) {
p[name] = 0;
}
p[name]++;
return p;
}, {});
countsExtended = Object.keys(counts).map(k => {
var circles_arr = nodes.filter(d=>d.band==k)
return {name: k, count: counts[k]}; circles_arr: circles_arr});
// get x-y coordinates of all tiles first without rendering the dotted bar chart
var dataAll = countsExtended.map(d=>d.count)
var arrays = []
dataAll.map((d,i) => {
var tiles = getTiles(d, i)
arrays.push(tiles)
})
var distributed = [].concat.apply([], arrays)
return distributed
function getTiles(num, counter) {
var tiles = []
var leftBuffer = screen.width <= 1024 ? 0 : width/4
var bottomBuffer = screen.width <=1024 ? 0 : 100
for(var i = 0; i < num; i++) {
var rowNumber = Math.floor(i / tilesPerRow)
tiles.push({
x: ((i % tilesPerRow) * tileSize) + (counter * barWidth) + tileSize + leftBuffer,
y: -(rowNumber + 1) * tileSize + height - bottomBuffer,
color: color[counter],
band: (counter+1).toString(),
id: counter + '-' + i, // index each node
radius: (tileSize/1.5)/2
});
}
return tiles
}
}
///////////////////////////////////////////////////////////////////////////
//////////////////////////// Updated node positions ///////////////////////
///////////////////////////////////////////////////////////////////////////
function update(data, type, DURATION, DELAY) {
circles = g.selectAll('circle').data(data, d=>d.id)
circles.exit().remove()
var entered_circles = circles
.enter()
.append('circle')
.style('opacity', 1)
.attr('id', d => d.id)
.attr('cx', function(d) {
//console.log(d.id, d.x)
return d.x })
.attr('cy', d => d.y)
.style('fill', d => d.color)
.attr('r', d => d.radius)
.call(d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended))
circles = circles.merge(entered_circles)
var t = d3.transition()
.duration(DURATION)
.ease(d3.easeQuadOut)
if(type=='transition_by_default'){
circles.transition(t)
.attr('cx', d => d.x)
.attr('cy', d => d.y)
} else if(type=='transition_by_index'){
// transition each node one by one within each group at the same time
category.map((d,i)=> {
circles.filter("circle[id^='" + i.toString() + "']")
.transition(t)
.delay(function(d,i){ return DELAY*i }) // transition each node one by one based on index
.attr('cx', d => d.x)
.attr('cy', d => d.y)
})
} else if(type=='transition_by_length'){
// transition each node one by one within each group at the same time
category.map((d,i)=> {
circles.filter("circle[id^='" + i.toString() + "']")
.transition(t)
.delay(function(d,i){ return d.length*DELAY }) // transition each node one by one based on length of trajectory
.attr('cx', d => d.x)
.attr('cy', d => d.y)
})
}
}
///////////////////////////////////////////////////////////////////////////
///////////////////////////// Helper functions ////////////////////////////
///////////////////////////////////////////////////////////////////////////
function randn_bm(min, max, skew) {
var u = 0, v = 0;
while(u === 0) u = Math.random(); //Converting [0,1) to (0,1)
while(v === 0) v = Math.random();
let num = Math.sqrt( -2.0 * Math.log( u ) ) * Math.cos( 2.0 * Math.PI * v );
num = num / 10.0 + 0.5; // Translate to 0 -> 1
if (num > 1 || num < 0) num = randn_bm(min, max, skew); // resample between 0 and 1 if out of range
num = Math.pow(num, skew); // Skew
num *= max - min; // Stretch to fill range
num += min; // offset to min
return num;
}
}()
body {
background-color: white;
color: black;
font-family: 'Karla', sans-serif;
}
.btn {
color: black;
background: transparent;
border: 2px solid black;
border-radius: 6px;
padding: 8px 16px;
text-align: center;
display: inline-block;
font-size: 0.8em;
margin: 4px 2px;
-webkit-transition-duration: 0.4s; /* Safari */
transition-duration: 0.4s;
cursor: pointer;
text-decoration: none;
text-transform: uppercase;
}
.btn:hover {
background-color: #E47D06;
color: black;
}
/* if mobile device max width 420px */
@media screen and (max-width: 576px){
label, input[type='radio'] {
font-size: 0.8em;
}
.btn {
padding: 4px 8px;
text-align: center;
display: inline-block;
font-size: 0.6em;
margin: 2px 1px;
}
}
#panel-1 {
text-align: left;
float:left;
color:black;
position: absolute;
width: 300px;
top: 50px;
left: 50px;
z-index: 1;
}
#panel-2 {
text-align: left;
float:left;
color:white;
position: absolute;
width: 300px;
top: 1000px;
left: 50px;
z-index: 1;
}
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width">
<script src="https://d3js.org/d3.v4.min.js"></script>
<script src="https://d3js.org/d3-color.v1.min.js"></script>
<script src="https://d3js.org/d3-interpolate.v1.min.js"></script>
<script src="https://d3js.org/d3-scale-chromatic.v1.min.js"></script>
<script src="https://d3js.org/d3-array.v2.min.js"></script>
<script src="https://unpkg.com/d3-simple-slider"></script>
<link rel="stylesheet" href="general.css" />
</head>
<body>
<div id="body">
<div class="modal">
<div class="modal-content2" id="modal-fullscreen">
<div id='panel-1'>
<label>First, select chart type to transition from: </label>
<div id="section">
<input name="start_type"
type="button"
class='btn run_cluster_sorted'
value="Cluster blob"/>
<input name="start_type"
type="button"
class="btn run_cluster_bubbles"
value="Cluster bubbles" />
<input name="start_type"
type="button"
class='btn reset_button'
value="Reset to scatter"
checked />
</div>
<label>Next, select transition type: </label>
<div id="section">
<input name="transition_type"
type="radio"
value="transition_by_index"
> Index<br>
<input name="transition_type"
type="radio"
value="transition_by_length"
> Length<br>
<input name="transition_type"
type="radio"
value="transition_by_default"
checked > Default<br>
</div>
<label>Finally, select transition parameters: </label>
<div id='slider-step1'></div>
<div id='slider-step2'></div>
<div id="section">
<input name="submit"
class="btn submit_button"
type="button"
value="Morph to dot plot!"/>
</div>
</div>
<div id='panel-2'>
<input name="start_type"
type="button"
class="btn run_add"
value="Add" />
<input name="start_type"
type="button"
class="btn run_move"
value="Move" />
<input name="type"
type="button"
class='btn changeFocitoIndustry'
value="Regroup"/>
<input name="type"
type="button"
class='btn run_change_foci'
value="Change Foci"/>
<input name="type"
type="button"
class="btn changeFocitoCrime"
value="Reset"/>
</div>
</div>
</div>
</div>
<script src="distribute_nodes.js"></script>
<script src="moving_bubbles.js"></script>
<script>
distribute.run()
moving_bubbles.run()
</script>
</body>
</html>
var moving_bubbles = function () {
///////////////////////////////////////////////////////////////////////////
///////////////////////////////// Globals /////////////////////////////////
///////////////////////////////////////////////////////////////////////////
const FOCI_LENGTH = 10;
const FOCI_STROKE_WIDTH = 2;
const HEIGHT = screen.width < 420 ? screen.width*0.90 : screen.width*0.50;
const INTERVAL_DURATION = 5000;
const MARGIN = 20;
const MUTATE_PROBABILITY = 0.02;
const CIRCLE_RADIUS = screen.width < 420 ? 2 : 3.5;
const WIDTH = screen.width < 420 ? screen.width*0.90 : screen.width*0.50;
var res = []
var centroids, progress, circles, timer
let center = [WIDTH / 2, HEIGHT / 2];
let centerRadius = Math.min(WIDTH / 2, HEIGHT / 2) * 0.68;
let foci = null;
let fociCount = null;
let nodeCount = null;
let nodes = null;
let svg = null;
var t = 0
var NUM_FLICKS = 30
var chargeStrength = screen.width < 420 ? -2 : -5
var fociStrength = 0.2
var simulation = d3.forceSimulation()
var modal = d3.select(".modal-content2")
var modalDiv = modal.append('div')
.attr('id', "div-content2-1")
.attr('width', WIDTH)
.attr('height', HEIGHT)
.attr('transform', 'translate(' + MARGIN + ',' + screen.height + ')')
.attr('float', 'left')
///////////////////////////////////////////////////////////////////////////
///////////////////////////// Create scales ///////////////////////////////
///////////////////////////////////////////////////////////////////////////
var money_laundering = ['#F48FB1', '#F06292', '#EC407A', '#E91E63', '#D81B60', '#C2185B', 'red']
var tax_customs_violation = ["#D4E157", "#CDDC39", "#C0CA33", "#AFB42B", "#9E9D24", "#827717", 'red']
var cybercrime = ['#c7e9c0','#a1d99b','#74c476','#41ab5d','#238b45','#005a32', 'red']
var organized_crime = ['#c6dbef','#9ecae1','#6baed6','#4292c6','#2171b5','#084594', 'red']
var terrorism = ['#d9d9d9','#bdbdbd','#969696','#737373','#525252','#252525', 'red']
var sanctions = ['#dadaeb','#bcbddc','#9e9ac8','#807dba','#6a51a3','#4a1486', 'red']
var trafficking = ['#fdd0a2','#fdae6b','#fd8d3c','#f16913','#d94801','#8c2d04', 'red']
var colors = [{key:'Money Laundering', colors: money_laundering},
{key:'Tax & Customs Violation', colors: tax_customs_violation},
{key:'Cybercrime', colors: cybercrime},
{key:'Organised Crime', colors: organized_crime},
{key:'Terrorism', colors: terrorism},
{key:'Sanctions', colors: sanctions},
{key:'Trafficking in Stolen Goods', colors: trafficking}]
var focisCrime = ['Money Laundering', 'Tax & Customs Violation', 'Cybercrime', 'Organised Crime', 'Terrorism', 'Sanctions', 'Trafficking in Stolen Goods']
var focisScore = ['0.2-0.39', '0.4-0.59', '0.6-0.69', '0.7-0.79', '0.8-0.99', '0.9-0.99', '1' ]
var colorScale = d3.scaleOrdinal()
.domain(focisScore)
var focis = [{key:'crime', categories: focisCrime}, {key:'score', categories: focisScore}]
var crimeFoci = onFociCountChange(focisCrime) // create a foci for each crime category
var scoreFoci = onFociCountChange(focisScore) // create a foci for each score category
var crimeFoci_new = []
Object.keys(crimeFoci).map(function(key, index) {
crimeFoci_new.push({
x: crimeFoci[index].x,
y: crimeFoci[index].y,
key: crimeFoci[index].key,
color: crimeFoci[index].color
})
})
var yScale = d3.scaleBand()
.domain(focisCrime)
.range([HEIGHT+120, 40])
/////////////////////////////////////////////////////////////////////////////
////////////////////////////////// INITIALIZE ///////////////////////////////
/////////////////////////////////////////////////////////////////////////////
return {
clear : function () {
modal.select("svg").remove()
},
run : function () {
svg = modalDiv.append("svg")
.attr("width", WIDTH + 2 * MARGIN)
.attr("height", HEIGHT + 2 * MARGIN)
.append("g")
.attr("class", "margin")
.attr("transform", `translate(${MARGIN}, ${MARGIN})`);
if(screen.width > 420){
svg.append('rect')
.attr("width", WIDTH)
.attr("height", HEIGHT)
.attr('fill', 'none')
.style('stroke-width', '2px')
.style('rx', '6px')
.style('stroke', 'black')
}
gCircle = svg.append('g').attr('class', 'circles')
d3.range(1,400).map((d,i) => {
res.push({
id: i,
crime0: crimeFoci[getRandomArbitrary(0,6)].key,
binned0 : scoreFoci[getRandomArbitrary(0,6)].key,
crime1: crimeFoci[getRandomArbitrary(0,6)].key,
binned1 : scoreFoci[getRandomArbitrary(0,6)].key,
crime2: crimeFoci[getRandomArbitrary(0,6)].key,
binned2 : scoreFoci[getRandomArbitrary(0,6)].key
})
})
init()
}
}
function init() {
var res_nested_crime = d3.nest()
.key(d=>d.crime0)
.entries(res)
nodes = []
res_nested_crime.map(d=> {
var n = createNodes(focis, d.key, d.values)
nodes.push(n)
})
nodes = [].concat.apply([], nodes)
//nodes = createNewNodes(focis, nodes) // create a new set of nodes to 'fly in'
nodes.forEach((d,i)=>{
d.class = d.binned0 == '1' ? "new" : undefined
})
simulate(nodes, crimeFoci, true, 'groupCrime') // kick off simulation
d3.select('.changeFocitoCrime').on("click", function () {
simulate_group(nodes, foci, true)
init()
});
d3.select('.run_add').on("click", function () { simulate(nodes, scoreFoci, false, 'pull') });
d3.select('.run_move').on("click", function () { changeNodeFoci(nodes, crimeFoci, false) });
d3.select('.run_align').on("click", function () { simulate(nodes, crimeFoci, false, 'align') });
d3.select('.run_change_foci').on("click", function () { simulate_group(nodes, crimeFoci, false) });
d3.select('.changeFocitoIndustry').on("click", function () { changeFocitoIndustry(nodes, scoreFoci, false) });
}
////////////////////////////////////////////////////////////////////////////////
////////////////////////////////// Simulate nodes //////////////////////////////
////////////////////////////////////////////////////////////////////////////////
function simulate(nodes, foci, bg, type) {
if(type=='groupCrime'){
nodes.forEach(d=>{
d.x1 = d.class=='new' ? foci[d.focus].x : d.x
d.y1 = d.class=='new' ? foci[d.focus].y : d.y
d.opacity = d.class=='new' ? 1 : 0
})
} else if (type=='pull'){
fociStrength = 0.1
nodes.forEach(d=>{
d.fx = null
d.fy = null
d.x1 = foci[d.focus].x
d.y1 = foci[d.focus].y
d.opacity = 1
})
} else if (type=='groupIndustry'){
nodes.forEach(d=>{
d.fx = null
d.fy = null
d.x1 = foci[d.focus].x
d.y1 = foci[d.focus].y
})
} else if (type=='pool'){
nodes.forEach(d=>{
d.fx = null
d.fy = null
d.x1 = WIDTH/2
d.y1 = HEIGHT/2
})
}
if(bg==true){
startSimulationBackground()
} else if(bg==false){
if(type=='groupCrime'){
updateCircles(nodes, 'groupCrime') // render nodes
}
startSimulation()
}
}
/////////////// Group nodes according to subgraphs (with simulation) ///////////////
function simulate_group(nodes, foci, terminator) {
setTimeout(function() { changeNodeGroup(NUM_FLICKS, foci, terminator) }, 1000)
// show progress bar to indicate time to end of animation
var segmentWidth = WIDTH-40
progress = svg.append('rect')
.attr('class', 'progress-bar')
.attr('rx', 10)
.attr('ry', 10)
.attr('fill', 'lightgray')
.attr('height', 5)
.attr('width', 0)
.attr('x', 20)
.attr('y', 10)
function moveProgressBar(t, NUM_FLICKS){
progress.transition()
.duration(800)
.attr('fill', 'black')
.attr('width', function(){
return t/NUM_FLICKS * segmentWidth;
});
}
function changeNodeGroup(NUM_FLICKS, foci, terminator) {
console.log(terminator)
var animation_interval = d3.interval(function(){
t += 1 // update time
if (t > NUM_FLICKS) { animation_interval.stop(); simulation.stop(); return true } // stop simulation after 10 timesteps
if (terminator==true) {
animation_interval.stop();
simulation.stop();
svg.select('.progress-bar').remove()
return true
}
assignOtherSubgraph(t)
moveProgressBar(t, NUM_FLICKS)
}, 800, d3.now() - 800)
function assignOtherSubgraph(t) {
var newFoci = getRandomArbitrary(1, 7)
var newI = getRandomArbitrary(0, 400)
nodes[newI].x = foci[newFoci].x
nodes[newI].y = foci[newFoci].y
simulation.nodes(nodes)
simulation.velocityDecay(0.4).alpha(0.5).restart()
}
}
}
//////////////////// Reinitialize simulation (show tick movement) ////////////////////
function startSimulation() {
var buffer = screen.width < 420 ? 0.5 : 2.5
simulation.nodes(nodes)
.force("charge", d3.forceManyBody().strength(chargeStrength).distanceMin(CIRCLE_RADIUS))
.force("collide", d3.forceCollide(CIRCLE_RADIUS + buffer))
.force("position-x", d3.forceX(d=>d.x1).strength(fociStrength))
.force('position-y', d3.forceY(d=>d.y1).strength(fociStrength))
.on("tick", onSimulationTick)
simulation.velocityDecay(0.3).alpha(0.5).restart()
}
function onSimulationTick() {
svg.selectAll("circle")
.attr('opacity', d => d.opacity)
.attr("cx", d => d.x)
.attr("cy", d => d.y)
}
//////////////////// Reinitialize simulation (in the background) ////////////////////
function startSimulationBackground() {
simulation.nodes(nodes)
.force("charge", d3.forceManyBody().strength(chargeStrength).distanceMin(CIRCLE_RADIUS))
.force("collide", d3.forceCollide(CIRCLE_RADIUS + 2.5))
.force("position-x", d3.forceX(d=>d.x1).strength(fociStrength))
.force('position-y', d3.forceY(d=>d.y1).strength(fociStrength))
.stop()
simulation.velocityDecay(0.3).alpha(0.5).restart()
for (var i = 0; i < 150; ++i) simulation.tick()
updateCircles(nodes, 'groupCrime') // render nodes
}
/////////////////////////////////////////////////////////////////////////////
/////////////////////// Create foci points and nodes ////////////////////////
/////////////////////////////////////////////////////////////////////////////
////////////////////// Assign focus point of each category //////////////////
function onFociCountChange(focis) {
fociCount = focis.length
foci = {};
for (let i = 0; i < fociCount; i++) {
let focus = createFocus(i, focis[i], fociCount);
foci[i] = focus;
}
return foci
}
/////////////////////// Calculate focus point of each category ///////////////
function createFocus(index, key, fociCount) {
let angle = 2 * Math.PI / fociCount * index;
return {
key: key,
index: index,
angle: angle,
color: colors.find(c=>c.key == key) ? colors.find(c=>c.key == key).colors[5] : null,
//color: d3.interpolateRainbow(index / fociCount),
x: center[0] + centerRadius * Math.cos(angle),
y: center[1] + centerRadius * Math.sin(angle)
};
}
////////////////////////////////// Create nodes //////////////////////////////
function createNodes(focis, key, data) {
var crime = focis.find(d=>d.key == 'crime').categories
var score = focis.find(d=>d.key == 'score').categories
var n = []
colorScale.range(colors.find(c=>c.key == key).colors)
data.map((d,i)=> {
n.push({
index: i, // unique index for each node
id: d.id, // unique ID for each entity
country: d.country,
subgraph: d.subgraph,
crime0: d.crime0,
binned0: d.binned0,
focus: crime.indexOf(d.crime0), //based on first crime category
focus1: crime.indexOf(d.crime1), //based on second crime category
focus2: crime.indexOf(d.crime2), //based on third crime category
focus_score: score.indexOf(d.binned0), // based on association score category
r: CIRCLE_RADIUS,
color: colorScale(d.binned0),
strokeFill: colorScale(d.binned0),
//x: crimeFoci_new.find(f=>f.key == d.crime0).x,
//y: crimeFoci_new.find(f=>f.key == d.crime0).y,
x: randBetween(0, WIDTH),
y: randBetween(0, HEIGHT),
same_subgraph: d.same_subgraph
})
})
return n
}
function createNewNodes(focis, nodes) {
var subgraphs = nodes.map(d=>d.subgraph).filter(onlyUnique)
var crime = focis.find(d=>d.key == 'crime').categories
var score = focis.find(d=>d.key == 'score').categories
var beyondWidth = [screen.width+150+Math.random(), -150+Math.random()]
var beyondHeight = [screen.height+150+Math.random(), -150+Math.random()]
var newNodes = []
d3.range(0, 100).map(d=> {
var binned0 = focisScore[getRandomArbitrary(0,score.length-1)] // randomly assigned a crime category
var crime0 = focisCrime[getRandomArbitrary(0,crime.length-1)] // randomly assigned an association score
colorScale.range(colors.find(c=>c.key == crime0).colors)
newNodes.push({
crime0: crime0,
binned0: binned0,
subgraph: subgraphs[getRandomArbitrary(0,subgraphs.length-1)],
focus: crime.indexOf(crime0),
focus_score: score.indexOf(binned0),
r: CIRCLE_RADIUS,
color: colorScale(binned0),
strokeFill: 'black',
x: beyondWidth[getRandomArbitrary(0,1)],
y: beyondHeight[getRandomArbitrary(0,1)],
class: 'new',
})
})
nodes.push(newNodes)
nodes = [].concat.apply([], nodes)
nodes.forEach((d,i)=>{
d.index = i
})
return nodes
}
///////////////////////// Update focus point of some nodes //////////////////////
function changeNodeFoci(nodes, focis, bg) {
nodes.map((d,i) => {
if (d.focus1) {
let point = d;
let newFocus = d.focus1; // change focus point
point.focus = newFocus;
}
})
simulate(nodes, focis, bg, 'pull')
}
/////////////////////// Update foci based on industry categories //////////////////
function changeFocitoIndustry(nodes, focis, bg) {
nodes.map((d,i) => {
if (d.focus_score!=-1) {
let point = d;
let newFocus = d.focus_score; // change focus point
point.focus = newFocus;
}
})
//updateLabels(scoreFoci, 'groupIndustry')
simulate(nodes, focis, bg, 'groupIndustry')
}
/////////////////////// Update foci based on score categories //////////////////
function changeFocitoScore(data) {
svg1.selectAll('text').remove()
svg1.selectAll('circle').remove()
svg.selectAll('text').remove()
var pointsBar = createDots(data, 'bar')
var pointsBar1 = createDots(data, 'tiledbar')
updateCircles(pointsBar, 'groupScore')
setTimeout(function(){
updateCircles(pointsBar1, 'groupScore')
}, 3000)
var ASFocis = []
focisScore.map((d,i)=>{
ASFocis.push({
key : d,
x : ((i*150)+100),
y : HEIGHT-100
})
})
updateLabels(ASFocis, 'groupScore')
}
///////////////////////////////////////////////////////////////////////////////////
//////////////////////// Update node, labels, misc positions //////////////////////
///////////////////////////////////////////////////////////////////////////////////
function updateCircles(data, type) {
circles = gCircle.selectAll("circle").data(data, d=>d.index);
circles.exit().remove()
var entered_circles = circles
.enter()
.append("circle")
.classed("node", true)
.classed("enter", true)
.attr("id", d => d.index)
.attr("r", d => d.r)
.attr("fill", d => d.color)
.attr('stroke', d => d.strokeFill)
.attr('stroke-width', '1px')
.attr("cx", d => d.x)
.attr("cy", d => d.y)
.attr("opacity", d => d.opacity)
circles = circles.merge(entered_circles)
var t = d3.transition()
.duration(2000)
.ease(d3.easeQuadOut)
if(type=='groupCrime'){
circles
.classed("enter", false)
.classed("update", true)
.transition().duration(500) // comment this line if you want to let simulation transition the nodes
.attr("fill", d => d.color)
.attr('cx', d => d.x)
.attr('cy', d => d.y)
.attr("opacity", d => d.opacity)
} else if(type=='groupScore'){
simulation.stop()
circles
.classed("enter", false)
.classed("update", true)
circles.transition(t)
.attr("fill", d => d.color)
.attr('cx', d => d.x)
.attr('cy', d => d.y)
}
}
function randBetween(min, max) {
return min + (max - min) * Math.random();
}
function getRandomArbitrary(min, max) {
return Math.round(Math.random() * (max - min) + min)
}
}()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment