D3js projection issues when fitting to BBox - d3.js

(My code is at the end)
My goal is to display a country map (provided in a topojson file) which automatically scale and translate to fit into an area and then display few dots on it, representing some cities (given their lat/long coordinates).
First part was easy. I found (don't remember if it was on SO or on bl.ocks.org) that we can use bounds to compute scale and translate. That works perfectly and my country adapt to its parent area.
First Question: Why the country doesn't behave the same if I scale/translate it with its transform attribute or with projection.scale().translate() ? I mean, when I use transform attribute the country adapts perfectly whereas projection.scale().translate() displays a small country in a corner.
Second part is displaying some cities on my map. My cities has coordinates (which are real ones) :
var cities = {
features: [
{
'type':'Feature',
'geometry':{
'type':'Polygon',
'coordinates': [2.351828, 48.856578] // Longitude, Latitude
},
'properties':{}
},
{
'type':'Feature',
'geometry':{
'type':'Polygon',
'coordinates': [5.726945, 45.187778] // Longitude, Latitude
},
'properties':{}
},
};
When I try to apply scale and translate parameters (to adapt with my country which has been scaled and translated) either with projection.scale().translate() or with transform attribute my cities are far far away from where they should be.
Second Question: Why I cannot use same scale/translate parameters on country and cities ? How can I properly display my cities where they should be ?
function computeAutoFitParameters(bounds, width, height) {
var dx = bounds[1][0] - bounds[0][0];
var dy = bounds[1][1] - bounds[0][1];
var x = (bounds[0][0] + bounds[1][0]) / 2;
var y = (bounds[0][1] + bounds[1][1]) / 2;
var scale = 0.9 / Math.max(dx / width, dy / height);
var translate = [width / 2 - scale * x, height / 2 - scale * y];
return {
scale : scale,
translate: translate
};
}
// element is the HTML area where the country has to fit.
var height = element.height();
var width = element.width();
var projection = d3.geo.miller();
var path = d3.geo.path().projection(projection);
// data is my country (a topojson file with BBox)
var topojsonCountry = topojson.feature(data, data.objects[country.id]).features;
var bounds = path.bounds(topojsonCountry[0]);
var params = computeAutoFitParameters(bounds, width, height);
var scale = params.scale;
var translate = params.translate;
var svg = d3.select(element[0]).append('svg')
.attr('width', width + 'px')
.attr('height', height + 'px');
svg.append('g')
.selectAll('path')
.data(topojsonCountry)
.enter()
.append('path')
.attr('d', path)
.attr('transform', 'translate(' + translate + ')scale(' + scale + ')');
svg.selectAll('circle')
.data(cities.features) // city is defined in the code above
.enter()
.append('circle')
.attr('transform', function(d) {
return 'translate(' + projection(d.geometry.coordinates) + ')';
)
.attr('r', '6px');
EDIT: I had removed too much code to simplify it. It's fixed now. The difference is that I have an array of cities to display rather than just one.
Thanks in advance.

I found out that I had to add null parameters to my projection. To sum up :
Create a minimal projection (and a path)
Apply null scale and translate parameters to the projection : projection.scale(1).translate([0, 0])
Compute real scale and translate parameters according to the bounding box
Display the country's map as before (no changes here)
Set computed scale and translate parameters to the projection : projection.scale(params.scale).translate(params.translate);
Draw the cities dots.
`
// element is the HTML area where the country has to fit.
var height = element.height();
var width = element.width();
var projection = d3.geo.miller();
var path = d3.geo.path().projection(projection);
projection.scale(1).translate([0, 0]) // This is new
// data is my country (a topojson file with BBox)
var topojsonCountry = topojson.feature(data, data.objects[country.id]).features;
var bounds = path.bounds(topojsonCountry[0]);
var params = computeAutoFitParameters(bounds, width, height);
var svg = d3.select(element[0]).append('svg')
.attr('width', width + 'px')
.attr('height', height + 'px');
svg.append('g')
.selectAll('path')
.data(topojsonCountry)
.enter()
.append('path')
.attr('d', path)
.attr('transform', 'translate(' + params.translate + ')scale(' + params.scale + ')');
projection.scale(params.scale).translate(params.translate); // This is new
svg.selectAll('circle')
.data(cities.features)
.enter()
.append('circle')
.attr('transform', function(d) {
return 'translate(' + projection(d.geometry.coordinates) + ')';
})
.attr('r', '6px')
.attr('fill', 'red');

Related

D3.js: How to Center svg map after Drag, Zoom, or MouseMove Events

My map projection and display goes awry when users drag, move the map (see: https://realtimeceap.brc.tamus.edu).
For example (duplicate event):
1. Select a field condition from dropdownlist.
2. Select a State from dropdownlist.
3. Select a County from dropdownlist.
4. Drag the map or move the mousewheel.
5. Then, select another State from dropdownlist. The map is not centered at the middle of the svg element and the scale is off, instead of scale 1.
Appreciate any help.
I reset the map on selecting a state as follows:
function resetMap() {
svg = d3.select("#svgMap2");
var w = 728;
var h = 500;
var project = d3.geoAlbersUsa()
.scale(1000)
.translate([w / 2, h / 2]);
var t = project.translate(); // the projection's default translation
var scale = project.scale;
//reset all features to original scale
d3.select("#svgMap2").select("#counties").selectAll(".county")
.transition()
.duration(750)
.style("stroke-width", "0.5px")
.style("stroke", "#808080")
.attr("transform", "translate(" + t[0] + "," + t[1] + ")scale(" + scale + ")");
}
Been working on this for a while and found the solution is so simple. I needed to reset the g element's transform attribute to null to redefine it. The id of my svg = svgMap2 and the id of my g element = counties. BTW, I'm using D3.js version 4.
function resetMap() {
var k = 1;
var t = [0, 0];
svg = d3.select("#svgMap2")
d3.select("#svgMap2").select("#counties").selectAll(".county").classed("active", false);
d3.select("#counties").attr("transform", null);
svg.select("#counties").selectAll(".county") //must use svg or it disables pan, zoom
.attr("width", width)
.attr("height", height)
.style("stroke-width", "0.5px")
.style("stroke", "#808080")
.attr("transform", "translate(" + t[0] + "," + t[1] + ")scale(" + k + ")");
}

D3 placing nodes labels inside circles in Mobile Patent Suits

I'm new to D3 and I'm using this template (http://bl.ocks.org/mbostock/1153292) to visualize my graph. However, my nodes' labels are shore and I want to place them inside circles not on their right hand-side.
Can anybody help me with that?
Thanks a lot
Append circle and text inside a group element and use text-achor property to align the text to the middle.
var nodes = svg.selectAll(".node")
.data(force.nodes())
.enter()
.append("g")
.attr("class","node")
.call(force.drag);
var circles = nodes.append("circle")
.attr("r", 6);
var texts = nodes.append("text")
.attr("text-anchor", "middle")
.text(function(d) {
return d.name;
});
Change tick function as shown below.
function tick() {
path.attr("d", linkArc);
nodes.attr("transform", transform);
}
Update
JSFiddle 1 for aligning text to the center.
If you would like enclose the text label within the circle, increase the radius of circle using the length of name property. Note that, you will have to update the link target positions this time, to adjust the marker positions relative to the circle radius.
var circles = nodes.append("circle")
.attr("r", function(d){ d.radius = d.name.length*3; return d.radius; }
);
function linkArc(d) {
var tX = d.target.x-d.target.radius,
dx = tX - d.source.x,
dy = d.target.y - d.source.y,
dr = Math.sqrt(dx * dx + dy * dy);
return "M" + d.source.x + "," + d.source.y + "A" + dr + "," + dr + " 0 0,1 " + tX + "," + d.target.y;
}
JSFiddle 2

D3 Geo Tile Baselayer Offset

I'm using d3.geo.tile() and have used it successfully before but this time the tile layer doesn't seem to draw at the same scale and translate as the point layer. The below code creates a map that pans and draws just fine, but draws the circles, which should be in the Mediterranean, in Africa. If I zoom in, it scales the tiles and circles just fine, it's as if my xy coordinates are off, but they aren't.
I get the feeling that it's actually drawing the base layer without offsetting and scaling it properly because it should be centering on the coordinates 12,42, but it's a great big mystery to me since this exact same code works fine in a different application.
If someone can spot some problem, or just a hint, that would help.
function createNewMap(){
width = 1200, height = 800;
var tile = d3.geo.tile()
.size([1200, 800]);
var projection = d3.geo.mercator()
.scale((1 << 12) / 2 / Math.PI)
.translate([width / 2, height / 2]);
var center = projection([12, 42]);
var zoom = d3.behavior.zoom()
.scale(projection.scale() * 2 * Math.PI)
.scaleExtent([1 << 10, 1 << 17])
.translate([width - center[0], height - center[1]])
.on("zoom", zoomed);
projection
.scale(1 / 2 / Math.PI)
.translate([0, 0]);
var svg = d3.select("#newMapId").append("svg")
.attr("width", width)
.attr("height", height)
.call(zoom);
var raster = svg.append("g");
var vector = svg.append("g");
vector.selectAll("g").data(dataModule.polisData).enter().append("g")
.attr("class", "sites")
.attr("transform", function(d) {return "translate(" + (projection([d.xcoord,d.ycoord])[0]) + "," + (projection([d.xcoord,d.ycoord])[1]) + ")scale("+(projection.scale())+")"})
.append("circle")
.attr("class", "sitecirc");
zoomed();
function zoomed() {
var tiles = tile
.scale(zoom.scale())
.translate(zoom.translate())
();
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", "d"][Math.random() * 4 | 0] + ".tiles.mapbox.com/v3/elijahmeeks.map-zm593ocx/" + 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]; });
vector
.attr("transform", "translate(" + zoom.translate() + ")scale(" + zoom.scale() + ")");
d3.selectAll(".sitecirc")
.attr("r", 10 / zoom.scale());
}
Your code appears to be based on my example that changes the SVG transform on zoom. Changing the transform is a nice technique when you have complex geometry that you just want to scale and translate when you pan or zoom — it’s typically faster than reprojecting everything — but it’s also more complex than changing the projection on zoom.
The code doesn’t change very much if you want to change the projection on zoom. In essence:
projection
.scale(zoom.scale() / 2 / Math.PI)
.translate(zoom.translate());
And then re-run your d3.geo.path to re-render. As shown in bl.ocks.org/9535021:
Also, fixing the projection and changing the transform can cause precision problems if you zoom in a lot. Another reason to only use that technique when it offers substantial performance gains by avoid reprojection. And here reprojecting is super-cheap because it’s just a handful of points.

D3 Map disable zoom on city label

i'm stuck in this problem for a few days, and i hope someone can help me.
Basically this is the problem:
I've a UK map , with city label and marker. I've enabled zoom and pan on this, but when i zoom in even marker and label grow in dimension.
I'd like to preserve marker poisiton during zoom and pan (London is always in its place) but don't scale the dimension of marker and label.
This is my zoom function
var zoomBehav= function(){
var t = d3.event.translate,
s = d3.event.scale;
t[0] = Math.min(width / 2 * (s - 1), Math.max(width / 2 * (1 - s), t[0]));
t[1] = Math.min(height / 2 * (s - 1) + 230 * s, Math.max(height / 2 * (1 - s) - 230 * s, t[1]));
zoom.translate(t);
g.style("stroke-width", 1 / s).attr("transform", "translate(" + t + ") scale(" + s + ")");
};
Anyone can help me?
ADDITIONAL INFO
I think i should write more info.
My svg is defined as
this.svg = d3.select("#"+this.config.mapContainer).append("svg")
.attr("id","svgMap")
.attr("width",width)
.attr("height",height)
.style("background-color", "lightblue")
.append("g").attr('id','first_g')
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")")
//Adding Zoom Rect to svg
this.svg.append("rect")
.attr("id", "d3e_zoomArea")
.attr("class", "d3e_zoomArea")
.attr("x", -width / 2)
.attr("y", -height / 2)
.attr("width", width)
.attr("height", height)
.attr('onmousedown', 'return false');
//Insert g elementi in svg
g = this.svg.append("g").attr('id','g_pathContainer');
//Set Actual Map Right Projection
/ create a first guess for the projection
subunits = topojson.feature(map, map.objects[level]);
center = d3.geo.centroid(subunits);
projection = d3.geo.mercator()
.scale(width / 2 / Math.PI) //Crea la scala di visualizzazione
.center(center) //Centra la posizione sulla mappa
.translate([0, 0]);
path = d3.geo.path().projection(projection);
// using the path determine the bounds of the current map and use
// these to determine better values for the scale and translation
var bounds = path.bounds(subunits);
var hscale = scale*width / (bounds[1][0] - bounds[0][0]);
var vscale = scale*height / (bounds[1][1] - bounds[0][1]);
scale = (hscale < vscale) ? hscale : vscale;
// new projection
projection = d3.geo.mercator().center(center)
.scale(scale).translate([0, 0]);
path = path.projection(projection);
zoom = d3.behavior.zoom()
.translate(projection.translate())
.scale(projection.scale())
.scaleExtent([this.config.scaleRange.scaleMin , this.config.scaleRange.scaleMax])
.on("zoom", zoomed);
d3.select("#first_g").call(zoom);
JSFIDDLE added
I've created a jsfiddle (http://jsfiddle.net/z9S7y/) to reproduce my problem. I've applied some initial transformation to base projection in order to center and scale the map in relation to actual country.

how do you draw linear line in scatter plot with d3.js

I am looking to implement ggplot2 type of graphs using d3.js library for interactivey purpose. I love ggplot2 but users are interested in interactive graphs. I've been exploring d3.js library and there seems to be lots of different graph capability but I really did not see any statistical graphs like linear line, forecast etc. Given a scatter plot, is it possible to also add linear line to the graph.
I have this sample script that draws scatter plot. How would I add linear line to this graph in d3.js?
// data that you want to plot, I've used separate arrays for x and y values
var xdata = [5, 10, 15, 20],
ydata = [3, 17, 4, 6];
// size and margins for the chart
var margin = {top: 20, right: 15, bottom: 60, left: 60}
, width = 960 - margin.left - margin.right
, height = 500 - margin.top - margin.bottom;
// x and y scales, I've used linear here but there are other options
// the scales translate data values to pixel values for you
var x = d3.scale.linear()
.domain([0, d3.max(xdata)]) // the range of the values to plot
.range([ 0, width ]); // the pixel range of the x-axis
var y = d3.scale.linear()
.domain([0, d3.max(ydata)])
.range([ height, 0 ]);
// the chart object, includes all margins
var chart = d3.select('body')
.append('svg:svg')
.attr('width', width + margin.right + margin.left)
.attr('height', height + margin.top + margin.bottom)
.attr('class', 'chart')
// the main object where the chart and axis will be drawn
var main = chart.append('g')
.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')')
.attr('width', width)
.attr('height', height)
.attr('class', 'main')
// draw the x axis
var xAxis = d3.svg.axis()
.scale(x)
.orient('bottom');
main.append('g')
.attr('transform', 'translate(0,' + height + ')')
.attr('class', 'main axis date')
.call(xAxis);
// draw the y axis
var yAxis = d3.svg.axis()
.scale(y)
.orient('left');
main.append('g')
.attr('transform', 'translate(0,0)')
.attr('class', 'main axis date')
.call(yAxis);
// draw the graph object
var g = main.append("svg:g");
g.selectAll("scatter-dots")
.data(ydata) // using the values in the ydata array
.enter().append("svg:circle") // create a new circle for each value
.attr("cy", function (d) { return y(d); } ) // translate y value to a pixel
.attr("cx", function (d,i) { return x(xdata[i]); } ) // translate x value
.attr("r", 10) // radius of circle
.style("opacity", 0.6); // opacity of circle
To add a line to your plot, all that you need to do is to append some line SVGs to your main SVG (chart) or to the group that contains your SVG elements (main).
Your code would look something like the following:
chart.append('line')
.attr('x1',x(10))
.attr('x2',x(20))
.attr('y1',y(5))
.attr('y2',y(10))
This would draw a line from (10,5) to (20,10). You could similarly create a data set for your lines and append a whole bunch of them.
One thing you might be interested in is the SVG path element. This is more common for lines than drawing one straight segment at a time. The documentation is here.
On another note you may find it easier to work with data in d3 if you create it all as one object. For example, if your data was in the following form:
data = [{x: 5, y:3}, {x: 10, y:17}, {x: 15, y:4}, {x: 20, y:6}]
You could use:
g.selectAll("scatter-dots")
.data(ydata) // using the values in the ydata array
.enter().append("svg:circle") // create a new circle for each value
.attr("cy", function (d) { return y(d.y); } ) //set y
.attr("cx", function (d,i) { return x(d.x); } ) //set x
This would eliminate potentially messy indexing if your data gets more complex.
UPDATE: Here is the relevant block: https://bl.ocks.org/HarryStevens/be559bed98d662f69e68fc8a7e0ad097
I wrote this function to calculate a linear regression from data, formatted as JSON.
It takes 5 parameters:
1) Your data
2) The column name of the data plotted on your x-axis
3) The column name of the data plotted on your y-axis
4) The minimum value of your x-axis
5) The minimum value of your y-axis
I got the formula for calculating a linear regression from http://classroom.synonym.com/calculate-trendline-2709.html
function calcLinear(data, x, y, minX, minY){
/////////
//SLOPE//
/////////
// Let n = the number of data points
var n = data.length;
var pts = [];
data.forEach(function(d,i){
var obj = {};
obj.x = d[x];
obj.y = d[y];
obj.mult = obj.x*obj.y;
pts.push(obj);
});
// Let a equal n times the summation of all x-values multiplied by their corresponding y-values
// Let b equal the sum of all x-values times the sum of all y-values
// Let c equal n times the sum of all squared x-values
// Let d equal the squared sum of all x-values
var sum = 0;
var xSum = 0;
var ySum = 0;
var sumSq = 0;
pts.forEach(function(pt){
sum = sum + pt.mult;
xSum = xSum + pt.x;
ySum = ySum + pt.y;
sumSq = sumSq + (pt.x * pt.x);
});
var a = sum * n;
var b = xSum * ySum;
var c = sumSq * n;
var d = xSum * xSum;
// Plug the values that you calculated for a, b, c, and d into the following equation to calculate the slope
// m = (a - b) / (c - d)
var m = (a - b) / (c - d);
/////////////
//INTERCEPT//
/////////////
// Let e equal the sum of all y-values
var e = ySum;
// Let f equal the slope times the sum of all x-values
var f = m * xSum;
// Plug the values you have calculated for e and f into the following equation for the y-intercept
// y-intercept = b = (e - f) / n = (14.5 - 10.5) / 3 = 1.3
var b = (e - f) / n;
// return an object of two points
// each point is an object with an x and y coordinate
return {
ptA : {
x: minX,
y: m * minX + b
},
ptB : {
y: minY,
x: (minY - b) / m
}
}
}

Resources