D3 cartography: lon/lat circles in wrong place on map (projection) - d3.js

I have a question about D3 cartography.
I am working on a little project and I am new to D3.
I have started out from this example: http://bl.ocks.org/mbostock/5914438
Instead of the showing the state-mesh, I would like to show circles on the map in certain locations (lon/lat). I am currently facing a problem that the circles are not on the correct spots on the map. I suspect the problem lies in the special projection that Mike uses. He uses a 1x1 square projection. Probably this is necessary for displaying the tiles. When I project the coordinates, the values are all between -1 and 1. I thought I could fix it by multiplying it width the height and width but it didn't work. Below is my code (snippet does not run because it is missing a file). Thanks for the assistance!
<!DOCTYPE html>
<meta charset="utf-8">
<style>
body {
margin: 0;
}
path {
fill: none;
stroke: red;
stroke-linejoin: round;
stroke-width: 1.5px;
}
circle {
fill: #fff;
fill-opacity: 0.4;
stroke: #111;
}
</style>
<svg>
</svg>
<script src="//d3js.org/d3.v4.min.js"></script>
<script src="//d3js.org/d3-tile.v0.0.min.js"></script>
<script src="//d3js.org/topojson.v1.min.js"></script>
<script>
var pi = Math.PI,
tau = 2 * pi;
var width = Math.max(960, window.innerWidth),
height = Math.max(500, window.innerHeight);
// 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()
.scaleExtent([1 << 9, 1 << 23])
.on("zoom", zoomed);
var svg = d3.select("svg")
.attr("width", width)
.attr("height", height);
var raster = svg.append("g");
var vector = svg.append("path");
var circle = svg.append("g")
d3.json("/data/flyingsites/AD.json", function(error, flyingsites) {
if (error) console.log(error);
// Compute the projected initial center.
var center = projection([6.2, 45.8]);//45,809718, 6,252314
// 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]));
//add flying sites
circle.selectAll("circle")
.data(flyingsites.features)
.enter().append("circle")
.attr('r',5)
.attr('cx',function(d) { return projection(d.geometry.coordinates)[0]*width})
.attr('cy',function(d) { return projection(d.geometry.coordinates)[1]*height})
.style('fill','red')
//console.log(flyingsites.features);
//console.log(circle);
});
function zoomed() {
var transform = d3.event.transform;
var tiles = tile
.scale(transform.k)
.translate([transform.x, transform.y])
();
vector
.attr("transform", transform)
.style("stroke-width", 1 / transform.k);
circle
.attr("transform", "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();
image.enter().append("image")
.attr("xlink:href", function(d) { return "http://" + "abc"[d[1] % 3] + ".tile.openstreetmap.org/" + d[2] + "/" + d[0] + "/" + d[1] + ".png"; })
.attr("x", function(d) { return d[0] * 256; })
.attr("y", function(d) { return d[1] * 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>

The approach you are taking won't work, for one, it doesn't consider the scale (just translate). This is critical as d3-tile uses geometric zooming - it applies a zoom transform (scale and translate) to all the vector elements (not the tiles), this is why the projection projects everything to a one pixel square area and never changes with the zoom.
To solve this, place your circles the same as the example places (and sizes) the polygons:
vector
.attr("d", path(topojson.mesh(us, us.objects.counties)));
circle
.attr("cx", projection(coord)[0])
.attr("cy", projection(coord)[1])
.attr("r", 5/(1<<12));
Both of these position features the same way: with the projection only, projecting to a one pixel square. The zoom applies the transform to cover the whole svg. Also, since we are scaling that one pixel to fit the svg, we want the radius to be scaled appropriately too.
Now we can apply a transform to the circles the same as the polygons:
circle
.attr("transform", transform);
Of course, we could scale the radius down each zoom too, using the zoom k to modify the size of the circle:
var pi = Math.PI,
tau = 2 * pi;
var width = Math.max(960, window.innerWidth),
height = Math.max(500, window.innerHeight);
// 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()
.scaleExtent([1 << 11, 1 << 14])
.on("zoom", zoomed);
var svg = d3.select("svg")
.attr("width", width)
.attr("height", height);
var raster = svg.append("g");
var vector = svg.append("circle");
// Compute the projected initial center.
var center = projection([-98.5, 39.5]);
// 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]));
var coord = [-100,40]
vector
.attr("cx", projection(coord)[0])
.attr("cy", projection(coord)[1])
.attr("r", 5/(1<<12));
function zoomed() {
var transform = d3.event.transform;
var tiles = tile
.scale(transform.k)
.translate([transform.x, transform.y])
();
vector
.attr("transform", transform)
.attr("r", 5/transform.k);
var image = raster
.attr("transform", stringify(tiles.scale, tiles.translate))
.selectAll("image")
.data(tiles, function(d) { return d; });
image.exit().remove();
image.enter().append("image")
.attr("xlink:href", function(d) { return "http://" + "abc"[d[1] % 3] + ".tile.openstreetmap.org/" + d[2] + "/" + d[0] + "/" + d[1] + ".png"; })
.attr("x", function(d) { return d[0] * 256; })
.attr("y", function(d) { return d[1] * 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 + ")";
}
<svg></svg>
<script src="https://d3js.org/d3.v4.min.js"></script>
<script src="https://d3js.org/d3-tile.v0.0.min.js"></script>
<script src="https://d3js.org/topojson.v1.min.js"></script>
Ultimately d3-tile can be a bit confusing to start with because you are using quite a few coordinate systems (tile, zoom, pixel, projected, geographic), and normally aren't projecting the entire map to a 1 pixel square.

Related

d3 incompatible types - version conflict?

I am starting out with d3 and would like to test it in my angular project. I've tried to run this doughnut chart from a reputable source: https://d3-graph-gallery.com/graph/donut_label.html
I am experiencing significant problems with incompatible types, even though I selected v6 on page and I am using d3 v6.0.0 on my machine. For example the line :
const data_ready = pie(Object.entries(data))
gives complaint that:
Argument of type [string, number][] is not assignable to parameter of type (number|{valueOf():number;})[]
Moving forward at
.attr('d', arc)
complains that no overload matches this call
in package.json I have:
dependencies:{
"d3": "6.0.0",
"d3-scale": "^4.0.2",
...
},
devDependencies:{
"#types/d3": "6.0.0",
"#types/d3-scale": "^4.0.2",
...
}
Usually it is a red flag when examples don't work, but the source seem reputable so I am asking for additional debugging help. Is this a problem with #types configuration? How should the code look like? Complete code:
ngAfterViewInit(){
// set the dimensions and margins of the graph
var width = 450
height = 450
margin = 40
// The radius of the pieplot is half the width or half the height (smallest one). I subtract a bit of margin.
var radius = Math.min(width, height) / 2 - margin
// append the svg object to the div called 'my_dataviz'
var svg = d3.select("#my_dataviz")
.append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")");
// Create dummy data
var data = {a: 9, b: 20, c:30, d:8, e:12, f:3, g:7, h:14}
// set the color scale
var color = d3.scaleOrdinal()
.domain(["a", "b", "c", "d", "e", "f", "g", "h"])
.range(d3.schemeDark2);
// Compute the position of each group on the pie:
var pie = d3.pie()
.sort(null) // Do not sort group by size
.value(function(d) {return d.value; })
var data_ready = pie(d3.entries(data))
// The arc generator
var arc = d3.arc()
.innerRadius(radius * 0.5) // This is the size of the donut hole
.outerRadius(radius * 0.8)
// Another arc that won't be drawn. Just for labels positioning
var outerArc = d3.arc()
.innerRadius(radius * 0.9)
.outerRadius(radius * 0.9)
// Build the pie chart: Basically, each part of the pie is a path that we build using the arc function.
svg
.selectAll('allSlices')
.data(data_ready)
.enter()
.append('path')
.attr('d', arc)
.attr('fill', function(d){ return(color(d.data.key)) })
.attr("stroke", "white")
.style("stroke-width", "2px")
.style("opacity", 0.7)
// Add the polylines between chart and labels:
svg
.selectAll('allPolylines')
.data(data_ready)
.enter()
.append('polyline')
.attr("stroke", "black")
.style("fill", "none")
.attr("stroke-width", 1)
.attr('points', function(d) {
var posA = arc.centroid(d) // line insertion in the slice
var posB = outerArc.centroid(d) // line break: we use the other arc generator that has been built only for that
var posC = outerArc.centroid(d); // Label position = almost the same as posB
var midangle = d.startAngle + (d.endAngle - d.startAngle) / 2 // we need the angle to see if the X position will be at the extreme right or extreme left
posC[0] = radius * 0.95 * (midangle < Math.PI ? 1 : -1); // multiply by 1 or -1 to put it on the right or on the left
return [posA, posB, posC]
})
// Add the polylines between chart and labels:
svg
.selectAll('allLabels')
.data(data_ready)
.enter()
.append('text')
.text( function(d) { console.log(d.data.key) ; return d.data.key } )
.attr('transform', function(d) {
var pos = outerArc.centroid(d);
var midangle = d.startAngle + (d.endAngle - d.startAngle) / 2
pos[0] = radius * 0.99 * (midangle < Math.PI ? 1 : -1);
return 'translate(' + pos + ')';
})
.style('text-anchor', function(d) {
var midangle = d.startAngle + (d.endAngle - d.startAngle) / 2
return (midangle < Math.PI ? 'start' : 'end')
})
}

How to use D3 zoom behavior with ViewBox instead of transform

I would like to take advantage of D3's zoom behavior functionality, but I need to do all translations/scaling of my SVG using the viewBox property instead of the transform method as shown in the D3 example: http://bl.ocks.org/mbostock/3680999
How can I achieve this same scale/translate using only the viewBox? Here's my code so far, which doesn't work well like the transform method.
function zoomed(d) {
if (!scope.drawLine) {
var scale = d3.event.scale;
var translation = d3.event.translate;
//This works, but I can't use it for reason's I won't go into now
//mapSVG_G.attr("transform", "translate(" + d3.event.translate + ")scale(" + d3.event.scale + ")");
var newViewBox = [
initialViewBox[0] - translation[0],
initialViewBox[1] - translation[1],
initialViewBox[2]/scale,
initialViewBox[3]/scale
];
mapSVG.attr('viewBox', newViewBox);
}
}
a bit off, but could serve you as a start:
main piece:
var newViewBox = [
-translate[0] / scale,
-translate[1] / scale,
width / scale,
height / scale
].join(" ");
whole example:
var width = 960,
height = 500;
var randomX = d3.random.normal(width / 2, 80),
randomY = d3.random.normal(height / 2, 80);
var data = d3.range(2000).map(function() {
return [
randomX(),
randomY()
];
});
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height)
.attr("viewBox", [0, 0, width, height].join(" "))
var vis = svg.append("g")
.call(d3.behavior.zoom().scaleExtent([1, 8]).on("zoom", zoom))
.append("g");
vis.append("rect")
.attr("class", "overlay")
.attr("width", width)
.attr("height", height);
vis.selectAll("circle")
.data(data)
.enter().append("circle")
.attr("r", 2.5)
.attr("transform", function(d) {
return "translate(" + d + ")";
});
function zoom() {
var scale = d3.event.scale;
var translate = d3.event.translate;
var newViewBox = [
-translate[0] / scale,
-translate[1] / scale,
width / scale,
height / scale
].join(" ");
svg.attr('viewBox', newViewBox);
}
.overlay {
fill: none;
pointer-events: all;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>

How to solve duplicate label in zoom

i have a problem when i do a zoom in my map because the labels appears duplicate. I know that my problem appears because in my zoom i dont delete the label. I know where is the problem, i need to delete the oldest label when i do a zoom but i dont know how and where to solve this.
Any idea? Ty for all.
<!DOCTYPE html>
<meta charset="utf-8">
<style>
body {
margin: 0;
}
path {
fill: none;
stroke: green;
stroke-linejoin: round;
stroke-width: 1.5px;
}
</style>
<body>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.5/d3.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/topojson/1.6.19/topojson.min.js"></script>
<script src="d3.geo.tile.min.js"></script>
<script>
var width = Math.max(960, window.innerWidth),
height = Math.max(600, window.innerHeight);
var tile = d3.geo.tile()
.size([width, height]);
var projection = d3.geo.mercator()
.scale((3 << 12) / 2 / Math.PI)
.translate([width / 2, height / 2]);
var center = projection([-3, 36]);
var path = d3.geo.path()
.projection(projection);
var zoom = d3.behavior.zoom()
.scale(projection.scale() * 2 * Math.PI)
.translate([width - center[0], height - center[1]])
.on("zoom", zoomed);
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
var raster = svg.append("g");
var g = svg.append("g");
var vector = svg.append("path");
d3.json("es.json", function(error, es) {
if (error) throw error;
svg.call(zoom);
vector.datum(topojson.mesh(es, es.objects.provinces));
zoomed();
});
function zoomed() {
d3.csv("data/country-capitals.csv", function(err, capitals) {
capitals.forEach(function(i){
addpoint(i.CapitalLongitude, i.CapitalLatitude, i.CapitalName );
});
});
var tiles = tile
.scale(zoom.scale())
.translate(zoom.translate())
();
projection
.scale(zoom.scale() / 2 / Math.PI)
.translate(zoom.translate());
vector
.attr("d", path);
var image = raster
.attr("transform", "scale(" + tiles.scale + ")translate(" + tiles.translate + ")")
.selectAll("image")
.data(tiles, function(d) { return d; });
image.exit()
.remove();
image.enter().append("image")
.attr("xlink:href", function(d) { return "http://" + ["a", "b", "c"][Math.random() * 3 | 0] + ".tile.openstreetmap.org/" + d[2] + "/" + d[0] + "/" + d[1] + ".png"; })
.attr("width", 1)
.attr("height", 1)
.attr("x", function(d) { return d[0]; })
.attr("y", function(d) { return d[1]; });
}
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>
CSV is here:
CountryName,CapitalName,CapitalLatitude,CapitalLongitude,CountryCode,ContinentName
Brazil,Brasilia,-15.783333333333333,-47.916667,BR,South America
Colombia,Bogota,4.6,-74.083333,CO,South America
Egypt,Cairo,30.05,31.250000,EG,Africa
France,Paris,48.86666666666667,2.333333,FR,Europe
Iraq,Baghdad,33.333333333333336,44.400000,IQ,Asia
South Korea,Seoul,37.55,126.983333,KR,Asia
Kosovo,Pristina,42.666666666666664,21.166667,KO,Europe
Mexico,Mexico City,19.433333333333334,-99.133333,MX,Central America
Before you zoom you can remove all the group containing text and circle like this:
function zoomed() {
d3.selectAll(".gpoint").remove();
d3.csv("my.csv", function(err, capitals) {
capitals.forEach(function(i){
addpoint(i.CapitalLongitude, i.CapitalLatitude, i.CapitalName );
});
});
//your code
Working code here

D3 pie chart element popout

I have created a simple pie chart using D3.js and I wish to pop out each element/path of the pie chart on click event of those elements.
Here is the pie chart I am talking about: jsfiddle.net/ankur881120/kt97oq57.
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) + ")";
Now I want to pop out say element in red color on click of red color element.
Looking for all of your suggestions, to solve this issue.
I just answered a very similar question about this yesterday. Your use case is different enough, so against my better judgement, I'll answer it again.
Essentially, add the click handler and transition your arc "group" (arc and text labels) together:
var arcs = vis.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)
// ADDED CLICK HANDLER
.on('click',function(d,i){
d3.select(this)
.transition()
.duration(500)
.attr("transform",function(d){
// this this group expanded out?
if (!d.data._expanded){
d.data._expanded = true;
var a = d.startAngle + (d.endAngle - d.startAngle)/2 - Math.PI/2;
var x = Math.cos(a) * 20;
var y = Math.sin(a) * 20;
// move it away from the circle center
return 'translate(' + x + ',' + y + ')';
} else {
d.data._expanded = false;
// move it back
return 'translate(0,0)';
}
});
});
Updated fiddle.
Complete code:
var canvasWidth = 300, //width
canvasHeight = 300, //height
outerRadius = 100, //radius
color = d3.scale.category20(); //builtin range of colors
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}];
var vis = d3.select("body")
.append("svg:svg") //create the SVG element inside the <body>
.data([dataSet]) //associate our data with the document
.attr("width", canvasWidth) //set the width of the canvas
.attr("height", canvasHeight) //set the height of the canvas
.append("svg:g") //make a group to hold our pie chart
.attr("transform", "translate(" + 1.5*outerRadius + "," + 1.5*outerRadius + ")") // relocate center of pie to 'outerRadius,outerRadius'
// This will create <path> elements for us using arc data...
var arc = d3.svg.arc()
.outerRadius(outerRadius);
var pie = d3.layout.pie() //this will create arc data for us given a list of values
.value(function(d) { return d.magnitude; }) // Binding each value to the pie
.sort( function(d) { return null; } );
// Select all <g> elements with class slice (there aren't any yet)
var arcs = vis.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)
.on('click',function(d,i){
d3.select(this)
.transition()
.duration(500)
.attr("transform",function(d){
if (!d.data._expanded){
d.data._expanded = true;
var a = d.startAngle + (d.endAngle - d.startAngle)/2 - Math.PI/2;
var x = Math.cos(a) * 20;
var y = Math.sin(a) * 20;
return 'translate(' + x + ',' + y + ')';
} else {
d.data._expanded = false;
return 'translate(0,0)';
}
});
});
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 data array
// 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; });
// Computes the angle of an arc, converting from radians to degrees.
function angle(d) {
var a = (d.startAngle + d.endAngle) * 90 / Math.PI - 90;
return a > 90 ? a - 180 : a;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>

D3 Rolling map updates d3.geo.circle, but not arcs

I built a visualization on top of http://bl.ocks.org/patricksurry/6621971.
Basically I added d3.geo.circle and d3.svg.arc on the map.
What I observed is when I pan/zoom the map, the circle remains intact, but the arc disappears.
When I inspected the elements in chrome, I saw that the attribute 'd' of arc path vanished, but for circle path, it got updated appropriately.
Can anyone help me understand why the updated projection got applied to circle path element but not in arc. Is there a way to force re-projection of arcs without have to remove and re-create them?
UPDATE 1: Since this question seemed difficult to recreate, and jsfiddle won't not allow uploading a geo-data file , I am posting the source here:
<!DOCTYPE html>
<meta charset="utf-8">
<style>
svg {
background-color: lavender;
border: 1px solid black;
}
path {
fill: oldlace;
stroke: #666;
stroke-width: .5px;
}
path.circle {
fill: red;
stroke: #666;
stroke-width: .5px;
}
path.arc1 {
fill: green;
}
</style>
<body>
<script src="http://d3js.org/d3.v3.min.js"></script>
<script src="http://d3js.org/topojson.v1.min.js"></script>
<script>
var width = 1600,
height = 400,
rotate = 60, // so that [-60, 0] becomes initial center of projection
maxlat = 83; // clip northern and southern poles (infinite in mercator)
var projection = d3.geo.mercator()
.rotate([rotate,0])
.scale(1) // we'll scale up to match viewport shortly.
.translate([width/2, height/2]);
// find the top left and bottom right of current projection
function mercatorBounds(projection, maxlat) {
var yaw = projection.rotate()[0],
xymax = projection([-yaw+180-1e-6,-maxlat]),
xymin = projection([-yaw-180+1e-6, maxlat]);
return [xymin,xymax];
}
// set up the scale extent and initial scale for the projection
var b = mercatorBounds(projection, maxlat),
s = width/(b[1][0]-b[0][0]),
scaleExtent = [s, 10*s];
projection.scale(scaleExtent[0]);
var zoom = d3.behavior.zoom()
.scaleExtent(scaleExtent)
.scale(projection.scale())
.translate([0,0]) // not linked directly to projection
.on("zoom", redraw);
var path = d3.geo.path()
.projection(projection);
var svg = d3.selectAll('body')
.append('svg')
.attr('width',width)
.attr('height',height)
.attr('id', 'svg')
.call(zoom);
d3.json("js/data/world-110m2.json", function ready(error, world) {
// adding geo paths
svg.selectAll('path')
.data(topojson.feature(world, world.objects.countries).features)
.enter().append('path')
// adding a circle
svg.append("path")
.datum(d3.geo.circle().angle(2).origin([-10, 0]))
.attr("d", path)
.attr("class", "circle");
redraw();
// adding a pie arc
var r = 10;
var p = Math.PI * 2;
var arc1 = d3.svg.arc()
.innerRadius(r - 5)
.outerRadius(r)
.startAngle(0);
var arcData = JSON.parse('[{ "lon" : "0", "lat":"0", "endAngle":"6.4" }]');
var arcs1 = svg.selectAll("path.arc1");
arcs1 = arcs1.data(arcData)
.enter()
.append("path")
.attr("class", "arc1")
.attr("fill", "green")
.attr("transform", function(d, i) { return "translate(" + projection([d.lon, d.lat])[0] + ", " + projection([d.lon, d.lat])[1] + ")"; })
.attr("d", arc1);
});
// track last translation and scale event we processed
var tlast = [0,0],
slast = null;
function redraw() {
if (d3.event) {
var scale = d3.event.scale,
t = d3.event.translate;
console.log(d3.event.scale + " [" +d3.event.translate + "]");
// if scaling changes, ignore translation (otherwise touch zooms are weird)
if (scale != slast) {
projection.scale(scale);
} else {
var dx = t[0]-tlast[0],
dy = t[1]-tlast[1],
yaw = projection.rotate()[0],
tp = projection.translate();
// use x translation to rotate based on current scale
projection.rotate([yaw+360.*dx/width*scaleExtent[0]/scale, 0, 0]);
// use y translation to translate projection, clamped by min/max
var b = mercatorBounds(projection, maxlat);
if (b[0][1] + dy > 0) dy = -b[0][1];
else if (b[1][1] + dy < height) dy = height-b[1][1];
projection.translate([tp[0],tp[1]+dy]);
}
// save last values. resetting zoom.translate() and scale() would
// seem equivalent but doesn't seem to work reliably?
slast = scale;
tlast = t;
}
svg.selectAll('path').attr('d', path);
}
</script>
I finally figured out what was going wrong. I was supposed to apply transformation on arc elements. So basically, in the redraw() method I did:
var scaleRatio = 1;
function redraw() {
if (d3.event) {
var scale = d3.event.scale,
t = d3.event.translate;
//console.log(d3.event.scale + " [" +d3.event.translate + "]");
// if scaling changes, ignore translation (otherwise touch zooms are weird)
if (scale != slast) {
projection.scale(scale);
} else {
var dx = t[0]-tlast[0],
dy = t[1]-tlast[1],
yaw = projection.rotate()[0],
tp = projection.translate();
// use x translation to rotate based on current scale
projection.rotate([yaw+360.*dx/width*scaleExtent[0]/scale, 0, 0]);
// use y translation to translate projection, clamped by min/max
var b = mercatorBounds(projection, maxlat);
if (b[0][1] + dy > 0) dy = -b[0][1];
else if (b[1][1] + dy < height) dy = height-b[1][1];
projection.translate([tp[0],tp[1]+dy]);
}
// save last values. resetting zoom.translate() and scale() would
// seem equivalent but doesn't seem to work reliably?
if(slast==null)
scaleRatio=1;
else
scaleRatio = scaleRatio * (scale/slast);
console.log(slast+'-' + scaleRatio);
slast = scale;
tlast = t;
}
svg.selectAll('path').attr('d', path);
svg.selectAll("path.arc1")
.attr("transform", function(d, i) { return "translate(" + projection([d.lon, d.lat])[0] + ", " + projection([d.lon, d.lat])[1] + ")scale(" + scaleRatio + ")" })
.attr("d", arc1);
}
But, I still have a knowledge gap, as to why svg.path elements require explicit re-projection, unlike d3.geo.path elements . Hope someone helps me on this.

Resources