美文网首页
12-D3.js地图

12-D3.js地图

作者: learninginto | 来源:发表于2021-11-08 10:01 被阅读0次

    一、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);
    
    });
    
    • 美国部分地图json数据(网上可下载)
    {
        "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 平面的方法。

    • 定义D3投影
    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

    相关文章

      网友评论

          本文标题:12-D3.js地图

          本文链接:https://www.haomeiwen.com/subject/raluzltx.html