D3实现站点路线图demo分享

发布于:2024-12-18 ⋅ 阅读:(131) ⋅ 点赞:(0)

分享一下通过D3实现的站点路线分布图,这是一个demo。效果图如下:

在这里插入图片描述

源码如下:
<template>
	<div class="map-test" ref="d3Chart">
		<div class="tooltip" id="popup-element">
			<span>{{ text }}</span>
			<i id="close-element" class="el-icon-close"></i>
		</div>
		<div class="mark" v-show="visible"></div>
	</div>
</template>

<script>
import * as d3 from "d3";

export default {
	name: "MapTest",
	components: {},
	data() {
		return {
			text: null,
			svgInstance: null, // d3元素实例
			popupInstance: null, // 弹窗实例
			visible: false,
			allData: [], // 全部点位数据
			allLineData: [], // 全部连线数据
			gsNamePointData: [], // 高速名称点位数据
		};
	},
	computed: {},
	methods: {
		createChart() {
			const width = window.innerWidth;
			const height = window.innerHeight;
			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.popupInstance = d3.select("#popup-element");
			const closeBtn = d3.select("#close-element");
			closeBtn.on("click", (event) => {
				this.popupInstance
					.transition()
					.duration(300)
					.style("opacity", 0) // 使弹窗逐渐透明
					.style("transform", "scale(0)"); // 缩小弹窗
			});

			// 全部点位信息
			const data = [
				{
					x: 500,
					y: 150,
					label: "湖州",
					code: 2117,
					source: 2117,
					target: 2115,
					type: "station",
					gsName: "申苏浙皖",
				},
				{
					x: 700,
					y: 150,
					label: "织里",
					code: 2115,
					source: 2115,
					target: 2113,
					type: "station",
					gsName: "申苏浙皖",
				},
				{
					x: 1200,
					y: 150,
					label: "南浔",
					code: 2113,
					source: 2113,
					target: null,
					type: "station",
					gsName: "申苏浙皖",
				},
				{
					x: 700,
					y: 350,
					label: "湖州东",
					code: 3233,
					source: 3233,
					target: 3231,
					type: "station",
					gsName: "申嘉湖",
				},
				{
					x: 1200,
					y: 350,
					label: "双林",
					code: 3231,
					source: 3231,
					target: 3229,
					type: "station",
					gsName: "申嘉湖",
				},
				{
					x: 1400,
					y: 350,
					label: "南浔南",
					code: 3229,
					source: 3229,
					target: null,
					type: "station",
					gsName: "申嘉湖",
				},
				{
					x: 700,
					y: 550,
					label: "钟管",
					code: 4049,
					source: 4049,
					target: 4051,
					type: "station",
					gsName: "杭绕西复线",
				},
				{
					x: 1200,
					y: 550,
					label: "新市西",
					code: 4051,
					source: 4051,
					target: 3206,
					type: "station",
					gsName: "杭绕西复线",
				},
				{
					x: 1350,
					y: 550,
					label: "新市枢纽",
					code: 3206,
					source: 3206,
					target: null,
					type: "station",
					gsName: "杭绕西复线",
				},
				{
					x: 800,
					y: 700,
					label: "雷甸",
					code: 3243,
					source: 3243,
					target: 3241,
					type: "station",
					gsName: "练杭",
				},
				{
					x: 1300,
					y: 700,
					label: "新安",
					code: 3241,
					source: 3241,
					target: 3239,
					type: "station",
					gsName: "练杭",
				},
				{
					x: 1500,
					y: 700,
					label: "新市",
					code: 3239,
					source: 3239,
					target: null,
					type: "station",
					gsName: "练杭",
				},
				{
					x: 900,
					y: 150,
					label: "织里枢纽",
					code: 5101,
					source: 5101,
					target: 5111,
					type: "hub",
					gsName: "沪杭高速",
				},
				{
					x: 900,
					y: 210,
					label: "织里东",
					code: 5111,
					source: 5111,
					target: 5113,
					type: "hh-station",
					gsName: "沪杭高速",
				},
				{
					x: 900,
					y: 280,
					label: "南浔西",
					code: 5113,
					source: 5113,
					target: 5102,
					type: "hh-station",
					gsName: "沪杭高速",
				},
				{
					x: 900,
					y: 350,
					label: "双林枢纽",
					code: 5102,
					source: 5102,
					target: 5115,
					type: "hh-station",
					gsName: "沪杭高速",
				},
				{
					x: 900,
					y: 410,
					label: "菱湖(分中心)",
					code: 5115,
					source: 5115,
					target: 5117,
					type: "hh-station",
					gsName: "沪杭高速",
				},
				{
					x: 900,
					y: 480,
					label: "千金",
					code: 5117,
					source: 5117,
					target: 5103,
					type: "hh-station",
					gsName: "沪杭高速",
				},
				{
					x: 900,
					y: 550,
					label: "士林枢纽",
					code: 5103,
					source: 5103,
					target: 5119,
					type: "hub",
					gsName: "沪杭高速",
				},
				{
					x: 960,
					y: 620,
					label: "下舍",
					code: 5119,
					source: 5119,
					target: 5104,
					type: "hh-station",
					gsName: "沪杭高速",
				},
				{
					x: 1050,
					y: 700,
					label: "新安枢纽",
					code: 5104,
					source: 5104,
					target: null,
					type: "hub",
					gsName: "沪杭高速",
				},
			];
			this.allData = data;
			const colorList = [
				"#409EFF",
				"#67C23A",
				"#E6A23C",
				"#F56C6C",
				"#909399",
			];
			// 获取“全部点位”连线数据
			this.allLineData = this.getLineData(data);
			const gsKeyToValue = [...new Set(data.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,
						index,
						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
						)
					);
				} else if (len == 1) {
					this.gsNamePointData.push(
						this.calculateGSNamePosition(
							gsPointList[len - 1],
							gsPointList[len - 1],
							ele
						)
					);
				}
			});

			// 画“全部点位”
			this.drawPoint(this.svgInstance, data);

			// 画“收费站名称”
			this.drawPointText(
				this.svgInstance,
				data,
				"label",
				0,
				-25,
				40,
				-10
			);
			// 画“收费站编码”
			this.drawPointText(this.svgInstance, data, "code", 0, -25, 40, 6);

			this.drawPointText(
				this.svgInstance,
				this.gsNamePointData,
				"label",
				0,
				-25,
				0,
				30,
                '#000',
                20,
                '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;
			}
			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 - 20)
				.attr("y", (d) => d.y - 20)
				.attr("width", 40)
				.attr("height", 40)
				.attr("href", (d) => {
					return this.setImgUrl(d.type);
				})
				.attr("r", 8)
				.on("mouseover", (event, d) => {
					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);

                    // 置灰“当前节点非相关节点”的连线
					svg.selectAll("line").classed("opacity-1", true);
                    // 高亮“当前节点相关节点”的连线
					const ele = svg
						.selectAll("line")
						.filter((l) => l.gsName === d.gsName);

					// 设置初始状态
					ele.classed("opacity-10", true)
						.style("stroke-dasharray", "25, 25")
						.style("stroke-dashoffset", 0);

					// 启动流水效果
					function startAnimation() {
						const length = 1000;
						ele.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) => {
					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);

                    // 置灰节点间的连线,停止动画
					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();
				});
		},
		/**
		 * 画“点文字”
		 * @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 = 14,
            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) => d[property]);
		},
		/**
		 *
		 * @param   {Object}    svg        d3实例
		 * @param   {Array}     linkData   点位连接数据
		 * @param   {String}    lineName   线名称
		 * @param   {String}    lineColor   线名称
		 * @returns {void}      无返回值
		 */
		drawLine(svg, linkData, lineName, lineColor) {
			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", 10) // 设置线条宽度
				.style("stroke-linecap", "square") // 设置端点样式
				.style("stroke-linejoin", "round"); // 设置连接点样式
		},
		/**
		 * 获取连线数据
		 * @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   距离
         * @returns {Object}    高速点位
         */
		calculateGSNamePosition(lastOne, lastTwo, label, distance = 100) {
			// 计算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;

			// 根据单位向量计算a3的位置,a3距离lastOne的横纵坐标都为200
			const a3X = lastOne.x + unitX * distance;
			const a3Y = lastOne.y + unitY * distance;

			// 返回a3的位置
			return { x: a3X, y: a3Y, label, gsName: label };
		},
	},
	created() {},
	mounted() {
		this.createChart();
	},
};
</script>

<style lang="less">
.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;
        background-color: pink;
        z-index: 9;
        transform: scale(0);
        font-size: 20px;
        display: flex;
        align-items: center;
        justify-content: center;
        opacity: 0;
        transition: opacity 0.5s ease, transform 0.5s ease;
        .el-icon-close {
            position: absolute;
            top: 0;
            right: 0;
            font-size: 16px;
        }
    }
    .opacity-10 {
        opacity: 1!important;
    }
    .opacity-2 {
        opacity: 0.2;
    }
    .opacity-1 {
        opacity: 0.1;
    }
    .mark {
        position: absolute;
        top: 0;
        left: 0;
        height: 100%;
        width: 100%;
        background: rgba(0,0,0,0.05);
        z-index: -1;
    }
}
</style>
D3 官网:https://d3js.org/
D3 API地址:https://d3js.org/api
功能描述:
  1. 画点、画线、画文本内容
  2. 鼠标悬浮点位,高亮关联点位、文本及其高速公路名称
  3. 鼠标悬浮展示动态流水线效果
  4. 弹窗效果已包含,未在demo中展示
数据分析:

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

  1. 每个点位中需要有相对位置坐标,前端根据视口范围计算,展示点位
  2. 点位需要包含类型,来判断展示对应的图表(门架、站点、枢纽等)
  3. 点位需要包含指向关系,作用是用来画连接线
  4. 点位需要包含高速名称,用于展示高速名称标识
  5. 点位必须按照顺序返回,否则连线会比较乱,视觉体验差
注意:
  • 高速公路名称是根据calculateGSNamePosition函数计算的出来的
后端返回的数据样例:
// x,y代表坐标
// label: 名称
// source:源
// target:指向目标
// type:类型
const data = {
  '沪杭高速': [
    {
      x: 900,
      y: 150,
      label: "织里枢纽",
      code: 5101,
      source: 5101,
      target: 5111,
      type: "hub",
    },
    {
      x: 900,
      y: 210,
      label: "织里东",
      code: 5111,
      source: 5111,
      target: 5113,
      type: "station",
    },
  ],
};

// x,y代表坐标
// label: 名称
// source:源
// target:指向目标
// type:类型
// gsName:高速名称
const data1 = [
  {
    x: 900,
    y: 150,
    label: "织里枢纽",
    code: 5101,
    source: 5101,
    target: 5111,
    type: "hub",
    gsName: "沪杭高速",
  },
  {
    x: 900,
    y: 210,
    label: "织里东",
    code: 5111,
    source: 5111,
    target: 5113,
    type: "station",
    gsName: "沪杭高速",
  },
];

网站公告

今日签到

点亮在社区的每一天
去签到