本文详解如何使用D3.js V4版本绘制中国物流地图,通过geoEquirectangular投影转换经纬度,实现从长沙出发的动态物流路线。重点解析如何利用SVG marker与CSS动画模拟货物移动及闪烁效果,并包含路线去重、资源清理等性能优化策略,提供完整代码示例。
D3.js物流地图实现概述
本文分享一段利用D3.js构建物流地图的示例代码,旨在为相关开发工作提供参考。
效果图展示

地图绘制与设计思路
设计思路解析
- 首先需绘制中国地图作为背景底图。
- 获取主要城市的经纬度坐标,用于确定物流路线的起点与终点。
- 当接收到新的物流订单时,在对应城市位置绘制一个具有闪烁效果的标记。
- 若目标城市此前已存在物流记录,则不再重复绘制静态路线。
- 每产生一笔新物流单,系统需生成一个沿预定路径向目标移动的动画标记。
- 动画执行完毕后,必须及时清理相关数据与DOM元素,以降低浏览器资源消耗。
代码实现步骤
具体编码实现
网页模板与D3.js引入
1. 构建网页基础模板
引入D3.js库。为便于调试,此处直接引用本地下载的d3.js文件;实际生产环境中建议替换为CDN路径或正确加载路径。需注意,本示例使用的是V4版本,其API与V3版本存在显著差异。
创建一个DIV容器,作为后续SVG绘图的挂载点。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf8">
<script type="text/javascript" src="../../static/d3/d3.js"></script>
<title>地图</title>
</head>
<body>
<div class="fxmap">
</div>
</body>
<script type="text/javascript"></script>
</html>
初始化SVG元素,后续所有绘图逻辑均封装在<script>标签内。
var width=1000 , height=800; // 定义SVG宽高
var svg = d3.select("body div.fxmap")
.append("svg")
.attr("width", "width)
.attr("height", height)
.style("background","#000"); //
定义SVG图形分组(Group),以便后续调用和管理不同图层。
- gmp分组:用于存储背景地图及起点标记。
- mline分组:用于存储起点至目标的连线及终点标记。
- button分组:用于放置测试用的控制按钮。
gmap = svg.append("g").attr("id", "map").attr("stroke", "white").attr("stroke-width",1);
mline = svg.append("g").attr("id", "moveto").attr("stroke", "#FFF").attr("stroke-width", 1.5).attr("fill","#FFF");
button = svg.append("g").attr("id", "button").attr("stroke", "white").attr("stroke-width", 1).attr("fill", "white");
投影函数配置
配置投影函数
- 经纬度数据无法直接用于平面绘图,需通过投影算法转换为平面坐标。D3.js提供了多种投影方法,本例选用geoEquirectangular()等距圆柱投影。
- projection函数负责将经纬度坐标映射为平面坐标。
- path函数则基于projection,将经纬度转换为SVG路径所需的绘制点坐标,从而避免手动构建path路径。
var projection = d3.geoEquirectangular()
.center([465,395]) // 指定投影中心,注意[]中的是经纬度
.scale(height)
.translate([width / 2, height / 2]);
var path = d3.geoPath().projection(projection);
Marker标记定义
定义Marker标记,供多条物流连线终点复用。
- 由于存在多个物流连线终点,统一使用一个marker标记进行调用以优化性能。
- 该标记由中心圆形与外圈组成,外圈的闪烁效果将单独通过动画实现。
marker = svg.append("defs")
.append("marker")
.append("marker")
.attr("id", "pointer")
.attr("viewBox","0 0 12 12") // 可见范围
.attr("markerWidth","12") // 标记宽度
.attr("markerHeight","12") // 标记高度
.attr("orient", "auto") //
.attr("markerUnits", "strokeWidth") // 随连接线宽度进行缩放
.attr("refX", "6") // 连接点坐标
.attr("refY", "6")
// 绘制标记中心圆
marker.append("circle")
.attr("cx", "6")
.attr("cy", "6")
.attr("r", "3")
.attr("fill", "white");
// 绘制标记外圆,之后在timer()中添加闪烁效果
marker.append("circle")
.attr("id", "markerC")
.attr("cx", "6")
.attr("cy", "6")
.attr("r", "5")
.attr("fill-opacity", "0")
.attr("stroke-width", "1")
.attr("stroke", "white");
地图绘制与起点标记
绘制中国地图并标记起点(长沙)。
地图数据源为china.json文件,此类文件在网络资源中较为常见。
// 记录长沙坐标
var changsha = projection([112.59,28.12]);
// 读取地图数据,并绘制中国地图
mapdata = [];
d3.json('china.json', function(error, data){
if (error)
console.log(error);
// 读取地图数据
mapdata = data.features;
// 绘制地图
gmap.selectAll("path")
.data(mapdata)
.enter()
.append("path")
.attr("d", path);
// 标记长沙
gmap.append("circle").attr("id","changsha")
.attr("cx", changsha[0])
.attr("cy", changsha[1])
.attr("r", "6")
.attr("fill", "yellow")
gmap.append("circle").attr("id","changshaC")
.attr("cx", changsha[0])
.attr("cy", changsha[1])
.attr("r", "10")
.attr("stroke-width", "2")
.attr("fill-opacity", "0");
});
物流路线与动画逻辑
定义绘制函数,实现从指定起点到终点的连线绘制,并在终点处添加marker标记。
- 该函数接收两个参数:终点城市名称(city)及其对应的经纬度数据(data)。
- 调用已建立的project()方法,将终点经纬度转换为平面坐标。
- 计算起点(长沙)与终点之间的欧几里得距离,该距离将作为线条长度及动画持续时间的参数。
- 在线条上绘制一个圆形标记,并实现从起点平滑移动至终点的动画效果。
- 标记到达终点后,立即从DOM中移除,以节省系统资源。
- 若该路线节点此前已绘制过,则跳过线条绘制,仅添加移动标记。
- 每处理一笔物流订单,对应城市的记录计数加1。
// 创建对象,保存每个城市的物流记录数量
var citylist = new Object();
// 创建方法,输入data坐标,绘制发射线
var moveto = function(city, data){
var pf = {x:projection([112.59,28.12])[0], y:projection([112.59,28.12])[1]};
var pt = {x:projection(data)[0], y:projection(data)[1]};
var distance = Math.sqrt((pt.x - pf.x)**2 + (pt.y - pf.y)**2);
if (city in citylist){
citylist[city]++;
}else{
mline.append("line")
.attr("x1", pf.x)
.attr("y1", pf.y)
.attr("x2", pt.x)
.attr("y2", pt.y)
.attr("marker-end","url(#pointer)")
.style("stroke-dasharray", " "+distance+", "+distance+" ")
.transition()
.duration(distance*30)
.styleTween("stroke-dashoffset", function(){
return d3.interpolateNumber(distance, 0);
});
citylist[city] = 1;
};
mline.append("circle")
.attr("cx", pf.x)
.attr("cy", pf.y)
.attr("r", 3)
.transition()
.duration(distance*30)
.attr("transform", "translate("+(pt.x-pf.x)+","+(pt.y-pf.y)+")")
.remove();
};
动画队列与测试验证
创建动画队列,实现标记外圈的闪烁视觉效果。
var scale = d3.scaleLinear();
scale.domain([0, 1000, 2000])
.range([0, 1, 0]);
var start = Date.now();
d3.timer(function(){
var s1 = scale((Date.now() - start)%2000);
// console.log(s1);
gmap.select("circle#changshaC")
.attr("stroke-opacity", s1);
marker.select("circle#markerC")
.attr("stroke-opacity", s1);
});
设置测试按钮及目标城市数据,用于验证功能。
var cityordinate = {
'哈尔滨':[126.5416150000,45.8088260000],
'石家庄':[116.46,39.92],
'北京':[116.39564503787867,39.92998577808024],
'上海':[121.480539,31.235929],
'广州':[113.271431,23.135336],
'重庆':[106.558434,29.568996],
'青岛':[120.38442818368189,36.10521490127382],
'福州':[119.30347,26.080429],
'兰州':[103.840521,36.067235],
'贵阳':[106.636577,26.653325],
'成都':[104.081534,30.655822],
'西安':[108.946466,34.347269],
'长春':[125.3306020000,43.8219540000],
'台湾':[120.961454,23.80406],
'呼和浩特':[111.7555090000,40.8484230000],
'澳门':[113.5494640000,22.1929190000],
'武汉':[114.3115820000,30.5984670000],
'昆明':[102.71460113878045,25.049153100453159],
'乌鲁木齐':[87.56498774111579,43.84038034721766],
'益阳':[112.36654664522563,28.58808777988717],
'南京':[118.77807440802562,32.05723550180587],
'武昌':[114.35362228468498,30.56486029278519],
};
// 随机获得目标城市
var cityname = [], total = 0;
for (var key in cityordinate){
cityname[total++] = key;
};
// 创建操作按钮,每次点击发射一条物流线
button.append("circle")
.attr("cx", width*0.9)
.attr("cy", height*0.8)
.attr("r", width/20)
.attr("text","click")
.attr("fill", "grey");
button.append("text")
.attr("x", width*0.87)
.attr("y", height*0.81)
.style("font-size", "30px")
.text("click");
button.on("click", function(){
var _index = ~~(Math.random() * total);
moveto(cityname[_index], cityordinate[cityname[_index]]);
});
以上内容为本文的全部讲解,希望对读者学习D3.js地图开发有所帮助,欢迎支持本站。