JS画图,画树形图(流程图、组织架构图),根据节点间的关系得到坐标,并渲染树形图

效果:在这里插入图片描述
代码:

import dagre from "dagre";

/** 传入的节点类型 */
export type NodeType = {
    id: React.Key;
    /** 节点宽度 */
    width: number;
    /** 节点高度 */
    height: number;
    nextIdList: React.Key[];
};

// 计算时附加的节点类型
export type NodeTypeWithCalc = {
    prevIdList: React.Key[];
};

/** 渲染时附加的节点类型 */
export type NodeTypeWithRender = NodeType &
    NodeTypeWithCalc & {
        level: number;
        x: number;
        y: number;
    };

/** 处理数据计算层级 */
export const calcDataLevel = (nodeList: (NodeType & NodeTypeWithCalc)[]) => {
    // 默认层级为0
    const defaultLevel = 0;
    // 深拷贝节点列表
    let newNodes: ((NodeType & NodeTypeWithCalc) & { level: number })[] = [
        ...nodeList,
    ].map((item) => ({
        ...item,
        level: defaultLevel,
    }));
    // 起始节点
    const startNode = newNodes?.find((item) => item.prevIdList.length === 0);
    // 递归计算节点的层级
    const calcLevel = (node: NodeType & { level: number }) => {
        node.nextIdList.forEach((nextId) => {
            const nextNode = newNodes.find((item) => item.id === nextId);
            if (nextNode?.level === defaultLevel) {
                nextNode.level = node.level + 1;
                calcLevel(nextNode);
            }
        });
    };
    if (startNode) {
        startNode.level = 1;
        calcLevel(startNode);
    }
    return newNodes;
};

/** 处理数据计算坐标Y */
export const calcDataPositionY = (pa: {
    nodeList: ((NodeType & NodeTypeWithCalc) & { level: number })[];
    nodeVerticalSpace: number;
}) => {
    const { nodeList, nodeVerticalSpace } = pa;
    // 深拷贝节点列表
    const newNodes: ((NodeType & NodeTypeWithCalc) & {
        level: number;
        y: number;
    })[] = [...nodeList].map((item) => ({
        ...item,
        y: 0,
    }));
    // 每层高度的最高值字典
    const levelHeightMap: Record<number, number> = {};
    // 计算每层的最高值
    newNodes.forEach((item) => {
        if (!levelHeightMap[item.level]) {
            levelHeightMap[item.level] = item.height;
        } else {
            levelHeightMap[item.level] = Math.max(
                levelHeightMap[item.level],
                item.height
            );
        }
    });
    // 根据层级和前置层最高的节点高度计算Y坐标
    newNodes.forEach((item) => {
        const prevNodes = newNodes.filter((node) =>
            item.prevIdList.includes(node.id)
        );
        if (prevNodes.length === 0) {
            item.y = 0;
        } else {
            const prevNode = prevNodes[0];
            const prevLevelHeight = levelHeightMap[prevNode.level];
            item.y = prevNode.y + prevLevelHeight + nodeVerticalSpace;
        }
    });
    return newNodes as NodeTypeWithRender[];
};

/** 处理数据计算坐标X */
export const calcDataPositionX = (pa: {
    nodeList: (NodeType & { level: number })[];
    nodeHorizontalSpace: number;
}) => {
    const { nodeList, nodeHorizontalSpace } = pa;
    // 深拷贝节点列表
    const newNodes: (NodeType & { level: number; x: number })[] = [
        ...nodeList,
    ].map((item) => ({
        ...item,
        x: 0,
    }));
    const g = new dagre.graphlib.Graph();
    // 设置图的默认值,算法我也不太懂,但是network-simplex会导致节点位置变化,所以这里设置为longest-path
    g.setGraph({
        rankDir: "BT",
        nodesep: nodeHorizontalSpace,
        ranker: "longest-path",
    });
    g.setDefaultEdgeLabel(function () {
        return {};
    });
    newNodes.forEach((node) => {
        g.setNode(node.id.toString(), {
            width: node.width,
            height: node.height,
        });
    });
    newNodes.forEach((node) => {
        node.nextIdList.forEach((nextId) => {
            g.setEdge(node.id.toString(), nextId.toString());
        });
    });
    dagre.layout(g);
    newNodes.forEach((node) => {
        const nodePosition = g.node(node.id.toString());
        if (nodePosition) node.x = nodePosition.x;
    });
    return newNodes as NodeTypeWithRender[];
};

/** 根据节点的nextIdList,得到prevIdList */
export const setNodePrevIdList = (data: NodeType[]) => {
    const newNodes: (NodeType & NodeTypeWithCalc)[] = [...data].map((item) => ({
        ...item,
        prevIdList: [],
    }));
    newNodes.forEach((item) => {
        item.nextIdList.forEach((nextId) => {
            const nextNode = newNodes.find((node) => node.id === nextId);
            if (nextNode) {
                nextNode.prevIdList.push(item.id);
            }
        });
    });
    return newNodes;
};

引用:

import {
    NodeTypeWithRender,
    calcDataLevel,
    calcDataPositionY,
    calcDataPositionX,
    type NodeType,
    setNodePrevIdList,
} from "./const";
import { useEffect, useMemo } from "react";
import { jsPlumb as jsplumbUI, type jsPlumbInstance } from "jsplumb";
import { jsplumbSetting, createConnections } from "./js_plumb_mixins";
import "./index.less";

type PropsType = {
    nodeList: NodeType[];
    /** 节点间的垂直间距 */
    nodeVerticalSpace?: number;
    /** 节点间的水平间距 */
    nodeHorizontalSpace?: number;
    renderNode?: (node: NodeTypeWithRender) => React.ReactNode;
};

// 要拉到外面,不然每次渲染都会重新创建一个实例(出现删除不了线)
const jsPlumb: jsPlumbInstance = jsplumbUI.getInstance();

/** 流程图组件,只适用树形控件(只能有一个起始点)*/
const FlowChart = (props: PropsType) => {
    // 用于渲染的节点列表
    const showNodeList = useMemo(() => {
        return calcDataPositionX({
            nodeList: calcDataPositionY({
                nodeList: calcDataLevel(setNodePrevIdList(props.nodeList)),
                nodeVerticalSpace: props.nodeVerticalSpace || 80,
            }),
            nodeHorizontalSpace: props.nodeHorizontalSpace || 20,
        });
    }, [props.nodeList]);

    // 初始化jsPlumb
    const initJsPlumb = () => {
        jsPlumb?.ready(function () {
            // 导入默认配置
            jsPlumb.importDefaults(jsplumbSetting);
            // 会使整个jsPlumb立即重绘。
            jsPlumb.setSuspendDrawing(false, true);
            // 绑定鼠标移入连线事件
            jsPlumb?.bind("mouseover", (conn: any) => {
                // 点击连线,不是点击连线上的path
                conn.setPaintStyle({ stroke: "#7aff00", strokeWidth: 2 });
            });
            // 绑定鼠标移出连线事件
            jsPlumb?.bind("mouseout", (conn: any) => {
                // 点击连线,不是点击连线上的path
                conn.setPaintStyle({ stroke: "#3a9ffc", strokeWidth: 2 });
            });
        });
    };

    useEffect(() => {
        initJsPlumb();
        return () => {
            if (!jsPlumb) return;
            jsPlumb.deleteEveryEndpoint();
            jsPlumb.deleteEveryConnection();
            jsPlumb.unbind();
        };
    }, []);

    useEffect(() => {
        setTimeout(() => {
            // 创建连线
            createConnections({
                dataArr: showNodeList.map((item) => ({
                    ...item,
                    nodeId: item.id.toString(),
                    bottom: item.nextIdList?.map((id) => id.toString()) || [],
                    top: item.prevIdList?.map((id) => id.toString()) || [],
                    left: [],
                    right: [],
                })),
                jsPlumb,
            });
        }, 10);
    }, [showNodeList]);

    return (
        <div className="flow-chart-wrap">
            {showNodeList.map(
                (item) =>
                    props.renderNode?.(item) || (
                        <div
                            id={item.id.toString()}
                            key={item.id}
                            className="node"
                            style={{
                                left: item.x,
                                top: item.y,
                                width: item.width,
                                height: item.height,
                            }}
                        >
                            {item.id}
                        </div>
                    )
            )}
        </div>
    );
};

export default FlowChart;

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值