12-D3.js地图

2021-11-08  本文已影响0人  learninginto

一、JSON与GeoJSON

GeoJSON 是基于 JSON 的、 为 Web 应用而编码地理数据的一个标准。实际上,GeoJSON 并不是另一种格式, 而只是 JSON 非常特定的一种使用方法。

var w = 500;
var h = 300;

//Define path generator, using the Albers USA projection
var path = d3.geoPath()
    .projection(d3.geoAlbersUsa());

//Create SVG element
var svg = d3.select("body")
    .append("svg")
    .attr("width", w)
    .attr("height", h);

//Load in GeoJSON data
d3.json("us-states.json", function (json) {

    //Bind data and create one path per GeoJSON feature
    svg.selectAll("path")
        .data(json.features)
        .enter()
        .append("path")
        .attr("d", path);

});
{
    "type": "FeatureCollection",
    "features": [
        {
            "type": "Feature",
            "id": "01",
            "properties": {
                "name": "Alabama"
            },
            "geometry": {
                "type": "Polygon",
                "coordinates": [
                    [
                        [
                            -87.359296,
                            35.00118
                        ],
                        [
                            -85.606675,
                            34.984749
                        ],
                        [
                            -85.431413,
                            34.124869
                        ],
                        [
                            -85.184951,
                            32.859696
                        ],
...

d3.json() 接受两个参数。第一个是要加载的文件的路径,第二个是在加载并解析 完 JSON 文件后执行的回调函数。

d3.json("us-states.json", function(json) {
    
    //Bind data and create one path per GeoJSON feature
    svg.selectAll("path")
        .data(json.features)
        .enter()
        .append("path")
        .attr("d", path);
});

d3.json() 与 d3.csv() 类似,都是异步执行的函数。换句话说, 浏览器在等待外部文件加载时,其他代码照样执行,不会受影响。因此,位于回调 函数后面的代码有可能先于回调函数本身执行

最后,我们把GeoJSON的地理特征绑定到新创建的path元素,为每个特征值创建一个path

path.png

现在的问题是,这张地图并没有覆盖美国全境。要纠正这个问题,需要 修改我们使用的投影(projection)。所谓投影,就是一种折中算法,一种把 3D 空间“投影”到 2D 平面的方法。

var projection = d3.geoAlbersUsa()
    .translate([w / 2, h / 2])

现在唯一要修改的,就是明确告诉路径生成器,应该使用这个自定义的投影来生成所有路径

var path = d3.geoPath()
    .projection(projection);
投影.png

同样的GeoJSON数据,添加一个scale()方法,但现在把投影居中了

var projection = d3.geoAlbersUsa()
    .translate([w / 2, h / 2])
    .scale([500]);
居中.png

再添加一个style()语句,修改一下路径填充设置的颜色

修改填充颜色.png

二、等值区域

不同区域填充了不同值(深或浅)或颜色,以反映关联数据值的地图

首先,创建一个比例尺,将数据值作为输入,返回不同的颜色。这是等值区域地图的核心所在:

var color = d3.scaleQuantize()          .range(["rgb(237,248,233)","rgb(186,228,179)","rgb(116,196,118)","rgb(49,163,84)","rgb(0,109,44)"]);

以量化的比例尺函数作为线性比例尺,但比例尺输出的则是离散的范围。这里输出的值可以是数值、颜色这里就是),或者其他你需要的值。这个比例尺适合把值分类为不同的组(bucket)。我们这里只分了 5 个组,实际上你想分几个就分几个。

等值区域

这里用到了数据us-ag-productivity-2004.csv

state,value
Alabama,1.1791
Arkansas,1.3705
Arizona,1.3847
California,1.7979
Colorado,1.0325
Connecticut,1.3209
Delaware,1.4345
...

这些数据来自美国农业部,报告内容是 2004 年每个州的农业生产力指标。

要加载这些数据,使用D3.csv(),在回调函数中,要设置彩色的量化比例尺的输入值域

d3.csv("us-ag-productivity.csv", function (data) {

    //在回调函数中,要设置彩色的量化比例尺的输入值域
    color.domain([
        d3.min(data, function (d) { return d.value; }),
        d3.max(data, function (d) { return d.value; })
    ]);

这里用到了 d3.min() 和 d3.max() 来计算并返回最小和最大的数据值,因此这个比例尺的输出值域是动态计算的。

接下来,跟前面一样,加载 JSON 地理数据。但不同的是,在这里我想把农业生产力的数据合并到 GeoJSON 中。为什么?因为我们一次只能给元素绑定一组数据。 GeoJSON 数据肯定必不可少,因为要据以生成路径,而我们还需要新的农业生产力数据。为此,就要把它们混合成一个巨大的数组,然后再把混合后的数据绑定到新创建的 path 元素。

d3.json("us-states.json", function (json) {

    //混合农业生产力数据和GeoJSON
    //循环农业生产力数据集中的每个值
    for (var i = 0; i < data.length; i++) {

        //取得州名
        var dataState = data[i].state;

        //取得数据值,并从字符串转换成浮点数
        var dataValue = parseFloat(data[i].value);

        //在GeoJSON中找到相应的州
        for (var j = 0; j < json.features.length; j++) {

            var jsonState = json.features[j].properties.name;

            if (dataState == jsonState) {
                //将数据复制到JSON中
                json.features[j].properties.value = dataValue;
                break;
            }
        }
    }

遍历取得州的数据值,把它放到json.features[j].properties.value中,保证它能被绑定到元素,并在将来需要时可以被取出来。

最后,像以前一样创建路径,只是通过 style() 要设置动态的值,如果数据不存在,则设置成默认的浅灰色。

svg.selectAll("path")
    .data(json.features)
    .enter()
    .append("path")
    .attr("d", path)
    .style("fill", function (d) {
        //Get data value
        var value = d.properties.value;

        if (value) {
            //If value exists…
            return color(value);
        } else {
            //If value is undefined…
            return "#ccc";
        }
    });

三、添加定位点

能够看到在那些生产力最高(或最低)的州有多少大城市会很有意思,也更有价值。我们只想得到最大的

50 个城市的数据,所以就把其他城市的数据都删掉了。再导出为 CSV,就有了以下数据:

rank,place,population,lat,lon
1,New York,8550405,40.71455,-74.007124
2,Los Angeles,3971883,34.05349,-118.245319
3,Chicago,2720546,41.88415,-87.632409
4,Houston,2296224,29.76045,-95.369784
5,Philadelphia,1567442,39.95228,-75.162454
6,Phoenix,1563025,33.44826,-112.075774
7,San Antonio,1469845,29.42449,-98.494619
8,San Diego,1394928,32.715695,-117.161719
9,Dallas,1300092,32.778155,-96.795404
10,San Jose,1026908,37.338475,-121.885794
……

在这个回调函数内部,可以用代码表达怎么用新创建的 circle 元素代表每个城市。 然后,根据各自的地理坐标,将它们定位到地图上:

d3.csv("us-cities.csv", function (data) {

    svg.selectAll("circle")
        .data(data)
        .enter()
        .append("circle")
        .attr("cx", function (d) {
            return projection([d.lon, d.lat])[0];
        })
        .attr("cy", function (d) {
            return projection([d.lon, d.lat])[1];
        })
        .attr("r", 5)
        .style("fill", "yellow")
        .style("stroke", "gray")
        .style("stroke-width", 0.25)
        .style("opacity", 0.75)
        .append("title")            //Simple tooltip
        .text(function (d) {
            return d.place + ": Pop. " + formatAsThousands(d.population);
        });

});

以上代码的关键是通过 attr() 语句设定 cx 和 cy 值。可以通过 d.lon 和d.lat 获得原始的经度和纬度。

但我们真正需要的,则是在屏幕上定位这些圆形的 x/y 坐标,而不是地理坐标。

因此就要借助 projection(),它本质上是一个二维比例尺方法。给 D3 的比例尺传入一个值,它会返回另一个值。对于投影而言,我们传入两个数值,返回两个数值。(投影和简单的比例尺的另一个主要区别是后台计算,前者要复杂得多,后者只是一个简单的归一化映射。)

地图投影接受一个包含两个值的数组作为输入,经度在前,纬度在后(这是 GeoJSON 格式规定的)。然后,投影就会返回一个包含两个值的数组,分别是屏幕上的 x/y 坐标值。因此,设定 cx 时使用 [0] 取得第一个值,也就是 x 坐标值;设定cy 时使用 [1] 取得第二个值,也就是 y 坐标值。

添加定位点.png

不过,这些点大小都一样啊,应该把人口数据反映到圆形大小上。为此,可以这样 引用人口数据:

.attr("r", function(d) {
    return Math.sqrt(parseInt(d.population) * 0.00004);
})

这里先取得 d.population,把它传给 parseInt() 实现从字符串到整数值的转换, 再随便乘一个小数降低其量级,最后得到其平方根(把面积转换为半径)。

最大的城市突出出来了。城市大小的差异很明显,这种情况可能更适合使用对数比例尺,尤其是在包含人口更少的城市的情况下。这时候就不用乘以0.00004 了,可以直接使用自定义的 D3 比例尺函数

表示城市的圆点面积对应着人口.png

这个例子的关键在于,我们整合了两个不同的数据集,把它们加载并显示在了地图上。(如果算上地理编码坐标,一共就是三个数据集)

四、移动地图

var projection = d3.geoAlbersUsa()
    .translate([w / 2, h / 2])
    .scale([2000]);
  1. 创建方向键时,添加相同的class名称,方便后面添加点击事件
  2. 为方向键添加唯一的id,通过 d3.select(this).attr("id")选择
  3. 点击后,移动时添加过渡动画
var createPanButtons = function () {

    //Create the clickable groups

    //North
    var north = svg.append("g")
        .attr("class", "pan")   //All share the 'pan' class
        .attr("id", "north");   //The ID will tell us which direction to head

    north.append("rect")
        .attr("x", 0)
        .attr("y", 0)
        .attr("width", w)
        .attr("height", 30);

    north.append("text")
        .attr("x", w / 2)
        .attr("y", 20)
        .html("&uarr;");

    //South
    var south = svg.append("g")
        .attr("class", "pan")
        .attr("id", "south");

    south.append("rect")
        .attr("x", 0)
        .attr("y", h - 30)
        .attr("width", w)
        .attr("height", 30);

    south.append("text")
        .attr("x", w / 2)
        .attr("y", h - 10)
        .html("&darr;");

    //West
    var west = svg.append("g")
        .attr("class", "pan")
        .attr("id", "west");

    west.append("rect")
        .attr("x", 0)
        .attr("y", 30)
        .attr("width", 30)
        .attr("height", h - 60);

    west.append("text")
        .attr("x", 15)
        .attr("y", h / 2)
        .html("&larr;");

    //East
    var east = svg.append("g")
        .attr("class", "pan")
        .attr("id", "east");

    east.append("rect")
        .attr("x", w - 30)
        .attr("y", 30)
        .attr("width", 30)
        .attr("height", h - 60);

    east.append("text")
        .attr("x", w - 15)
        .attr("y", h / 2)
        .html("&rarr;");

    //Panning interaction

    d3.selectAll(".pan")
        .on("click", function () {

            //Get current translation offset
            var offset = projection.translate();

            //Set how much to move on each click
            var moveAmount = 50;

            //Which way are we headed?
            var direction = d3.select(this).attr("id");

            //Modify the offset, depending on the direction
            switch (direction) {
                case "north":
                    offset[1] += moveAmount;  //Increase y offset
                    break;
                case "south":
                    offset[1] -= moveAmount;  //Decrease y offset
                    break;
                case "west":
                    offset[0] += moveAmount;  //Increase x offset
                    break;
                case "east":
                    offset[0] -= moveAmount;  //Decrease x offset
                    break;
                default:
                    break;
            }

            //Update projection with new offset
            projection.translate(offset);

            //Update all paths and circles
            svg.selectAll("path")
          .transition()
                .attr("d", path);

            svg.selectAll("circle")
        .transition()
                .attr("cx", function (d) {
                    return projection([d.lon, d.lat])[0];
                })
                .attr("cy", function (d) {
                    return projection([d.lon, d.lat])[1];
                });
        });
};
  1. 给上下左右四个方向添加的矩形,添加CSS样式
.pan rect {
    fill: black;
    opacity: 0.2;
}

.pan text {
    fill: black;
    font-size: 18px;
    text-anchor: middle;
}

.pan:hover rect,
.pan:hover text {
    fill: blue;
}

五、添加拖拽效果

随着鼠标的移动,增加偏移量

var dragging = function(d) {

    //Log out d3.event, so you can see all the goodies inside
    console.log(d3.event);

    //获取当前的偏移量
    var offset = projection.translate();

    //随着鼠标的移动,增加偏移量
    offset[0] += d3.event.dx;
    offset[1] += d3.event.dy;

    //使用新的偏移量更新投影
    projection.translate(offset);

    //更新所有路径和圆
    svg.selectAll("path")
        .attr("d", path);

    svg.selectAll("circle")
        .attr("cx", function(d) {
            return projection([d.lon, d.lat])[0];
        })
        .attr("cy", function(d) {
            return projection([d.lon, d.lat])[1];
        });

}

//定义拖拽行为
var drag = d3.drag()
                .on("drag", dragging);

//创建一个容器,所有可平移元素都将位于其中
var map = svg.append("g")
            .attr("id", "map")
            .call(drag);  //绑定拖拽行为

创建一个新的不可见背景矩形以捕获拖动事件

map.append("rect")
    .attr("x", 0)
    .attr("y", 0)
    .attr("width", w)
    .attr("height", h)
    .attr("opacity", 0);

六、添加鼠标滚轮缩放

  1. 创建一个容器,所有可缩放的元素都将位于其中
  2. 初始化时,大概找一个国家的中心点
  3. 计算缩放比例时,乘以初始的系数
//Define what to do when panning or zooming
var zooming = function(d) {

    // console.log(d3.event.transform);

    //New offset array
    var offset = [d3.event.transform.x, d3.event.transform.y];

    //计算新的文件
    var newScale = d3.event.transform.k * 2000;

    //使用新的偏移和比例更新投影
    projection.translate(offset)
                .scale(newScale);

    //更新所有路径和圆
    svg.selectAll("path")
        .attr("d", path);

    svg.selectAll("circle")
        .attr("cx", function(d) {
            return projection([d.lon, d.lat])[0];
        })
        .attr("cy", function(d) {
            return projection([d.lon, d.lat])[1];
        });

}

//定义缩放行为
var zoom = d3.zoom()
                .on("zoom", zooming);

//初始化时,大概找一个国家的中心点
var center = projection([-97.0, 39.0]);

//Create a container in which all zoom-able elements will live
var map = svg.append("g")
            .attr("id", "map")
            .call(zoom)  //Bind the zoom behavior
            .call(zoom.transform, d3.zoomIdentity  //初始缩放变换
                .translate(w/2, h/2)
                .scale(0.25)
                .translate(-center[0], -center[1]));

点击后使用固定的平移按钮缩放地图,触发缩放事件,按x、y缩放

d3.selectAll(".pan")
.on("click", function () {

    //Set how much to move on each click
    var moveAmount = 50;

    //Set x/y to zero for now
    var x = 0;
    var y = 0;

    //Which way are we headed?
    var direction = d3.select(this).attr("id");

    //Modify the offset, depending on the direction
    switch (direction) {
        case "north":
            y += moveAmount;  //Increase y offset
            break;
        case "south":
            y -= moveAmount;  //Decrease y offset
            break;
        case "west":
            x += moveAmount;  //Increase x offset
            break;
        case "east":
            x -= moveAmount;  //Decrease x offset
            break;
        default:
            break;
    }

    // 触发缩放事件,按x、y缩放
    map.transition()
        .call(zoom.translateBy, x, y);

});

七、添加放大缩小按钮

  1. 右下角添加放大和缩小按钮
  2. 点击加号,放大为1.5倍;点击减号,缩小到0.75倍
var createZoomButtons = function () {

    //Create the clickable groups

    //Zoom in button
    var zoomIn = svg.append("g")
        .attr("class", "zoom")  //All share the 'zoom' class
        .attr("id", "in")       //The ID will tell us which direction to head
        .attr("transform", "translate(" + (w - 110) + "," + (h - 70) + ")");

    zoomIn.append("rect")
        .attr("x", 0)
        .attr("y", 0)
        .attr("width", 30)
        .attr("height", 30);

    zoomIn.append("text")
        .attr("x", 15)
        .attr("y", 20)
        .text("+");

    //Zoom out button
    var zoomOut = svg.append("g")
        .attr("class", "zoom")
        .attr("id", "out")
        .attr("transform", "translate(" + (w - 70) + "," + (h - 70) + ")");

    zoomOut.append("rect")
        .attr("x", 0)
        .attr("y", 0)
        .attr("width", 30)
        .attr("height", 30);

    zoomOut.append("text")
        .attr("x", 15)
        .attr("y", 20)
        .html("&ndash;");

    //Zooming interaction

    d3.selectAll(".zoom")
        .on("click", function () {

            //Set how much to scale on each click
            var scaleFactor;

            //Which way are we headed?
            var direction = d3.select(this).attr("id");

            //Modify the k scale value, depending on the direction
            switch (direction) {
                case "in":
                    scaleFactor = 1.5;
                    break;
                case "out":
                    scaleFactor = 0.75;
                    break;
                default:
                    break;
            }

            //This triggers a zoom event, scaling by 'scaleFactor'
            map.transition()
                .call(zoom.scaleBy, scaleFactor);

        });

};
放大和缩小.png

限制放大和缩小的倍数,避免无效的无限制放大

var zoom = d3.zoom()
    .scaleExtent([0.2, 2.0])
    .translateExtent([[-1200, -700], [1200, 700]])
    .on("zoom", zooming);

迅速定位到某一位置/重置

<div>
    <button id="pnw">Pacific Northwest</button>
    <button id="reset">Reset</button>
</div>
d3.select("#pnw")
    .on("click", function () {

        map.transition()
            .call(zoom.transform, d3.zoomIdentity
                .translate(w / 2, h / 2)
                .scale(0.9)
                .translate(600, 300));

    });

//Bind 'Reset' button behavior
d3.select("#reset")
    .on("click", function () {

        map.transition()
            .call(zoom.transform, d3.zoomIdentity  //Same as the initial transform
                .translate(w / 2, h / 2)
                .scale(0.25)
                .translate(-center[0], -center[1]));

    });
重置.png

添加label样式和数值

.label {
    font-family: Helvetica, sans-serif;
    font-size: 11px;
    fill: black;
    text-anchor: middle;
}
var zooming = function (d) {
    svg.selectAll(".label")
    .attr("x", function (d) {
      return path.centroid(d)[0]
    })
    .attr("y", function (d) {
      return path.centroid(d)[1]
    })
}

可以使用d3中的format将数据格式化

var formatDecimals = d3.format(".3");  //e.g. converts 1.23456 to "1.23"

和地图坐标绑定

map.selectAll("text")
    .data(json.features)
    .enter()
    .append("text")
    .attr("class", "label")
    .attr("x", function (d) {
        return path.centroid(d)[0]
    })
    .attr("y", function (d) {
        return path.centroid(d)[1]
    })
    .text(function(d){
        if(d.properties.value){
            return formatDecimals(d.properties.value)
        }
    })
label数据.png
上一篇下一篇

猜你喜欢

热点阅读