How to center a choropleth? - d3.js

I use dc choropleth graph and want to make it responsive, so the width and height adjusts automatically to the container. My map is from western europe, so roughly square.
var width=450;
var projection = d3.geoEquirectangular()
.center([37,47])
.scale(width*1.3);
var geojson=topojson.feature(europe,europe.objects.countries);
var graph = dc.geoChoroplethChart(dom)
.height(width)
.width(width)
.projection(projection)
.overlayGeoJson(geojson.features,"countries",function(d){return d.id})
...
It works fine when I am at a fixed size (450), but if I adjust it, the map isn't centered anymore and I have to manually adjust.
I tried the fitExtend function, but can't get it work properly either.
Ideally, I'd like to apply a transform to truncate a bit of the map on the east (remove part of turkey) and on the west and adjust the size so it centers properly on western europe (50°7′2.23″N 9°14′51.97″E for my manual adjustments).
Any idea how to do that?

Related

Multiple maps with d3.js: change values of scale and center

I’m building a (d3 v4) cartographic visualization which allows the user to switch between many datasets (json files) and two different regions (administrative units of a country and smaller administrative units into its capital city). Actually the switch from one to another dataset on the initial country level works well, through buttons and jquery.
Problem: it’s a bit less convincing when switching to a map/dataset about the capital city, as the projection is initially set for the whole country and consequently the user has to zoom many times to visualize properly the map of the capital city. I would like to change the values of .scale and .center when calling the projection but after several trials I haven’t found how to do it.
As I only have two different regions to show, my intuition was to set first values of scale and center and to change them to other values (I know the values of .scale and .center I would like to use in both cases) when the user switches to a map of the capital city through a function. Is there any possibility to switch easily these values? Do you have any suggestion to solve this problem?
As I load the json file path into a function when the user clicks on the button to switch to another dataset, I was trying to load the value of scale the same way but I’m probably doing wrong. It seems that the part of the code about the projection can't be put in a function?
Thanks for your help!
Small part of my code:
var width = 1100, height = 770;
var projection = d3.geoConicConformal()
.scale(19000) // value I would like to which when the region changes
.center([4.45, 50.53]) // value I would like to which when the region changes
.translate([width/2,height/2]);
var svg = d3.select( "#mapcontainer" )
.append( "svg" )
.attr("width", width)
.attr("height", height)
.style("border", "solid 1px black");
var path = d3.geoPath()
.projection(projection);
var color, jsonfile, legendtext;
function load (jsonfile, legendtext, color) {
d3.selectAll(".currentmap").remove() ;
d3.json(jsonfile, function(error, belgique) {
g.selectAll("path")
.data(belgique.features)
.enter()
.append("path")
.attr("d", path)
.style("stroke", "#fff")
.attr( "class", "currentmap")
.style("fill", function(d) {
var value = d.properties.DATA;
if (value) {return color(value);}
else {return "rgb(250,110,110)"}
});
})
};
//one of the following function for each map
function BGQprovinces() {
jsonfile = "ATLAS/NewGeoJson/bgq-data1-provinces.json";
legendText [= …];
color = d3.scaleOrdinal()
.domain( […])
.range([…]);
load(jsonfile, legendtext, color) ;
};
;
There area few approaches to accomplish this.
fitSize and fitExtent
One is to modify the projection scale and translate as opposed to scale and center. This is nearly the same operation, but translate pans the projected plane and center will pan the unprojected plane. To do so you need to use projection.fitSize([width,height],geojsonObject), or projection.fitExtent([[x0,y0],[x1,y1]],geojsonObject). The latter will allow margins of say, the first coordinate provided is the top left and the second coordinate provided is the bottom right of a bounding box in which the feature will be constrained.
d3.json(jsonfile, function(error, belgique) {
projection.fitSize([width,height], belgique);
// now draw as you would:
d3.selectAll(".currentmap").remove() ;
g.selectAll("path")
.data(belgique.features)
.enter()
.append("path")
.attr("d", path)
...
Note that for showing all of a country you need to have a feature that shows the whole country or a feature collection that shows all the parts of a country. You cannot use an array with fitSize or fitExtent, if you have an array of features, you can create a feature collection by using:
var featureCollection = {"type":"featureCollection","features":featureArray}
For your case, I'd suggest using fitSize or fitExtent.
centroid
If you really wanted to modify the center attribute as opposed to translate, or perhaps you want to change the rotation (a more likely outcome for conic conformals in many parts of the world, Belgium should be fine), then you need the geographic coordinates of the center. One way of a handful to do this is to get the centroid of a feature from path.geoCentroid:
var centroid = path.geoCentroid(geojsonObject);
Then use that to set the projection parameters to rotate:
projection.rotate([ -centroid[0],-centroid[1] ])
projection.center([0,0])
or to center:
projection.rotate([0,0])
projection.center(centroid)
Or a combination of both (depending on map projection type). Now you can apply fitSize or fitExtent, the feature is in the middle already, but now we can set the scale. The reason I suggest this as a potential answer is because not all projections, concic projections in particular, will give desired results by modifying only scale along with translate and/or center.
Of course for conic projections, you may need to find a way to set the parallels as well, but I'll leave that for another answer if it ever comes up.

MKMapView is misaligned to its region property

I want to display a certain map region in MKMapView but when I put a rectangular overlay on the map with the very same parameters it is displayed misaligned vertically. It looks good enough close to the equator but the misalignment is increasing with the latitude and the span.
This is for a mac app, but it should be the same for iOS.
This is my relevant code:
MKCoordinateRegion mapRegion = MKCoordinateRegionMake(CLLocationCoordinate2DMake(latCenter, lonCenter), MKCoordinateSpanMake(mapWidthY, mapWidthX));
self.radarMap.region = mapRegion;
CLLocationCoordinate2D coordinates[4];
coordinates[0] = CLLocationCoordinate2DMake(latCenter+mapWidthY/2, lonCenter+mapWidthX/2);
coordinates[1] = CLLocationCoordinate2DMake(latCenter+mapWidthY/2, lonCenter-mapWidthX/2);
coordinates[2] = CLLocationCoordinate2DMake(latCenter-mapWidthY/2, lonCenter-mapWidthX/2);
coordinates[3] = CLLocationCoordinate2DMake(latCenter-mapWidthY/2, lonCenter+mapWidthX/2);
self.boundaryOverlay = [MKPolygon polygonWithCoordinates:coordinates count:4];
[self.radarMap addOverlay:self.boundaryOverlay];
It shows this: (Notice the blue rect overlay is moved up so the upper region is not displayed):
Instead of something like this: (I'm aware of that it is displayed in aspect fill):
When you set the region property of an MKMapView object, MapKit adjusts the value of the region property so that it matches the actual region that's visible. That means that the actual value of region isn't going to be exactly what you assigned to it. So instead of creating the polygon using the region that you assigned to the map, you should get the updated value of region from the MKMapView object and use that to create the polygon.
MKCoordinateRegion mapRegion = MKCoordinateRegionMake(CLLocationCoordinate2DMake(latCenter, lonCenter), MKCoordinateSpanMake(mapWidthY, mapWidthX));
self.radarMap.region = mapRegion;
CLLocationCoordinate2D coordinates[4];
// Get the actual region that MapKit is using
MKCoordinateRegion actualMapRegion = self.radarMap.region;
CLLocationDegrees actualLatCenter = actualMapRegion.center.latitude;
CLLocationDegrees actualLonCenter = actualMapRegion.center.longitude;
CLLocationDegrees actualLatSpan = actualMapRegion.span.latitudeDelta;
CLLocationDegrees actualLonSpan = actualMapRegion.span.longitudeDelta;
// And use that to create the polygon
coordinates[0] = CLLocationCoordinate2DMake(actualLatCenter+ actualLatSpan/2, actualLonCenter+ actualLonSpan/2);
coordinates[1] = CLLocationCoordinate2DMake(actualLatCenter+ actualLatSpan/2, actualLonCenter-actualLonSpan/2);
coordinates[2] = CLLocationCoordinate2DMake(actualLatCenter-actualLatSpan/2, actualLonCenter-actualLonSpan/2);
coordinates[3] = CLLocationCoordinate2DMake(actualLatCenter-actualLatSpan/2, actualLonCenter+ actualLonSpan/2);
self.boundaryOverlay = [MKPolygon polygonWithCoordinates:coordinates count:4];
[self.radarMap addOverlay:self.boundaryOverlay];
I was curious about the increasing misalignment that you were seeing as you moved north. It occurred to me that you were probably using a fixed ratio for the mapWidthX and mapWidthY. MapKit uses a projection that is non-conformal. One consequence of that is that the map gets stretched in the North-South direction, with more stretching the farther you get from the equator.
If you create your region using a ratio that's correct for the equator, it will be incorrect as you move toward the poles. MKMapView will take the region you give it and display something close to it. But the farther you get from the equator, the more of an adjustment it needs to make. And the bigger the difference between the region you give it and the actual region it uses.

NVD3.js multiChart x-axis labels is aligned to lines, but not bars

I am using NVD3.js multiChart to show multiple lines and bars in the chart. All is working fine, but the x-axis labels is aligned only to the line points, not bars. I want to correctly align labels directly below the bars as it should. But I get this:
With red lines I marked where the labels should be.
I made jsFiddle: http://jsfiddle.net/n2hfN/
Thanks!
As #Miichi mentioned, this is a bug in nvd3...
I'm surprised that they have a TODO to "figure out why the value appears to be shifted" because it's pretty obvious... The bars use an ordinal scale with .rangeBands() and the line uses a linear scale, and the two scales are never made to relate to one another, except in that they share the same endpoints.
One solution would be to take the ordinal scale from the bars, and simply adjust it by half of the bar width to make the line's x-scale. That would put the line points in the center of the bars. I imagine that something similar is done in the nv.models.linePlusBarChart that #LarsKotthoff mentioned.
Basically, your line's x-scale would look something like this:
var xScaleLine = function(d) {
var offset = xScaleBars.rangeBand() / 2;
return xScaleBars(d) + offset;
};
...where xScaleBars is the x-scale used for the bar portion of the chart.
By combing through the source code for nvd3, it seems that this scale is accessible as chart.bars1.scale().
Maybe someday the authors of nvd3 will decide that their kludge of a library deserves some documentation. For now, I can show you the kind of thing that would solve the problem, by making a custom chart, and showing how the two scales would relate.
First, I'll use your data, but separate the line and bar data into two arrays:
var barData = [
{"x":0,"y":6500},
{"x":1,"y":8600},
{"x":2,"y":17200},
{"x":3,"y":15597},
{"x":4,"y":8600},
{"x":5,"y":814}
];
var lineData = [
{"x":0,"y":2},
{"x":1,"y":2},
{"x":2,"y":4},
{"x":3,"y":6},
{"x":4,"y":2},
{"x":5,"y":5}
];
Then set up the scales for the bars. For the x-scale, I'll use an ordinal scale and rangeRoundBands with the default group spacing for nvd3's multiBar which is 0.1. For the y-scale I'll use a regular linear scale, using .nice() so that the scale doesn't end on an awkward value as it does by default in nvd3. Having some space above the largest value gives you some context, which is "nice" to have when trying to interpret a chart.
var xScaleBars = d3.scale.ordinal()
.domain(d3.range(barData.length))
.rangeRoundBands([0, w], 0.1);
var yScaleBars = d3.scale.linear()
.domain([0, d3.max(barData, function(d) {return d.y;})])
.range([h, 0])
.nice(10);
Now here's the important part. For the line's x-scale, don't make a separate scale, but just make it a function of the bars' x-scale:
var xScaleLine = function(d) {
var offset = xScaleBars.rangeBand() / 2;
return xScaleBars(d) + offset;
};
Here's the complete example as a JSBin. I've tried to document the major sections with comments so it's easy to follow the overall logic of it. If you can figure out from the nvd3 source code exactly what each of the elements of the multiChart are called and how to set the individual scales of the constituent parts, then you might be able to just plug in the new scale.
My feeling on it is that you need to have a pretty good handle on how d3 works to do anything useful with nvd3, and if you want to customize it, you're probably better off just rolling your own chart. That way you have complete knowledge and control of what the element classes and variable names of the parts of your chart are, and can do whatever you want with them. If nvd3 ever gets proper documentation, maybe this will become a simple fix. Good luck, and I hope this at least helps you get started.

Focus/Context Brushing + Pan/Zoom graph - How to limit panning

I've managed to make a d3.js line+area graph sync with focus/context brushing and pan/zoom, with a small example here:
http://jsfiddle.net/MtXvx/8/
I'm having trouble limiting the panning to stop at the original domain boundaries, while also working nicely with the brush. This is to prevent users from losing the graph in their view.
While I have tried manually detecting when panning has exceeded boundaries and then setting zoom.translate([0,0]), such as in these examples:
d3.js scatter plot - zoom/drag boundaries, zoom buttons, reset zoom, calculate median
Limiting domain when zooming or panning in D3.js
d3.js scatter plot - zoom/drag boundaries, zoom buttons, reset zoom, calculate median
...as I do here at line 183:
//If exceed original domain, limit panning by resetting translate
if (x.domain()[0] < x0.domain()[0]) {
zoom.translate([0, 0]);
}
The problem occurs when:
1) Create a brush region in the small context graph
2) Pan the big focus graph all the way towards the earliest date
3) Graph jumps when panning is almost at the boundary
Would appreciate any help to prevent the jumping from happening, or if there is any other way to limit the panning (and eventually the zooming out too) to the original domain boundaries.
Regarding limiting the zoom-out, setting:
var zoom = d3.behavior.zoom().x(x).scaleExtent([1,10]).on("zoom", zoomed);
...does not work nicely because the zoom-out would be limited to the brush region instead of the full extent of the graph data.
Much thanks!
I had similar problems combining D3 Brushing and Zoom & Pan, but figured it out eventually. I found the key to limit the panning is to reset the translate of the zoom behavior object. Specifically, here is my zoom callback function:
function zoomed() {
var t = d3.event.translate;
var s = d3.event.scale;
var size = width*s;
t[0] = Math.min(t[0], 0);
t[0] = Math.max(t[0], width-size);
zoom.translate(t);
focus.select(".area").attr("d", area);
focus.select(".x.axis").call(xAxis);
var brushExtent = [x.invert(0), x.invert(width)];
context.select(".brush").call(brush.extent(brushExtent));
}
While not part of your question, also an important part to make the whole demo work right is to update the zoom translate and scale when brushing is done, so here is my brushed callback:
function brushed() {
x.domain(brush.empty() ? x2.domain() : brush.extent());
focus.select(".area").attr("d", area);
focus.select(".x.axis").call(xAxis);
var s = x.domain();
var s_orig = x2.domain();
var newS = (s_orig[1]-s_orig[0])/(s[1]-s[0]);
var t = (s[0]-s_orig[0])/(s_orig[1]-s_orig[0]);
var trans = width*newS*t;
zoom.scale(newS);
zoom.translate([-trans,0]);
}
Here is a complete example based one of D3 examples: http://bl.ocks.org/sbreslav/be9af0d809b49864b7d8
to limit the extent of the panning on the graph you could use clamp, although I couldn't see where or if you were using a scale in that fiddle (actually it didn't appear to be working). Here's a simple example in a fiddle

d3 autospace overlapping tick labels

Is there a way in d3 to not draw overlapping tick labels? For example, if I have a bar chart, but the bars are only 5 pixels wide and the labels are 10 pixels wide, I end up with a cluttered mess. I'm currently working on an implementation to only draw the labels when they do not overlap. I can't find any existing way to do that, but wasn't sure if anyone else had dealt with this problem.
There is no way of doing this automatically in D3. You can set the number of ticks or the tick values explicitly (see the documentation), but you'll have to figure out the respective numbers/values yourself. Another option would be to rotate the labels such that there is less chance of them overlapping.
Alternatively, like suggested in the other answer, you could try using a force layout to place the labels. To clarify, you would use the force layout on the labels only -- this is completely independent of the type of chart. I have done this in this example, which is slightly more relevant than the one linked in the other answer.
Note that if you go with the force layout solution, you don't have to animate the position of the labels. You could simply compute the force layout until it converges and then plot the labels.
I've had a similar problem with multiple (sub-)axis, where the last tick overlaps my vertical axis in some situations (depending on the screen width), so I've just wrote a little function that compares the position of the end of the text label with the position of the next axis. This code is very specific to my use case, but could adapted easily to your needs:
var $svg = $('#svg');
// get the last tick of each of my sub-axis
$('.tick-axis').find('.tick:last-of-type').each(function() {
// get position of the end of this text field
var endOfTextField = $(this).offset().left + $(this).find('text').width();
// get the next vertical axis
var $nextAxis = $('line[data-axis="' + $(this).closest('.tick-axis').attr('data-axis') + '"]');
// there is no axis on the very right, so just use the svg width
var positionOfAxis = ($nextAxis.length > 0) ? $nextAxis.offset().left : $svg.offset().left + $svg.width();
// hide the ugly ones!
if (endOfTextField > positionOfAxis) {
$(this).attr('class', 'tick hide');
}
});
The ticks with color: aqua are the hidden ones:

Resources