分享通过D3实现的站点路线分布图demo,后续会继续更新其他功能。
功能点
-
点位弹窗
效果图如下:
-
轨迹高亮
效果图如下:
-
添加路线箭头
箭头展示逻辑:根据高速路线最后两个点位,计算得出箭头的点位
效果图如下:
-
清空画布
效果图如下:
-
画布自适应
自适应逻辑:根据点位的经纬度和画布容器的宽高计算得出点位在容器内的坐标,监听窗口大小的变化,进而重新计算
效果图如下:
-
直线和曲线
效果图如何下:
源码如下:
// data.js
export const pointData = [
{
lat: 30.64234,
lon: 120.266129,
label: "湖州",
code: 2117,
source: 2117,
target: 2115,
type: "station",
gsName: "申苏浙皖",
position: 'start',
},
{
lat: 30.43121,
lon: 120.3899,
label: "织里",
code: 2115,
source: 2115,
target: 2113,
type: "station",
gsName: "申苏浙皖",
position: 'transition',
},
{
lat: 30.39120,
lon: 120.493281,
label: "南浔",
code: 2113,
source: 2113,
target: null,
type: "station",
gsName: "申苏浙皖",
position: 'end',
},
{
lat: 30.3129,
lon: 120.3892,
label: "湖州东",
code: 3233,
source: 3233,
target: 3231,
type: "station",
gsName: "申嘉湖",
position: 'start',
},
{
lat: 30.40219,
lon: 120.384172,
label: "双林",
code: 3231,
source: 3231,
target: 3229,
type: "station",
gsName: "申嘉湖",
position: 'transition',
},
{
lat: 30.244234,
lon: 120.2491291,
label: "南浔南",
code: 3229,
source: 3229,
target: null,
type: "station",
gsName: "申嘉湖",
position: 'end',
},
{
lat: 30.529328,
lon: 120.36172,
label: "钟管",
code: 4049,
source: 4049,
target: 4051,
type: "station",
gsName: "杭绕西复线",
position: 'start',
},
{
lat: 30.237215,
lon: 120.32129,
label: "新市西",
code: 4051,
source: 4051,
target: 3206,
type: "station",
gsName: "杭绕西复线",
position: 'transition',
},
{
lat: 30.482192,
lon: 120.43321,
label: "新市枢纽",
code: 3206,
source: 3206,
target: null,
type: "station",
gsName: "杭绕西复线",
position: 'end',
},
{
lat: 30.4011,
lon: 120.1691291,
label: "雷甸",
code: 3243,
source: 3243,
target: 3241,
type: "station",
gsName: "练杭",
position: 'start',
},
{
lat: 30.432910,
lon: 120.330291,
label: "新安",
code: 3241,
source: 3241,
target: 3239,
type: "station",
gsName: "练杭",
position: 'transition',
},
{
lat: 30.235074,
lon: 120.2374643,
label: "新市",
code: 3239,
source: 3239,
target: null,
type: "station",
gsName: "练杭",
position: 'end',
},
{
lat: 30.46217,
lon: 120.3748291,
label: "织里枢纽",
code: 5101,
source: 5101,
target: 5111,
type: "hub",
gsName: "沪杭高速",
position: 'start',
},
{
lat: 30.30291,
lon: 120.2391291,
label: "织里东",
code: 5111,
source: 5111,
target: 5113,
type: "hh-station",
gsName: "沪杭高速",
position: 'transition',
},
{
lat: 30.18392,
lon: 120.5891291,
label: "南浔西",
code: 5113,
source: 5113,
target: 5102,
type: "hh-station",
gsName: "沪杭高速",
position: 'transition',
},
{
lat: 30.3874,
lon: 120.42716,
label: "双林枢纽",
code: 5102,
source: 5102,
target: 5115,
type: "hh-station",
gsName: "沪杭高速",
position: 'transition',
},
{
lat: 30.23847,
lon: 120.435243,
label: "菱湖(分中心)",
code: 5115,
source: 5115,
target: 5117,
type: "hh-station",
gsName: "沪杭高速",
position: 'transition',
},
{
lat: 30.62261,
lon: 120.37263,
label: "千金",
code: 5117,
source: 5117,
target: 5103,
type: "hh-station",
gsName: "沪杭高速",
position: 'transition',
},
{
lat: 30.573625,
lon: 120.19238,
label: "士林枢纽",
code: 5103,
source: 5103,
target: 5119,
type: "hub",
gsName: "沪杭高速",
position: 'transition',
},
{
lat: 30.426152,
lon: 120.23746,
label: "下舍",
code: 5119,
source: 5119,
target: 5104,
type: "hh-station",
gsName: "沪杭高速",
position: 'transition',
},
{
lat: 30.473621,
lon: 120.329182,
label: "新安枢纽",
code: 5104,
source: 5104,
target: null,
type: "hub",
gsName: "沪杭高速",
position: 'end',
},
];
export const colorList = [
"#409EFF",
"#67C23A",
"#E6A23C",
"#F56C6C",
"#909399",
]
<template>
<div class="d3-container">
<div class="action-panel">
<input
type="button"
class="margin-right-6"
@click="setTooltipStatus"
:value="tooltipStatus ? '弹窗关闭' : '弹窗开启'"
/>
<input
type="button"
class="margin-right-6"
@click="setTraceStatus"
:value="traceStatus ? '轨迹高亮关闭' : '轨迹高亮开启'"
/>
<input
type="button"
class="margin-right-6"
@click="setArrowStatus"
:value="arrowStatus ? '不显示箭头' : '显示箭头'"
/>
<input
type="button"
class="margin-right-6"
@click="setLineStatus"
:value="lineStatus ? '直线连接' : '非直线连接'"
/>
<input type="button" @click="clearSvg" value="清空画布" />
</div>
<div class="map-test" ref="d3Chart">
<div class="tooltip" id="popup-element">
<span>{{ text }}</span>
<i id="close-element" class="el-icon-close"></i>
<span class="arrow"></span>
</div>
</div>
<div class="empty" v-if="allData.length == 0">
<span>暂无数据</span>
</div>
</div>
</template>
<script>
import * as d3 from "d3";
import { pointData, colorList } from "./data";
export default {
name: "MapTest",
components: {},
data() {
return {
text: null,
svgInstance: null, // d3元素实例
popupInstance: null, // 弹窗实例
closeBtnInstance: null, // 关闭
allData: [], // 全部点位数据
allLineData: [], // 全部连线数据
gsNamePointData: [], // 高速名称点位数据
tooltipStatus: false, // 弹窗状态
traceStatus: false, // 轨迹高亮状态
arrowStatus: false, // 显示箭头状态
lineStatus: false, // 连接线状态
};
},
computed: {},
methods: {
createChart() {
let width = this.$refs.d3Chart.offsetWidth;
let height = this.$refs.d3Chart.offsetHeight;
if (!this.svgInstance) {
this.svgInstance = d3
.select(this.$refs.d3Chart)
.append("svg")
.attr("width", width)
.attr("height", height)
.attr("viewBox", `0 0 ${width} ${height}`)
.attr("preserveAspectRatio", "xMidYMid slice");
}
this.renderAction(width, height);
},
getData() {
this.allData = pointData;
if (this.allData.length) {
this.$nextTick(() => {
this.createChart();
});
} else {
this.clearSvg();
}
},
/**
* 执行渲染操作
* @param {Number} width 容器宽度
* @param {Number} height 容器高度
* @returns {Null} void
*/
renderAction(width, height) {
const pointConvert = this.pointDataFormat(pointData, width, height);
if (!pointConvert.length) return;
if (this.arrowStatus) {
// 全部点位信息
this.allData = this.pushArrowPoint(pointConvert);
} else {
this.allData = pointConvert;
}
// 获取“全部点位”连线数据
this.allLineData = this.getLineData(this.allData);
// 获取高速名称集合
const gsKeyToValue = [
...new Set(pointData.map((ele) => ele.gsName)),
];
// 高速名称数据
gsKeyToValue.forEach((ele, index) => {
const line = this.allLineData.filter(
(item) => item.gsName === ele
);
if (line.length > 0) {
this.drawLine(
this.svgInstance,
line,
ele,
colorList[index]
);
}
const gsPointList = this.allData.filter(
(item) => item.gsName === ele
);
const len = gsPointList.length;
if (len >= 2) {
this.gsNamePointData.push(
this.calculateGSNamePosition(
gsPointList[len - 1],
gsPointList[len - 2],
ele,
this.arrowStatus ? 50 : 100,
"after",
"text"
)
);
} else if (len == 1) {
this.gsNamePointData.push(
this.calculateGSNamePosition(
gsPointList[len - 1],
gsPointList[len - 1],
ele,
this.arrowStatus ? 50 : 100,
"after",
"text"
)
);
}
});
// 画“全部点位”
this.drawPoint(this.svgInstance, this.allData);
// 画“收费站编码”
this.drawPointText(
this.svgInstance,
this.allData,
"code",
0,
-20,
20,
6
);
// 画“收费站名称”
this.drawPointText(
this.svgInstance,
this.allData,
"label",
0,
-15,
20,
-10,
"#000",
15
);
// 画高速名称
this.drawPointText(
this.svgInstance,
this.gsNamePointData,
"label",
0,
-25,
0,
30,
"#000",
16,
"bold"
);
},
/**
* 通过判断type返回目标图片的地址
* @param {String} type 图片类型
* @returns {String} url 目标图片的地址
*/
setImgUrl(type) {
let url;
switch (type) {
case "gantry":
url = require("../../../assets/equipmentIcon.png");
break;
case "station":
url = require("../../../assets/dataIcon.png");
break;
case "hub":
url = require("../../../assets/userIcon.png");
break;
case "hh-station":
url = require("../../../assets/homeIcon.png");
break;
default:
url = require("../../../assets/user.png");
break;
}
return url;
},
/**
* 画点
* @param {Object} svg d3实例
* @param {Array} pointData 点位数据
* @param {String} type 点位类型
* @returns {void} 无返回值
*/
drawPoint(svg, pointData) {
// 根据类型设置图标地址
svg.selectAll(".point")
.data(pointData)
.enter()
.append("image")
.attr("class", (d) => {
return `.point-${d.type}`;
})
.attr("id", (d) => {
return `id-${d.code}`;
})
.attr("x", (d) => d.x - 10)
.attr("y", (d) => d.y - 10)
.attr("width", 20)
.attr("height", 20)
.attr("href", (d) => {
return d.type ? this.setImgUrl(d.type) : "";
})
.attr("r", 8)
.on("mouseover", (event, d) => {
if (this.traceStatus) {
// this.visible = true;
// 置灰“当前节点非相关节点”的文本
svg.selectAll("text").classed("opacity-1", true);
// 高亮“当前节点相关节点”的文本
svg.selectAll("text")
.filter((n) => n.gsName == d.gsName)
.classed("opacity-10", true);
// 置灰“当前节点非相关节点”的图标
svg.selectAll("image").classed("opacity-1", true);
// 高亮“当前节点相关节点”的图标
svg.selectAll("image")
.filter((n) => n.gsName == d.gsName)
.classed("opacity-10", true);
let flowLine = null;
if (!this.lineStatus) {
// 置灰“当前节点非相关节点”的连线
svg.selectAll("line").classed("opacity-1", true);
// 高亮“当前节点相关节点”的连线
flowLine = svg
.selectAll("line")
.filter((l) => l.gsName === d.gsName);
// 设置初始状态
flowLine
.classed("opacity-10", true)
.style("stroke-dasharray", "25, 25")
.style("stroke-dashoffset", 0);
} else {
svg.selectAll("path#linkGenerator").classed(
"opacity-1",
true
);
flowLine = svg
.selectAll("path#linkGenerator")
.filter((ele) => ele.gsName === d.gsName);
// 设置初始状态
flowLine
.classed("opacity-10", true)
.style("stroke-dasharray", "25, 25")
.style("stroke-dashoffset", 0);
}
// 启动流水效果
function startAnimation() {
const length = 1000;
flowLine
.style("stroke-dasharray", "25,25") // 设置虚线的总长度
.style("stroke-dashoffset", length) // 设置初始的偏移量
.transition() // 创建过渡动画
.duration(10000) // 设置动画时长
.ease(d3.easeLinear) // 使用线性过渡
.style("stroke-dashoffset", 0) // 让偏移量为 0,从而产生流水效果
.on("end", startAnimation); // 动画结束时递归调用
}
// 启动动画
startAnimation();
}
})
.on("mouseout", (event, d) => {
if (this.traceStatus) {
// this.visible = false;
// 置灰节点的文本
svg.selectAll("text").classed("opacity-1", false);
svg.selectAll("text")
.filter((n) => n.gsName == d.gsName)
.classed("opacity-10", false);
// 置灰节点的图标
svg.selectAll("image")
.filter((n) => n.gsName == d.gsName)
.classed("opacity-10", false);
svg.selectAll("image").classed("opacity-1", false);
if (!this.lineStatus) {
// 置灰节点间的连线,停止动画
svg.selectAll("line").classed("opacity-1", false);
svg.selectAll("line")
.filter((l) => l.gsName === d.gsName)
.classed("opacity-10", false)
.style("stroke-dasharray", "0,0")
.interrupt();
} else {
svg.selectAll("path#linkGenerator").classed(
"opacity-1",
false
);
svg.selectAll("path#linkGenerator")
.filter((l) => l.gsName === d.gsName)
.classed("opacity-10", false)
.style("stroke-dasharray", "0,0")
.interrupt();
}
}
})
.on("click", (event, d) => {
if (this.tooltipStatus) {
// 获取点击的图标左上角位置 (x, y)
const iconX = d.x;
const iconY = d.y;
event.stopPropagation(); // 阻止事件冒泡
if (this.popupInstance.style("opacity") == 1) {
this.popupInstance
.style("opacity", 0)
.style("transform", "scale(0)");
}
this.text = d.label;
this.openPopup(iconX, iconY, d);
}
});
},
/**
* 画“点文字”
* @param {Object} svg d3实例
* @param {Array} pointData 点位数据
* @param {String} property 展示文字的属性
* @param {Number} x 横向(x轴)偏移量
* @param {Number} y 纵向(y轴)偏移量
* @param {Number} dx 文本之间的间距
* @param {Number} dy 文本之间的间距
* @param {String} color 文本之间的间距
* @param {Number} fontSize 文字大小
* @param {Number} fontWeight 文字加粗
* @returns {void} 无返回值
*/
drawPointText(
svg,
pointData,
property,
x = 0,
y = 0,
dx = 0,
dy = 0,
color = "#000",
fontSize = 12,
fontWeight = 400
) {
svg.selectAll(`.text-${property}`)
.data(pointData)
.enter()
.append("text")
.attr("class", `.text-${property}`)
.attr("x", (d) => d.x + x + dx)
.attr("y", (d) => d.y + y)
.attr("text-anchor", "middle")
.attr("fill", color)
.attr("font-size", fontSize)
.attr("font-weight", fontWeight)
.append("tspan")
.attr("x", (d) => d.x + dx)
.attr("dy", dy)
.text((d) => {
if (property === "code") {
return d.type ? d[property] : "";
}
return d[property];
});
},
/**
*
* @param {Object} svg d3实例
* @param {Array} linkData 点位连接数据
* @param {String} lineName 线名称
* @param {String} lineColor 连接线颜色
* @returns {void} 无返回值
*/
drawLine(svg, linkData, lineName, lineColor) {
if (this.arrowStatus) {
// 创建箭头标记
svg.append("defs")
.append("marker")
.attr("id", `arrowhead-${lineName}`)
.attr("viewBox", "0 0 10 10")
.attr("refX", 5)
.attr("refY", 5)
.attr("markerWidth", 4)
.attr("markerHeight", 4)
.attr("orient", "auto")
.append("path")
.attr("d", "M 0 0 L 10 5 L 0 10 Z")
.attr("class", "arrow")
.style("fill", lineColor); // 设置箭头颜色
}
if (!this.lineStatus) {
svg.selectAll(`.line-${lineName}`)
.data(linkData)
.enter()
.append("line")
.attr("class", `.line-${lineName}`)
.attr("x1", (d) => d.source.x)
.attr("y1", (d) => d.source.y)
.attr("x2", (d) => d.target.x)
.attr("y2", (d) => d.target.y)
.style("stroke", lineColor)
.style("stroke-width", 4) // 设置线条宽度
.style("stroke-linecap", "square") // 设置端点样式
.style("stroke-linejoin", "round") // 设置连接点样式
.attr("marker-end", (d, i) => {
return d.target.type && this.arrowStatus
? null
: `url(#arrowhead-${lineName})`;
});
} else {
// 连接线弯曲设置,路径高亮处有问题需要调整
const linkGenerator = d3
.linkHorizontal()
.x((d) => d.x)
.y((d) => d.y);
svg.selectAll(`.line-${lineName}`)
.data(linkData)
.join("path")
.attr("class", `.line-${lineName}`)
.attr("id", "linkGenerator")
.attr("d", (d, i) => {
return linkGenerator({
source: d.source,
target: d.target,
});
})
.style("stroke", lineColor)
.attr("stroke-width", 2)
.attr("fill", "none")
.attr("marker-end", (d, i) => {
return d.target.type && this.arrowStatus
? null
: `url(#arrowhead-${lineName})`;
});
}
},
/**
* 获取连线数据
* @param {Array} linkData 点位连接数据
* @returns {void} 无返回值
*/
getLineData(linkData) {
const res = [];
// 创建一个站点代码与站点对象的映射
const stationMap = linkData.reduce((map, station) => {
map[station.code] = station;
return map;
}, {});
// 遍历原始的站点列表来构建最终的结果
for (let i = 0; i < linkData.length; i++) {
const currentStation = linkData[i];
const targetCode = currentStation.target;
// 如果目标站点存在
if (targetCode && stationMap[targetCode]) {
const targetStation = stationMap[targetCode];
// 创建一个新的对象,将source和target配对
res.push({
source: currentStation,
target: targetStation,
gsName: currentStation.gsName,
});
// 标记该站点的目标站点为null,防止重复配对
stationMap[targetCode] = null;
}
}
return res;
},
/**
* 通过倒数前两个点位计算高速名称的点位数据
* @param {Object} lastOne 倒数第一个点位
* @param {Object} lastTwo 倒数第二个点位
* @param {String} label 高速名称
* @param {Number} distance 距离
* @param {String} direction 方向
* @param {Number} type 类型(text:高速名称,arrow:首尾箭头)
* @returns {Object} 高速点位
*/
calculateGSNamePosition(
lastOne,
lastTwo,
label,
distance,
direction,
type
) {
// 计算lastOne到lastTwo的向量
const vx = lastOne.x - lastTwo.x;
const vy = lastOne.y - lastTwo.y;
// 计算lastOne到lastTwo的距离
const dist = Math.sqrt(vx * vx + vy * vy);
// 计算单位向量
const unitX = vx / dist;
const unitY = vy / dist;
let newX, newY;
if (direction === "front") {
// 计算反向单位向量
const reverseUnitX = -unitX;
const reverseUnitY = -unitY;
// 根据反向单位向量计算前一个点的位置,前一个点距离lastOne的横纵坐标为指定的距离
newX = lastOne.x - reverseUnitX * distance;
newY = lastOne.y - reverseUnitY * distance;
} else if (direction === "after") {
// 根据单位向量计算a3的位置,a3距离lastOne的横纵坐标都为200
newX = lastOne.x + unitX * distance;
newY = lastOne.y + unitY * distance;
}
if (type === "text") {
return { x: newX, y: newY, label, gsName: label };
} else if (type === "arrow") {
const num =
new Date().getTime() + parseInt(Math.random() * 10000);
return {
x: newX,
y: newY,
// type: "station",
type: null,
gsName: label,
code: num,
source: lastOne.code,
target: num,
};
}
},
/**
* 添加首尾箭头
* @param {Array} data 点位数据
* @returns {Array} 点位数据
*/
pushArrowPoint(data) {
// 创建一个新的数组来存储最终的结果
const result = [];
// 遍历原始数组
for (let i = 0; i < data.length; i++) {
// 当前项
const current = data[i];
// // 如果position为"start",先插入type为"arrow"的数据
// if (current.position === "start") {
// if (data[i + 1].gsName === current.gsName) {
// // 计算首部箭头坐标
// const frontArrow = this.calculateGSNamePosition(
// current,
// data[i + 1],
// current.gsName,
// 80,
// "front",
// "arrow"
// );
// result.push(frontArrow); // 插入箭头数据
// }
// }
// 插入当前项
result.push(current);
// 如果position为"end",再插入type为"arrow"的数据
if (current.position === "end") {
if (data[i - 1].gsName === current.gsName) {
// 计算尾部箭头坐标
const afterArrow = this.calculateGSNamePosition(
current,
data[i - 1],
current.gsName,
80,
"after",
"arrow"
);
result.push(afterArrow); // 插入箭头数据
current.target = afterArrow.code;
}
}
}
return result;
},
/**
* 初始化弹窗相关实例对象
* @returns {void}
*/
initPopup() {
if (!this.popupInstance)
this.popupInstance = d3.select("#popup-element");
if (!this.closeBtnInstance)
this.closeBtnInstance = d3.select("#close-element");
this.closeBtnInstance.on("click", (event) => {
this.closePopup();
});
// 弹窗模式下,支持点击空白关闭弹窗
d3.select("body").on("click", (event) => {
// 判断点击的地方是否为弹窗外部
if (
this.tooltipStatus &&
this.popupInstance &&
!this.popupInstance.node().contains(event.target) &&
!d3.select(event.target).classed("point")
) {
this.closePopup();
}
});
},
/**
* 关闭弹窗
* @returns {void}
*/
closePopup() {
this.popupInstance
.transition()
.duration(100)
.style("opacity", 0)
.style("transform", "scale(0)");
this.text = null;
},
/**
* 展示弹窗
* @param {Number} x 横坐标
* @param {Number} y 纵坐标
* @returns {void}
*/
openPopup(x, y) {
this.popupInstance
.transition()
.duration(200)
.style("left", `${x - 100}px`)
.style("top", `${y - 60}px`)
.style("opacity", 1)
.style("transform", "scale(1)");
},
setTooltipStatus() {
this.tooltipStatus = !this.tooltipStatus;
if (this.tooltipStatus) {
this.initPopup();
// 初始化弹窗
alert("弹窗功能开启");
} else {
alert("弹窗功能关闭");
}
},
setTraceStatus() {
this.traceStatus = !this.traceStatus;
if (this.traceStatus) {
alert("轨迹高亮功能开启");
} else {
alert("轨迹高亮功能关闭");
}
},
setArrowStatus() {
this.arrowStatus = !this.arrowStatus;
this.clearSvg();
if (this.arrowStatus) {
alert("显示箭头");
} else {
alert("不显示箭头");
}
this.createChart();
},
setLineStatus() {
this.lineStatus = !this.lineStatus;
this.clearSvg();
if (this.lineStatus) {
alert("直线连接");
} else {
alert("非直线连接");
}
this.createChart();
},
// 清空画布
clearSvg() {
this.allData = [];
this.allLineData = [];
this.gsNamePointData = [];
d3.select(this.$refs.d3Chart)
.selectAll("image, text, line, marker, path")
.remove();
},
/**
* 坐标系点位数据格式化
* @param {Array} data 点位数据
* @param {Number} width 容器宽度
* @param {Number} height 容器高度
* @returns {Array} 点位数据
*/
pointDataFormat(data, width, height) {
// 过滤无效点位
const _data = data.filter((ele) => ele.lat && ele.lon);
if (!_data.length) return [];
// 初始化最大最小值
let latMin = Infinity,
latMax = -Infinity;
let lonMin = Infinity,
lonMax = -Infinity;
// 单次遍历数组,计算最大最小值
for (let i = 0; i < data.length; i++) {
const { lat, lon } = data[i];
if (lat < latMin) latMin = lat;
if (lat > latMax) latMax = lat;
if (lon < lonMin) lonMin = lon;
if (lon > lonMax) lonMax = lon;
}
// 此处 减去 200 为了保证点位都显示在容器内,后续点位的横纵坐标 +100
width -= 200;
height -= 200;
return data.map((ele) => ({
...ele,
x: ((ele.lon - lonMin) / (lonMax - lonMin)) * width + 100,
y:
height -
((ele.lat - latMin) / (latMax - latMin)) * height +
100,
}));
},
},
created() {},
mounted() {
this.getData();
window.addEventListener("resize", () => {
setTimeout(() => {
this.clearSvg();
this.createChart();
}, 200);
});
},
};
</script>
<style lang="less" scoped>
.d3-container {
height: 100%;
width: 100%;
position: relative;
.map-test {
height: 100%;
width: 100%;
overflow: hidden;
cursor: pointer;
position: relative;
svg {
width: 100%;
height: 100%;
cursor: pointer;
}
.tooltip {
position: absolute;
width: 200px;
height: 40px;
z-index: 9;
transform: scale(0);
font-size: 20px;
display: flex;
align-items: center;
justify-content: center;
opacity: 0;
background: #fff;
border-radius: 4px;
box-shadow: 0 10px 15px 0 rgba(0, 0, 0, .1);
word-break: break-all;
border: 1px solid #ebeef5;
transition: opacity 0.5s ease, transform 0.5s ease;
.el-icon-close {
position: absolute;
top: 0;
right: 0;
font-size: 16px;
}
.arrow {
position: absolute;
width: 0;
height: 0;
bottom: -8px;
border-left: 6px solid transparent;
border-right: 6px solid transparent;
border-top: 8px solid #fff; /* 这个颜色就是倒三角形的颜色 */
}
}
::v-deep .opacity-10 {
opacity: 1!important;
}
::v-deep .opacity-2 {
opacity: 0.2;
}
::v-deep .opacity-1 {
opacity: 0.1;
}
}
.empty {
height: 100%;
width: 100%;
display: flex;
position: absolute;
z-index: 10;
top: 0;
span {
margin: auto;
}
}
.action-panel {
height: 40px;
width: auto;
box-shadow: 0 4px 15px 0 rgba(0, 0, 0, .1);
position: absolute;
top: 12px;
left: 12px;
border-radius: 4px;
display: flex;
align-items: center;
background-color: #fff;
padding: 12px;
z-index: 99;
.margin-right-6 {
margin-right: 6px;
}
}
}
</style>
D3 官网:https://d3js.org/
D3 API地址:https://d3js.org/api
-
安装
npm install d3
-
使用
import * as d3 from 'd3'
功能描述:
- 画点、画线、画文本内容
- 鼠标悬浮点位,高亮关联点位、文本及其高速公路名称
- 鼠标悬浮展示动态流水线效果
- 弹窗效果,点击点位展示点位信息
- 画布自适应,根据窗口大小重新计算点位坐标
- 画布清空
- 连线尾部添加箭头
数据分析:
当前数据是前端mock数据,后续应用在实际项目中需要后端给到的数据格式是:
- 每个点位中需要有经纬度坐标,前端根据视口范围计算,展示点位
- 点位需要包含类型,来判断展示对应的图表(门架、站点、枢纽等)
- 点位需要包含指向关系,作用是用来画连接线
- 点位需要包含高速名称,用于展示高速名称标识
- 点位必须按照顺序返回,否则连线会比较乱,视觉体验差
注意:
- 高速公路名称是根据
calculateGSNamePosition函数
计算的出来的 - 经纬度转窗口坐标是通过
pointDataFormat
函数计算
数据样例:
// lat,lon代表经纬度
// label: 名称
// source:源
// target:指向目标
// type:类型
const data = {
'沪杭高速': [
{
lat: 30.573625,
lon: 120.19238,
label: "织里枢纽",
code: 5101,
source: 5101,
target: 5111,
type: "hub",
},
{
lat: 30.62261,
lon: 120.37263,
label: "织里东",
code: 5111,
source: 5111,
target: 5113,
type: "station",
},
],
};
// lat,lon代表经纬度
// label: 名称
// source:源
// target:指向目标
// type:类型
// gsName:高速名称
const data1 = [
{
lat: 30.573625,
lon: 120.19238,
label: "织里枢纽",
code: 5101,
source: 5101,
target: 5111,
type: "hub",
gsName: "沪杭高速",
},
{
lat: 30.62261,
lon: 120.37263,
label: "织里东",
code: 5111,
source: 5111,
target: 5113,
type: "station",
gsName: "沪杭高速",
},
];
空
7. 连线尾部添加箭头
数据分析:
当前数据是前端mock数据,后续应用在实际项目中需要后端给到的数据格式是:
- 每个点位中需要有经纬度坐标,前端根据视口范围计算,展示点位
- 点位需要包含类型,来判断展示对应的图表(门架、站点、枢纽等)
- 点位需要包含指向关系,作用是用来画连接线
- 点位需要包含高速名称,用于展示高速名称标识
- 点位必须按照顺序返回,否则连线会比较乱,视觉体验差
注意:
- 高速公路名称是根据
calculateGSNamePosition函数
计算的出来的 - 经纬度转窗口坐标是通过
pointDataFormat
函数计算
数据样例:
// lat,lon代表经纬度
// label: 名称
// source:源
// target:指向目标
// type:类型
const data = {
'沪杭高速': [
{
lat: 30.573625,
lon: 120.19238,
label: "织里枢纽",
code: 5101,
source: 5101,
target: 5111,
type: "hub",
},
{
lat: 30.62261,
lon: 120.37263,
label: "织里东",
code: 5111,
source: 5111,
target: 5113,
type: "station",
},
],
};
// lat,lon代表经纬度
// label: 名称
// source:源
// target:指向目标
// type:类型
// gsName:高速名称
const data1 = [
{
lat: 30.573625,
lon: 120.19238,
label: "织里枢纽",
code: 5101,
source: 5101,
target: 5111,
type: "hub",
gsName: "沪杭高速",
},
{
lat: 30.62261,
lon: 120.37263,
label: "织里东",
code: 5111,
source: 5111,
target: 5113,
type: "station",
gsName: "沪杭高速",
},
];