d3 graticule with fill color? - javascript

I am trying to display a grid on a world map where each grid cell is filled with a color based on some data (e.g., temperature or humidity). I am trying to adapt the simple world map example here: http://techslides.com/demos/d3/d3-worldmap-boilerplate.html
I thought I might be able to use the built-in d3 graticule and add a fill color, like this:
g.append("path")
.datum(graticule)
.attr("class", "graticule")
.attr("d", path)
.style("fill", function(d, i) { return color(Math.floor((Math.random() * 20) + 1)); });
That doesn't work, though. Is there a way to fill in the grid cells generated by graticule? If not, what's the best way to go about overlaying a lat,long grid on the map with filled cells?

I created d3-grid-map to solve a specific problem of placing sparse global 0.5 degree grid cells on a d3 map by drawing on canvas layers. It should support other grid sizes with some effort. It handles a couple of forms of javascript typed array inputs, but it could use more generalization.

To do something like this, first create data set with all the N/S/E/W to define the limits.
var data set = [{W: -5.0, N: 50.0, E: 10.0, S: 40.0 }, {W: -95.0, N: 50.0, E: -40.0, S: 40.0 }];
Next post you load your world JSON add the path like this.
d3.json("http://techslides.com/demos/d3/data/world-topo.json", function(error, world) {
var countries = topojson.feature(world, world.objects.countries).features;
topo = countries;
draw(topo);
//iterate over the dataset created above for making paths.
dataset.forEach(function(bb){
var arc = d3.geo.graticule()
.majorExtent([[bb.W, bb.S], [bb.E, bb.N]])
//this will append the path to the g group so that it moves accordingly on translate/zoom
g.append("path")
.attr("class", "arc")
.attr("d", path(arc.outline()));
});
});
On Css add:
.arc {
fill: red;[![enter image description here][1]][1]
fill-opacity: 0.3;
stroke: black;
stroke-opacity: 0.5;
}
Full JS here:
d3.select(window).on("resize", throttle);
var zoom = d3.behavior.zoom()
.scaleExtent([1, 8])
.on("zoom", move);
var width = document.getElementById('container').offsetWidth-60;
var height = width / 2;
var dataset = [{W: -5.0, N: 50.0, E: 10.0, S: 40.0 }, {W: -95.0, N: 50.0, E: -40.0, S: 40.0 }];
var topo,projection,path,svg,g;
var tooltip = d3.select("#container").append("div").attr("class", "tooltip hidden");
setup(width,height);
function setup(width,height){
projection = d3.geo.mercator()
.translate([0, 0])
.scale(width / 2 / Math.PI);
path = d3.geo.path()
.projection(projection);
svg = d3.select("#container").append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")")
.call(zoom);
g = svg.append("g");
}
d3.json("http://techslides.com/demos/d3/data/world-topo.json", function(error, world) {
var countries = topojson.feature(world, world.objects.countries).features;
topo = countries;
draw(topo);
dataset.forEach(function(bb){
var arc = d3.geo.graticule()
.majorExtent([[bb.W, bb.S], [bb.E, bb.N]])
g.append("path")
.attr("class", "arc")
.attr("d", path(arc.outline()));
});
});
function draw(topo) {
var country = g.selectAll(".country").data(topo);
country.enter().insert("path")
.attr("class", "country")
.attr("d", path)
.attr("id", function(d,i) { return d.id; })
.attr("title", function(d,i) { return d.properties.name; })
.style("fill", function(d, i) { return d.properties.color; });
//ofsets plus width/height of transform, plsu 20 px of padding, plus 20 extra for tooltip offset off mouse
var offsetL = document.getElementById('container').offsetLeft+(width/2)+40;
var offsetT =document.getElementById('container').offsetTop+(height/2)+20;
//tooltips
country
.on("mousemove", function(d,i) {
var mouse = d3.mouse(svg.node()).map( function(d) { return parseInt(d); } );
tooltip
.classed("hidden", false)
.attr("style", "left:"+(mouse[0]+offsetL)+"px;top:"+(mouse[1]+offsetT)+"px")
.html(d.properties.name)
})
.on("mouseout", function(d,i) {
tooltip.classed("hidden", true)
});
}
function redraw() {
width = document.getElementById('container').offsetWidth-60;
height = width / 2;
d3.select('svg').remove();
setup(width,height);
draw(topo);
}
function move() {
var t = d3.event.translate;
var s = d3.event.scale;
var h = height / 3;
t[0] = Math.min(width / 2 * (s - 1), Math.max(width / 2 * (1 - s), t[0]));
t[1] = Math.min(height / 2 * (s - 1) + h * s, Math.max(height / 2 * (1 - s) - h * s, t[1]));
zoom.translate(t);
g.style("stroke-width", 1 / s).attr("transform", "translate(" + t + ")scale(" + s + ")");
}
var throttleTimer;
function throttle() {
window.clearTimeout(throttleTimer);
throttleTimer = window.setTimeout(function() {
redraw();
}, 200);
}
Image:

Related

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>

How to plot markers using OSM + d3-tile?

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);
...
}

Section Text label doesn't display in D3.JS drill down pie chart

I want to develop "drill down" "pie chart" using "D3.JS". I found the below sample, and perfect to me to use.
D3 Js sample drill down pie chart
The above sample is perfect fit for me.
Additionally from the sample drill down pie chart, I want to place TEXT LABEL in each section of pie chart divided.
I followed many samples,
Sample 1 - pie chart with section text label
Sample 2 - JSFiddle - pie chart with section text label
Based on the above samples for placing text label in pie chart sections, I followed the below code, I tried adding sample "var dataSet", "var arcs = svg.selectAll("g.slice")" code.
But, when I execute the program, it doesn't display any text label in centre of each section in pie chart.
Could someone guide me to fix this please?
<!doctype html>
<html>
<head>
<head>
<meta charset="utf-8">
<title>Drill down pie chart test</title>
<script type="text/javascript" src="http://d3js.org/d3.v2.min.js?2.9.6"></script>
<style type="text/css">
body {
text-align: center;
padding: 50px;
font-family: "Helvetica Neue",Arial,Sans-serif;
font-weight: 200;
color: #333;
}
.header {
font-size: 20px;
}
.sector {
cursor: pointer;
}
.slice text {
font-size: 16pt;
font-family: Arial;
}
</style>
</head>
<body>
<script type="text/javascript">
// Globals
var width = 500,
height = 400,
margin = 50,
radius = Math.min(width - margin, height - margin) / 2,
// Pie layout will use the "val" property of each data object entry
pieChart = d3.layout.pie().sort(null).value(function(d){return d.val;}),
arc = d3.svg.arc().outerRadius(radius),
MAX_SECTORS = 15, // Less than 20 please
colors = d3.scale.category20();
var dataSet = [
{"legendLabel":"One", "magnitude":20},
{"legendLabel":"Two", "magnitude":40},
{"legendLabel":"Three", "magnitude":50},
{"legendLabel":"Four", "magnitude":16},
{"legendLabel":"Five", "magnitude":50},
{"legendLabel":"Six", "magnitude":8},
{"legendLabel":"Seven", "magnitude":30}];
//mydata = {"Medical", "Agriculture", "Security"};
var st = {};
st.data = [{"label":"less than a week","value":169,"pos":0},{"label":"1 week - 30 days","value":1,"pos":1},{"label":"30 - 90 days","value":22,"pos":2},{"label":"90 - 180 days","value":35,"pos":3},{"label":"180 days - 1 year","value":47,"pos":4},{"label":"more than 1 year","value":783,"pos":5}] ;
// Synthetic data generation ------------------------------------------------
var data = [];
var numSectors = 8; //Math.ceil(Math.random()*MAX_SECTORS);
for(i = -1; i++ < numSectors; ) {
var children = [];
var numChildSectors = Math.ceil(Math.random()*MAX_SECTORS);
var color = colors(i);
for( j=-1; j++ < numChildSectors; ){
// Add children categories with shades of the parent color
children.push(
{ cat: "cat"+((i+1)*100+j),
val: Math.random(),
color: d3.rgb(color).darker(1/(j+1))
});
}
data.push({
cat: "cat"+i,
val: Math.random(),
color: color,
children: children});
}
// --------------------------------------------------------------------------
// SVG elements init
var svg = d3.select("body").append("svg").data([dataSet]).attr("width", width).attr("height", height),
defs = svg.append("svg:defs"),
// .data(pieChart)
// Declare a main gradient with the dimensions for all gradient entries to refer
mainGrad = defs.append("svg:radialGradient")
.attr("gradientUnits", "userSpaceOnUse")
.attr("cx", 0).attr("cy", 0).attr("r", radius).attr("fx", 0).attr("fy", 0)
.attr("id", "master"),
// The pie sectors container
arcGroup = svg.append("svg:g")
.attr("class", "arcGroup")
.attr("filter", "url(#shadow)")
.attr("transform", "translate(" + (width / 2) + "," + (height / 2) + ")"),
// Header text
header = svg.append("text").text("Biotechnology")
.attr("transform", "translate(10, 20)").attr("class", "header");
//svg.append("text").attr("text-anchor", "middle").text("$" + "sample"),
//svg.append("text").text("sample").attr("text-anchor", "middle")
/*svg.append("text")
.attr("transform", "translate(" + arcGroup.centroid(d) + ")")
.attr("dy", ".35em")
.attr("text-anchor", "middle")
.text("sample");
*/
// Declare shadow filter
var shadow = defs.append("filter").attr("id", "shadow")
.attr("filterUnits", "userSpaceOnUse")
.attr("x", -1*(width / 2)).attr("y", -1*(height / 2))
.attr("width", width).attr("height", height);
shadow.append("feGaussianBlur")
.attr("in", "SourceAlpha")
.attr("stdDeviation", "4")
.attr("result", "blur");
shadow.append("feOffset")
.attr("in", "blur")
.attr("dx", "4").attr("dy", "4")
.attr("result", "offsetBlur");
shadow.append("feBlend")
.attr("in", "SourceGraphic")
.attr("in2", "offsetBlur")
.attr("mode", "normal");
/* var arcs = svg.selectAll("g.slice")
arcs.append("text")
.attr("transform", function(d) { return "translate(" + arc.centroid(d) + ")"; })
.attr("dy", ".35em")
.attr("text-anchor", "middle")
.text(function(d) { return d.value; });
*/
// Redraw the graph given a certain level of data
function updateGraph(cat){
var currData = data;
// Simple header text
if(cat != undefined){
currData = findChildenByCat(cat);
d3.select(".header").text("Biotechnology → "+cat);
} else {
d3.select(".header").text("Biotechnology");
}
// Create a gradient for each entry (each entry identified by its unique category)
var gradients = defs.selectAll(".gradient").data(currData, function(d){return d.cat;});
gradients.enter().append("svg:radialGradient")
.attr("id", function(d, i) { return "gradient" + d.cat; })
.attr("class", "gradient")
.attr("xlink:href", "#master");
gradients.append("svg:stop").attr("offset", "0%").attr("stop-color", getColor );
gradients.append("svg:stop").attr("offset", "90%").attr("stop-color", getColor );
gradients.append("svg:stop").attr("offset", "100%").attr("stop-color", getDarkerColor );
/*var arcs = defs.selectAll("g.slice").data(pie).enter().append("svg:g").attr("class","slice");
arcs.append("svg:text").attr("transform", function(d){
d.innerRadius = 0;
d.outerRadius = r;
return "translate(" + arc.centroid(d) + ")";}).attr("text-anchor", "middle").text( function(d, i) {
return (data[i].value / tot ) * 100 > 10 ? ((data[i].value / tot ) * 100).toFixed(1) + "%" : "";
}
).attr("fill","#fff")
.classed("slice-label",true);
*/
// Create a sector for each entry in the enter selection
var paths = arcGroup.selectAll("path")
.data(pieChart(currData), function(d) {return d.data.cat;} );
paths.enter().append("svg:path").attr("class", "sector");
// Each sector will refer to its gradient fill
paths.attr("fill", function(d, i) { return "url(#gradient"+d.data.cat+")"; })
.transition().duration(1000).attrTween("d", tweenIn).each("end", function(){
this._listenToEvents = true;
});
// Mouse interaction handling
paths.on("click", function(d){
if(this._listenToEvents){
// Reset inmediatelly
d3.select(this).attr("transform", "translate(0,0)")
// Change level on click if no transition has started
paths.each(function(){
this._listenToEvents = false;
});
updateGraph(d.data.children? d.data.cat : undefined);
}
})
.on("mouseover", function(d){
// Mouseover effect if no transition has started
if(this._listenToEvents){
// Calculate angle bisector
var ang = d.startAngle + (d.endAngle - d.startAngle)/2;
// Transformate to SVG space
ang = (ang - (Math.PI / 2) ) * -1;
// Calculate a 10% radius displacement
var x = Math.cos(ang) * radius * 0.1;
var y = Math.sin(ang) * radius * -0.1;
d3.select(this).transition()
.duration(250).attr("transform", "translate("+x+","+y+")");
}
})
.on("mouseout", function(d){
// Mouseout effect if no transition has started
if(this._listenToEvents){
d3.select(this).transition()
.duration(150).attr("transform", "translate(0,0)");
}
});
// Collapse sectors for the exit selection
paths.exit().transition()
.duration(1000)
.attrTween("d", tweenOut).remove();
// NEWLY ADDED START
// Select all <g> elements with class slice (there aren't any yet)
var arcs = svg.selectAll("g.slice")
// Associate the generated pie data (an array of arcs, each having startAngle,
// endAngle and value properties)
.data(pie)
// This will create <g> elements for every "extra" data element that should be associated
// with a selection. The result is creating a <g> for every object in the data array
.enter()
// Create a group to hold each slice (we will have a <path> and a <text>
// element associated with each slice)
.append("svg:g")
.attr("class", "slice"); //allow us to style things in the slices (like text)
arcs.append("svg:path")
//set the color for each slice to be chosen from the color function defined above
.attr("fill", function(d, i) { return color(i); } )
//this creates the actual SVG path using the associated data (pie) with the arc drawing function
.attr("d", arc);
// Add a legendLabel to each arc slice...
arcs.append("svg:text")
.attr("transform", function(d) { //set the label's origin to the center of the arc
//we have to make sure to set these before calling arc.centroid
d.outerRadius = outerRadius + 50; // Set Outer Coordinate
d.innerRadius = outerRadius + 45; // Set Inner Coordinate
return "translate(" + arc.centroid(d) + ")";
})
.attr("text-anchor", "middle") //center the text on it's origin
.style("fill", "Purple")
.style("font", "bold 12px Arial")
<!-- .text(function(d, i) { return dataSet[i].legendLabel; }); //get the label from our original dat -->
.text(function(d, i) { return "Test"; }); //get the label from our original dat
// Add a magnitude value to the larger arcs, translated to the arc centroid and rotated.
arcs.filter(function(d) { return d.endAngle - d.startAngle > .2; }).append("svg:text")
.attr("dy", ".35em")
.attr("text-anchor", "middle")
//.attr("transform", function(d) { return "translate(" + arc.centroid(d) + ")rotate(" + angle(d) + ")"; })
.attr("transform", function(d) { //set the label's origin to the center of the arc
//we have to make sure to set these before calling arc.centroid
d.outerRadius = outerRadius; // Set Outer Coordinate
d.innerRadius = outerRadius/2; // Set Inner Coordinate
return "translate(" + arc.centroid(d) + ")rotate(" + angle(d) + ")";
})
.style("fill", "White")
.style("font", "bold 12px Arial")
.text(function(d) { return d.data.magnitude; });
function angle(d) {
var a = (d.startAngle + d.endAngle) * 90 / Math.PI - 90;
return a > 90 ? a - 180 : a;
}
// END
}
// "Fold" pie sectors by tweening its current start/end angles
// into 2*PI
function tweenOut(data) {
data.startAngle = data.endAngle = (2 * Math.PI);
var interpolation = d3.interpolate(this._current, data);
this._current = interpolation(0);
return function(t) {
return arc(interpolation(t));
};
}
// "Unfold" pie sectors by tweening its start/end angles
// from 0 into their final calculated values
function tweenIn(data) {
var interpolation = d3.interpolate({startAngle: 0, endAngle: 0}, data);
this._current = interpolation(0);
return function(t) {
return arc(interpolation(t));
};
}
// Helper function to extract color from data object
function getColor(data, index){
return data.color;
}
// Helper function to extract a darker version of the color
function getDarkerColor(data, index){
return d3.rgb(getColor(data, index)).darker();
}
function findChildenByCat(cat){
for(i=-1; i++ < data.length - 1; ){
if(data[i].cat == cat){
return data[i].children;
}
}
return data;
}
//.text(function(d, i) { return categorydata[i].label; });
// Start by updating graph at root level
updateGraph();
</script>
<!-- <p>Drill down pie chart test by Marc Baiges Camprubí (marcbc#gmail.com) in D3.js -->
</body>
</html>
Instead of doing this:
var arcs = svg.selectAll("g.slice")
do this
var arcs = arcGroup.selectAll("g.slice")
reason so that the text label and the path of the pie all are in same group.
Give proper inner and outer radius for placing the labels in the center (so that the centroid is calculated on basis of the new inner outer radius of the arc)
arcs.append("svg:text")
.attr("transform", function(d) { //set the label's origin to the center of the arc
//we have to make sure to set these before calling arc.centroid
d.outerRadius = radius - 20; // Set Outer Coordinate
d.innerRadius = radius - 100; // Set Inner Coordinate
return "translate(" + arc.centroid(d) + ")";
})
Next give proper data in the text:
.text(function(d, i) { return "Test"; }); //get the label from our original data
do this
.text(function(d, i) { return d.data.cat; }); //get the label from our original data
working code here

How to Access Angular Controller Scope from Within Embedded D3 Script

I have an SVG map that is being rendered with D3 embedded within an HTML document. I know this is NOT the way to do this. Anyway, I'd like my Angular controller to be alerted when a country in the map is clicked. I have access to this data, and can console log all of the information related to a given country upon click, but I haven't figured out how to access the controller scope from within the D3 script. Any ideas?
HTML file, if it helps:
<html>
<head>
<meta charset="utf-8" />
<link rel="stylesheet" type="text/css" href="../styles/style.css">
<script src="js/d3.min.js"></script>
<script src="js/topojson.v1.min.js"></script>
</head>
<body ng-app='myApp'>
<div class="main" ng-controller="mainController">
<div countryClick="selectCountry()" id="container" ng-controller="mainController">
<script type='text/javascript'>
var dom_el = document.querySelector('[ng-controller="mainController"]');
var ng_el = angular.element(dom_el);
var ng_el_scope = ng_el.scope();
var things = ng_el_scope.things;
// d3.select(window).on("resize", throttle);
var zoom = d3.behavior.zoom()
.scaleExtent([1, 9]);
// .on("zoom", move); // Disables user move and zoom without breaking everything
var width = document.getElementById('container').offsetWidth;
var height = width / 2.5; //Originally 2
var topo,projection,path,svg,g;
var graticule = d3.geo.graticule();
var tooltip = d3.select("#container").append("div").attr("class", "tooltip hidden");
setup(width,height);
// debugger;
function setup(width,height){
// debugger;
projection = d3.geo.mercator()
.translate([(width/2), (height/2)])
.scale( width / 2 / Math.PI);
path = d3.geo.path().projection(projection);
svg = d3.select("#container").append("svg")
.attr("width", width)
.attr("height", height)
.call(zoom)
.on("click", click)
.append("g");
g = svg.append("g");
}
d3.json("data/world-topo-min.json", function(error, world) {
var countries = topojson.feature(world, world.objects.countries).features;
topo = countries;
draw(topo);
});
function draw(topo) {
// Draws equator
g.append("path")
.datum({type: "LineString", coordinates: [[-180, 0], [-90, 0], [0, 0], [90, 0], [180, 0]]})
.attr("class", "equator")
.attr("d", path);
var country = g.selectAll(".country").data(topo);
country.enter().insert("path")
.attr("class", "country")
.attr("d", path)
.attr("id", function(d,i) { return d.id; })
.attr("title", function(d,i) { return d.properties.name; })
.style("fill", "#F8F8F8")
.style("stroke", "gray")
//offsets for tooltips
var offsetL = document.getElementById('container').offsetLeft+20;
var offsetT = document.getElementById('container').offsetTop+10;
//tooltips
country
.on("mousemove", function(d,i) {
console.log('mousemove',d);
var mouse = d3.mouse(svg.node()).map( function(d) { return parseInt(d); } );
tooltip.classed("hidden", false)
.attr("style", "left:"+(mouse[0]+offsetL)+"px;top:"+(mouse[1]+offsetT)+"px")
.html(d.properties.name);
})
.on("mouseout", function(d,i) {
tooltip.classed("hidden", true);
})
.on("click", function (d,i) {
//THIS IS WHERE I WANT TO REGISTER THE CLICK WITH MY ANGULAR CONTROLLER
console.log(d.properties.name); //Logs country name
});
}
function redraw() {
width = document.getElementById('container').offsetWidth;
height = width / 2;
d3.select('svg').remove();
setup(width,height);
draw(topo);
}
function move() {
var t = d3.event.translate;
var s = d3.event.scale;
zscale = s;
var h = height/4;
console.log('moving with t: ' + t + ' ,s: ,' + s + ' and h: ' + h);
t[0] = Math.min(
(width/height) * (s - 1),
Math.max( width * (1 - s), t[0] )
);
t[1] = Math.min(
h * (s - 1) + h * s,
Math.max(height * (1 - s) - h * s, t[1])
);
zoom.translate(t);
g.attr("transform", "translate(" + t + ")scale(" + s + ")");
//Removed this because it screws things up when there is an initial stroke set on countries:
//adjust the country hover stroke width based on zoom level
// d3.selectAll(".country").style("stroke-width", 1.5 / s);
}
var throttleTimer;
function throttle() {
window.clearTimeout(throttleTimer);
throttleTimer = window.setTimeout(function() {
redraw();
}, 200);
}
//geo translation on mouse click in map
function click() {
var latlon = projection.invert(d3.mouse(this));
console.log(latlon);
}
//function to add points and text to the map (used in plotting capitals)
function addpoint(lat,lon,text) {
var gpoint = g.append("g").attr("class", "gpoint");
var x = projection([lat,lon])[0];
var y = projection([lat,lon])[1];
gpoint.append("svg:circle")
.attr("cx", x)
.attr("cy", y)
.attr("class","point")
.attr("r", 1.5);
//conditional in case a point has no associated text
if(text.length>0){
gpoint.append("text")
.attr("x", x+2)
.attr("y", y+2)
.attr("class","text")
.text(text);
}
}
</script>
</div>
</div>
<script src='../../bower_components/angular/angular.js'></script>
<script src='../../bower_components/angular-ui-router/release/angular-ui-router.js'></script>
<script src='../js/app.js'></script>
</body>
</html>
I followed this tutorial to integrate my d3 code into an angular directive: http://www.ng-newsletter.com/posts/d3-on-angular.html
It takes about 15 minutes and will make your d3/angular integration much easier in the long run.
If you don't have 15 minutes than you can try something like this: AngularJS. How to call controller function from outside of controller component

Select d3 node by its datum

I’d like to select a node in a callback without using d3.select(this).
I have some code that draws a pie…
function drawPie(options) {
options || (options = {});
var data = options.data || [],
element = options.element,
radius = options.radius || 100,
xOffset = Math.floor(parseInt(d3.select(element).style('width'), 10) / 2),
yOffset = radius + 20;
var canvas = d3.select(element)
.append("svg:svg")
.data([data])
.attr("width", options.width)
.attr("height", options.height)
.append("svg:g")
.attr("transform", "translate(" + xOffset + "," + yOffset + ")");
var arc = d3.svg.arc()
.outerRadius(radius);
var pie = d3.layout.pie()
.value(function(data) {
return data.percentageOfSavingsGoalValuation;
});
var arcs = canvas.selectAll("g.slice")
.data(pie)
.enter()
.append("svg:g")
.attr("class", "slice");
arcs.append("svg:path")
.on("mouseover", divergeSlice);
You’ll notice at the end I have a call to divergeSlice(). That looks like this:
function divergeSlice(datum, index) {
var angle = (datum.endAngle + datum.startAngle) / 2,
x = Math.sin(angle) * 10,
y = -Math.cos(angle) * 10;
d3.select(this)
.transition()
.attr("transform", "translate(" + x + ", " + y + ")");
}
This works, but I’d like to accomplish this without using this as I mentioned earlier. When I log the datum object, I get something like the following:
{
data: {
uniqueID: "XX00X0XXXX00"
name: "Name of value"
percentageOfValuation: 0.4
totalNetAssetValue: 0
}
endAngle: 5.026548245743669
innerRadius: 80
outerRadius: 120
startAngle: 2.5132741228718345
value: 0.4
}
How could I use d3.select() to find a path that holds datum.data.uniqueID that is equal to "XX00X0XXXX00"?
You can't do this directly with .select() as that uses DOM selectors. What you can do is select all the candidates and then filter:
d3.selectAll("g")
.filter(function(d) { return d.data.uniqueID === myDatum.data.uniqueID; });
However, it would be much easier to simply assign this ID as an ID to the DOM element and then select based on that:
var arcs = canvas.selectAll("g.slice")
.data(pie)
.enter()
.append("svg:g")
.attr("id", function(d) { return d.data.uniqueID; })
.attr("class", "slice");
d3.select("#" + myDatum.data.uniqueID);

Categories