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>
Related
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();
}
})
})
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>
I'm working on a map project where we render a map using OSM tiles and d3-tile project. I'm trying to put markers on it. However projection(long,lat) returns weird values which misplaces the markers for instance -0.4777943611111111, -0.3832333211677277 for New York:
newyork = [-74.2605518, 40.6971478];
svg.selectAll("circle")
.data([newyork]).enter()
.append("circle")
.attr("cx", function (d) { console.log(projection(d)); return -projection(d)[0]; })
.attr("cy", function (d) { return -projection(d)[1]; })
. attr("r", "20px")
.attr("fill", "red")
Full source code below
<!DOCTYPE html>
<meta charset="utf-8">
<style>
body {
margin: 0;
}
</style>
<svg></svg>
<script src="https://d3js.org/d3.v4.min.js"></script>
<script src="https://unpkg.com/d3-tile#0.0.4/build/d3-tile.js"></script>
<script>
var tau = 2 * Math.PI;
var width = 960;
height = 500;
// Initialize the projection to fit the world in a 1×1 square centered at the origin.
var projection = d3.geoMercator()
.scale(1 / tau)
.translate([0, 0]);
var path = d3.geoPath()
.projection(projection);
var tile = d3.tile()
.size([width, height]);
var zoom = d3.zoom()
.on("zoom", zoomed);
var svg = d3.select("svg")
.attr("width", width)
.attr("height", height);
var raster = svg.append("g");
// Center at US
var center = projection([-98.5, 39.5]);
console.log("Center " + center[0]);
// Apply a zoom transform equivalent to projection.{scale,translate,center}.
svg.call(zoom)
.call(zoom.transform, d3.zoomIdentity
.translate(width / 2, height / 2)
.scale(1 << 12)
.translate(-center[0], -center[1]));
newyork = [-74.2605518, 40.6971478];
console.log(projection(newyork))
svg.selectAll("circle")
.data([newyork]).enter()
.append("circle")
.attr("cx", function (d) { console.log(projection(d)); return -projection(d)[0]; })
.attr("cy", function (d) { return -projection(d)[1]; })
. attr("r", "20px")
.attr("fill", "red")
function zoomed() {
var transform = d3.event.transform;
var tiles = tile
.scale(transform.k)
.translate([transform.x, transform.y])
();
var image = raster
.attr("transform", stringify(tiles.scale, tiles.translate))
.selectAll("image")
.data(tiles, function(d) {
return d;
});
image.exit().remove();
// enter:
var entered = image.enter().append("image");
// update:
image = entered.merge(image)
.attr('xlink:href', function(d) {
return 'http://' + 'abc' [d.y % 3] + '.tile.openstreetmap.org/' +
d.z + '/' + d.x + '/' + d.y + '.png';
})
.attr('x', function(d) {
return d.x * 256;
})
.attr('y', function(d) {
return d.y * 256;
})
.attr("width", 256)
.attr("height", 256);
}
function stringify(scale, translate) {
var k = scale / 256,
r = scale % 1 ? Number : Math.round;
return "translate(" + r(translate[0] * scale) + "," + r(translate[1] * scale) + ") scale(" + k + ")";
}
</script>
Any help is appreciated. Thanks!
For anyone looking for the answer found it here.: D3 cartography: lon/lat circles in wrong place on map (projection)
The trick is in the zoomed function transform the circle:
function zoomed() {
...
vector
.attr("transform", transform)
.attr("r", 5/transform.k);
...
}
Consider the following code
var width = 960,
height = 500;
var vertices = d3.range(100).map(function(d) {
return [Math.random() * width, Math.random() * height];
});
var voronoi = d3.geom.voronoi()
.clipExtent([[0, 0], [width, height]]);
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height)
.on("mousemove", function() { vertices[0] = d3.mouse(this); redraw(); });
var path = svg.append("g").selectAll("path");
svg.selectAll("circle")
.data(vertices.slice(1))
.enter().append("circle")
.attr("transform", function(d) { return "translate(" + d + ")"; })
.attr("r", 1.5);
redraw();
function redraw() {
path = path
.data(voronoi(vertices), polygon);
path.exit().remove();
path.enter().append("path")
.attr("class", function(d, i) { return "q" + (i % 9) + "-9"; })
.attr("d", polygon);
path.order();
}
function polygon(d) {
return "M" + d.join("L") + "Z";
}
How can I add a new Polygon with a CLICK & at the same time draw a center dot as well ?
You have a good start. In addition to the mousemove listener on the svg you also need a click listener. With this you can just add a new vertex each time the user clicks. I've done this by adding a variable to the redraw function to distinguish between redraws triggered by a click. You might be able to find a cleaner way to do this, but hopefully this helps!
var width = 960,
height = 500;
var vertices = d3.range(100).map(function(d) {
return [Math.random() * width, Math.random() * height];
});
var voronoi = d3.geom.voronoi()
.clipExtent([[0, 0], [width, height]]);
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height)
.on("mousemove", function() { vertices[0] = d3.mouse(this); redraw(); })
.on('click', function() {
vertices.push(d3.mouse(this));
redraw(true);
});
var path = svg.append("g").selectAll("path");
var circle = svg.selectAll("circle");
redraw();
function redraw(fromClick) {
var data = voronoi(vertices);
path = path
.data(data, polygon);
path.exit().remove();
path.enter().append("path")
.attr("class", function(d, i) { return "q" + (i % 9) + "-9"; })
.attr("d", polygon);
path.order();
circle = circle.data(vertices)
circle.attr("transform", function(d) { return "translate(" + d + ")"; })
circle.enter().append("circle")
.attr("transform", function(d) { return "translate(" + d + ")"; })
.attr("r", 1.5)
.attr('fill', fromClick ? 'white' : '')
circle.exit().remove();
}
function polygon(d) {
return d ? "M" + d.join("L") + "Z" : null;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
I am new to D3.js and I am drawing a bullet chart for a website I am working on. I took the code from here as a starting point:
http://bl.ocks.org/jugglinmike/6004102
I have a chart drawn, and for a basic case, I do not need to worry about switching the data shown. I really need to, however, have the entire chart and all of its elements scale with the window resize.
Right now, I have two files, bullet.js and draw_bullet.js. This is the code for draw_bullet.js:
var margin = {top: 5, right: 40, bottom: 20, left: 120},
width = ($(window).width() * .3) - margin.left - margin.right,
height = 50 - margin.top - margin.bottom;
var chart = d3.bullet()
.width(width)
.height(height);
function fillChart() {
d3.json("/static/response.json", function(error, data) {
var svg = d3.select("#zone1").selectAll("svg")
.data(data)
.enter().append("svg")
.attr("class", "bullet")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")")
.call(chart1);
var title = svg.append("g")
.style("text-anchor", "end")
.attr("transform", "translate(-6," + height / 2 + ")");
title.append("text")
.attr("class", "title")
.text(function(d) { return d.title; });
title.append("text")
.attr("class", "subtitle")
.attr("dy", "1em")
.text(function(d) { return d.subtitle; });
});
}
And my code for bullet.js:
d3.bullet = function() {
var orient = "left", // TODO top & bottom
reverse = false,
duration = 0,
ranges = bulletRanges,
measures = bulletMeasures,
width = parseInt(d3.select("#zone1").style("width"), 10),
height = 30,
tickFormat = null;
// For each small multiple…
function bullet(g) {
g.each(function(d, i) {
var rangez = ranges.call(this, d, i).slice().sort(d3.descending),
measurez = measures.call(this, d, i).slice().sort(d3.descending),
g = d3.select(this);
// Compute the new x-scale.
var x1 = d3.scale.linear()
.domain([0, Math.max(rangez[0], measurez[0])])
.range(reverse ? [width, 0] : [0, width]);
// Retrieve the old x-scale, if this is an update.
var x0 = this.__chart__ || d3.scale.linear()
.domain([0, Infinity])
.range(x1.range());
// Stash the new scale.
this.__chart__ = x1;
// Derive width-scales from the x-scales.
var w0 = bulletWidth(x0),
w1 = bulletWidth(x1);
// Update the range rects.
var range = g.selectAll("rect.range")
.data(rangez);
range.enter().append("rect")
.attr("class", function(d, i) { return "range s" + i; })
.attr("width", w0)
.attr("height", height)
.attr("x", reverse ? x0 : 0)
.transition()
.duration(duration)
.attr("width", w1)
.attr("x", reverse ? x1 : 0);
range.transition()
.duration(duration)
.attr("x", reverse ? x1 : 0)
.attr("width", w1)
.attr("height", height);
// Update the measure rects.
var measure = g.selectAll("rect.measure")
.data(measurez);
measure.enter().append("rect")
.attr("class", function(d, i) { return "measure s" + i; })
.attr("width", w0)
.attr("height", height / 3)
.attr("x", reverse ? x0 : 0)
.attr("y", height / 3)
.transition()
.duration(duration)
.attr("width", w1)
.attr("x", reverse ? x1 : 0);
measure.transition()
.duration(duration)
.attr("width", w1)
.attr("height", height / 3)
.attr("x", reverse ? x1 : 0)
.attr("y", height / 3);
// Compute the tick format.
var format = tickFormat || x1.tickFormat(8);
// Update the tick groups.
var tick = g.selectAll("g.tick")
.data(x1.ticks(8), function(d) {
return this.textContent || format(d);
});
// Initialize the ticks with the old scale, x0.
var tickEnter = tick.enter().append("g")
.attr("class", "tick")
.attr("transform", bulletTranslate(x0))
.style("opacity", 1e-6);
tickEnter.append("line")
.attr("y1", height)
.attr("y2", height * 7 / 6);
tickEnter.append("text")
.attr("text-anchor", "middle")
.attr("dy", "1em")
.attr("y", height * 7 / 6)
.text(format);
// Transition the entering ticks to the new scale, x1.
tickEnter.transition()
.duration(duration)
.attr("transform", bulletTranslate(x1))
.style("opacity", 1);
// Transition the updating ticks to the new scale, x1.
var tickUpdate = tick.transition()
.duration(duration)
.attr("transform", bulletTranslate(x1))
.style("opacity", 1);
tickUpdate.select("line")
.attr("y1", height)
.attr("y2", height * 7 / 6);
tickUpdate.select("text")
.attr("y", height * 7 / 6);
// Transition the exiting ticks to the new scale, x1.
tick.exit().transition()
.duration(duration)
.attr("transform", bulletTranslate(x1))
.style("opacity", 1e-6)
.remove();
});
d3.timer.flush();
}
// left, right, top, bottom
bullet.orient = function(x) {
if (!arguments.length) return orient;
orient = x;
reverse = orient == "right" || orient == "bottom";
return bullet;
};
// ranges (bad, satisfactory, good)
bullet.ranges = function(x) {
if (!arguments.length) return ranges;
ranges = x;
return bullet;
};
// measures (actual, forecast)
bullet.measures = function(x) {
if (!arguments.length) return measures;
measures = x;
return bullet;
};
bullet.width = function(x) {
if (!arguments.length) return width;
width = x;
return bullet;
};
bullet.height = function(x) {
if (!arguments.length) return height;
height = x;
return bullet;
};
bullet.tickFormat = function(x) {
if (!arguments.length) return tickFormat;
tickFormat = x;
return bullet;
};
bullet.duration = function(x) {
if (!arguments.length) return duration;
duration = x;
return bullet;
};
return bullet;
};
function bulletRanges(d) {
return d.ranges;
}
function bulletMeasures(d) {
return d.measures;
}
function bulletTranslate(x) {
return function(d) {
return "translate(" + x(d) + ",0)";
};
}
function bulletWidth(x) {
var x0 = x(0);
return function(d) {
return Math.abs(x(d) - x0);
};
}
})();
I am using jQuery and I know I have to embed a function in $(window).resize(), however I have tried many different things and none of them seem to adjust correctly. I try to set chart.width() to my new value in the function, and later when I call the width of the chart, it shows it as being my new value, but doesn't adjust its view on the screen. Is it necessary to redraw the entire chart and all its elements to resize? And then, also, when I tried to rescale the range for the chart, I found it very difficult to do so because the range variable is embedded in the anonymous function inside of bullet.js. Any help you can provide in pointing me in the right direction would be awesome. I tried using this tutorial, but it didn't seem to apply to my situation too much because they are different types of charts.
http://eyeseast.github.io/visible-data/2013/08/28/responsive-charts-with-d3/
Thanks!