阅读数:

react项目中基于d3.js实现炫酷的中国地图

0

说明

最近项目需要按照中国地图区域进行展示数据,按照颜色的深浅来表示实际数据的大小,并且在鼠标浮动的时候显示具体的省份名称和其数量,
搜寻了一圈 hightchart, echart, viser,bizchart 均不能满足产品的需要,只能自己搞了。先看下效果:

效果图如下
0

实现

思路:

1、基石:我们选择d3.js 为我们提供了强大的svg画图功能
2、地图数据: 寻寻觅觅发现一款工具datav
可以快速的导出我们想要的区域地图的geojson结构
3、实际业务数据的映射
4、鼠标浮动的交互


先画地图,画地图之前我们先看下geojson的结构,
由于下载下来的geojson中,南海群岛是和整体地图纵向排列的,而产品实际需要将其展示在地图的左侧:哭!。于是乎曲线救国,发现群岛的数据
properties.name为空,以及最外侧的虚线是在海南省path[0]以为的数据中。发现了其path路径就好办了,先画整体地图,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
mapData = ChinaJson.features.filter((m) => m.properties.name);
mapData = mapData.map((item) => {
if (item.properties.name === "海南省") {
return {
...item,
geometry: {
...item.geometry,
coordinates: [item.geometry.coordinates[0]],
},
};
}
return {
...item,
};
});

过滤出除南海群岛以外的json数据,同理画左侧南海群岛的时候

1
ChinaJson.features.filter((m) => m.properties.name === "海南省" || !m.properties.name)

过滤出南海群岛和海南省的数据。

有了地图数据,下面就是d3大显身手的时候了

  • 先创建一个svg元素

    1
    2
    3
    4
    5
    let svg = d3
    .select("#" + id)
    .append("svg")
    .attr("width", width)
    .attr("height", height);
  • 利用d3 和json生成path路径值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//生成路径生成器
generatePath(scale, width, height) {
let self = this;
let projection = self.generateProjection(scale, width, height);
return d3.geoPath().projection(projection);
}


generateProjection(scale, width, height) {
return d3
.geoMercator()
.center([105, 31])
.scale(scale)
.translate([width / 2, height / 2 + 5]);
}
let self = this;
let path = self.generatePath(255, width, height);
  • 准备业务数据
    格式 [{name:”北京市”, value: 200},…]
    整个地图的色域从浅到深我们可以通过js方法动态生成,但是需要解决一个同样的值的时候,色值应该是一样的才对。
    可以先将业务数据利用value进行group分组,每个分组的色值是一样的即可,再进行从大到小排序
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
let { data = [] } = this.props;
const _data = groupBy(data, "value");
let i = -1;
let tempData = [];
Object.keys(_data).forEach((k) => {
i++;
_data[k] = _data[k].map((d) => ({
...d,
idx: i,
}));
tempData.push(..._data[k]);
});
data = tempData;
data.sort((a, b) => b.value - a.value);
//按照实际业务数据大小生成渐变色数组
const colors = gradientColor("#23F1FF", "#032352", data.length);
data = data.map((d, i) => {
return {
...d,
color: colors[i],
};
});
  • 画整体地图
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
svg.append("g")
.attr("fill", "#032352")
.selectAll("path")
.data(mapData)
.enter()
.append("path")
.attr("stroke", "#21E4FF")
.attr("stroke-width", 1)
.attr("fill", function (d, i) {
// data为实际的业务数据,name为关键字和地图json 进行匹配
const cur = data.find((_d) => _d.name === d.properties.name);
return (cur && cur.color) || "#032352";
})
.attr("d", path)
.attr("style", "opacity:0.6")

效果图
2

  • 画群岛地图
    方法同画整体,只是用一些css3的样式,将其移动到合适的位置即可
    效果图
    2

  • 添加交互
    鼠标移到具体省份后,需要从当前省份的固定中心开始画折线到地图上方固定位置,直接用d3 path就可以,终点固定,拐点也是固定的,只要解决起点的问题接口,
    有两种思路,一种是利用d3提供的求几何中心的方法

1
2
3
4
5
6
7
8
9
getBoundingBoxCenter(selection) {
// get the DOM element from a D3 selection
// you could also use "this" inside .each()
let element = selection.node();
// use the native SVG interface to get the bounding box
let bbox = element.getBBox();
// return the center of the bounding box
return [bbox.x + bbox.width / 2, bbox.y + bbox.height / 2];
}

另一种是利用d3的geoMercator来计算

1
2
3
4
5
6
7
const projection = d3
.geoMercator()
.center([105, 31])
.scale(255)
.translate([width / 2, height / 2 + 5]);
//d.properties.center geojson区域中心点
const local = projection(d.properties.center);

有了三个点,很容易的就可以画出一条并且起点有锚点的折线来,至于固定区域的展示,可以用html div固定在具体位置,通过数据来显隐即可。

我采用几何中心算法,最终效果
3

需要注意几个细节:
1、没数据的省份不需要
2、鼠标浮动到起始点锚点或者折线的时候,会触发区域的mouseout,导致tip一直在remove->draw->remove,一闪一闪的效果不太友好,
归根其实就是冒泡的问题,但是查了一圈,d3的冒泡没找到实际性的方法,只能曲线救国,在mouseout的时候判断下d3.event.relatedTarget
的样式是否包含 map-tip ,如果是说明鼠标到了折线或者锚点的位置,那不销毁tip即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
.on("mouseover", function (d) {
const obj = d3.select(this);
obj.attr("style", "opacity:1");
const curName = d.properties.name;
const cur = data.find((_d) => _d.name === curName);
if (!cur) {
_that.setState({
tip: {
title: "",
value: "",
},
});
return;
}
//仍在当前区域不再重新渲染
if (_that.state.tip.title === curName) {
return;
}
_that.setState({
tip: {
title: curName,
value: cur.value || 0,
},
});
let [centerX, centerY] = _that.getBoundingBoxCenter(obj);
// 微调几何中心
if (curName.indexOf("内蒙古") !== -1) {
centerY += 15;
} else if (curName.indexOf("广东") !== -1) {
centerY -= 5;
} else if (curName.indexOf("甘肃") !== -1) {
centerX += 15;
}
//画线
svg.selectAll(".map-tip-line").remove();
svg.selectAll(".map-tip-line-dot-big").remove();
svg.selectAll(".map-tip-line-dot").remove();
svg.append("g")
.attr("class", "map-tip-line")
.append("path")
.attr("d", function (d, index) {
let ret = [];
ret.push(
`M${centerX - width / 2}`,
`${centerY - height / 2}`,
`L0`,
`-70`,
`L0`,
`-90`,
`M-22`,
`-90`,
`L22`,
`-90`,
);
return ret;
})
.attr("stroke", "#21E4FF")
.attr("stroke-width", 1)
.attr("fill", "none")
.attr("transform", "translate(" + width / 2 + "," + (height / 2 + 5) + ")");
svg.append("g")
.attr("class", "map-tip-line-dot")
.attr("fill", "#fff")
.append("circle")
.attr("class", "map-tip-line-dot-circle")
.attr("cx", centerX - width / 2)
.attr("cy", centerY - height / 2)
.attr("r", 2)
.attr("stroke", "rgba(255,255,255,0.43)")
.attr("stroke-width", 1)
.attr("transform", "translate(" + width / 2 + "," + (height / 2 + 5) + ")");
svg.append("g")
.attr("class", "map-tip-line-dot-big")
.attr("fill", "rgba(255,255,255,0.25)")
.append("circle")
.attr("class", "map-tip-line-dot-circle")
.attr("cx", centerX - width / 2)
.attr("cy", centerY - height / 2)
.attr("r", 4)
.attr("transform", "translate(" + width / 2 + "," + (height / 2 + 5) + ")");
})
.on("mouseout", function (d) {
// 解决区域内tip mouseenter导致的p mouseout
if (
d3.event.relatedTarget &&
d3.event.relatedTarget.className &&
d3.event.relatedTarget.className.baseVal &&
d3.event.relatedTarget.className.baseVal.indexOf("map-tip") !== -1
) {
return;
}
const obj = d3.select(this);
obj.attr("style", "opacity:0.6");
svg.selectAll(".map-tip-line").remove();
svg.selectAll(".map-tip-line-dot-big").remove();
svg.selectAll(".map-tip-line-dot").remove();
_that.setState({
tip: {
title: "",
value: "",
},
});
});
  • 添加省份文字
    基于上述的求起点位置的方法,我们已经求出了中心问题,剩下的就d3.text即可
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
svg.append("g")
.selectAll("text")
.data(mapData)
.enter()
.append("text")
.text(function (d) {
return d.properties.name.substr(0, 2
})
.attr("fill", "#ffffff")
.attr("font-size", "0.25rem"););
})
.attr("x", function (d) {
const local = projection(d.properties.center);
return local[0];
})
.attr("y", function (d) {
const local = projection(d.properties.center);
return local[1];

效果图 4

最后

可以封装的再健壮些,比如增加是否显示文本flag,是否支持鼠标浮动展示flag、支持渐变色系等。


^-^欢迎回复交流^-^


0
赏点咖啡钱^.^