当前位置: 首页 > article >正文

D3实现站点路线图demo分享

分享通过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'
    
功能描述:
  1. 画点、画线、画文本内容
  2. 鼠标悬浮点位,高亮关联点位、文本及其高速公路名称
  3. 鼠标悬浮展示动态流水线效果
  4. 弹窗效果,点击点位展示点位信息
  5. 画布自适应,根据窗口大小重新计算点位坐标
  6. 画布清空
  7. 连线尾部添加箭头
数据分析:

当前数据是前端mock数据,后续应用在实际项目中需要后端给到的数据格式是:

  1. 每个点位中需要有经纬度坐标,前端根据视口范围计算,展示点位
  2. 点位需要包含类型,来判断展示对应的图表(门架、站点、枢纽等)
  3. 点位需要包含指向关系,作用是用来画连接线
  4. 点位需要包含高速名称,用于展示高速名称标识
  5. 点位必须按照顺序返回,否则连线会比较乱,视觉体验差
注意:
  • 高速公路名称是根据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数据,后续应用在实际项目中需要后端给到的数据格式是:

  1. 每个点位中需要有经纬度坐标,前端根据视口范围计算,展示点位
  2. 点位需要包含类型,来判断展示对应的图表(门架、站点、枢纽等)
  3. 点位需要包含指向关系,作用是用来画连接线
  4. 点位需要包含高速名称,用于展示高速名称标识
  5. 点位必须按照顺序返回,否则连线会比较乱,视觉体验差
注意:
  • 高速公路名称是根据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: "沪杭高速",
  },
];

http://www.kler.cn/a/540389.html

相关文章:

  • 使用Qt+opencv实现游戏辅助点击工具-以阴阳师为例
  • SQL自学,mysql从入门到精通 --- 第 14天,主键、外键的使用
  • 爬虫技巧汇总
  • 机器学习中常用的数据预处理方法
  • RabbitMQ 从入门到精通:从工作模式到集群部署实战(五)
  • 微服务 day01 注册与发现 Nacos OpenFeign
  • 【Deepseek】本地部署Deepseek
  • C# OpenCV机器视觉:对位贴合
  • 【开源免费】基于SpringBoot+Vue.JS校园网上店铺系统(JAVA毕业设计)
  • QNX800 run in Raspberry Pi
  • DeepSeek 实践总结
  • Vue全流程--Vue3组合一ref与reactive(实现响应式)
  • 零阶保持器(ZOH)变换和Tustin离散化变换以及可视化
  • 大语言模型RAG,transformer
  • 微信小程序分包异步化
  • 【时时三省】(C语言基础)基础习题1
  • Open Liberty使用指南及开发示例(二)
  • C++基础知识学习记录—补充
  • 2.10作业
  • 说一下 Tcp 粘包是怎么产生的?
  • ElasticSearch进阶
  • 服务器使用宝塔面板Docker应用快速部署 DeepSeek-R1模型,实现Open WebUI访问使用
  • Godot开发框架探索#2
  • 线程状态示意图
  • ElasticSearch 分页技术详解:实现方式与最佳实践
  • python之keyring库:安全密码管理库,不同平台service_name、username的获取