用 d3.js 生成可以互动的中国地图


这是一个 step by step 的教程,不讲解原理,想深入了解请咨询google和github。

1,首先,我们需要地图坐标

Nature earth 可以下载地图坐标,这里有1:10m,1:50m,1:110m三种精度的数据可选,第一种最详细,文件尺寸也最大,后面两种文件较小,但是不包含省市信息(除了北美)。

这个页面下载 1:10m 的地图
国家: Download countries (5.11 MB) version 3.1.0
省:Download states and provinces (13.97 MB) version 3.0.0
省会: Download populated places (1.46 MB) version 3.0.0

【这里不演示省会信息,但是处理方法相同】下载后解压缩可以看到多种数据格式,我们需要 .shp 文件。
也就是这两个文件:
ne_10m_admin_0_countries.shp【8.8M】
ne_10m_admin_1_states_provinces.shp【21.6M】

2,转换坐标文件

下载的文件包含全球所有国家和地区,我们现在只需要中国的地图数据。

现在需要用到 Geospatial Data Abstraction Library – GDAL 的 ogr2ogr 工具把shp文件转换为GeoJSON。
假设你用 OS X 并且安装了 brew,运行:

> brew install gdal

安装后运行:

> ogr2ogr -f GeoJSON -where "SU_A3 = 'CHN' OR SU_A3='TWN'" countries.json ne_10m_admin_0_countries.shp

同样处理省市文件:

> ogr2ogr -f GeoJSON -where "gu_a3 = 'CHN'" states.json ne_10m_admin_1_states_provinces.shp

转换后的文件就可以用 d3.js 来生成地图啦。

3,压缩坐标文件

但是有个问题,经过上面转换后,文件尺寸有

countries.json【651KB】
states.json 【2.6MB】

不可能直接用到网页中啊。还好已经有人做过这一步工作了, http://www.mapshaper.org/上传上面的文件,会展示出来地图信息,拖动滚动条调整精度,国家地图我选择4.0%,省市地图我选择 25%。

然后选择导出GeoJSON。

countries.json【651KB】 -> china_countries_min.json 【25KB】
states.json 【2.6MB】 -> states_min.json【767KB】

减少了很多,不过还是有点大,我们需要用另外一个工具 topojson 进一步去除不必要的信息,假设你用 OS X 并且安装了 Node.js,运行:

> npm install -g topojson

分别处理以上两个文件:

> topojson --id-property SU_A3 -p name=NAME -p name -o china_countries_topo.json china_countries_min.json
> topojson --id-property adm1_cod_1 -p name -o states_topo.json states_min.json

处理后:

countries.json【651KB】 -> china_countries_min.json 【25KB】-> china_countries_topo.json【6KB】
states.json 【2.6M】 -> states_min.json【767KB】-> states_topo.json【104KB】

文件小到网页加载可以接受了,不过,在 mapshapher.org 这一步,可以试试调成精度更低的省市级别的文件,进一步减小文件尺寸。

3,生成地图

终于来到了最后一步,展示地图,废话不多说直接看代码:

<!DOCTYPE html>
<meta charset="utf-8">
<style>
    #map {
        background-color: #fff;
        border: 1px solid #ccc;
        overflow:hidden;
    }
    .background {
        fill: none;
        pointer-events: all;
    }
    #countries, #states {
        fill: #cde;
        stroke: #fff;
        stroke-linejoin: round;
        stroke-linecap: round;
    }
    #countries .active, #states .active {
        fill: #89a;
    }
    #countries .active:hover,
    #states .active:hover {
        fill: #EAEAEA;
    }
    .province:hover{
        fill: #bad6fc;
    }
    #cities {
        stroke-width: 0;
    }
    .city {
        fill: #345;
        stroke: #fff;
    }
    pre.prettyprint {
        border: 1px solid #ccc;
        margin-bottom: 0;
        padding: 9.5px;
    }
</style>
<body>
<div id="map"></div>

<script src="//code.jquery.com/jquery-1.11.1.min.js"></script>
<script src="//d3js.org/d3.v3.min.js"></script>
<script src="//d3js.org/topojson.v1.min.js"></script>

<script>

    var m_width = $("#map").width(),
            width = 960,
            height = 800,
            centered;

    var projection = d3.geo.mercator()
            .center([104, 36])
            .scale(850)
            .translate([width/2, height/2]);

    var path = d3.geo.path()
            .projection(projection);

    var svg = d3.select("#map").append("svg")
            .attr("preserveAspectRatio", "xMidYMid")
            .attr("viewBox", "0 0 " + width + " " + height)
            .attr("width", m_width)
            .attr("height", m_width * height / width);

    svg.append("rect")
            .attr("class", "background")
            .attr("width", width)
            .attr("height", height)
            .on("click", clicked);

    var g = svg.append("g");

    d3.json("./json/china_countries_topo.json", function(error, us) {
        g.append("g")
                .attr("id", "countries")
                .selectAll("path")
                .data(topojson.feature(us, us.objects.china_countries_min).features)
                .enter().append("path")
                .attr("d", path)
                .on("click", clicked);
    });
    d3.json("./json/china_states_topo.json", function(error, us) {
        g.append("g")
                .attr("id", "states")
                .selectAll("path")
                .data(topojson.feature(us, us.objects.states_min).features)
                .enter().append("path")
                .attr("d", path)
                .attr("class", "province")
                .on("click", clicked);
    });

    function clicked(d) {
        console.log(d);
        var x, y, k;

        if (d && centered !== d) {
            var centroid = path.centroid(d);
            x = centroid[0];
            y = centroid[1];
            k = 4;
            centered = d;
        } else {
            x = width / 2;
            y = height / 2;
            k = 1;
            centered = null;
        }

        g.selectAll("path")
                .classed("active", centered && function(d) { return d === centered; });

        g.transition()
                .duration(750)
                .attr("transform", "translate(" + width / 2 + "," + height / 2 + ")scale(" + k + ")translate(" + -x + "," + -y + ")")
                .style("stroke-width", 1.5 / k + "px");
    }

</script>
</body>

4,说好的互动呢

上面的代码就可以互动啊,可以放大缩小。
d3.js 可以给地图节点绑定多种事件,并传入当前节点信息。svg地图也可以由css、javascript控制样式,可以做很多操作。

最终效果演示:http://900m.pro/demo/geo/

参考阅读及用到的工具:

Interactive Map with d3.js
【 D3.js 入门系列 — 10 】 地图的绘制
1:10m Cultural Vectors
http://www.mapshaper.org/
国家地区代码
d3.js
topojson