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: "沪杭高速",
  },
];

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/967497.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【系统架构设计师】操作系统 ③ ( 存储管理 | 页式存储弊端 - 段式存储引入 | 段式存储 | 段表 | 段表结构 | 逻辑地址 的 合法段地址判断 )

文章目录 一、页式存储弊端 - 段式存储引入1、页式存储弊端 - 内存碎片2、页式存储弊端 - 逻辑结构不匹配3、段式存储引入 二、段式存储 简介1、段式存储2、段表3、段表 结构4、段内地址 / 段内偏移5、段式存储 优缺点6、段式存储 与 页式存储 对比 三、逻辑地址 的 合法段地址…

物联网软件开发与应用方向应该怎样学习,学习哪些内容,就业方向是怎样?(文末领取整套学习视频,课件)物联网硬件开发与嵌入式系统

随着物联网技术的飞速发展&#xff0c;物联网软件开发与应用方向成为了众多开发者关注的焦点。那么&#xff0c;如何在这个领域中脱颖而出呢&#xff1f;本文将为你提供一份详细的学习指南&#xff0c;帮助你从零开始&#xff0c;逐步掌握物联网软件开发与应用的核心技能。 一…

Linux——基础命令1

$&#xff1a;普通用户 #&#xff1a;超级用户 cd 切换目录 cd 目录 &#xff08;进入目录&#xff09; cd ../ &#xff08;返回上一级目录&#xff09; cd ~ &#xff08;切换到当前用户的家目录&#xff09; cd - &#xff08;返回上次目录&#xff09; pwd 输出当前目录…

OpenFeign远程调用返回的是List<T>类型的数据

在使用 OpenFeign 进行远程调用时&#xff0c;如果接口返回的是 List 类型的数据&#xff0c;可以通过以下方式处理&#xff1a; 直接定义返回类型为List Feign 默认支持 JSON 序列化/反序列化&#xff0c;如果服务端返回的是 List的JSON格式数据&#xff0c;可以直接在 Feig…

向量数据库简单对比

文章目录 一、Chroma二、Pinecone/腾讯云VectorDB/VikingDB三、redis四、Elasticsearch五、Milvus六、Qdrant七、Weaviate八、Faiss 一、Chroma 官方地址&#xff1a; https://www.trychroma.com/优点 ①简单&#xff0c;非常简单构建服务。 ②此外&#xff0c;Chroma还具有自…

字符指针、数组指针和函数指针

1. 字符指针变量 1.1 简单例子 字符指针 char* 在C语言中主要由两种用法&#xff1a; 1.用于存放一个字符变量的地址。 2.用字符指针接收一个字符串。 这里并不是将整个字符串的地址存入 pstr 指针&#xff0c;指针变量 pstr 中存放的是常量字符串的首字符 h 的地址。 以一个…

【Linux网络编程】之守护进程

【Linux网络编程】之守护进程 进程组进程组的概念组长进程 会话会话的概念会话ID 控制终端控制终端的概念控制终端的作用会话、终端、bash三者的关系 前台进程与后台进程概念特点查看当前终端的后台进程前台进程与后台进程的切换 作业控制相关概念作业状态&#xff08;一般指后…

JS宏进阶:XMLHttpRequest对象

一、概述 XMLHttpRequest简称XHR&#xff0c;它是一个可以在JavaScript中使用的对象&#xff0c;用于在后台与服务器交换数据&#xff0c;实现页面的局部更新&#xff0c;而无需重新加载整个页面&#xff0c;也是Ajax&#xff08;Asynchronous JavaScript and XML&#xff09;…

怎么查看电脑显存大小(查看电脑配置)

这里提供一个简单的方法查看 winr打开cmd 终端输入dxdiag进入DirectX 点击显示查看设备的显示内存&#xff08;VRAM&#xff09; 用这个方法查看电脑配置和显存是比较方便的 dxdiag功能 Dxdiag是Windows的DirectX诊断工具&#xff0c;其主要作用包括但不限于以下几点&#…

优惠券平台(一):基于责任链模式创建优惠券模板

前景概要 系统的主要实现是优惠券的相关业务&#xff0c;所以对于用户管理的实现我们简单用拦截器在触发接口前创建一个单一用户。 // 用户属于非核心功能&#xff0c;这里先通过模拟的形式代替。后续如果需要后管展示&#xff0c;会重构该代码 UserInfoDTO userInfoDTO new…

【机器学习】数据预处理之scikit-learn的Scaler与自定义Scaler类进行数据归一化

scikit-learn的Scaler数据归一化 一、摘要二、训练数据集和测试数据集的归一化处理原则三、scikit-learn中的Scalar类及示例四、自定义StandardScaler类进行数据归一化处理五、小结 一、摘要 本文主要介绍了scikit-learn中Scaler的使用方法&#xff0c;特别强调了数据归一化在…

机器学习中过拟合和欠拟合问题处理方法总结

目录 一、背景二、过拟合(Overfitting)2.1 基本概念2.2 过拟合4个最主要的特征2.3 防止过拟合的11个有效方法 三、欠拟合&#xff08;Underfitting&#xff09;3.1 基本概念3.2 欠拟合的4个特征3.3 防止欠拟合的11个有效方法 四、总结五、参考资料 一、背景 在机器学习模型训练…

ABP框架9——自定义拦截器的实现与使用

一、AOP编程 AOP定义:面向切片编程&#xff0c;着重强调功能&#xff0c;将功能从业务逻辑分离出来。AOP使用场景&#xff1a;处理通用的、与业务逻辑无关的功能&#xff08;如日志记录、性能监控、事务管理等&#xff09;拦截器:拦截方法调用并添加额外的行为&#xff0c;比如…

基于YoloV11和驱动级鼠标模拟实现Ai自瞄

本文将围绕基于 YoloV11 和驱动级鼠标实现 FPS 游戏 AI 自瞄展开阐述。 需要着重强调的是&#xff0c;本文内容仅用于学术研究和技术学习目的。严禁任何个人或组织将文中所提及的技术、方法及思路应用于违法行为&#xff0c;包括但不限于在各类游戏中实施作弊等违规操作。若因违…

示例代码:C# MQTTS双向认证(客户端)(服务器EMQX)

初级代码游戏的专栏介绍与文章目录-CSDN博客 我的github&#xff1a;codetoys&#xff0c;所有代码都将会位于ctfc库中。已经放入库中我会指出在库中的位置。 这些代码大部分以Linux为目标但部分代码是纯C的&#xff0c;可以在任何平台上使用。 源码指引&#xff1a;github源…

记录IMX6ULL开发板上移植SQLite3并运行Qt程序

文章目录 概要移植SQLite3Qt程序部署实验现象 概要 基于上一章对使用Qt运行对应的实验实例来完成对用户使用ui界面完成对SQLite数据库的增删改查等操作。本文旨在对上一句节的Qt程序部署到IMX6ULL开发板&#xff0c;并且完成对SQLite数据库在IMX6ULL开发板上的移植。 移植SQ…

达梦数据库(DM)线程管理

目录标题 达梦数据库&#xff08;DM&#xff09;线程管理笔记一、DM 线程架构概述二、DM 主要线程类型及功能&#xff08;一&#xff09;监听线程&#xff08;二&#xff09;工作线程&#xff08;三&#xff09;IO 线程&#xff08;四&#xff09;调度线程&#xff08;五&#…

02.10 TCP之文件传输

1.思维导图 2.作业 服务器代码&#xff1a; #include <stdio.h> #include <string.h> #include <unistd.h> #include <stdlib.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <pthread.h> …

Node.js 环境配置

什么是 Node.js Node.js 是一个基于 Chrome V8 JavaScript 引擎的 JavaScript 运行时环境&#xff0c;它允许你在服务器端运行 JavaScript。传统上&#xff0c;JavaScript 主要用于浏览器中的前端开发&#xff0c;而 Node.js 使得 JavaScript 也能够在服务器上执行&#xff0c;…

【办公类-53-04】20250209Python模仿制作2024学年第二学期校历

背景需求&#xff1a; 马上开学了&#xff0c;又要制作校历&#xff08;删划节假日&#xff09;。之前我都是用网络的图片&#xff0c;然后在PPT里修改。 存在问题&#xff1a; 网络校历是从周日开始的&#xff0c;但日常我们老师做教案&#xff0c;都是默认从周一到周五&…