Search code examples
jsond3.jsdata-visualizationgeojsontopojson

Double clicking Europe data visualization


I would like to create a choropleth map of Europe with the possibility of zooming. 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 (NUTS 2), each of which is colored according to a second measure.

Here an example: enter image description here

Suppose that Europe is composed of 5 countries: Country1, ..., Country5. Each country is colored according to the first measure (suppose the number of inhabitants). When the user double-clicks on Country4, the map is zoomed so that Country4 is at the center of the screen and viewed entirely. The adjacent countries are possibly cropped and blurred.

Country4 is now displayed as composed of its regions (R1, ..., R6). These regions are colored according to the second measure (suppose the per capita income). In the second situation, I would like the non-selected countries (therefore Country1, 2, 3 and 5) to be still colored according to measure 1.

So I would like something like this but with the ability to double click and view each country in more detail.

How can I do something like that? I haven't found examples that could be useful to me.

I found these json files and this one that I think they are useful (but I don't know how to use them).

Thank you


I found this file representing the nuts2 (regions) and this representing the nuts0 (countries).

How can I merge both? The idea is to start from nuts2.json and add the information of nuts0.json, but how can I do with geometries and arcs? I wouldn't like to create inconsistencies..


Solution

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