1. 介绍Vue和jsmind
Vue是一个渐进式的JavaScript框架,它易于上手,具有高效、灵活和可组合等特点。Vue常用于构建单页应用程序(single-page application, SPA),为数据驱动视图提供了一个类似于Model-View-ViewModel(MVVM)的架构。
而jsmind是一款开源的JavaScript思维导图库,基于HTML5、SVG、CSS和Canvas等技术。它可以在各种现代浏览器和移动设备中运行,并且支持多种语言、多种导图类型和多种主题皮肤。
2. 实现思维导图的自动布局
2.1 布局算法
在jsmind中,思维导图的布局算法是基于树形结构的,它的核心思想是通过一系列递归操作对树的布局进行调整。
具体地说,它会按照如下的步骤进行:
/**
* 对于每个节点
* 根据其子节点计算出其子树的大小和相对位置
* 根据其父节点计算出其所在位置
* 如果该节点有子节点,则对其子节点递归执行布局算法
*/
其中,计算子树大小和相对位置的核心代码如下:
/**
* 对于每个节点,计算其所有子节点所在矩形框的大小和相对位置
* 并返回这些信息组成的数组
*/
function computeSubtreeLayout(node) {
// 计算子节点在x轴方向上的偏移量
let x_offset = 0;
for (let i = 0; i < node.children.length; i++) {
let sub_node = node.children[i];
let sub_layout = computeSubtreeLayout(sub_node);
sub_node.layout = sub_layout;
x_offset += sub_layout.box.width + sub_layout.offset_x;
}
// 计算子节点在y轴方向上的偏移量
let y_offset = node.topic === null ? 0 : node.style.margin_top;
// 计算子树的高度和宽度
let subtree_height = 0;
let subtree_width = Math.max(...node.children.map(child => child.layout.box.width));
for (let i = 0; i < node.children.length; i++) {
let sub_node = node.children[i];
sub_node.layout.box.x = x_offset + i * subtree_width + i * sub_node.layout.offset_x;
sub_node.layout.box.y = y_offset + subtree_height;
subtree_height += sub_node.layout.box.height + sub_node.layout.offset_y;
}
// 返回计算结果
return {
box: {
x: 0,
y: 0,
width: subtree_width,
height: subtree_height === 0 ? node.style.margin_bottom : subtree_height - node.style.margin_bottom
},
offset_x: node.style.hspace,
offset_y: node.style.vspace
};
}
其中,每个节点的布局信息包括了节点所在矩形框的位置和大小、节点子树的大小和相对位置等,这些信息都可以通过计算得到。
2.2 自动布局实现
在Vue中,实现思维导图的自动布局主要是通过以下几个步骤完成:
在Vue的模板中,创建一个jsmind的canvas元素,并自定义属性用于存储jsmind的实例对象。
在Vue的JavaScript代码中,使用jsmind的API加载和初始化思维导图,然后获取思维导图的根节点,并将其传递给自定义的布局函数中进行布局调整。
在布局函数中,计算出每个节点的布局信息,并通过jsmind的API将其更新到视图上。
将自定义的布局函数注册到jsmind的实例对象中,在每次思维导图数据更新时自动调用。
下面是对应的代码实现:
Vue.component('jsmind', {
template: '<canvas ref="canvas"></canvas>',
mounted() {
// 获取canvas对象和jsmind对象
let canvas = this.$refs.canvas;
let jm = jsMind.show({
container: canvas,
theme: 'primary'
}, this.data);
// 定义自定义布局函数
function layout() {
let root = jm.get_root();
let layout = computeSubtreeLayout(root);
root.layout = layout;
jm.update_node(root);
// 定义递归函数
function layoutNode(node) {
for (let i = 0; i < node.children.length; i++) {
let sub_node = node.children[i];
sub_node.layout = sub_node.layout || computeSubtreeLayout(sub_node);
jm.update_node(sub_node);
layoutNode(sub_node);
}
}
layoutNode(root);
}
// 注册自定义布局函数
jm.set_node_layout(layout);
// 自动调用自定义布局函数
layout();
},
props: ['data']
})
其中,计算子树大小和相对位置的布局函数computeSubtreeLayout()已经在前面进行了解释。
3. 实现思维导图的智能调整
3.1 调整算法
在实际使用中,用户往往会对思维导图进行修改和扩展,这时需要在保证布局美观的前提下对思维导图进行智能调整。
具体地说,它会按照如下的步骤进行:
/**
* 对于新增节点
* 根据其相邻节点计算出其所在位置
* 如果该节点有子节点,则对其子节点递归执行布局算法
*/
其中,计算相邻节点位置的核心代码如下:
/**
* 对于新增节点,以其相邻节点的位置为基准进行调整
*/
function adjustLayout(node) {
// 获取其相邻节点
let siblings = getSiblingH(node);
if (siblings.length === 0) return;
// 获取相邻节点所在列的中心位置
let col_x = siblings.reduce((a, b) => a + (b.layout.box.x + b.layout.box.width) / 2, 0) / siblings.length;
// 获取其所在列的上下边界
let col_y_min = Math.min(...siblings.map(sibling => sibling.layout.box.y));
let col_y_max = Math.max(...siblings.map(sibling => sibling.layout.box.y + sibling.layout.box.height));
// 更新节点位置
if (node.layout.box.y >= col_y_max) {
node.layout.box.x = col_x - node.layout.box.width / 2;
node.layout.box.y = col_y_max + node.layout.offset_y;
} else if (node.layout.box.y + node.layout.box.height <= col_y_min) {
node.layout.box.x = col_x - node.layout.box.width / 2;
node.layout.box.y = col_y_min - node.layout.box.height - node.layout.offset_y;
}
// 递归更新子节点位置
for (let i = 0; i < node.children.length; i++) {
let sub_node = node.children[i];
adjustLayout(sub_node);
}
}
其中,获取节点相邻节点的函数getSiblingH()代码如下:
/**
* 获取水平上相邻的节点
* 包括左边界隔开的节点、右边界隔开的节点和相邻的节点
*/
function getSiblingH(node) {
let siblings = [];
let parent = node.parent;
let direction = node.direction;
if (!parent) return siblings;
for (let i = 0; i < parent.children.length; i++) {
let sibling = parent.children[i];
if (sibling === node) continue;
if (sibling.direction === direction) {
siblings.push(sibling);
} else if (direction === 'right' && sibling.layout.box.x >= node.layout.box.x + node.layout.box.width + node.layout.offset_x) {
siblings.push(sibling);
} else if (direction === 'left' && sibling.layout.box.x + sibling.layout.box.width + sibling.layout.offset_x <= node.layout.box.x) {
siblings.push(sibling);
}
}
return siblings;
}
其中,相邻节点可以是在同一列中的节点,也可以是在相邻列中的节点。
3.2 智能调整实现
在Vue中,实现思维导图的智能调整主要是通过以下几个步骤完成:
在Vue的JavaScript代码中,定义自定义的智能调整函数 adjustLayout()。
将自定义的智能调整函数注册到jsmind的实例对象中,在每次思维导图数据更新时自动调用。
为用户上下拖动节点或者手动添加和删除节点时,调用自定义的智能调整函数,让思维导图重新进行布局和调整。
下面是对应的代码实现:
Vue.component('jsmind', {
template: '<canvas ref="canvas"></canvas>',
mounted() {
// 获取canvas对象和jsmind对象
let canvas = this.$refs.canvas;
let jm = jsMind.show({
container: canvas,
theme: 'primary'
}, this.data);
// 定义自定义布局函数
function layout() {
let root = jm.get_root();
let layout = computeSubtreeLayout(root);
root.layout = layout;
jm.update_node(root);
// 定义递归布局函数
function layoutNode(node) {
for (let i = 0; i < node.children.length; i++) {
let sub_node = node.children[i];
sub_node.layout = sub_node.layout || computeSubtreeLayout(sub_node);
jm.update_node(sub_node);
layoutNode(sub_node);
}
}
layoutNode(root);
}
// 注册自定义布局函数
jm.set_node_layout(layout);
// 定义自定义调整函数
function adjust() {
let all_nodes = jm.get_node();
for (let i = 0; i < all_nodes.length; i++) {
let node = all_nodes[i];
adjustLayout(node);
jm.update_node(node);
}
}
// 注册自定义调整函数
jm.set_node_adjust(function() {
adjust();
});
// 自动调用自定义布局函数和自定义调整函数
layout();
adjust();
},
props: ['data']
})
其中,为用户上下拖动节点或者手动添加和删除节点的调整代码如下:
let jm = ...;
// 创建jsmind实例
// ...
// 添加节点
jm.add_node(parent_node, 'right', newNodeData);
// 调用自定义的调整函数
jm.call_node_adjust();
需要注意的是,为了避免频繁调用布局和调整函数,可以在Vue组件的watch选项中监听数据变化,然后在延迟一定的时间后再调用布局和调整函数。
4. 总结
通过以上的介绍可以发现,Vue和jsmind可以很好地配合使用实现思维导图的自动布局和智能调整,同时还可以根据具体的需要进行定制和扩展。这样一来,我们就可以在开发思维导图相关应用程序的时候大大缩短开发周期和成本,并且提高产品的用户体验。