树结构布局

树结构布局

前言

本文讲解如何实现图形化树结构布局。

布局规则:

  • 根节点始终处于画布中间
  • 同级节点不能相互重叠
  • 父节点永远处于子节点的水平中间位置

在这里插入图片描述

准备工作

以上图为例进行树结构布局设计,我们以每个节点的中心位置作为节点坐标。

width:画布宽度

nodeWidth: 节点的宽度

nodeHeight: 节点的高度,

levelHeight: 节点间的垂直间距(即每层间的垂直距离)

distance: 节点间水平间距(即同层节点间的距离)

初始化树形节点坐标

/*
    *nodeProps:属性节点的相关配置属性
    *node:当前节点信息
    *parentNode:父节点信息
    * 
*/
const generatePonint = (nodeProps, node, parentNode) => {
    let nodeWidth = nodeProps.nodeWidth;            //节点宽度
    let nodeHeight = nodeProps.nodeHeight;          //节点高度
    let levelHeight = nodeProps.levelHeight;        //节点与父节点间的间距
    let distance = nodeProps.distance;              //节点与兄弟节点间的间距
    let parentX = nodeProps.width / 2;              //父节点横坐标
    let parentY = -levelHeight - nodeHeight / 2;    //父节点纵坐标
    let nodeIndex = 0;                              //节点位于兄弟节点间的位置
    let nodeNum = 1;                                //兄弟节点个数
    if (parentNode) {
        nodeIndex = parentNode.children.findIndex(item => item.id == node.id);
        nodeNum = parentNode.children.length;
        parentX = parentNode.x;
        parentY = parentNode.y;
    }
    node.y = parentY + nodeHeight + levelHeight;
    node.x = parentX - (nodeNum * nodeWidth + (nodeNum - 1) * distance) / 2 + nodeIndex * (nodeWidth + distance) + nodeWidth / 2;
    if (node.children && node.children.length > 0) {
        let childLength = node.children.length;
        for (let i = 0; i < childLength; i++) {
            generatePonint(nodeProps, node.children[i], node);
        }
    }
    return node;
}

同层节点所占宽度=节点数 * 节点宽度+(节点数-1)*节点水平间距

当前节点所在x轴位置=节点下标 * (节点宽度+节点水平间距) +节点宽度的一半

因为下标是从0开始计算的所以少算了一个节点位置,其中点即是nodeWidth / 2

节点相较于左边线的偏移量=父节点x轴位置-同层节点所占宽度/2

节点X轴坐标=节点偏移量+当前节点x轴所在位置

节点Y轴坐标=父节点Y坐标+节点高度+节点垂直间距

在这里插入图片描述

节点偏移

初始化节点只是理想化的效果,此时只是将节点分层级给出了一个坐标,并不能保证节点间是否重复。

因为所有节点都是基于父节点的位置进行坐标计算的,而父节点的水平间距是固定的,那么当层级越多,相邻节点就会出现相互覆盖现象。

公共方法

//获取当前节点的父节点
const getParentNode = (node, root) => {
    //如果当前节点为根节点直接返回null
    if (node.id === root.id) {
        return null;
    }
    if (root.children && root.children.length > 0) {
        for (let child of root.children) {
            if (child.id === node.id) {
                return root;
            } else {
                let parentNode = getParentNode(node, child);
                if (parentNode) {
                    return parentNode;
                }
            }
        }
    }
    return null;
}

//获取相同深度的节点列表
const getDepthNode = (depth, root, list) => {
    if (root.depth === depth) {
        list.push(root);
        return list;
    }
    if (root.children && root.children.length > 0) {
        for (let node of root.children) {
            getDepthNode(depth, node, list)
        }
    }
    return list
}

//获取直系二代祖先节点
const getSameAncestorsNode = (node, nextNode, root) => {
    let parentNode = getParentNode(node, root);
    let nextParentNode = getParentNode(nextNode, root);
    if (parentNode.id == nextParentNode.id) {
        return node;
    } else {
        return getSameAncestorsNode(parentNode, nextParentNode, root);
    }
}

//获取祖宗节点
const getAncestorsNode = (node, root) => {
    let parentNode = getParentNode(node, root);
    if (parentNode) {
        if (parentNode.id == root.id) {
            return node;
        } else {
            let ancestorsNode = getAncestorsNode(parentNode, root);
            if (ancestorsNode) {
                return ancestorsNode;
            }
        }
    }
}

//获取兄弟节点
const getBrotherNode = (node, root) => {
    let parentNode = getParentNode(node, root);
    if (parentNode) {
        return parentNode.children;
    }
}

可以将树结构当成血缘谱,相同的祖先节点既是不同的子节点其祖先为同一人,在树结构中所有的子节点都是根节点的子孙。

偏移计算

//节点偏移
const offsetPoint = (nodeProps, node, root) => {
    let brotherList = getDepthNode(node.depth, root, []);
    let nodeDiffer = nodeProps.nodeWidth + nodeProps.distance;
    let nodeIndex = brotherList.findIndex(item => item.id === node.id);
    let nextNode = brotherList[nodeIndex + 1];
    if (nextNode) {
        let nextParentNode = getParentNode(nextNode, root);
        let parentNode = getParentNode(node, root);
        //判断重叠,只需判断不同父节点的相邻2个节点是否在固定节点宽度内
        if (nextParentNode && parentNode && nextParentNode.id != parentNode.id && nextNode.x - node.x < nodeDiffer) {
            let offsetValue = node.x - nextNode.x + nodeDiffer;//相邻2个节点需要的偏移量
            //查找2重复节点的共同祖先,并在共同祖先下查找当前节点共同祖先的第二代节点
            let sameAncestorsNode = getSameAncestorsNode(node, nextNode, root);
            if (sameAncestorsNode) {
                //将当前节点共同祖先的第二代节点层次偏移,第二代节点及其左边的节点向左偏移,其他的向右偏移
                //例如当前节点的第二代祖先层级为3,同层坐标为1,那么层级为3的0,1节点左移,其他节点右移
                offsetNodeAndChildPoint(sameAncestorsNode, root, offsetValue);
                //因为上面已经把底层节点全部移动了,而当前祖先节点位置居中,所以,当前节点的所有父级暂时不动其他父节点对应移动
                offsetParentPoint(sameAncestorsNode, root, offsetValue);
                //因为右边的节点全部右移了,导致位置不居中,所以当前节点及其左边的节点向左移动(包含子节点)
                //offsetParenAndChildtPoint(sameAncestorsNode, root, offsetValue);
            }
        }
    }
    // return root;
}

//子节点偏移
const offsetChildPoint = (node, root, offsetValue, pos) => {
    if (node.children && node.children.length > 0) {
        for (let child of node.children) {
            if (pos == "left") {
                child.x = child.x - offsetValue / 2;
                offsetChildPoint(child, root, offsetValue, pos)
            } else {
                child.x = child.x + offsetValue / 2;
                offsetChildPoint(child, root, offsetValue, pos)
            }
        }
    }
}

//节点及其子节点偏移
const offsetNodeAndChildPoint = (node, root, offsetValue) => {
    let brotherList = getDepthNode(node.depth, root, []);
    let nodeIndex = brotherList.findIndex(item => item.id === node.id);
    for (let i = 0; i < brotherList.length; i++) {
        if (nodeIndex == brotherList.length - 1) {
            if (i < nodeIndex) {
                brotherList[i].x = brotherList[i].x - offsetValue / 2;
                offsetChildPoint(brotherList[i], root, offsetValue, 'left');
            } else {
                brotherList[i].x = brotherList[i].x + offsetValue / 2;
                offsetChildPoint(brotherList[i], root, offsetValue, 'right');
            }
        } else {
            if (i <= nodeIndex) {
                brotherList[i].x = brotherList[i].x - offsetValue / 2;
                offsetChildPoint(brotherList[i], root, offsetValue, 'left');
            } else {
                brotherList[i].x = brotherList[i].x + offsetValue / 2;
                offsetChildPoint(brotherList[i], root, offsetValue, 'right');
            }
        }
    }
}

//节点父节点偏移
const offsetParentPoint = (node, root, offsetValue) => {
    let parentNode = getParentNode(node, root);
    let brotherList = getDepthNode(parentNode.depth, root, []);
    let nodeIndex = brotherList.findIndex(item => item.id === parentNode.id);
    for (let i = 0; i < brotherList.length; i++) {
        if (i < nodeIndex) {
            brotherList[i].x = brotherList[i].x - offsetValue / 2;
        } else if (i > nodeIndex) {
            brotherList[i].x = brotherList[i].x + offsetValue / 2;
        }
    }
    let grandfather = getParentNode(parentNode, root);
    if (grandfather) {
        offsetParentPoint(parentNode, root, offsetValue)
    }
}

//节点父节点偏移
const offsetParenAndChildtPoint = (node, root, offsetValue) => {
    let parentNode = getParentNode(node, root);
    if (parentNode.id == root.id) {
        return;
    }
    let brotherList = getDepthNode(parentNode.depth, root, []);
    let nodeIndex = brotherList.findIndex(item => item.id === parentNode.id);
    if(nodeIndex!=0){
      return;
    }
    for (let i = 0; i < brotherList.length; i++) {
        if (nodeIndex == brotherList.length - 1) {
            if (i < nodeIndex) {
                brotherList[i].x = brotherList[i].x - offsetValue / 2;
                offsetChildPoint(brotherList[i], root, offsetValue, 'left');
            } else {
                brotherList[i].x = brotherList[i].x + offsetValue / 2;
                offsetChildPoint(brotherList[i], root, offsetValue, 'right');
            }
        } else {
            if (i <= nodeIndex) {
                brotherList[i].x = brotherList[i].x - offsetValue / 2;
                offsetChildPoint(brotherList[i], root, offsetValue, 'left');
            }
        }
    }
    let grandfather = getParentNode(parentNode, root);
    if (grandfather) {
        offsetParenAndChildtPoint(parentNode, root, offsetValue)
    }
}

偏移原理

  1. 只有存在同级的下一个兄弟节点才可能出现覆盖效果。

  2. 只用不同父元素的相邻节点才会出现覆盖效果

  3. 我们这里采用从中间向左右分别偏移,即重复的节点,左侧的节点向左偏移一半位置,右侧的节点向右偏移一半位置,这样既可达到相邻2节点不重复。

    • 如果只是对当前节点进行偏移,那么它可能会和同级的节点重复,所以对重复的左侧节点,其子节点和兄弟节点都将左移;对重复右侧节点,其子节点和兄弟节点都将右移。

    • 此时我们会发现父节点因为未移动所以已经不再中间位置了,那我们父节点必须也做到相应的左移和右移。

    • 因为根节点始终在页面的水平中间位置,并不会移动,此时可以发现偏移节点的公共祖先其实并不需要移动位置。所以在公共祖先下,只需将左侧重复元素的直系父节点向左偏移,右侧重复元素的直系父元素向右偏移。

    • 节点的偏移必定带动同级节点的偏移,即左侧重复节点向左偏移时,其左侧的所有节点都需向左偏移,右侧重复节点向右偏移时,其右侧的所有节点都将向右偏移

  4. 移动逻辑:

    • 从二代祖先节点开始,移动同级节点及其子节点。
    • 因为祖先节点下的所有层级节点都偏移了,那么除祖先节点不变外,其他与祖先节点平级的所有的节点及其父节点都应该进行相应的偏移,以确保同一层级非同一祖先的节点位置正确,直到根节点。
    • 假如祖先节点层位于该层第一个位置,那么其右侧的节点均向右移动了,而祖先节点的位置没变动,所以祖先节点及其左侧的节点均应向左移动。

在这里插入图片描述

画布宽高自适应

//获取最大深度
const getMaxDepth = (root) => {
    if (root) {
        let maxDepth = 1;
        if (root.children && root.children.length > 0) {
            for (let node of root.children) {
                maxDepth = Math.max(maxDepth, getMaxDepth(node) + 1);
                let nodeIndex = root.children.findIndex(item => item.id == node.id);
                if (root.children[nodeIndex + 1]) {
                    maxDepth = Math.max(maxDepth, getMaxDepth(root.children[nodeIndex + 1]) + 1);
                }
            }
        }
        return maxDepth;
    }
}

//获取最大横坐标节点
const getMaxPointX = (root, maxPointX = 0) => {
    if (root) {
        maxPointX = Math.max(maxPointX, root.x);
        if (root.children && root.children.length > 0) {
            for (let node of root.children) {
                maxPointX = getMaxPointX(node, maxPointX)
            }
        }
    }
    return maxPointX;
}

//获取最小横坐标节点
const getMinPointX = (root, minPointX = 0) => {
    if (root) {
        minPointX = Math.min(minPointX, root.x);
        if (root.children && root.children.length > 0) {
            for (let node of root.children) {
                minPointX = getMinPointX(node, minPointX)
            }
        }
    }
    return minPointX;
}

//获取画布最大宽度
const getMaxCanvasWidth = (root, nodeProps) => {
    let rootX = root.x;
    let maxPointX = getMaxPointX(root, rootX);
    let minPointX = getMinPointX(root, rootX);
    let minX = Math.abs(minPointX - rootX - nodeProps.nodeWidth / 2);
    let maxCanvasWidth = Math.max(minX, maxPointX - rootX + nodeProps.nodeWidth / 2);
    return Math.ceil(maxCanvasWidth * 2);
}

const getMaxCanvasHeight = (root, nodeProps) => {
    let maxDepth = getMaxDepth(root);
    let maxHeight = nodeProps.nodeHeight * maxDepth + nodeProps.levelHeight * (maxDepth - 1);
    return Math.ceil(maxHeight);
}

使用方法

//为节点添加x,y坐标
    dealData(canvas, treeData, nodeSetting, scale) {
        const width = canvas.width;
        const height = canvas.height;
        let nodeProps = {
            width: width,
            nodeWidth: nodeSetting.nodeWidth * scale,
            nodeHeight: nodeSetting.nodeHeight * scale,
            levelHeight: nodeSetting.levelHeight * scale,
            distance: nodeSetting.distance * scale
        }

        TreeUtil.generatePonint(nodeProps, treeData, null);
        TreeUtil.offsetRoot(nodeProps, treeData, treeData);
        let maxCanvasWidth = TreeUtil.getMaxCanvasWidth(treeData, nodeProps);
        if (maxCanvasWidth > width) {
            canvas.width = maxCanvasWidth;
            this.dealData(canvas, treeData, nodeSetting, scale);
        }

        let maxCanvasHeight = TreeUtil.getMaxCanvasHeight(treeData, nodeProps);
        if (maxCanvasHeight > height) {
            canvas.height = maxCanvasHeight;
            this.dealData(canvas, treeData, nodeSetting, scale);
        }
    }

canvas:画布对象

treeData:树形数据

nodeSetting:节点相关配置信息

scale:缩放比例,1/像素比

  • 2
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值