链表数据解析流程节点坐标信息

270 阅读3分钟

目前业务功能需要实现动态配置节点信息后使用echarts生成节点流程图,类似git提交时间线 目前后端只能提供前后节点信息 无法提供各节点准确的在图表中的坐标信息以此原因由前端实现链表生成节点坐标信息

通过分析将步骤拆分为三大步骤

  1. 构建前后节点关系映射
  2. 构建节点流程可能路径
  3. 合并路径反推节点坐标信息

1.构建前后节点关系映射

  function buildPreNodeMap(nodeInfo) {
		const root = []; // 创建根节点关系
		const disableNode = []; // 储存跳过节点信息
		const preNodeMap = { // 初始化映射对象
			root 
		};
		nodeInfo.forEach((item) => {
			const {
				haveWorkHour, // 是否需要跳过本节点
				nodeCode: code, // 重命名后端字段 本节点
				preNodeCode: pCode // 重命名后端字段 前节点
			} = item;
			if (!haveWorkHour) {
				// 如果没有工时
				disableNode.push(code); // 推入跳过节点
				// return;
			}
			if (pCode) {
				// 如果有前节点
				const pCodes = pCode.split(","); // 拆分前节点
				pCodes.forEach((_pcode) => {
					// 如果有前节点
					const arr = preNodeMap[_pcode] || []; // 提取节点在map中的的数组
					arr.push(code);
					if (!preNodeMap[_pcode]) {
						preNodeMap[_pcode] = arr; // 推送到前节点映射中
					}
				});
			} else {
				root.push(code); // 推送到根节点映射中
			}
		});
		const preNodeArr = Object.values(preNodeMap);
		disableNode.forEach((code) => { // 开始处理需要跳过节点
			const next = preNodeMap[code];
			if (next) { // 如果他有下一个节点 替换跳过的节点为下一个节点
				preNodeArr.forEach((arr) => {
					const index = arr.indexOf(code);
					if (index > -1) {
						arr.splice(index, 1, ...next);
					}
				});
			}
		});
		Object.assign(this, {
			preNodeMap,
			disableNode,
		});
	}

2.构建节点流程可能路径

function buildLink(){
    const res = [];  // 节点路径存储
    let links = [
            [].concat(this.preNodeMap.root) // 初始化根节点路径
    ];
    while (links.length) {// 如果还有路径没有走完
            const link = links.shift(); //弹出第一个路径
            let code = link[link.length - 1]; // 提取路径最后的节点
            while (code) { // 如果还有节点信息
                    const next = [].concat(this.preNodeMap[code]); // 拷贝下一个节点数据
                    if (next) {  // 如果存在
                            code = next.shift();
                            if (next.length) {
                                    next.forEach((c) => {
                                    // 循环处理多个节点产生新的路径
                                            links.push([].concat(link, c));
                                    });
                            }
                       
                            code && link.push(code);
                    } else {
                            code = null;
                    }
            }
            res.push(link);
    }
    res.sort((a, b) => b.length - a.length); //根据路径长短排序所有路径
    const start = res.shift(); // 弹出最长的
    const a = res.map((item) => {
            let arr = [];
            item.forEach((code) => { // 对比最长的路径出现的节点如果缺失使用null填充缺失数列补齐间隔
                    const index = start.indexOf(code);
                    if (arr.length < index) {
                            arr.push(...new Array(index - arr.length).fill(null));
                    }

                    arr.push(code);
            });
            return arr;
    });
    this.links = [].concat([start], a); // 重新合成所有路径
}

3.合并路径反推节点坐标信息

function buildXy(){

	buildXY() {
		const xy = {};
		const centerY = this.getMax() / 2;
		if (this.links.length === 1) {
			this.links[0].forEach((code, x) => {
				xy[code] = [x, centerY];
			});
		} else {
			this.links[0].forEach((item, x) => { // 开始处理节点路径
				const data = [];
				this.links.forEach((el) => {
					data.push(el[x]);
				});
				const merge = [...new Set(data)];
				if (merge.length === 1) {
					xy[data[0]] = [x, centerY];
				} else {
					merge.forEach((code, y) => {
						if (code) {
							xy[code] = [x, y];
						}
					});
				}
			});
		}
		this.xy = xy;
	}
}

完整实现类


class Nodes {
	preNodeMap = {
		root: []
	};
	disableNode = [];
	links = [];
	xy = {};
	nodeData = []; // 节点的相关信息
	constructor(nodeInfo, nodeData, toDoPlanList) {
		this.buildPreNodeMap(nodeInfo);
		this.buildLink();
		this.buildXY();
		this.buildNodeData(nodeData, toDoPlanList); // 构建节点信息
		console.log(this);
	}
	buildPreNodeMap(nodeInfo) {
		const root = [];
		const disableNode = [];
		const preNodeMap = {
			root
		};
		nodeInfo.forEach((item) => {
			const {
				haveWorkHour,
				nodeCode: code,
				preNodeCode: pCode
			} = item;
			if (!haveWorkHour) {
				// 如果没有工时
				disableNode.push(code); // 推入禁用节点
				// return;
			}
			if (pCode) {
				// 如果有前节点
				const pCodes = pCode.split(","); // 拆分前节点
				pCodes.forEach((_pcode) => {
					// 如果有前节点
					const arr = preNodeMap[_pcode] || []; // 提取节点在map中的的数组
					arr.push(code);
					if (!preNodeMap[_pcode]) {
						preNodeMap[_pcode] = arr; // 推送到前节点映射中
					}
				});
			} else {
				root.push(code); // 推送到根节点映射中
			}
		});
		const preNodeArr = Object.values(preNodeMap);
		disableNode.forEach((code) => {
			const next = preNodeMap[code];
			if (next) {
				preNodeArr.forEach((arr) => {
					const index = arr.indexOf(code);
					if (index > -1) {
						arr.splice(index, 1, ...next);
					}
				});
			}
		});
		Object.assign(this, {
			preNodeMap,
			disableNode,
		});
	}
	buildLink() {
		[
			['G000',]
		]
		const res = [];
		let links = [
			[].concat(this.preNodeMap.root)
		];
		while (links.length) {
			const link = links.shift(); //['G000']
			let code = link[link.length - 1]; // G000
			while (code) {
				const next = [].concat(this.preNodeMap[code]);
				if (next) {
					code = next.shift();
					if (next.length) {
						next.forEach((c) => {
							links.push([].concat(link, c));
						});
					}
					code && link.push(code);
				} else {
					code = null;
				}
			}
			res.push(link);
		}
		res.sort((a, b) => b.length - a.length);
		const start = res.shift(); // 弹出最长的
		const a = res.map((item) => {
			let arr = [];
			item.forEach((code) => {
				const index = start.indexOf(code);
				if (arr.length < index) {
					arr.push(...new Array(index - arr.length).fill(null));
				}

				arr.push(code);
			});
			return arr;
		});
		this.links = [].concat([start], a);
	}
	buildXY() {
		const xy = {};
		const centerY = this.getMax() / 2;
		if (this.links.length === 1) {
			this.links[0].forEach((code, x) => {
				xy[code] = [x, centerY];
			});
		} else {
			this.links[0].forEach((item, x) => { // 开始处理节点路径
				const data = [];
				this.links.forEach((el) => {
					data.push(el[x]);
				});
				const merge = [...new Set(data)];
				if (merge.length === 1) {
					xy[data[0]] = [x, centerY];
				} else {
					merge.forEach((code, y) => {
						if (code) {
							xy[code] = [x, y];
						}
					});
				}
			});
		}
		this.xy = xy;
	}
	buildNodeData(nodeData, toDoPlanList) {
		const map = toDoPlanList.reduce((prv, next) => {
			const {
				nodeCode
			} = next;
			prv[nodeCode] = next;
			return prv;
		}, {});
		this.nodeData = nodeData.map((item, i) => {
			//   // categories: [
			//   //   { name: "提前完成", itemStyle: { color: "#0079FE" } },
			//   //   { name: "延误完成", itemStyle: { color: "#F31F1F" } },
			//   //   { name: "进行中", itemStyle: { color: "#70B603" } },
			//   //   { name: "未开始/无此节点", itemStyle: { color: "#D7D7D7" } },
			//   // ],
			const {
				nodeCode
			} = item;

			const [x, y] = this.xy[nodeCode] || [0, 0];

			return {
				label: {
					position: "top"
				},
				category: ["提前完成", "延误完成", "进行中", "未开始"].indexOf(
					map[nodeCode].nodeFinishStatus
				),
				value: item.nodeName,
				name: nodeCode,
				x: x * 100,
				y: y * 100,
			};
		});
	}
	getMax() {
		return Object.values(this.preNodeMap).reduce(
			(prv, next) => Math.max(prv, next.length),
			0
		);
	}
	getLinks() {
		const links = [];
		Object.keys(this.preNodeMap).forEach((k) => {
			if (k == 0 || this.disableNode.includes(k)) return;
			const next = this.preNodeMap[k];
			next.forEach((key) => {
				links.push({
					source: k,
					target: key
				});
			});
		});
		return links;
	}
	getNodeData() {
		return this.nodeData;
	}
}

下班了待续..