This is a very broad question and contains several questions and goals (map zooming, choropleth creation, two tiered maps), consequently, any answer will be broad - but not necessarily unhelpful. My answer will not solve every issue in the question, but should help in creating your intended final vision. I'm going to focus on what appears to be the key question:
I would also like to see that when the user double-clicks on a
country, the country in question is zoomed and divided into regions,
each of which is colored according to a second measure.
Granted you say "also" which suggests this is secondary, but your pictures and title appear to be more concerned with the two tier effect, and there are many examples and questions of choropleths, but few on interactive two tiered maps.
I take it the key challenge is subdividing the regions, to do this you will need some sort of common identifier between parent and child regions. In your geojson or topojson you could add necessary identifiers if needed. Ideally, your geojson might look like:
Parent/Country:
{
"type":"Feature",
"properties"{ "country":NAME ... },
"geometry": { ... }
}
Child/Region:
{
"type":"Feature",
"properties"{ "country":NAME, "regionName": NAME ... },
"geometry": { ... }
}
When a country is clicked on (or any other event such as double click), you want to draw the children regions based on the shared identifier:
country.on("click", function(d) {
// remove other regions
d3.selectAll(".region").remove();
// filter out relevant regions of a geojson of all regions
var countryRegions = geojson.features.filter(function(region) {
return region.properties.country == d.properties.country;
})
// append the regions
svg.selectAll(".region")
.data(countryRegions)
.enter()
.append()
.attr("class",".region")
})
If you had geojson files with standardized naming, you could use the file name as the shared property, doing something along the lines of:
country.on("click", function(d) {
// remove other regions
d3.selectAll(".region").remove();
// get appropriate geojson:
d3.json(d.properties.country+".json", function(error, regions) {
// draw region features
})
})
You could up the transparency of the countries on click/other event by adding something like: country.style("opacity",0.4)
in the on click/other event, a blur will add a bit more complexity. Either should enhance the two tiered effect. Cropping is unnecessary when dealing with countries - countries rarely overlap, and in any event, new features are generally drawn above old features (which eliminates any visual overlap resulting from imprecision of coordinates).
That is all there is to a two tiered effect, using the same principle you could easily create a three tiered map, populating a selected region with subregions.
Building on that I'll very briefly touch on zooming:
Using the geojson/topojson that contains the region, you can then change the projection to reflect the extent of the features - which allows you to zoom to those features:
projection.fitSize([width,height],geojsonObject);
Note that if filtering an array of features, fitSize of fitExtent won't work unless you place the features in a feature collection (and both require v4):
var featureCollection = {type:"FeatureCollection",features:features};
To accomplish a smooth zoom, you'll need to transition the projection with transition.attrTween. This is a bit tricky, as you need to interpolate both projection translation and projection scale (and depending on the map projection and type, perhaps rotation).
Alternatively, you could zoom by manipulating the svg instead, there are many examples of and questions about how to achieve this effect (I use the other approach in my example below).
The above will let you: zoom to regions, draw relevant regions, and transition between regions/views.
I've produced a simple generic example, using dummy geographic data, which is operational here, (using single click events), the key parts are more heavily commented below (excepting the transition function, see the example to see it). This example will require heavy adaptation to match your intended data and visualization.
I am using a few variables that are not declared in the below code (See the example for the full code), but they are mostly standard: the geoPath (path
), the geoProjection (projection
), width, height, etc, but also baseProjection
which is the starting projection. I'm also using dummy data, hence my use of d3.geoIdentity
rather than a more standard projection.
// get the parent geojson
d3.json("geojson.json", function(error, geojson) {
if (error) throw error;
// get the regions:
d3.json("geojsonSubdivisions.json", function(error, subdivisions) {
if (error) throw error;
// a color scale for the countries:
var color = d3.scaleLinear().range(["steelblue","darkblue"]).domain([0,4]);
// a color scale for the regions:
var subdivisionColor = ["lightsalmon","salmon","coral"];
// refine the two projections, one for the default/base, and one for the current
baseProjection.fitSize([width,height],geojson);
projection.fitSize([width,height],geojson);
// append the countries:
svg.append("g")
.attr("class", "topLevel")
.selectAll("path")
.data(geojson.features)
.enter()
.append("path")
.attr("fill",function(d,i) { return color(i); })
.attr("opacity",0.7)
.attr("d", path)
.style("stroke","black")
.style("stroke-width",0)
// style on mouseover:
.on("mouseover", function() {
d3.select(this)
.style("stroke-width",15)
.raise();
})
// undo mouseover styles:
.on("mouseout", function(d,i) {
d3.select(this)
.style("stroke-width", 0 );
})
// now zoom in when clicked and show subdivisions:
.on("click", function(d) {
// remove all other subdivisions:
d3.selectAll(".subdivision")
.remove();
// get new features:
var features = subdivisions.features.filter(function(feature) { return feature.id == d.id });
// draw new features
svg.selectAll(null)
.data(features)
.enter()
.append("path")
.attr("class","subdivision")
.attr("fill", function(d,i) { return subdivisionColor[i] })
.attr("d", path)
.style("stroke","black")
.style("stroke-width",0)
.on("click", function() {
zoom(projection,baseProjection); // zoom out when clicked
d3.selectAll(".subdivision")
.remove(); // remove regions when clicked
})
// style on mouseover
.on("mouseover", function() {
d3.select(this)
.style("stroke-width",5)
.raise(); // raise it so stroke is not under anything
})
// undo style changes on mouseout
.on("mouseout", function(d,i) {
d3.select(this)
.style("stroke-width", 0 );
})
.raise()
// make a feature collection of the regions:
var featureCollection = { "type":"FeatureCollection", "features": features }
// zoom to the selected area:
// if current projection is default projection:
if ( projection.translate().toString() === baseProjection.translate().toString() && projection.scale() === baseProjection.scale() ) {
zoom(baseProjection,projection.fitExtent([[50,50],[width-50,height-50]],featureCollection));
}
// current projection != default, zoom out and then in:
else {
// provide an end projection point for the transition:
var endProjection = d3.geoIdentity()
.reflectY(true)
.fitExtent([[50,50],[width-50,height-50]],featureCollection)
zoom(projection,endProjection,baseProjection);
}
});
});
});