求思维导图自动计算节点位置的算法如何改进?

需求:实现一个树结构的数据结构,实现类似思维导图的效果,计算出每一个节点的坐标,并且不重复,不交叉,充分利用空间。

参考文章:https://juejin.cn/post/7085158485607841805
文中代码:参考上面文章末尾处

文章中实现的思维导图效果如下(左图):
https://wanglin2.github.io/tree_layout_demo/

左图是文章中实现的效果,右图是我参考代码放到vue项目中的效果,如下:

具体代码如下:

DrawTree.js

export default class DrawTree {
  constructor(tree, parent = null, depth = 0, number = 1) {
    this.name = tree.name;
    this.width = tree.width || 30;
    this.height = tree.height || 20;
    this.y = -1;
    this.x = depth;
    this.children = tree.children.map((child, index) => {
      return new DrawTree(child, this, depth + 1, index + 1);
    });
    this.parent = parent;
    // 线程节点,也就是指向下一个轮廓节点
    this.thread = null;
    // 根据左兄弟定位的x与根据子节点中间定位的x之差
    this.mod = 0;
    // 要么指向自身,要么指向所属树的根
    this.ancestor = this;
    this.change = this.shift = 0;
    // 最左侧的兄弟节点
    this._lmost_sibling = null;
    // 这是它在兄弟节点中的位置索引 1...n
    this.number = number;
    // 该节点子节点的最大宽度
    // this.maxChildrenWidth = 0
    this.minY = 0;
    this.maxY = 0;
    this.offset = 0;
  }

  // 有线程返回线程节点,否则返回最右侧的子节点,也就是树的右轮廓
  right() {
    return (
      this.thread ||
      (this.children.length > 0
        ? this.children[this.children.length - 1]
        : null)
    );
  }

  // 有线程返回线程节点,否则返回最左侧的子节点,也就是树的左轮廓
  left() {
    return this.thread || (this.children.length > 0 ? this.children[0] : null);
  }

  // 获取前一个兄弟节点
  left_brother() {
    let n = null;
    if (this.parent) {
      for (let i = 0; i < this.parent.children.length; i++) {
        let node = this.parent.children[i];
        if (node === this) {
          return n;
        } else {
          n = node;
        }
      }
    }
    return n;
  }

  // 获取同一层级第一个兄弟节点,如果第一个是自身,那么返回null
  get_lmost_sibling() {
    if (
      !this._lmost_sibling &&
      this.parent &&
      this !== this.parent.children[0]
    ) {
      this._lmost_sibling = this.parent.children[0];
    }
    return this._lmost_sibling;
  }

  // 同一层级第一个兄弟节点
  get leftmost_sibling() {
    return this.get_lmost_sibling();
  }
}

index.vue(使用)

<template>
  <div id="treeBox">
    <div
      v-for="(node, idx) in nodes"
      :key="`node-${idx}`"
      class="node"
      :style="{
        width: `${node.width}px`,
        height: `${node.height}px`,
        left: `${node.x}px`,
        top: `${node.y}px`,
      }"
    >
      {{ node.id }}{{ node.name }}
    </div>
  </div>
</template>

<script>
import DrawTree from "./DrawTree";

const NODE_SPACE = 20;
const NODE_SPACE_X = 50;
// const SIZE = 800;

// 第一次递归
const firstwalk = (v) => {
  if (v.children.length === 0) {
    // 当前节点是叶子节点且存在左兄弟节点,则其x坐标等于其左兄弟的x坐标加上间距distance
    if (v.leftmost_sibling) {
      v.y = v.left_brother().y + NODE_SPACE;
    } else {
      // 当前节点是叶节点无左兄弟,那么x坐标为0
      v.y = 50;
    }
  } else {
    // default_ancestor初始指向第一个子节点
    let default_ancestor = v.children[0];
    // let maxChildrenWidth = 0
    v.children.forEach((child) => {
      firstwalk(child);
      // 找出子节点的最大宽度
      // if (child.width > maxChildrenWidth) {
      //     maxChildrenWidth = child.width
      // }
      default_ancestor = apportion(child, default_ancestor);
    });
    // v.maxChildrenWidth = maxChildrenWidth
    // 将shift分摊添加到中间的节点上,也就是添加到节点的x及mod值上
    execute_shifts(v);

    // 子节点的中点
    let midpoint = (v.children[0].y + v.children[v.children.length - 1].y) / 2;
    let w = v.left_brother();
    if (w) {
      // 如果是非叶子节点则其x坐标等于其左兄弟的x坐标加上间距distance
      v.y = w.y + NODE_SPACE;
      // 同时记录下偏移量(x坐标与子节点的中点之差)
      v.mod = v.y - midpoint;
    } else {
      // 没有左兄弟节点,x坐标直接是子节点的中点
      v.y = midpoint;
    }
  }
  return v;
};

// 修正子孙节点定位
const apportion = (v, default_ancestor) => {
  let leftBrother = v.left_brother();
  if (leftBrother) {
    // 四个节点指针
    let vInnerRight = v; // 右子树左轮廓
    let vOuterRight = v; // 右子树右轮廓
    let vInnerLeft = leftBrother; // 当前节点的左兄弟节点,左子树右轮廓
    let vOuterLeft = v.leftmost_sibling; // 当前节点的最左侧的兄弟节点,左子树左轮廓

    // 累加mod值,它们的父节点是同一个,所以往上它们要加的mod值也是一样的,那么在后面shift值计算时vInnerLeft.y + 父节点.mod - (vInnerRight.y + 父节点.mod),父节点.mod可以直接消掉,所以不加上面的祖先节点的mod也没关系
    let sInnerRight = vInnerRight.mod;
    let sOuterRight = vOuterRight.mod;
    let sInnerLeft = vInnerLeft.mod;
    let sOuterLeft = vOuterLeft.mod;

    // 一直遍历到叶子节点
    while (vInnerLeft.right() && vInnerRight.left()) {
      // 更新指针
      vInnerLeft = vInnerLeft.right();
      vInnerRight = vInnerRight.left();
      vOuterLeft = vOuterLeft.left();
      vOuterRight = vOuterRight.right();

      // 节点v下面的每一层右轮廓节点都关联v
      vOuterRight.ancestor = v;

      // 左侧节点减右侧节点
      let shift =
        vInnerLeft.y + sInnerLeft + NODE_SPACE - (vInnerRight.y + sInnerRight);
      if (shift > 0) {
        let _ancestor = ancestor(vInnerLeft, v, default_ancestor);
        // 大于0说明存在交叉,那么右侧的树要向右移动
        move_subtree(_ancestor, v, shift);
        // v.mod,也就是右侧子树增加了shift,sInnerRight、sOuterRight当然也要同步增加
        sInnerRight += shift;
        sOuterRight += shift;
      }

      // 累加当前层节点mod
      sInnerRight += vInnerRight.mod;
      sOuterRight += vOuterRight.mod;
      sInnerLeft += vInnerLeft.mod;
      sOuterLeft += vOuterLeft.mod;
    }

    // 将线程从浅的树的外侧设置到深的树的内侧
    if (vInnerLeft.right() && !vOuterRight.right()) {
      vOuterRight.thread = vInnerLeft.right();
      // 修正因为线程影响导致mod累加出错的问题,深的树减浅树
      vOuterRight.mod += sInnerLeft - sOuterRight;
    } else {
      if (vInnerRight.left() && !vOuterLeft.left()) {
        vOuterLeft.thread = vInnerRight.left();
        vOuterLeft.mod += sInnerRight - sOuterLeft;
      }
      default_ancestor = v;
    }
  }
  return default_ancestor;
};

// 移动子树
const move_subtree = (leftV, v, shift) => {
  let subTrees = v.number - leftV.number; // 索引相减,得到之间被分隔的数量
  let average = shift / subTrees; // 平分偏移量
  v.shift += shift; // 完整的shift值添加到v节点的shift属性上
  v.change -= average;
  leftV.change += average;

  v.y += shift; // 自身移动
  v.mod += shift; // 后代节点移动
};

// 应用分摊
const execute_shifts = (v) => {
  let change = 0;
  let shift = 0;
  // 从后往前遍历子节点
  for (let i = v.children.length - 1; i >= 0; i--) {
    let node = v.children[i];
    node.y += shift;
    node.mod += shift;

    change += node.change;
    shift += node.shift + change;
  }
};

// 找出节点所属的根节点
const ancestor = (vInnerLeft, v, default_ancestor) => {
  // 如果vInnerLeft节点的ancestor指向的节点是v节点的兄弟,那么符合要求
  if (v.parent.children.includes(vInnerLeft.ancestor)) {
    return vInnerLeft.ancestor;
  } else {
    // 否则使用default_ancestor指向的节点
    return default_ancestor;
  }
};

// 第二次遍历
const second_walk = (v, m = 0, depth = 0, s = 0) => {
  // 初始x值加上所有祖宗节点的mod修正值
  v.y += m;
  v.x = depth * NODE_SPACE_X + s + NODE_SPACE_X;
  v.children.forEach((child) => {
    // let maxWidth = 0
    // if (v.parent) {
    //     maxWidth = v.parent.maxChildrenWidth
    // } else {
    //     maxWidth = v.width
    // }
    second_walk(child, m + v.mod, depth + 1, s + v.width);
  });
};

// 第三次遍历
const third_walk = (tree) => {
  let selfMinY = tree.y - tree.height / 2;
  let selfMaxY = tree.y + tree.height / 2;
  // 计算每个节点的minY和maxY
  if (tree.children.length > 0) {
    let minY = Infinity;
    let maxY = -Infinity;
    tree.children.forEach((child) => {
      third_walk(child);
      if (child.minY < minY) {
        minY = child.minY;
      }
      if (child.maxY > maxY) {
        maxY = child.maxY;
      }
    });

    tree.minY = selfMinY < minY ? selfMinY : minY;
    tree.maxY = selfMaxY > maxY ? selfMaxY : maxY;
  } else {
    tree.minY = selfMinY;
    tree.maxY = selfMaxY;
  }
  // 判断是否和左兄弟有交叉
  if (tree.left_brother()) {
    if (tree.minY < tree.left_brother().maxY + NODE_SPACE) {
      let o = tree.left_brother().maxY - tree.minY + NODE_SPACE;
      tree.offset = o; // 用于移动子节点
      tree.y += o; // 移动自身
      tree.minY += o; // 更新minY、maxY
      tree.maxY += o;
    }
  }
};

// 第四次遍历
const fourth_walk = (tree, o = 0) => {
  tree.y += o;
  tree.children.forEach((child) => {
    fourth_walk(child, o + tree.offset);
  });
  let len = tree.children.length;
  if (len <= 0) {
    return;
  }
  // 重新居于子节点中间
  let mid = (tree.children[0].y + tree.children[len - 1].y) / 2;
  tree.y = mid;
};

const buchheim = (tree) => {
  let dt = firstwalk(tree);
  second_walk(dt);
  third_walk(dt);
  fourth_walk(dt);
  console.log(dt);
  return dt;
};


export default {
  data() {
    return {
      nodes: [],
    };
  },

  mounted() {
    let treeData4 = {
      name: "Q",
      children: [
        {
          name: "G0",
          children: [],
        },
        {
          name: "G",
          children: [
            {
              name: "C",
              width: 50,
              height: 70,
              children: [
                {
                  name: "B",
                  width: 20,
                  height: 50,
                  children: [
                    {
                      name: "A",
                      children: [],
                    },
                  ],
                },
              ],
            },
            {
              name: "F",
              children: [
                {
                  name: "D",
                  children: [],
                },
                {
                  name: "E",
                  width: 150,
                  height: 60,
                  children: [],
                },
                {
                  name: "E2",
                  children: [],
                },
              ],
            },
          ],
        },
        {
          name: "H",
          children: [],
        },
        {
          name: "H2",
          width: 100,
          height: 40,
          children: [],
        },
        {
          name: "P",
          children: [
            {
              name: "I",
              width: 100,
              height: 50,
              children: [],
            },
            {
              name: "O",
              children: [
                {
                  name: "J",
                  children: [],
                },
                {
                  name: "K",
                  width: 100,
                  height: 60,
                  children: [],
                },
                {
                  name: "L",
                  children: [],
                },
                {
                  name: "M",
                  children: [],
                },
                {
                  name: "N",
                  children: [],
                },
              ],
            },
          ],
        },
        {
          name: "P2",
          children: [],
        },
      ],
    };
    let tree = new DrawTree(treeData4);

    //获取所有节点的坐标
    tree = buchheim(tree);

  },
  methods: {
    
  },
};
</script>

<style scoped>
.treeBox {
  position: relative;
}
.node {
  position: absolute;
  border: 1px solid red;
}
</style>

在index.vue文件中,通过tree = buchheim(tree)已经计算出每一个节点的坐标,实现的效果和参考文章一样了,但是现在需要的效果是:

1、根节点主动提供坐标,不需要计算,其他子节点基于根节点计算坐标,因为有如下场景:

1.1 还原思维导图,喜欢还原在原始位置,所以根节点位置是已知的;
1.2 添加子节点,根节点位置是不变的,其他子节点自动通过计算调整位置;

2、添加删除子节点时,如何高效更新每一个节点的坐标(根节点除外),如果没有的话,只能添加、删除子节点后,重新过一边代码,重新计算所有节点位置了;

———————————————————————— 错误示例:

根节点Q定义的坐标为:100、100,但是生成的坐标如图。

——————————————————————————————————————补充,当节点数据高度比较大时,节点重叠了

let treeData4 = {
    "Id": "badda68c-201c-4a68-b0bf-f63df8a8e392",
    "ParentId": null,
    "width": 320,
    "height": 362,
    "x": 500,
    "y": 600,
    "children": [
      {
        "Id": "2bf239e0-4cec-5995-bf96-175b824d9643",
        "ParentId": "badda68c-201c-4a68-b0bf-f63df8a8e392",
        "width": 320,
        "height": 392,
        "children": [
          {
            "Id": "a611067b-2959-4dee-a06d-e32a4e3865c6",
            "ParentId": "2bf239e0-4cec-5995-bf96-175b824d9643",
            "width": 320,
            "height": 136
          },
          {
            "Id": "c6b28473-9cde-4963-91e2-a16618cae556",
            "ParentId": "2bf239e0-4cec-5995-bf96-175b824d9643",
            "width": 320,
            "height": 108
          }
        ]
      },
      {
        "Id": "d5d15567-f71b-44bb-9158-8b6a1300c60b",
        "ParentId": "badda68c-201c-4a68-b0bf-f63df8a8e392",
        "width": 320,
        "height": 392
      }
    ]
}
阅读 3.5k
avatarAI BotBETA

改进思维导图自动计算节点位置的算法

1. 根节点主动提供坐标

对于根节点主动提供坐标的情况,我们可以在初始化 DrawTree 类时传入根节点的坐标,并在计算过程中使用这些坐标。具体步骤如下:

  1. 修改 DrawTree 类的构造函数

    • 添加一个参数来接收根节点的坐标(例如 rootXrootY)。
    • 如果当前节点是根节点,则使用传入的坐标;否则,使用现有的计算逻辑。
  2. 调整 buchheim 函数

    • 在调用 firstwalk 之前,将根节点的坐标设置到 tree 对象上。

修改后的 DrawTree 类构造函数示例:

export default class DrawTree {
  constructor(tree, parent = null, depth = 0, number = 1, rootX = 0, rootY = 0) {
    this.name = tree.name;
    this.width = tree.width || 30;
    this.height = tree.height || 20;
    this.y = (parent === null && rootY !== undefined) ? rootY : -1;
    this.x = (parent === null && rootX !== undefined) ? rootX : depth;
    // 后续代码保持不变...
  }
}

mounted 钩子中调用 buchheim 函数时传入根节点的坐标:

mounted() {
  let treeData4 = {
    // ...(数据保持不变)
  };
  let rootX = 200; // 假设根节点的 X 坐标
  let rootY = 100; // 假设根节点的 Y 坐标
  let tree = new DrawTree(treeData4, null, 0, 1, rootX, rootY);

  tree = buchheim(tree);
  
  // 更新 nodes 数组以包含计算后的节点坐标
  this.nodes = this.flattenTree(tree); // 需要添加 flattenTree 函数来扁平化树结构
},

methods: {
  flattenTree(tree) {
    let result = [];
    function traverse(node) {
      result.push({
        id: node.name,
        name: node.name,
        width: node.width,
        height: node.height,
        x: node.x,
        y: node.y,
      });
      node.children.forEach(child => traverse(child));
    }
    traverse(tree);
    return result;
  }
}

2. 添加删除子节点时高效更新节点坐标

为了高效更新节点坐标,我们需要避免重新计算整个树。这里提供两种思路:

  • 局部更新:只重新计算受影响的子树。
  • 缓存中间结果:在节点添加或删除时,缓存中间结果以便快速恢复。
局部更新
  1. 确定受影响的子树

    • 当添加或删除节点时,确定哪些子树需要重新计算。
  2. 重新计算受影响的子树

    • 对受影响的子树调用 firstwalksecond_walk 等函数进行重新计算。
缓存中间结果
  1. 缓存节点位置

    • 在节点上缓存其位置信息。
  2. 更新缓存

    • 在添加或删除节点时,只更新相关节点的缓存,并重新计算必要的中间结果。

下面是一个简化的示例,展示了如何在添加或删除节点时局部更新受影响的子树:

// 假设我们有一个 addNode 函数来添加节点
function addNode(parent, newNode) {
  // 将新节点添加到父节点的 children 中
  parent.children.push(new DrawTree(newNode, parent, parent.depth + 1));
  
  // 重新计算受影响的子树
  recomputeSubtree(parent.children[parent.children.length - 1]);
}

function removeNode(parent, nodeToRemove) {
  // 从父节点的 children 中移除节点
  let index = parent.children.findIndex(child => child === nodeToRemove);
  if (index !== -1) {
    parent.children.splice(index, 1);
  }
  
  // 重新计算受影响的子树(这里可能需要根据实际情况调整)
  // 注意:如果移除的是非叶子节点,其所有后代也需要被重新计算
  recomputeSubtree(parent);
}

function recomputeSubtree(node) {
  // 从当前节点开始重新计算子树的位置
  // 这里可以调用类似 firstwalk、second_walk 等函数
  // 但需要注意,这些函数可能需要调整以支持局部更新
  // 例如,它们应该只影响从当前节点开始的子树,而不是整个树
  // 这里只是一个示例,实际实现需要根据你的具体需求来编写
  // ...
}

请注意,上述 recomputeSubtree 函数是一个占位符,你需要根据你的具体需求来实现它。这通常涉及到递归地调用布局算法中的相关函数,但只针对受影响的子树。

通过这些改进,你可以实现根节点主动提供坐标,并在添加或删除子节点时高效地更新节点坐标。

2 个回答

固定一下根节点位置:

const buchheim = (tree, rootX = 0, rootY = 0) => {
  let dt = firstwalk(tree);
  
  // Calculate the offset needed to move root to desired position
  const xOffset = rootX - dt.x;
  const yOffset = rootY - dt.y;
  
  // Apply the offset in second_walk
  second_walk(dt, yOffset, 0, xOffset);
  third_walk(dt);
  fourth_walk(dt);
  return dt;
};

在添加/删除节点更新:

class DrawTree {
  // ... existing code ...

  updateSubtreeLayout(startNode = this) {
    // Only recalculate from the modified subtree
    const parent = startNode.parent;
    if (!parent) {
      // Root node position is fixed
      return;
    }

    let current = parent;
    while (current) {
      // Reset positioning data
      current.mod = 0;
      current.shift = 0;
      current.change = 0;
      current.thread = null;
      current._lmost_sibling = null;
      
      firstwalk(current);
      second_walk(current, current.parent ? current.parent.mod : 0);
      third_walk(current);
      fourth_walk(current);
      
      current = current.parent;
    }
  }

  addChild(childData) {
    const child = new DrawTree(childData, this, this.x + 1);
    this.children.push(child);
    this.updateSubtreeLayout(child);
    return child;
  }

  removeChild(child) {
    const index = this.children.indexOf(child);
    if (index > -1) {
      this.children.splice(index, 1);
      this.updateSubtreeLayout(this);
    }
  }
}

DrawTree.js

export default class DrawTree {
  constructor(tree, parent = null, depth = 0, number = 1, x = 0, y = 0) {
    this.name = tree.name;
    this.width = tree.width || 30;
    this.height = tree.height || 20;
    this.x = x;
    this.y = y;
    this.children = tree.children.map((child, index) => {
      return new DrawTree(child, this, depth + 1, index + 1);
    });
    this.parent = parent;
    this.thread = null;
    this.mod = 0;
    this.ancestor = this;
    this.change = this.shift = 0;
    this._lmost_sibling = null;
    this.number = number;
    this.minY = 0;
    this.maxY = 0;
    this.offset = 0;
  }

  right() {
    return (
      this.thread ||
      (this.children.length > 0
        ? this.children[this.children.length - 1]
        : null)
    );
  }

  left() {
    return this.thread || (this.children.length > 0 ? this.children[0] : null);
  }

  left_brother() {
    let n = null;
    if (this.parent) {
      for (let i = 0; i < this.parent.children.length; i++) {
        let node = this.parent.children[i];
        if (node === this) {
          return n;
        } else {
          n = node;
        }
      }
    }
    return n;
  }

  get_lmost_sibling() {
    if (
      !this._lmost_sibling &&
      this.parent &&
      this !== this.parent.children[0]
    ) {
      this._lmost_sibling = this.parent.children[0];
    }
    return this._lmost_sibling;
  }

  get leftmost_sibling() {
    return this.get_lmost_sibling();
  }
}

// 布局算法
const firstwalk = (v) => {
  if (v.children.length === 0) {
    if (v.leftmost_sibling) {
      v.y = v.left_brother().y + NODE_SPACE;
    } else {
      v.y = v.y || 50;
    }
  } else {
    let default_ancestor = v.children[0];
    v.children.forEach((child) => {
      firstwalk(child);
      default_ancestor = apportion(child, default_ancestor);
    });
    execute_shifts(v);
    let midpoint = (v.children[0].y + v.children[v.children.length - 1].y) / 2;
    let w = v.left_brother();
    if (w) {
      v.y = w.y + NODE_SPACE;
      v.mod = v.y - midpoint;
    } else {
      v.y = midpoint;
    }
  }
  return v;
};

const apportion = (v, default_ancestor) => {
  let leftBrother = v.left_brother();
  if (leftBrother) {
    let vInnerRight = v;
    let vOuterRight = v;
    let vInnerLeft = leftBrother;
    let vOuterLeft = v.leftmost_sibling;
    let sInnerRight = vInnerRight.mod;
    let sOuterRight = vOuterRight.mod;
    let sInnerLeft = vInnerLeft.mod;
    let sOuterLeft = vOuterLeft.mod;

    while (vInnerLeft.right() && vInnerRight.left()) {
      vInnerLeft = vInnerLeft.right();
      vInnerRight = vInnerRight.left();
      vOuterLeft = vOuterLeft.left();
      vOuterRight = vOuterRight.right();
      vOuterRight.ancestor = v;

      let shift = vInnerLeft.y + sInnerLeft + NODE_SPACE - (vInnerRight.y + sInnerRight);
      if (shift > 0) {
        let _ancestor = ancestor(vInnerLeft, v, default_ancestor);
        move_subtree(_ancestor, v, shift);
        sInnerRight += shift;
        sOuterRight += shift;
      }

      sInnerRight += vInnerRight.mod;
      sOuterRight += vOuterRight.mod;
      sInnerLeft += vInnerLeft.mod;
      sOuterLeft += vOuterLeft.mod;
    }

    if (vInnerLeft.right() && !vOuterRight.right()) {
      vOuterRight.thread = vInnerLeft.right();
      vOuterRight.mod += sInnerLeft - sOuterRight;
    } else {
      if (vInnerRight.left() && !vOuterLeft.left()) {
        vOuterLeft.thread = vInnerRight.left();
        vOuterLeft.mod += sInnerRight - sOuterLeft;
      }
      default_ancestor = v;
    }
  }
  return default_ancestor;
};

const move_subtree = (leftV, v, shift) => {
  let subTrees = v.number - leftV.number;
  let average = shift / subTrees;
  v.shift += shift;
  v.change -= average;
  leftV.change += average;
  v.y += shift;
  v.mod += shift;
};

const execute_shifts = (v) => {
  let change = 0;
  let shift = 0;
  for (let i = v.children.length - 1; i >= 0; i--) {
    let node = v.children[i];
    node.y += shift;
    node.mod += shift;
    change += node.change;
    shift += node.shift + change;
  }
};

const ancestor = (vInnerLeft, v, default_ancestor) => {
  if (v.parent.children.includes(vInnerLeft.ancestor)) {
    return vInnerLeft.ancestor;
  } else {
    return default_ancestor;
  }
};

const second_walk = (v, m = 0, depth = 0, s = 0) => {
  v.y += m;
  v.x = depth * NODE_SPACE_X + s + NODE_SPACE_X;
  v.children.forEach((child) => {
    second_walk(child, m + v.mod, depth + 1, s + v.width);
  });
};

const third_walk = (tree) => {
  let selfMinY = tree.y - tree.height / 2;
  let selfMaxY = tree.y + tree.height / 2;
  if (tree.children.length > 0) {
    let minY = Infinity;
    let maxY = -Infinity;
    tree.children.forEach((child) => {
      third_walk(child);
      if (child.minY < minY) {
        minY = child.minY;
      }
      if (child.maxY > maxY) {
        maxY = child.maxY;
      }
    });

    tree.minY = selfMinY < minY ? selfMinY : minY;
    tree.maxY = selfMaxY > maxY ? selfMaxY : maxY;
  } else {
    tree.minY = selfMinY;
    tree.maxY = selfMaxY;
  }
  if (tree.left_brother()) {
    if (tree.minY < tree.left_brother().maxY + NODE_SPACE) {
      let o = tree.left_brother().maxY - tree.minY + NODE_SPACE;
      tree.offset = o;
      tree.y += o;
      tree.minY += o;
      tree.maxY += o;
    }
  }
};

const fourth_walk = (tree, o = 0) => {
  tree.y += o;
  tree.children.forEach((child) => {
    fourth_walk(child, o + tree.offset);
  });
  let len = tree.children.length;
  if (len <= 0) {
    return;
  }
  let mid = (tree.children[0].y + tree.children[len - 1].y) / 2;
  tree.y = mid;
};

const buchheim = (tree, rootX, rootY) => {
  tree.x = rootX;
  tree.y = rootY;
  let dt = firstwalk(tree);
  second_walk(dt);
  third_walk(dt);
  fourth_walk(dt);
  return dt;
};

index.vue

<template>
  <div id="treeBox">
    <div
      v-for="(node, idx) in nodes"
      :key="`node-${idx}`"
      class="node"
      :style="{
        width: `${node.width}px`,
        height: `${node.height}px`,
        left: `${node.x}px`,
        top: `${node.y}px`,
      }"
    >
      {{ node.name }}
    </div>
  </div>
</template>

<script>
import DrawTree from "./DrawTree";

const NODE_SPACE = 20;
const NODE_SPACE_X = 50;

export default {
  data() {
    return {
      nodes: [],
    };
  },

  mounted() {
    let treeData4 = {
      name: "Q",
      x: 100, // 根节点的初始x坐标
      y: 100, // 根节点的初始y坐标
      children: [
        {
          name: "G0",
          children: [],
        },
        {
          name: "G",
          children: [
            {
              name: "C",
              width: 50,
              height: 70,
              children: [
                {
                  name: "B",
                  width: 20,
                  height: 50,
                  children: [
                    {
                      name: "A",
                      children: [],
                    },
                  ],
                },
              ],
            },
            {
              name: "F",
              children: [
                {
                  name: "D",
                  children: [],
                },
                {
                  name: "E",
                  width: 150,
                  height: 60,
                  children: [],
                },
                {
                  name: "E2",
                  children: [],
                },
              ],
            },
          ],
        },
        {
          name: "H",
          children: [],
        },
        {
          name: "H2",
          width: 100,
          height: 40,
          children: [],
        },
        {
          name: "P",
          children: [
            {
              name: "I",
              width: 100,
              height: 50,
              children: [],
            },
            {
              name: "O",
              children: [
                {
                  name: "J",
                  children: [],
                },
                {
                  name: "K",
                  children: [],
                },
                {
                  name: "P2",
                  children: [],
                },
              ],
            },
          ],
        },
      ],
    };

    let tree = new DrawTree(treeData4, null, 0, 1, treeData4.x, treeData4.y);
    tree = buchheim(tree, treeData4.x, treeData4.y);
    this.nodes = this.flattenTree(tree);
  },

  methods: {
    flattenTree(tree) {
      let nodes = [];
      let traverse = (node) => {
        nodes.push(node);
        node.children.forEach((child) => traverse(child));
      };
      traverse(tree);
      return nodes;
    },

    addNode(parentName, newNode) {
      let parentNode = this.findNode(this.nodes, parentName);
      if (parentNode) {
        parentNode.children.push(new DrawTree(newNode, parentNode, parentNode.depth + 1, parentNode.children.length + 1));
        this.updateTree();
      }
    },

    removeNode(nodeName) {
      let node = this.findNode(this.nodes, nodeName);
      if (node && node.parent) {
        let index = node.parent.children.indexOf(node);
        if (index > -1) {
          node.parent.children.splice(index, 1);
          this.updateTree();
        }
      }
    },

    findNode(nodes, name) {
      for (let node of nodes) {
        if (node.name === name) {
          return node;
        }
        let found = this.findNode(node.children, name);
        if (found) {
          return found;
        }
      }
      return null;
    },

    updateTree() {
      let root = this.nodes[0];
      let tree = new DrawTree(root, null, 0, 1, root.x, root.y);
      tree = buchheim(tree, root.x, root.y);
      this.nodes = this.flattenTree(tree);
    },
  },
};
</script>

<style scoped>
#treeBox {
  position: relative;
}
.node {
  position: absolute;
  border: 1px solid red;
}
</style>
撰写回答
你尚未登录,登录后可以
  • 和开发者交流问题的细节
  • 关注并接收问题和回答的更新提醒
  • 参与内容的编辑和改进,让解决方法与时俱进
推荐问题