d3.js dynamically append swarms with variable force bounds - javascript

I have a grid system that I'm using to display basic data:
var columns = 5;
var spacing = 250;
var vSpacing = 180;
var bankG = graphGroup.selectAll('.bank')
.data(data)
.enter()
.append('g')
.attr('class', 'bank')
.attr('id', (d, i) => 'bank' + i)
.attr('transform', (d, k) => {
var horSpace = (k % columns) * spacing;
var vertSpace = ~~((k / columns)) * vSpacing;
return "translate(" + horSpace + "," + vertSpace + ")";
});
And my simplified data set looks like:
var data = [
{'bank':'bank1','q1':100,'q2':120,'products':d3.range(20).map(function(v) {return {name: v.toString()+'_fund'} })},
{'bank':'bank2','q1':120,'q2':130,'products':d3.range(24).map(function(v) {return {name: v.toString()+'_fund'} })},
{'bank':'bank3','q1':140,'q2':150,'products':d3.range(80).map(function(v) {return {name: v.toString()+'_fund'} })},
{'bank':'bank4','q1':100,'q2':150,'products':d3.range(30).map(function(v) {return {name: v.toString()+'_fund'} })},
{'bank':'bank5','q1':90,'q2':120,'products':d3.range(10).map(function(v) {return {name: v.toString()+'_fund'} })},
{'bank':'bank6','q1':80,'q2':130,'products':d3.range(70).map(function(v) {return {name: v.toString()+'_fund'} })}
];
I've already appended rects and circles to form the scaffolding of the visual. Next I want to append swarm clusters around the circle that we see that juts off to the right-hand side of each rect. The idea is that each mini circle will represent one product; hence I'm trying to point the swarm cluster to the data in products. Also important is that each swarm is to assume initial force logic using the static right-hand circles I appended above; call them placeholders. Full snippet below:
var margins = {
top: 200,
bottom: 300,
left: 100,
right: 100
};
var height = 450;
var width = 1100;
var totalWidth = width + margins.left + margins.right;
var totalHeight = height + margins.top + margins.bottom;
var svg = d3.select('body')
.append('svg')
.attr('width', totalWidth)
.attr('height', totalHeight);
var graphGroup = svg.append('g')
.attr('transform', "translate(" + margins.left + "," + margins.top + ")");
var tsvData = d3.tsv('202105-xb.tsv');
//tsvData.then(function(rawData) {
//var data = rawData.map(function(d) {
//return {quota20:+d.quota20, quota21:+d.quota21, bank:d.bank, products:d3.range(+d.products).map(function(v) {return {name: v.toString()+'_fund'} })}
//});
var data = [
{'bank':'bank1','q1':100,'q2':120,'products':d3.range(20).map(function(v) {return {name: v.toString()+'_fund'} })},
{'bank':'bank2','q1':120,'q2':130,'products':d3.range(24).map(function(v) {return {name: v.toString()+'_fund'} })},
{'bank':'bank3','q1':140,'q2':150,'products':d3.range(80).map(function(v) {return {name: v.toString()+'_fund'} })},
{'bank':'bank4','q1':100,'q2':150,'products':d3.range(30).map(function(v) {return {name: v.toString()+'_fund'} })},
{'bank':'bank5','q1':90,'q2':120,'products':d3.range(10).map(function(v) {return {name: v.toString()+'_fund'} })},
{'bank':'bank6','q1':80,'q2':130,'products':d3.range(70).map(function(v) {return {name: v.toString()+'_fund'} })}
];
var rScale = d3.scaleLinear()
.range([5, 25])
.domain([0, 4000]);
var columns = 5;
var spacing = 250;
var vSpacing = 180;
var bankG = graphGroup.selectAll('.bank')
.data(data)
.enter()
.append('g')
.attr('class', 'bank')
.attr('id', (d, i) => 'bank' + i)
.attr('transform', (d, k) => {
var horSpace = (k % columns) * spacing;
var vertSpace = ~~((k / columns)) * vSpacing;
return "translate(" + horSpace + "," + vertSpace + ")";
});
bankG.append('rect')
.attr('x',0)
.attr('y',0)
.attr('width', 50)
.attr('height', 50)
.style('fill', "#003366");
bankG.append('line')
.attr('x1',50)
.attr('x2',90)
.attr('y1',25)
.attr('y2',25)
.style('stroke',"#a6a6a6");
bankG.append('circle')
.attr('cx', 90)
.attr('cy', 25)
.attr('r', 12)
.style('fill',"#003366");
var forceNodes = data.each().products;
forceNodes.push({fx:0,fy:0,placeholder:true});
var simulation = d3.forceSimulation(forceNodes)
.force("x", d3.forceX(function(d) {
return 20;
}).strength(0.2))
.force("y", d3.forceY(function(d) {
return 12.5;
}).strength(0.1))
.force("collide", d3.forceCollide().radius(function(d) {
return d.placeholder ? 12 : 4;
}).iterations(1))
.stop();
simulation.tick(75);
d3.select(this).selectAll(null)
.data(forceNodes.filter(function(d) { return !d.placeholder }))
.enter()
.append("circle")
.attr("r", 3)
.attr("cx", function(d) { return d.x;})
.attr("cy", function(d) { return d.y;})
.style('stroke',"none")
.style('fill', function(d) {return "#4f81b9"; })
//})
<script src="https://d3js.org/d3.v5.min.js"></script>
It seems I didn't quite access the data correctly for the swarm bit:
Uncaught TypeError: data.each is not a function
Question
Essentially, I'm trying to figure out how I get my swarm clusters to read the product data and append tightly around my existing normal circles? (presumably requiring variable force logic like in my attempt)

I'm not sure if I'm interpreting quite correctly, but if so, a solution could look like this:
You have nested data, the parent g, line, and rectangle have a different datum than the children in the force layout. So first we append the parent g and the line and rectangle contained by it. I'm not appending what I understand to be the placeholder circle node as we can do that with the rest, otherwise, this is the same as before.
Now we can apply a force layout to the nodes contained in the products property:
bankG.each(function(bank) {
bank.products.push({fx:0,fy:0,placeholder:true})
var simulation = d3.forceSimulation(bank.products)
.force("x", d3.forceX(function(d) {
return 20;
}).strength(0.2))
.force("y", d3.forceY(function(d) {
return 12.5;
}).strength(0.1))
.force("collide", d3.forceCollide().radius(function(d) {
return d.placeholder ? 12 : 4;
}).iterations(1))
.stop();
simulation.tick(75);
})
With the products array updated with positional data based on the force layout, we can now do our nested enter. To do so we use the products property of the each parent g as the data array for the selection of children:
bankG.selectAll(null)
.data(function(d) { return d.products; })
.enter()
.append("circle")
.attr("r", function(d) {
...
Here's a quick example:
var margins = {
top: 200,
bottom: 300,
left: 100,
right: 100
};
var height = 450;
var width = 1100;
var totalWidth = width + margins.left + margins.right;
var totalHeight = height + margins.top + margins.bottom;
var svg = d3.select('body')
.append('svg')
.attr('width', totalWidth)
.attr('height', totalHeight);
var graphGroup = svg.append('g')
.attr('transform', "translate(" + margins.left + "," + margins.top + ")");
var tsvData = d3.tsv('202105-xb.tsv');
var data = [
{'bank':'bank1','q1':100,'q2':120,'products':d3.range(20).map(function(v) {return {name: v.toString()+'_fund'} })},
{'bank':'bank2','q1':120,'q2':130,'products':d3.range(24).map(function(v) {return {name: v.toString()+'_fund'} })},
{'bank':'bank3','q1':140,'q2':150,'products':d3.range(80).map(function(v) {return {name: v.toString()+'_fund'} })},
{'bank':'bank4','q1':100,'q2':150,'products':d3.range(30).map(function(v) {return {name: v.toString()+'_fund'} })},
{'bank':'bank5','q1':90,'q2':120,'products':d3.range(10).map(function(v) {return {name: v.toString()+'_fund'} })},
{'bank':'bank6','q1':80,'q2':130,'products':d3.range(70).map(function(v) {return {name: v.toString()+'_fund'} })}
];
var rScale = d3.scaleLinear()
.range([5, 25])
.domain([0, 4000]);
var columns = 5;
var spacing = 250;
var vSpacing = 180;
var bankG = graphGroup.selectAll('.bank')
.data(data)
.enter()
.append('g')
.attr('class', 'bank')
.attr('id', (d, i) => 'bank' + i)
.attr('transform', (d, k) => {
var horSpace = (k % columns) * spacing;
var vertSpace = ~~((k / columns)) * vSpacing;
return "translate(" + horSpace + "," + vertSpace + ")";
});
bankG.append('rect')
.attr('x',0)
.attr('y',0)
.attr('width', 50)
.attr('height', 50)
.style('fill', "#003366");
bankG.append('line')
.attr('x1',50)
.attr('x2',90)
.attr('y1',25)
.attr('y2',25)
.style('stroke',"#a6a6a6");
// For each bankG:
bankG.each(function(bank) {
bank.products.push({fx:0,fy:0,placeholder:true})
var simulation = d3.forceSimulation(bank.products)
.force("x", d3.forceX(function(d) {
return 20;
}).strength(0.2))
.force("y", d3.forceY(function(d) {
return 12.5;
}).strength(0.1))
.force("collide", d3.forceCollide().radius(function(d) {
return d.placeholder ? 12 : 4;
}).iterations(1))
.stop();
simulation.tick(75);
})
bankG.selectAll(null)
.data(function(d) { return d.products; })
.enter()
.append("circle")
.attr("r", function(d) {
return d.placeholder ? 12 : 3;
})
.attr("cx", function(d) { return d.x + 90})
.attr("cy", function(d) { return d.y + 25})
.style('stroke',"none")
.style('fill', function(d) {
return d.placeholder ? "#003366" : "#4f81b9";
})
//})
<script src="https://d3js.org/d3.v5.min.js"></script>

Related

d3js beeswarm with force simulation

I try to do a beeswarm plot with different radius; inspired by this code
The issue I have, is that my point are offset regarding my x axis:
The point on the left should be at 31.7%. I don't understand why, so I would appreciate if you could guide me. This could be improved by changing the domain of x scale, but this can't match the exact value; same issue if I remove the d3.forceCollide()
Thank you,
Data are available here.
Here is my code:
$(document).ready(function () {
function tp(d) {
return d.properties.tp60;
}
function pop_mun(d) {
return d.properties.pop_mun;
}
var margin = {top: 20, right: 20, bottom: 20, left: 40},
width = 1280 - margin.right - margin.left,
height = 300 - margin.top - margin.bottom;
var svg = d3.select("body")
.append("svg")
.attr("viewBox", `0 0 ${width} ${height}`)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
var z = d3.scaleThreshold()
.domain([.2, .3, .4, .5, .6, .7])
.range(["#35ff00", "#f1a340", "#fee0b6",
"#ff0000", "#998ec3", "#542788"]);
var loading = svg.append("text")
.attr("x", (width) / 2)
.attr("y", (height) / 2)
// .attr("dy", ".35em")
.style("text-anchor", "middle")
.text("Simulating. One moment please…");
var formatPercent = d3.format(".0%"),
formatNumber = d3.format(".0f");
d3.json('static/data/qp_full.json').then(function (data) {
features = data.features
//1 create scales
var x = d3.scaleLinear()
.domain([0, d3.max(features, tp)/100])
.range([0, width - margin.right])
var y = d3.scaleLinear().domain([0, 0.1]).range([margin.left, width - margin.right])
var r = d3.scaleSqrt().domain([0, d3.max(features, pop_mun)])
.range([0, 25]);
//2 create axis
var xAxis = d3.axisBottom(x).ticks(20)
.tickFormat(formatPercent);
svg.append("g")
.attr("class", "x axis")
.call(xAxis);
var nodes = features.map(function (node, index) {
return {
radius: r(node.properties.pop_mun),
color: '#ff7f0e',
x: x(node.properties.tp60 / 100),
y: height + Math.random(),
pop_mun: node.properties.pop_mun,
tp60: node.properties.tp60
};
});
function tick() {
for (i = 0; i < nodes.length; i++) {
var node = nodes[i];
node.cx = node.x;
node.cy = node.y;
}
}
setTimeout(renderGraph, 10);
function renderGraph() {
// Run the layout a fixed number of times.
// The ideal number of times scales with graph complexity.
// Of course, don't run too long—you'll hang the page!
const NUM_ITERATIONS = 1000;
var force = d3.forceSimulation(nodes)
.force('charge', d3.forceManyBody().strength(-3))
.force('center', d3.forceCenter(width / 2, height/2))
.force('x', d3.forceX(d => d.x))
.force('y', d3.forceY(d => d.y))
.force('collide', d3.forceCollide().radius(d => d.radius))
.on("tick", tick)
.stop();
force.tick(NUM_ITERATIONS);
force.stop();
svg.selectAll("circle")
.data(nodes)
.enter()
.append("circle")
.attr("cx", d => d.x)
.attr("cy", d => d.y)
.attr("r", d => d.radius)
.style("fill", d => z(d.tp60/100))
.on("mouseover", function (d, i) {
d3.select(this).style('fill', "orange")
console.log(i.tp60,i)
svg.append("text")
.attr("id", "t")
.attr("x", function () {
return d.x - 50;
})
.attr("y", function () {
return d.y - 50;
})
.text(function () {
return [x.invert(i.x), i.tp60]; // Value of the text
})
})
.on("mouseout", function (d, i) {
d3.select("#t").remove(); // Remove text location
console.log(i)
d3.select(this).style('fill', z(i.tp60/100));
});
loading.remove();
}
})
})

d3js v4 bubble chart - getting force/gravity effects back

I am working on a d3 application - which features a bubble chart. I have a version which is displaying - but the old force code from version 3 - but I am unsure how to incorporate version 4 force effects. I want to give the bubbles a bit of animation - charge/gravity type effects so there is always some movement.
//old code with no force effects
http://jsfiddle.net/xzd9eamt/2/
var $this = $('.bubblechart');
var data = [{
"label": "Chinese",
"value": 20
}, {
"label": "American",
"value": 10
}, {
"label": "Indian",
"value": 50
}];
var width = $this.data('width'),
height = $this.data('height');
var color = d3.scaleOrdinal()
.range(["#ff5200", "red", "green"]);
var margin = {
top: 20,
right: 15,
bottom: 30,
left: 20
},
width = width - margin.left - margin.right,
height = height - margin.top - margin.bottom;
var svg = d3.select($this[0])
.append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr('class', 'bubblechart')
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
var bubbles = svg.append('g').attr('class', 'bubbles');
var force = d3.forceSimulation()
.force("collide", d3.forceCollide(12))
.force("center", d3.forceCenter(width / 2, height / 2))
.nodes(data)
//.on("tick", tick);
var bubbles = svg.append("g")
.attr("class", "bubbles")
data = funnelData(data, width, height);
var padding = 4;
var maxRadius = d3.max(data, function(d) {
return parseInt(d.radius)
});
var scale = (width / 6) / 100;
var nodes = bubbles.selectAll("circle")
.data(data);
// Enter
nodes.enter()
.append("circle")
.attr("class", "node")
.attr("cx", function(d) {
return d.x;
})
.attr("cy", function(d) {
return d.y;
})
.attr("r", 10)
.style("fill", function(d, i) {
return color(i);
})
//.call(d3.drag());
// Update
nodes
.transition()
.delay(300)
.duration(1000)
.attr("r", function(d) {
return d.radius * scale;
})
// Exit
nodes.exit()
.transition()
.duration(250)
.attr("cx", function(d) {
return d.x;
})
.attr("cy", function(d) {
return d.y;
})
.attr("r", 1)
.remove();
draw('all');
function funnelData(data, width, height) {
function getRandom(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
var max_amount = d3.max(data, function(d) {
return parseInt(d.value)
})
var radius_scale = d3.scalePow().exponent(0.5).domain([0, max_amount]).range([2, 85])
$.each(data, function(index, elem) {
elem.radius = radius_scale(elem.value) * .8;
elem.all = 'all';
elem.x = getRandom(0, width);
elem.y = getRandom(0, height);
});
return data;
}
function draw(varname) {
var foci = {
"all": {
name: "All",
x: width / 2,
y: height / 2
}
};
//force.on("tick", tick(foci, varname, .55));
//force.start();
}
function tick(foci, varname, k) {
return function(e) {
data.forEach(function(o, i) {
var f = foci[o[varname]];
o.y += (f.y - o.y) * k * e.alpha;
o.x += (f.x - o.x) * k * e.alpha;
});
nodes
.each(collide(.1))
.attr("cx", function(d) {
return d.x;
})
.attr("cy", function(d) {
return d.y;
});
}
}
function collide(alpha) {
var quadtree = d3.geom.quadtree(data);
return function(d) {
var r = d.radius + maxRadius + padding,
nx1 = d.x - r,
nx2 = d.x + r,
ny1 = d.y - r,
ny2 = d.y + r;
quadtree.visit(function(quad, x1, y1, x2, y2) {
if (quad.point && (quad.point !== d)) {
var x = d.x - quad.point.x,
y = d.y - quad.point.y,
l = Math.sqrt(x * x + y * y),
r = d.radius + quad.point.radius + padding;
if (l < r) {
l = (l - r) / l * alpha;
d.x -= x *= l;
d.y -= y *= l;
quad.point.x += x;
quad.point.y += y;
}
}
return x1 > nx2 || x2 < nx1 || y1 > ny2 || y2 < ny1;
});
};
}
body {
background: #eeeeee;
}
.line {
fill: none;
stroke-width: 2px;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script>
<script src="https://d3js.org/d3.v4.min.js"></script>
<h1>BubbleChart I</h1>
<div class="bubblechart" data-width="300" data-height="300" />
There are a couple of things you need to know with d3 versus d4.
Firstly, nodes was empty, because the new nodes were not added to the selection automatically. Since d3 v4, you need to call .merge() to combine the update and enter parts of a selection. This article is a really good summary.
Secondly, there is no need to do all this data wrangling yourself. d3-force updates d.x and d.y automatically with the correct values, so you can remove the collide function and the part where you calculate d.x and d.y. All you need to do is just draw the circles at the correct place.
var $this = $('.bubblechart');
var data = [{
"label": "Chinese",
"value": 20
}, {
"label": "American",
"value": 10
}, {
"label": "Indian",
"value": 50
}];
var width = $this.data('width'),
height = $this.data('height');
var color = d3.scaleOrdinal()
.range(["#ff5200", "red", "green"]);
var margin = {
top: 20,
right: 15,
bottom: 30,
left: 20
},
width = width - margin.left - margin.right,
height = height - margin.top - margin.bottom;
var svg = d3.select($this[0])
.append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr('class', 'bubblechart')
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
var bubbles = svg.append('g').attr('class', 'bubbles');
var force = d3.forceSimulation()
.force("collide", d3.forceCollide(12))
.force("center", d3.forceCenter(width / 2, height / 2))
.nodes(data);
var bubbles = svg.append("g")
.attr("class", "bubbles")
data = funnelData(data, width, height);
var padding = 4;
var maxRadius = d3.max(data, function(d) {
return parseInt(d.radius)
});
var scale = (width / 6) / 100;
var nodes = bubbles.selectAll("circle")
.data(data);
// Enter
nodes.enter()
.append("circle")
.attr("class", "node")
.attr("cx", function(d) {
return d.x;
})
.attr("cy", function(d) {
return d.y;
})
.attr("r", 10)
.style("fill", function(d, i) {
return color(i);
})
.call(d3.drag());
// Update
nodes
.transition()
.delay(300)
.duration(1000)
.attr("r", function(d) {
return d.radius * scale;
})
// Exit
nodes.exit()
.transition()
.duration(250)
.attr("cx", function(d) {
return d.x;
})
.attr("cy", function(d) {
return d.y;
})
.attr("r", 1)
.remove();
draw('all');
function funnelData(data, width, height) {
function getRandom(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
var max_amount = d3.max(data, function(d) {
return parseInt(d.value)
})
var radius_scale = d3.scalePow().exponent(0.5).domain([0, max_amount]).range([2, 85])
$.each(data, function(index, elem) {
elem.radius = radius_scale(elem.value) * .8;
elem.all = 'all';
elem.x = width / 2;
elem.y = height / 2;
});
return data;
}
function draw(varname) {
var foci = {
"all": {
name: "All",
x: width / 2,
y: height / 2
}
};
force.on("tick", tick(foci, varname, .55));
}
function tick(foci, varname, k) {
return function(e) {
bubbles.selectAll("circle")
.attr("cx", function(d) {
return d.x;
})
.attr("cy", function(d) {
return d.y;
});
}
}
body {
background: #eeeeee;
}
.line {
fill: none;
stroke-width: 2px;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script>
<script src="https://d3js.org/d3.v4.min.js"></script>
<h1>BubbleChart I</h1>
<div class="bubblechart" data-width="300" data-height="300" />

D3.js v5 modular swarm clusters (variable radius?)

I want to create a visual whereby a swarm contains one big circle and a bunch of satellite circles clinging around it. For a simple demonstration, I have prepared a small version of the data set; each item in the array should have one big circle and then however many smaller circles clinging to it:
var data = [
{'wfoe':'wfoe1','products':d3.range(20)},
{'wfoe':'wfoe2','products':d3.range(40)},
{'wfoe':'wfoe3','products':d3.range(10)}
];
Here is a snippet of my progress:
var margins = {
top: 100,
bottom: 300,
left: 100,
right: 100
};
var height = 250;
var width = 900;
var totalWidth = width + margins.left + margins.right;
var totalHeight = height + margins.top + margins.bottom;
var svg = d3.select('body')
.append('svg')
.attr('width', totalWidth)
.attr('height', totalHeight);
var graphGroup = svg.append('g')
.attr('transform', "translate(" + margins.left + "," + margins.top + ")");
var data = [
{'wfoe':'wfoe1','products':d3.range(20)},
{'wfoe':'wfoe2','products':d3.range(40)},
{'wfoe':'wfoe3','products':d3.range(10)}
];
var columns = 4;
var spacing = 250;
var vSpacing = 250;
var fmcG = graphGroup.selectAll('.fmc')
.data(data)
.enter()
.append('g')
.attr('class', 'fmc')
.attr('id', (d, i) => 'fmc' + i)
.attr('transform', (d, k) => {
var horSpace = (k % columns) * spacing;
var vertSpace = ~~((k / columns)) * vSpacing;
return "translate(" + horSpace + "," + vertSpace + ")";
});
var xScale = d3.scalePoint()
.range([0, width])
.domain([0, 100]);
var rScale = d3.scaleThreshold()
.range([50,5])
.domain([0,1]);
data.forEach(function(d, i) {
d.x = (i % columns) * spacing;
d.y = ~~((i / columns)) * vSpacing;
});
var simulation = d3.forceSimulation(data)
.force("x", d3.forceX(function(d,i) {
return (i % columns) * spacing;
}).strength(0.1))
.force("y", d3.forceY(function(d,i) {
return ~~((i / columns)) * vSpacing;
}).strength(0.01))
.force("collide", d3.forceCollide(function(d,i) { return rScale(i)}))
.stop();
simulation.tick(75);
fmcG.selectAll(null)
.data(data)
.enter()
.append("circle")
.attr("r", function(d,i) {
return rScale(i)
})
.attr("cx", function(d) {
return d.x;
})
.attr("cy", function(d) {
return d.y;
})
.style('fill',"#003366");
<script src="https://d3js.org/d3.v5.min.js"></script>
I want to quickly point out that the big circle doesn't represent any data point (they are just going to house a name / logo). I just thought that including it in the simulation data would be the easiest way to introduce the needed force logic for the swarm circles. I thought that an elegant solution would be to use a threshold scale and let the first (i=0) datum always be the biggest circle. Here is what I mean:
var rScale = d3.scaleThreshold()
.range([0, 1])
.domain([50, 5]);
fmcG.selectAll(null)
.data(data)
.enter()
.append("circle")
.attr("r", function(d,i) {
return rScale(i)
})
.attr("cx", function(d) {
return d.x;
})
.attr("cy", function(d) {
return d.y;
})
.style('fill',"#003366");
The result I mentioned above (three big circles with little circles all around them) was not achieved, and in fact very few circles were appended and the variable radius component didn't seem to be working as I thought it would. (also no errors displayed in the log).
Question
How can I iteratively create swarms that start with one big circle and append subsequent smaller circles around the initial big circle, as applicable to the sample data set?
You could use a force simulation, like below, only this gives non-deterministic results. However, it's really good when you want to gradually add more nodes. In the below solution, I gave all related nodes a link to the center node, but didn't draw it. This made it possible for linked nodes to attract heavily.
On the other hand, you could also use a bubble chart if you want D3 to find the optimal packing solution for you, without the force working on them. Only downside is you'd have to call the packing function with all nodes every time, and the other nodes might shift because of the new one.
var margins = {
top: 100,
bottom: 300,
left: 100,
right: 100
};
var height = 250;
var width = 900;
var totalWidth = width + margins.left + margins.right;
var totalHeight = height + margins.top + margins.bottom;
var svg = d3.select('body')
.append('svg')
.attr('width', totalWidth)
.attr('height', totalHeight);
var graphGroup = svg.append('g')
.attr('transform', "translate(" + margins.left + "," + margins.top + ")");
var data = [{
'wfoe': 'wfoe1',
'products': d3.range(20).map(function(v) {
return v.toString() + '_wfoe1';
})
},
{
'wfoe': 'wfoe2',
'products': d3.range(40).map(function(v) {
return v.toString() + '_wfoe2';
})
},
{
'wfoe': 'wfoe3',
'products': d3.range(10).map(function(v) {
return v.toString() + '_wfoe3';
})
}
];
var columns = 4;
var spacing = 250;
var vSpacing = 250;
function dataToNodesAndLinks(d) {
// Create one giant array of points and
// one link between each wfoe and each product
var nodes = [{
id: d.wfoe,
center: true
}];
var links = [];
d.products.forEach(function(p) {
nodes.push({
id: p,
center: false
});
links.push({
source: d.wfoe,
target: p
});
});
return {
nodes: nodes,
links: links
};
}
var fmcG = graphGroup.selectAll('.fmc')
.data(data.map(function(d, i) {
return dataToNodesAndLinks(d, i);
}))
.enter()
.append('g')
.attr('class', 'fmc')
.attr('id', (d, i) => 'fmc' + i)
.attr('transform', (d, k) => {
var horSpace = (k % columns) * spacing;
var vertSpace = ~~((k / columns)) * vSpacing;
return "translate(" + horSpace + "," + vertSpace + ")";
});
var xScale = d3.scalePoint()
.range([0, width])
.domain([0, 100]);
var rScale = d3.scaleThreshold()
.range([50, 5])
.domain([0, 1]);
fmcG.selectAll("circle")
.data(function(d) {
return d.nodes;
})
.enter()
.append("circle")
.attr("id", function(d) {
return d.id;
})
.attr("r", function(d, i) {
return d.center ? rScale(i) * 5 : rScale(i);
})
.style('fill', function(d) { return d.center ? "darkred" : "#003366"; })
fmcG
.each(function(d, i) {
d3.forceSimulation(d.nodes)
.force("collision", d3.forceCollide(function(d) {
return d.center ? rScale(i) * 5 : rScale(i);
}))
.force("center", d3.forceCenter(0, 0))
.force("link", d3
.forceLink(d.links)
.id(function(d) {
return d.id;
})
.distance(0)
.strength(2))
.on('tick', ticked);
});
function ticked() {
fmcG.selectAll("circle")
.attr("transform", function(d) {
return "translate(" + d.x + "," + d.y + ")";
});
}
<script src="https://d3js.org/d3.v5.js"></script>

D3 stacked segment chart, sort by totals

I have been trying to create a stacked segment chart for the past few days and between all the trigonometry and key stacking it has truly been a nightmare for me to apply what seems to be the most basic of tweaks. Currently my segment chart has data that is sorted in descending order based on Active equity. I want to change this to have the data sorted by total.
const csvData = `State,Active equity,Passive equity,Fixed income,Mixed assets
BlackRock,1,17,0,0
Fidelity,13,2,0,0
SSgA,12,0,0,0
Hang Seng,11,0,0,0
UBS,9,0,0,1
Schroders,6,0,2,1
JP Morgan,5,2,0,1
Value Partners,1,0,6,0
First State,5,0,0,0
Invesco,4,1,0,0
HSBC,1,1,1,1
DBS,0,2,1,0
BOCI,1,1,1,0
CSOP,0,2,1,0
Principal,1,1,0,0
Allianz,2,1,0,0
Yuanta,0,2,1,0
Manulife,1,0,1,0
Aberdeen,2,0,0,0
Mirae,1,1,0,0
,0,0,0,0`;
//const data = d3.csvParse(csvData, d3.autoType);
var margins = {top:20, bottom:300, left:30, right:100};
var height = 600;
var width = 900;
var totalWidth = width+margins.left+margins.right;
var totalHeight = height+margins.top+margins.bottom;
var outerRadius = (400 / 2);
var innerRadius = 15;
var svg = d3.select('body')
.append('svg')
.attr('width', totalWidth)
.attr('height', totalHeight);
var graphGroup = svg.append('g')
.attr('transform', "translate(250,250)");
var x = d3.scaleBand()
.range([0, 2 * Math.PI])
.align(0);
var y = d3.scaleRadial()
.range([innerRadius, outerRadius]);
var z = d3.scaleOrdinal()
.range(["#003366", "#4f81b9", "#95b3d7", "#f6d18b"]);
d3.csv("csvData", function(d, i, columns) {
for (i = 1, t = 0; i < columns.length; ++i) t += d[columns[i]] = +d[columns[i]];
d.total = t;
return d;
}, function(error, data) {
if (error) throw error;
weave(data, function(a, b) { return b[data.columns[6]] - a[data.columns[6]]; });
x.domain(data.map(function(d) { return d.State; }));
y.domain([0, d3.max(data, function(d) { return d.total; })*1.3]);
z.domain(data.columns.slice(1));
graphGroup.append('g')
.selectAll('g')
.data(d3.stack().keys(data.columns.slice(1))(data))
.enter().append("g")
.selectAll(".bg-arc2")
.data(function(d) { return d; })
.enter().append("path")
.attr("d", d3.arc()
.innerRadius(innerRadius)
.outerRadius(outerRadius+2)
.startAngle(function(d) { return x(d.data.State); })
.endAngle(function(d) { return x(d.data.State) + x.bandwidth()*.90; })
.padAngle(0.1)
.padRadius(innerRadius))
.attr('class','bg-arc2')
.attr('fill','none')
.attr('stroke-width','4px')
.attr('stroke','#003366');
graphGroup.append('circle')
.attr('cx',0)
.attr('cy',0)
.attr('r',200)
.style('fill','#d9d9d9');
graphGroup.append('g')
.selectAll('g')
.data(d3.stack().keys(data.columns.slice(1))(data))
.enter().append("g")
.selectAll(".bg-arc")
.data(function(d) { return d; })
.enter().append("path")
.attr("d", d3.arc()
.innerRadius(innerRadius)
.outerRadius(outerRadius)
.startAngle(function(d) { return x(d.data.State); })
.endAngle(function(d) { return x(d.data.State) + x.bandwidth()*.95; })
.padAngle(0.1)
.padRadius(innerRadius))
.attr('class','bg-arc')
.attr('fill','#fff');
graphGroup.append('circle')
.attr('cx',0)
.attr('cy',0)
.attr('r',innerRadius)
.style('fill','#fff');
var stackedData = d3.stack().keys(data.columns.slice(1))(data);
var stackedData2 = stackedData.sort(function(a,b) { return d3.descending(a[0].data.total, b[0].data.total)});
console.log(stackedData[0][0].data.total)
console.log(stackedData2);
graphGroup.append("g")
.selectAll("g")
.data(stackedData)
.enter().append("g")
.attr("fill", function(d) { return z(d.key); })
.selectAll("path")
.data(function(d) { return d; })
.enter().append("path")
.attr("d", d3.arc()
.innerRadius(function(d) { return y(d[0]); })
.outerRadius(function(d) { return y(d[1]); })
.startAngle(function(d) { return x(d.data.State); })
.endAngle(function(d) { return x(d.data.State) + x.bandwidth()*.95; })
.padAngle(0.04)
.padRadius(innerRadius));
var label = graphGroup.append("g")
.selectAll("g")
.data(data)
.enter().append("g")
.attr("text-anchor", "middle")
.attr("transform", function(d) { return "rotate(" + ((x(d.State) + x.bandwidth() / 2) * 180 / Math.PI - 90) + ")translate(" + (outerRadius+25) + ",0)"; });
label.append("text")
.attr("transform", function(d) { return (x(d.State) + x.bandwidth() / 2 + Math.PI / 2) % (2 * Math.PI) < Math.PI ? "rotate(90)translate(0,16)" : "rotate(-90)translate(0,-9)"; })
.text(function(d) { return d.State; });
var yAxis = graphGroup.append("g")
.attr("text-anchor", "end");
var yTick = yAxis
.selectAll("g")
.data(y.ticks(10).slice(1))
.enter().append("g");
});
function weave(array, compare) {
var i = -1, j, n = array.sort(compare).length, weave = new Array(n);
while (++i < n) weave[i] = array[(j = i << 1) >= n ? (n - i << 1) - 1 : j];
while (--n >= 0) array[n] = weave[n];
}
<script src="https://d3js.org/d3.v4.min.js"></script>
<script src="https://gist.githubusercontent.com/mbostock/3686329aa6e1f5938df8eef12ec353fe/raw/1ab722df937c3ac86cac8292e34cfc7279b017f8/d3-scale-radial.js"></script>
Relevant code here:
var stackedData = d3.stack().keys(data.columns.slice(1))(data);
var stackedData2 = stackedData.sort(function(a,b) { return d3.descending(a[0].data.total, b[0].data.total)});
console.log(stackedData[0][0].data.total)
console.log(stackedData2);
I checked via the console log to make sure I was slicing at the correct place, which I was, as confirmed by console.log(stackedData[0][0].data.total) which returned the correct value of 18.
However I can't apply the sort as desired. The data is still sorted by Active equity and not total.
Question
The default sort for stacked radial charts seems to be whatever the first variable is. In my case, it's Active equity. With that in mind, based off my progress above, what is keeping me from applying the descending order sort by data.total as opposed to the default?
Your question is not clear: are you talking about sorting the segments within each slice, or are you talking about sorting the slices themselves?
For the first case, there is an method named order, that you can use with the stack generator (the link goes to v5 docs, which are a bit different from v4). However, because you said "I want to change [the order] to have the data sorted by total", it seems to me that you're talking about sorting the slices. If that is correct, two observations: it's not sorted by Active equity, right now it's just the order of the objects in the original data array.
For sorting by total you just need to change that array:
data.sort(function(a, b) {
return b.total - a.total;
});
Also, get rid of the weave function.
Here is the result:
(function(global, factory) {
typeof exports === "object" && typeof module !== "undefined" ? factory(exports, require("d3-scale")) :
typeof define === "function" && define.amd ? define(["exports", "d3-scale"], factory) :
(factory(global.d3 = global.d3 || {}, global.d3));
}(this, function(exports, d3Scale) {
'use strict';
function square(x) {
return x * x;
}
function radial() {
var linear = d3Scale.scaleLinear();
function scale(x) {
return Math.sqrt(linear(x));
}
scale.domain = function(_) {
return arguments.length ? (linear.domain(_), scale) : linear.domain();
};
scale.nice = function(count) {
return (linear.nice(count), scale);
};
scale.range = function(_) {
return arguments.length ? (linear.range(_.map(square)), scale) : linear.range().map(Math.sqrt);
};
scale.ticks = linear.ticks;
scale.tickFormat = linear.tickFormat;
return scale;
}
exports.scaleRadial = radial;
Object.defineProperty(exports, '__esModule', {
value: true
});
}));
const csvData = `State,Active equity,Passive equity,Fixed income,Mixed assets
BlackRock,1,17,0,0
Fidelity,13,2,0,0
SSgA,12,0,0,0
Hang Seng,11,0,0,0
UBS,9,0,0,1
Schroders,6,0,2,1
JP Morgan,5,2,0,1
Value Partners,1,0,6,0
First State,5,0,0,0
Invesco,4,1,0,0
HSBC,1,1,1,1
DBS,0,2,1,0
BOCI,1,1,1,0
CSOP,0,2,1,0
Principal,1,1,0,0
Allianz,2,1,0,0
Yuanta,0,2,1,0
Manulife,1,0,1,0
Aberdeen,2,0,0,0
Mirae,1,1,0,0
,0,0,0,0`;
const data = d3.csvParse(csvData, function(d, i, columns) {
for (i = 1, t = 0; i < columns.length; ++i) t += d[columns[i]] = +d[columns[i]];
d.total = t;
return d;
});
data.sort(function(a, b) {
return b.total - a.total;
});
var margins = {
top: 20,
bottom: 300,
left: 30,
right: 100
};
var height = 600;
var width = 900;
var totalWidth = width + margins.left + margins.right;
var totalHeight = height + margins.top + margins.bottom;
var outerRadius = (400 / 2);
var innerRadius = 15;
var svg = d3.select('body')
.append('svg')
.attr('width', totalWidth)
.attr('height', totalHeight);
var graphGroup = svg.append('g')
.attr('transform', "translate(250,250)");
var x = d3.scaleBand()
.range([0, 2 * Math.PI])
.align(0);
var y = d3.scaleRadial()
.range([innerRadius, outerRadius]);
var z = d3.scaleOrdinal()
.range(["#003366", "#4f81b9", "#95b3d7", "#f6d18b"]);
x.domain(data.map(function(d) {
return d.State;
}));
y.domain([0, d3.max(data, function(d) {
return d.total;
}) * 1.3]);
z.domain(data.columns.slice(1));
graphGroup.append('g')
.selectAll('g')
.data(d3.stack().keys(data.columns.slice(1))(data))
.enter().append("g")
.selectAll(".bg-arc2")
.data(function(d) {
return d;
})
.enter().append("path")
.attr("d", d3.arc()
.innerRadius(innerRadius)
.outerRadius(outerRadius + 2)
.startAngle(function(d) {
return x(d.data.State);
})
.endAngle(function(d) {
return x(d.data.State) + x.bandwidth() * .90;
})
.padAngle(0.1)
.padRadius(innerRadius))
.attr('class', 'bg-arc2')
.attr('fill', 'none')
.attr('stroke-width', '4px')
.attr('stroke', '#003366');
graphGroup.append('circle')
.attr('cx', 0)
.attr('cy', 0)
.attr('r', 200)
.style('fill', '#d9d9d9');
graphGroup.append('g')
.selectAll('g')
.data(d3.stack().keys(data.columns.slice(1))(data))
.enter().append("g")
.selectAll(".bg-arc")
.data(function(d) {
return d;
})
.enter().append("path")
.attr("d", d3.arc()
.innerRadius(innerRadius)
.outerRadius(outerRadius)
.startAngle(function(d) {
return x(d.data.State);
})
.endAngle(function(d) {
return x(d.data.State) + x.bandwidth() * .95;
})
.padAngle(0.1)
.padRadius(innerRadius))
.attr('class', 'bg-arc')
.attr('fill', '#fff');
graphGroup.append('circle')
.attr('cx', 0)
.attr('cy', 0)
.attr('r', innerRadius)
.style('fill', '#fff');
var stackedData = d3.stack()
.keys(data.columns.slice(1))
(data);
graphGroup.append("g")
.selectAll("g")
.data(stackedData)
.enter().append("g")
.attr("fill", function(d) {
return z(d.key);
})
.selectAll("path")
.data(function(d) {
return d;
})
.enter().append("path")
.attr("d", d3.arc()
.innerRadius(function(d) {
return y(d[0]);
})
.outerRadius(function(d) {
return y(d[1]);
})
.startAngle(function(d) {
return x(d.data.State);
})
.endAngle(function(d) {
return x(d.data.State) + x.bandwidth() * .95;
})
.padAngle(0.04)
.padRadius(innerRadius));
var label = graphGroup.append("g")
.selectAll("g")
.data(data)
.enter().append("g")
.attr("text-anchor", "middle")
.attr("transform", function(d) {
return "rotate(" + ((x(d.State) + x.bandwidth() / 2) * 180 / Math.PI - 90) + ")translate(" + (outerRadius + 25) + ",0)";
});
label.append("text")
.attr("transform", function(d) {
return (x(d.State) + x.bandwidth() / 2 + Math.PI / 2) % (2 * Math.PI) < Math.PI ? "rotate(90)translate(0,16)" : "rotate(-90)translate(0,-9)";
})
.text(function(d) {
return d.State;
});
var yAxis = graphGroup.append("g")
.attr("text-anchor", "end");
var yTick = yAxis
.selectAll("g")
.data(y.ticks(10).slice(1))
.enter().append("g");
function weave(array, compare) {
var i = -1,
j, n = array.sort(compare).length,
weave = new Array(n);
while (++i < n) weave[i] = array[(j = i << 1) >= n ? (n - i << 1) - 1 : j];
while (--n >= 0) array[n] = weave[n];
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.13.0/d3.min.js"></script>

implement tooltip in radar chart with d3.js

var margin = {top: 30, right: 20, bottom: 30, left: 70},
h= 500;
w = 960;
ruleColor = "#CCC";
minVal = 0;
maxVal = 100;
width = 960 - margin.left - margin.right,
height = 500 - margin.top - margin.bottom;
var viz = d3.select("#radar")
.append('svg:svg')
.attr('width', w)
.attr('height', h)
.attr('class', 'vizSvg');
viz.append("svg:rect")
.attr('id', 'axis-separator')
.attr('x', 0)
.attr('y', 0)
.attr('height', 0)
.attr('width', 0)
.attr('height', 0);
vizBody = viz.append("svg:g")
.attr('id', 'body');
var heightCircleConstraint = 500 - margin.top - margin.bottom;
var widthCircleConstraint = width = 960 - margin.left - margin.right,
circleConstraint = d3.min([heightCircleConstraint, widthCircleConstraint]);
var radius = d3.scale.linear().domain([minVal, maxVal]).range([0, (circleConstraint / 2)]);
var radiusLength = radius(maxVal);
var centerXPos = widthCircleConstraint / 2 + margin.left;
var centerYPos = heightCircleConstraint / 2 + margin.top;
vizBody.attr("transform",
"translate(" + centerXPos + ", " + centerYPos + ")");
d3.json("data/radar.json", function(error, data) {
var hours = [];
var series = [[]];
data.QualitySummaryObject.forEach(function(d,i) {
series[0][i] = d.extractPercentage;
hours[i] = d.extractorName;
});
for (i = 0; i < series.length; i += 1) {
series[i].push(series[i][0]);
}
//console.log(series.length);
var radialTicks = radius.ticks(5);
vizBody.selectAll('.circle-ticks').remove();
vizBody.selectAll('.line-ticks').remove();
var circleAxes = vizBody.selectAll('.circle-ticks')
.data(radialTicks)
.enter().append('svg:g')
.attr("class", "circle-ticks");
circleAxes.append("svg:circle")
.attr("r", function (d, i) {
return radius(d);
})
.attr("class", "circle")
.style("stroke", ruleColor)
.style("fill", "none");
circleAxes.append("svg:text")
.attr("text-anchor", "middle")
.attr("dy", function (d) {
return -1 * radius(d);
})
.text(String);
var lineAxes = vizBody.selectAll('.line-ticks')
.data(hours)
.enter().append('svg:g')
.attr("transform", function (d, i) {
return "rotate(" + ((i / hours.length * 360) - 90) +
")translate(" + radius(maxVal) + ")";
})
.attr("class", "line-ticks");
lineAxes.append('svg:line')
.attr("x2", -1 * radius(maxVal))
.style("stroke", ruleColor)
.style("fill", "none");
lineAxes.append('svg:text')
.text(String)
.attr("text-anchor", "middle")
.attr("transform","translate(15) rotate(90)");
//var highlightedDotSize = 4;
var groups = vizBody.selectAll('.series').data(series);
//console.log(hours.length);
groups.enter().append("svg:g")
.attr('class', 'series')
.style('fill', "green")
.style('stroke',"#ccc");
//groups.exit().remove();
//console.log(Math.PI);
var lines = groups.append('svg:path')
.attr("class", "line")
/*.attr("d", d3.svg.line.radial()
.radius(function (d) {
return 10;
})
.angle(function (d, i) {
if (i == hours.length) {
i = 0;
} //close the line
return (i / hours.length) * 2 * Math.PI;
}))*/
.style("stroke-width", 1)
.style("fill", "rgba(124,240,10,0.1)");
/*groups.selectAll(".curr-point")
.data(function (d) {
return [d[0]];
})
.enter().append("svg:circle")
.attr("class", "curr-point")
.attr("r", 0);
groups.selectAll(".clicked-point")
.data(function (d) {
return [d[0]];
})
.enter().append("svg:circle")
.attr('r', 0)
.attr("class", "clicked-point");*/
lines.attr("d", d3.svg.line.radial()
.radius(function (d) {
return radius(d);
})
.angle(function (d, i) {
if (i === hours.length) {
i = 0;
} //close the line
return (i / hours.length) * 2 * Math.PI;
}));
});
i implemented this code to create radar chart with a json data here is the json data format
{
"QualitySummaryObject": [
{
"extractPercentage": 68.81964,
"extractorName": "Classification"
},
{
"extractPercentage": 74.09091,
"extractorName": "Keyword Match"
},
{
"extractPercentage": 54.62963,
"extractorName": "LocationBroadcast"
},
{
"extractPercentage": 98.91892,
"extractorName": "Qualification"
},
{
"extractPercentage": 98.76923,
"extractorName": "User Profile Location"
},
{
"extractPercentage": 80.15909,
"extractorName": "Valid Person Name"
},
]
}
Now i want to put tooltip on each node point .. but i am not able to get any idea how to do that any body can help?
Here is an example of Twitter Bootstrap tooltips running on SVGs with D3 http://markhansen.co.nz/stolen-vehicles-pt2/
To get it working on newer versions see Why doesn't Twitter Bootstrap popover work with SVG elements? You'll need to use a 2.3.0+ version of bootstrap or the fix I posted in that thread.

Categories