简体   繁体   中英

d3 geo Change topojson file on click/zoom

I have a world map build in D3.js For performance's sake I am using world-atlas 110m version, but I want to increase the map detail as I zoom in.

I also want to change projection and, therefore topojson file when I click on the United States. (That is to use geoAlbersUsa(), and render US states)

I've got some very basic functionality on changing the map projection on click, but I am struggling on how to change the topojson file.

import * as d3 from "d3";
import { feature } from 'topojson';

var projections = [
  {name: "mercator", value: d3.geoMercator()},
  {name: "orthograpic", value: d3.geoOrthographic()},
  {name: "Us", value: d3.geoAlbersUsa()} ]

let projection = projections[1].value;
const svg = d3.select('svg');
const graticule = d3.geoGraticule();
let pathGenerator = d3.geoPath(projection);
let country;

const g = svg.append('g');
          

g.append('path')
    .attr('class', 'sphere')
    .attr('d', pathGenerator({type: 'Sphere'}));

g.append('path')
    .datum(graticule)
    .attr("class", "graticule")
    .attr("d", pathGenerator);

g.call(d3.drag()
.on("drag", (event, d) => {
  //if (projection === projections[1].value){
  const rotate = projection.rotate()
    const k = 75 / projection.scale()
    projection.rotate([
      rotate[0] + event.dx * k,
      rotate[1] - event.dy * k
    ])
    pathGenerator = d3.geoPath().projection(projection)
    svg.selectAll(".graticule").attr("d", pathGenerator)
    svg.selectAll(".country").attr("d", pathGenerator)
  //}
  //else{return}
}));

function update(){
  pathGenerator = d3.geoPath().projection(projection)
  svg.selectAll("path").attr("d", pathGenerator)
  svg.selectAll("path").transition().duration(750)
  svg.selectAll(".sphere").attr("d", pathGenerator({type:'Sphere'}))
    
}

Promise.all([
  d3.tsv('https://unpkg.com/world-atlas@1.1.4/world/110m.tsv'),
  d3.json('https://unpkg.com/world-atlas@1.1.4/world/110m.json'),
  d3.json("states-albers-10m.json")
]).then(([tsvData, topoJSONdata, usTopoJson]) => {
const countryName = tsvData.reduce((accumulator, d) => { //for each..
    accumulator[d.iso_n3] = d.name;
    return accumulator;
  })

const countries = feature(topoJSONdata, topoJSONdata.objects.countries);
  console.log(projection)
g.selectAll('.country').data(countries.features)
    .enter().append('path')
      .attr('class', 'country')
      .attr('d', pathGenerator)
      .on("click", function(event,d) {
        country = countryName[d.id];
        if(country === "United States" && projection !== projections[0].value){
          console.log(projection);
          projection = projections[0].value
          update();
        }
        else{
          projection = projections[1].value
          update();
        }
      })
})

g.call(d3.zoom().on('zoom', (event) => {
  g.attr('transform', event.transform, )
}))

LIVE DEMO: https://vizhub.com/Glebenator/810613a0a8584310abdcbcdaa78a368a

If you want to change features/data source/whatever with zoom, you'll need to get the current zoom state, which is contained in the the event passed to the zoom event listener (or d3.event before d3 v6).

This gives us a relatively easy zoom listener:

 .on("zoom", function(event) {

   // if zoomed in sufficiently:
   if(event.transform.k > 4) {
     /*  draw the detailed features */
   }
   // otherwise:
   else {
    /* draw the undetailed features */
   }
   // update the paths:      
   features.attr("d",path)
 })

Below I combine this with some semantic zooming, with which you can more easily apply a projection clip extent, which will stop the path generator from drawing features outside of the desired extent. It'll still project their points, but doesn't have to draw them. There are other ways to get performance gains, but I'll leave them for another day. The semantic zoom uses the zoom's translate and the original projection translate (the latter needs to be scaled by the zoom scale), and combines the zoom and projection scales:

  projection.translate([t.x+240*t.k,t.y+120*t.k]).scale(baseScale*t.k);

My example below is not fancy in how it loads data, it loads a base layer, and then it loads a more detailed file. Once the more detailed file loads, it applies a zoom behavior that allows you to zoom into a more detailed feature set (continents vs countries). There are lots of ways you might want to do this and my example is merely demonstrative.

For an optimization, you could see if the zoom level has crossed a threshold by storing the previous zoom value and only calling an re-render/update function if the new value has crossed a threshold.

 var svg = d3.select("body").append("svg").attr("width", 480).attr("height", 240); var baseScale = 480/Math.PI/2; var projection = d3.geoMercator().scale(baseScale).translate([240,120]).clipExtent([[0,0],[480,240]]) var path = d3.geoPath(projection); d3.json("https://cdn.jsdelivr.net/npm/world-atlas@2/land-110m.json").then(function(land) { // draw world initially: var world = topojson.feature(land, land.objects.land); var features = update([world]).attr("d",path); d3.json("https://cdn.jsdelivr.net/npm/world-atlas@2/countries-110m.json").then(function(countries) { // get a more detailed set of features: var detail = topojson.feature(countries, countries.objects.countries).features; // apply a zoom behavior: var zoom = d3.zoom().on("zoom", function(event) { var t = event.transform; // Semantic zoom using the projection: projection.translate([t.x+240*tk,t.y+120*tk]).scale(baseScale*tk); // if zoomed in sufficiently: if(tk > 1) { // draw the detailed features: features = update(detail); } // otherwise: else { // draw the undetailed features: features = update([world]); } // update the paths: features.attr("d",path) }) svg.call(zoom); }) }) function update(data) { return svg.selectAll("path").data(data).join("path"); }
 path { stroke-width: 1px; stroke: black; fill: #aaa; }
 <script src="https://cdnjs.cloudflare.com/ajax/libs/d3/6.2.0/d3.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/topojson/3.0.2/topojson.min.js"></script>

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM