最近做的一个d3地图任务,需求是画出中国地图,在指定区域显示亮色并在指定城市显示光晕动画,做下笔记备忘。
标几个未来可能会再次遇到的需求点:
地图中指定区域设置颜色
通过.style(fill) 回调返回自定义填充色,指定区域上的text也可通过此方法自定义颜色
let gmap = svg.append("g")
gmap.selectAll("path")
.data(mapdata)
.enter()
.append("path")
.style("fill", (d, i, a) => {
if (Object.keys(cityordinate).includes(d.properties.name)) {
return 'rgba(29, 223, 201, 1)'
}
return "rgba(62, 241, 230, .4)"
});
光晕动画
用d3.interval 设置circle的大小和透明度;d3.scaleLinear设置映射参数和对应值的区间,用时间差取余作为参数形成3000毫秒一周期,绘制光晕效果
var scale = d3.scaleLinear();
scale.domain([0, 1800, 3000])//设置淡>亮>淡的趋势
.range([0, 0.9, 0]);
var scale2 = d3.scaleLinear();
scale2.domain([0, 3000])
.range([0, 8]);//设置从小变大的趋势
var start = Date.now();
this.dInter = d3.interval(function () {
var s1 = scale((Date.now() - start) % 3000);
var s2 = scale2((Date.now() - start) % 3000) < 0 ? -scale2((Date.now() - start) % 3000) : scale2((Date.now() - start) % 3000);
gmap.select("circle#hangzhouC")
.attr("fill-opacity", s1)
.attr("r", s2)
marker.select("circle#markerC")
.attr("fill-opacity", s1)
.attr("r", s2)
}, 100);
画两点射线
- 调用之前建立的project()方法,将终点经纬度转换为平面坐标。
- 计算起点(长沙)和终点之前的距离,做为线条长度和动画时间参数。
- 在线条上绘制一个圆形标记,并实现从起点到终点的移动动画。
- 标记移动到终点后,即删除,节省资源。
// 创建方法,输入data坐标,绘制发射线,此处按需求把连线stroke设为透明
let mline = svg.append("g").attr("id", "moveto").attr("stroke", "rgba(255, 192, 67, 0)").attr("stroke-width", 1.5).attr("fill", "#FFC043");
var moveto = function (city, data) {
var pf = { x: projection([120.21, 30.25])[0], y: projection([120.21, 30.25])[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);
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);
});
mline.append("circle")
.attr("cx", pf.x)
.attr("cy", pf.y)
.attr("r", 2)
.transition()
.duration(distance * 30)
.attr("transform", "translate(" + (pt.x - pf.x) + "," + (pt.y - pf.y) + ")")
.remove(); //射线头的样式,射线到达目标后移出线头的圆
};
完整的代码
initMap = async () => {
let cityordinate = await this.getAllJoinMedical() //获取已入驻城市数据
var width = 750.43 * screenWidth / 1920, height = screenWidth <= minWidth ? 620 * screenHeight / 1080 : 690 * screenHeight / 1080; // 定义SVG宽高
let svg
if (!this.svg) { //因为父组件会刷新,这里防止画出第二个地图
svg = d3.select(".fxmap")
.append("svg")
.attr("width", width)
.attr("height", height)
} else {
svg = this.svg
}
const defs = svg.append("defs"); //插入defs
const linearGradient = defs //defs中插入
.append("linearGradient")
.attr("id", "gradient"); //设置对应id
linearGradient //linearGradient中插入stop元素
.append("stop")
.attr("offset", "0%") //设置坡度,下同
.attr("stop-color", '#60F3F9');//设置对应颜色,下同
linearGradient //linearGradient中插入stop元素
.append("stop")
.attr("offset", "20%") //设置坡度,下同
.attr("stop-color", '#60F3F9');//设置对应颜色,下同
linearGradient //linearGradient中插入stop元素
.append("stop")
.attr("offset", "98%") //设置坡度,下同
.attr("stop-color", '#19B2BC');//设置对应颜色,下同
const lineDefs = svg.append("defs"); //插入defs
const linearGradient2 = lineDefs //defs中插入
.append("linearGradient")
.attr("id", "lineGradient"); //设置对应id
linearGradient2 //linearGradient中插入stop元素
.append("stop")
.attr("offset", "0%") //设置坡度,下同
.attr("stop-color", '#60F3F9');//设置对应颜色,下同
linearGradient2 //linearGradient中插入stop元素
.append("stop")
.attr("offset", "20%") //设置坡度,下同
.attr("stop-color", '#60F3F9');//设置对应颜色,下同
linearGradient2 //linearGradient中插入stop元素
.append("stop")
.attr("offset", "98%") //设置坡度,下同
.attr("stop-color", '#19B2BC');//设置对应颜色,下同
this.svg = svg
let gmap2 = svg.append("g").attr("id", "map2") //因为南海诸岛面积小不显色,边框又和背景颜色相同,故重建画布用填充色画边框,将南海数据抽出填进去
.attr("stroke", 'rgba(62, 241, 230, .4)').attr("stroke-width", 1)
.attr("fill", "rgba(62, 241, 230, .4)");
let gmap = svg.append("g").attr("id", "map")
.attr("stroke", 'rgba(4, 23, 62, .6)').attr("stroke-width", 1)
// var projection = d3.geoEquirectangular()
var projection = d3.geoMercator() //不同投影函数效果不同
.center([465, 395]) // 指定投影中心,注意[]中的是经纬度
.scale(height - 50) //根据什么进行缩放
.translate([width / 2 + 17, screenWidth <= minWidth ? height / 2 - 10 : height / 2 - 45]); //图形在画布中偏移设置
// var projection = d3
// .geoStereographic()//球形投影
// .center([465, 395]) // 指定投影中心,注意[]中的是经纬度
// .scale(height)
// // .clipAngle(180)
// .translate([width / 2, height / 2+20])
var path = d3.geoPath().projection(projection);
let marker = svg.append("defs")
.append("marker")
.append("marker")
.attr("id", "pointer")
.attr("viewBox", "0 0 24 24") // 可见范围
.attr("markerWidth", "24") // 标记宽度
.attr("markerHeight", "24") // 标记高度
.attr("orient", "auto") //
.attr("markerUnits", "strokeWidth") // 随连接线宽度进行缩放
.attr("refX", "6") // 连接点坐标
.attr("refY", "6")
// 绘制标记中心圆
marker.append("circle")
.attr("cx", "6")
.attr("cy", "6")
.attr("r", "2")
.attr("fill", "#FFC043");
// 绘制标记外圆,之后在timer()中添加闪烁效果
marker.append("circle")
.attr("id", "markerC")
.attr("cx", "6")
.attr("cy", "6")
.attr("r", "0")
.attr("fill-opacity", "0")
.attr("fill", "#FFC043")
// 记录杭州坐标
var hangzhou = projection([120.219375416, 30.2592444615]);
// 读取地图数据,并绘制中国地图
let mapdata = [];
// 读取地图数据
mapdata = chinaJson.features;
let mapdata2 = chinaJson2.features;
// 绘制地图
gmap2.selectAll("path") //画行政区边界
.data(mapdata2)
.enter()
.append("path")
.attr("d", path)
.style("fill", (d, i, a) => {
if (Object.keys(cityordinate).includes(d.properties.name)) { //已入驻地图显示另一种颜色
return 'rgba(29, 223, 201, 1)'
}
return "rgba(62, 241, 230, .4)"
});
gmap.selectAll("path")
.data(mapdata)
.enter()
.append("path")
.attr("d", path)
.style("fill", (d, i, a) => {
if (Object.keys(cityordinate).includes(d.properties.name)) {
return 'rgba(29, 223, 201, 1)'
}
return "rgba(62, 241, 230, .4)"
});
svg
.selectAll('text')
.data(mapdata)
.enter()
.append('text') //显示行政区名
.text((d, i) => {
return d.properties.name
})
.attr('font-size', 14)
.attr("x", function (d) {
let xNum = d.properties.centroid && Number(projection(d.properties.centroid)[0]) - (d.properties.name.length) * 14 / 2 || 0
if (d.properties.name == '香港') return xNum + 20
return xNum
})
.attr("y", function (d) {
let yNum = d.properties.centroid && projection(d.properties.centroid)[1] || 0
if (d.properties.name == '内蒙古') return yNum + 20
if (d.properties.name == '澳门') return yNum + 12
return yNum
})
.attr("stroke-width", 0)
.style('fill', (d, i, a) => {
if (Object.keys(cityordinate).includes(d.properties.name)) { return 'rgba(3, 2, 27, 1)' }
return "rgba(29, 223, 201, 1)"
})
// 标记杭州
gmap.append("circle").attr("id", "hangzhou")
.attr("cx", hangzhou[0])
.attr("cy", hangzhou[1])
.attr("r", "4")
.attr("fill", "#FFC043")
gmap.append("circle").attr("id", "hangzhouC")
.attr("cx", hangzhou[0])
.attr("cy", hangzhou[1])
.attr("r", "0")
.attr("stroke-width", "2")
.attr("fill-opacity", "0")
.attr("fill", "#FFC043")
.attr("stroke-opacity", 0)
.attr("stroke", "#FFC043")
// 创建方法,输入data坐标,绘制发射线,此处按需求把连线stroke设为透明
let mline = svg.append("g").attr("id", "moveto").attr("stroke", "rgba(255, 192, 67, 0)").attr("stroke-width", 1.5).attr("fill", "#FFC043");
var moveto = function (city, data) {
var pf = { x: projection([120.21, 30.25])[0], y: projection([120.21, 30.25])[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);
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);
});
mline.append("circle")
.attr("cx", pf.x)
.attr("cy", pf.y)
.attr("r", 2)
.transition()
.duration(distance * 30)
.attr("transform", "translate(" + (pt.x - pf.x) + "," + (pt.y - pf.y) + ")")
.remove(); //射线头的样式,射线到达目标后移出线头的圆
};
gmap2
.selectAll('text')
.data(mapdata2)
.enter()
.append('text')
.text((d, i) => {
return d.properties.name
})
.attr('font-size', 14)
.attr("x", function (d) {
let xNum = d.properties.centroid && Number(projection(d.properties.centroid)[0]) - (d.properties.name.length) * 14 / 2 || 0
return xNum
})
.attr("y", function (d) {
let yNum = d.properties.centroid && projection(d.properties.centroid)[1] || 0
return yNum + 5
})
.attr("stroke-width", 0)
.style('fill', (d, i, a) => {
if (Object.keys(cityordinate).includes(d.properties.name)) { return 'rgba(3, 2, 27, 1)' }
return "rgba(29, 223, 201, 1)"
})
// .style('fill', 'rgba(29, 223, 201, 1)')
var scale = d3.scaleLinear(); //映射函数
scale.domain([0, 1800, 3000])//映射区间,参数传入1800时返回0.9,设置淡>亮>淡的趋势
.range([0, 0.9, 0]);
var scale2 = d3.scaleLinear();
scale2.domain([0, 3000])
.range([0, 8]);//设置从小变大的趋势
var start = Date.now();
this.dInter = d3.interval(function () {
var s1 = scale((Date.now() - start) % 3000); //三秒为周期由小到大循环
var s2 = scale2((Date.now() - start) % 3000) < 0 ? -scale2((Date.now() - start) % 3000) : scale2((Date.now() - start) % 3000);
gmap.select("circle#hangzhouC")
.attr("fill-opacity", s1)
.attr("r", s2)
marker.select("circle#markerC")
.attr("fill-opacity", s1)
.attr("r", s2)
}, 100);
for (var key in cityordinate) {
if (key == '杭州') continue
moveto(key, cityordinate[key]); //发射线
};
}
技术过程借鉴:无鱼二饼