diff --git a/README.md b/README.md index 001334b2..29f08922 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ # 特性 - [x] 插件化架构,除核心功能外,其他功能作为插件提供,按需使用,减小打包体积 -- [x] 支持逻辑结构图、思维导图、组织结构图、目录组织图四种结构 +- [x] 支持逻辑结构图、思维导图、组织结构图、目录组织图、时间轴、鱼骨图六种结构 - [x] 内置多种主题,允许高度自定义样式,支持注册新主题 - [x] 支持快捷键 - [x] 节点内容支持图片、图标、超链接、备注、标签、概要 diff --git a/index.html b/index.html index dbc59c96..17221fbc 100644 --- a/index.html +++ b/index.html @@ -1 +1 @@ -一个简单的web思维导图实现
\ No newline at end of file +一个简单的web思维导图实现
\ No newline at end of file diff --git a/simple-mind-map/index.js b/simple-mind-map/index.js index bb99095c..7ebbc2e4 100644 --- a/simple-mind-map/index.js +++ b/simple-mind-map/index.js @@ -18,6 +18,8 @@ const defaultOpt = { readonly: false, // 布局 layout: CONSTANTS.LAYOUT.LOGICAL_STRUCTURE, + // 如果结构为鱼骨图,那么可以通过该选项控制倾斜角度 + fishboneDeg: 45, // 主题 theme: 'default', // 内置主题:default(默认主题) // 主题配置,会和所选择的主题进行合并 diff --git a/simple-mind-map/src/Node.js b/simple-mind-map/src/Node.js index fa208f11..528f1635 100644 --- a/simple-mind-map/src/Node.js +++ b/simple-mind-map/src/Node.js @@ -8,7 +8,6 @@ import nodeCommandWrapsMethods from './utils/nodeCommandWraps' import nodeCreateContentsMethods from './utils/nodeCreateContents' import { CONSTANTS } from './utils/constant' - // 节点类 class Node { // 构造函数 @@ -58,7 +57,7 @@ class Node { this.children = opt.children || [] // 节点内容的容器 this.group = null - this.shapeNode = null// 节点形状节点 + this.shapeNode = null // 节点形状节点 // 节点内容对象 this._imgData = null this._iconData = null @@ -95,19 +94,19 @@ class Node { // 是否需要重新layout this.needLayout = false // 概要相关方法 - Object.keys(nodeGeneralizationMethods).forEach((item) => { + Object.keys(nodeGeneralizationMethods).forEach(item => { this[item] = nodeGeneralizationMethods[item].bind(this) }) // 展开收起按钮相关方法 - Object.keys(nodeExpandBtnMethods).forEach((item) => { + Object.keys(nodeExpandBtnMethods).forEach(item => { this[item] = nodeExpandBtnMethods[item].bind(this) }) // 命令的相关方法 - Object.keys(nodeCommandWrapsMethods).forEach((item) => { + Object.keys(nodeCommandWrapsMethods).forEach(item => { this[item] = nodeCommandWrapsMethods[item].bind(this) }) // 创建节点内容的相关方法 - Object.keys(nodeCreateContentsMethods).forEach((item) => { + Object.keys(nodeCreateContentsMethods).forEach(item => { this[item] = nodeCreateContentsMethods[item].bind(this) }) // 初始化 @@ -339,6 +338,9 @@ class Node { this.active(e) }) this.group.on('mousedown', e => { + if (this.isRoot && e.which === 3) { + e.stopPropagation() + } if (!this.isRoot) { e.stopPropagation() } @@ -346,9 +348,16 @@ class Node { if (e.ctrlKey) { this.isMultipleChoice = true let isActive = this.nodeData.data.isActive - if (!isActive) this.mindMap.emit('before_node_active', this, this.renderer.activeNodeList) + if (!isActive) + this.mindMap.emit( + 'before_node_active', + this, + this.renderer.activeNodeList + ) this.mindMap.execCommand('SET_NODE_ACTIVE', this, !isActive) - this.mindMap.renderer[isActive ? 'removeActiveNode' : 'addActiveNode'](this) + this.mindMap.renderer[isActive ? 'removeActiveNode' : 'addActiveNode']( + this + ) this.mindMap.emit( 'node_active', isActive ? null : this, @@ -414,7 +423,8 @@ class Node { if (!this.group) { return } - let { enableNodeTransitionMove, nodeTransitionMoveDuration } = this.mindMap.opt + let { enableNodeTransitionMove, nodeTransitionMoveDuration } = + this.mindMap.opt // 需要移除展开收缩按钮 if (this._expandBtn && this.nodeData.children.length <= 0) { this.removeExpandBtn() @@ -429,15 +439,9 @@ class Node { if (!isLayout && enableNodeTransitionMove) { this.group .animate(nodeTransitionMoveDuration) - .translate( - this.left - t.translateX, - this.top - t.translateY - ) + .translate(this.left - t.translateX, this.top - t.translateY) } else { - this.group.translate( - this.left - t.translateX, - this.top - t.translateY - ) + this.group.translate(this.left - t.translateX, this.top - t.translateY) } } @@ -453,12 +457,15 @@ class Node { updateNodeShape() { if (!this.shapeNode) return const shape = this.getShape() - this.style[shape === CONSTANTS.SHAPE.RECTANGLE ? 'rect' : 'shape'](this.shapeNode) + this.style[shape === CONSTANTS.SHAPE.RECTANGLE ? 'rect' : 'shape']( + this.shapeNode + ) } // 递归渲染 render(callback = () => {}) { - let { enableNodeTransitionMove, nodeTransitionMoveDuration } = this.mindMap.opt + let { enableNodeTransitionMove, nodeTransitionMoveDuration } = + this.mindMap.opt // 节点 // 重新渲染连线 this.renderLine() @@ -553,7 +560,10 @@ class Node { this.hideGeneralization() if (this.parent) { let index = this.parent.children.indexOf(this) - this.parent._lines[index].hide() + this.parent._lines[index] && this.parent._lines[index].hide() + this._lines.forEach(item => { + item.hide() + }) } // 子节点 if (this.children && this.children.length) { @@ -577,6 +587,9 @@ class Node { if (this.parent) { let index = this.parent.children.indexOf(this) this.parent._lines[index] && this.parent._lines[index].show() + this._lines.forEach(item => { + item.show() + }) } // 子节点 if (this.children && this.children.length) { @@ -596,6 +609,13 @@ class Node { return } let childrenLen = this.nodeData.children.length + // 切换为鱼骨结构时,清空根节点和二级节点的连线 + if ( + this.mindMap.opt.layout === CONSTANTS.LAYOUT.FISHBONE && + (this.isRoot || this.layerIndex === 1) + ) { + childrenLen = 0 + } if (childrenLen > this._lines.length) { // 创建缺少的线 new Array(childrenLen - this._lines.length).fill(0).forEach(() => { diff --git a/simple-mind-map/src/Render.js b/simple-mind-map/src/Render.js index 28b9e83f..aac78ecd 100644 --- a/simple-mind-map/src/Render.js +++ b/simple-mind-map/src/Render.js @@ -3,6 +3,8 @@ import LogicalStructure from './layouts/LogicalStructure' import MindMap from './layouts/MindMap' import CatalogOrganization from './layouts/CatalogOrganization' import OrganizationStructure from './layouts/OrganizationStructure' +import Timeline from './layouts/Timeline' +import Fishbone from './layouts/Fishbone' import TextEdit from './TextEdit' import { copyNodeTree, simpleDeepClone, walk } from './utils' import { shapeList } from './Shape' @@ -18,7 +20,13 @@ const layouts = { // 目录组织图 [CONSTANTS.LAYOUT.CATALOG_ORGANIZATION]: CatalogOrganization, // 组织结构图 - [CONSTANTS.LAYOUT.ORGANIZATION_STRUCTURE]: OrganizationStructure + [CONSTANTS.LAYOUT.ORGANIZATION_STRUCTURE]: OrganizationStructure, + // 时间轴 + [CONSTANTS.LAYOUT.TIMELINE]: Timeline, + // 时间轴2 + [CONSTANTS.LAYOUT.TIMELINE2]: Timeline, + // 鱼骨图 + [CONSTANTS.LAYOUT.FISHBONE]: Fishbone, } // 渲染 @@ -65,7 +73,7 @@ class Render { layouts[this.mindMap.opt.layout] ? layouts[this.mindMap.opt.layout] : layouts[CONSTANTS.LAYOUT.LOGICAL_STRUCTURE] - )(this) + )(this, this.mindMap.opt.layout) } // 绑定事件 diff --git a/simple-mind-map/src/layouts/Base.js b/simple-mind-map/src/layouts/Base.js index dd6e1679..56213d58 100644 --- a/simple-mind-map/src/layouts/Base.js +++ b/simple-mind-map/src/layouts/Base.js @@ -157,6 +157,37 @@ class Base { }) } + // 更新子节点多个属性 + updateChildrenPro(children, props) { + children.forEach(item => { + Object.keys(props).forEach((prop) => { + item[prop] += props[prop] + }) + if (item.children && item.children.length && !item.hasCustomPosition()) { + // 适配自定义位置 + this.updateChildrenPro(item.children, props) + } + }) + } + + // 递归计算节点的宽度 + getNodeAreaWidth(node) { + let widthArr = [] + let loop = (node, width) => { + if (node.children.length) { + width += node.width / 2 + node.children.forEach(item => { + loop(item, width) + }) + } else { + width += node.width + widthArr.push(width) + } + } + loop(node, 0) + return Math.max(...widthArr) + } + // 二次贝塞尔曲线 quadraticCurvePath(x1, y1, x2, y2) { let cx = x1 + (x2 - x1) * 0.2 @@ -266,6 +297,11 @@ class Base { generalizationNodeMargin } } + + // 获取节点实际存在几个子节点 + getNodeActChildrenLength(node) { + return node.nodeData.children && node.nodeData.children.length + } } export default Base diff --git a/simple-mind-map/src/layouts/CatalogOrganization.js b/simple-mind-map/src/layouts/CatalogOrganization.js index 4cf06569..6089a04d 100644 --- a/simple-mind-map/src/layouts/CatalogOrganization.js +++ b/simple-mind-map/src/layouts/CatalogOrganization.js @@ -87,11 +87,11 @@ class CatalogOrganization extends Base { totalLeft += cur.width + marginX }) } else { - let totalTop = node.top + node.height + marginY + node.expandBtnSize + let totalTop = node.top + node.height + marginY + (this.getNodeActChildrenLength(node) > 0 ? node.expandBtnSize : 0) node.children.forEach(cur => { cur.left = node.left + node.width * 0.5 cur.top = totalTop - totalTop += cur.height + marginY + node.expandBtnSize + totalTop += cur.height + marginY + (this.getNodeActChildrenLength(cur) > 0 ? cur.expandBtnSize : 0) }) } } @@ -115,7 +115,7 @@ class CatalogOrganization extends Base { let areaWidth = this.getNodeAreaWidth(node) let difference = areaWidth - node.width if (difference > 0) { - this.updateBrothersLeft(node, difference / 2) + this.updateBrothersLeft(node, difference) } } // 调整top @@ -124,36 +124,24 @@ class CatalogOrganization extends Base { let marginY = this.getMarginY(layerIndex + 1) let totalHeight = node.children.reduce((h, item) => { - return h + item.height + return h + item.height + (this.getNodeActChildrenLength(item) > 0 ? item.expandBtnSize : 0) }, 0) + - (len + 1) * marginY + - len * node.expandBtnSize + len * marginY this.updateBrothersTop(node, totalHeight) } }, - null, + (node, parent, isRoot) => { + if (isRoot) { + let { right, left } = this.getNodeBoundaries(node, 'h') + let childrenWidth = right - left + let offset = (node.left - left) - (childrenWidth - node.width) / 2 + this.updateChildren(node.children, 'left', offset) + } + }, true ) } - // 递归计算节点的宽度 - getNodeAreaWidth(node) { - let widthArr = [] - let loop = (node, width) => { - if (node.children.length) { - width += node.width / 2 - node.children.forEach(item => { - loop(item, width) - }) - } else { - width += node.width - widthArr.push(width) - } - } - loop(node, 0) - return Math.max(...widthArr) - } - // 调整兄弟节点的left updateBrothersLeft(node, addWidth) { if (node.parent) { @@ -161,38 +149,15 @@ class CatalogOrganization extends Base { let index = childrenList.findIndex(item => { return item === node }) - // 存在大于一个节点时,第一个或最后一个节点自身也需要移动,否则两边不对称 - if ( - (index === 0 || index === childrenList.length - 1) && - childrenList.length > 1 - ) { - let _offset = index === 0 ? -addWidth : addWidth - node.left += _offset - if ( - node.children && - node.children.length && - !node.hasCustomPosition() - ) { - this.updateChildren(node.children, 'left', _offset) - } - } childrenList.forEach((item, _index) => { - if (item.hasCustomPosition()) { + if (item.hasCustomPosition() || _index <= index) { // 适配自定义位置 return } - let _offset = 0 - if (_index < index) { - // 左边的节点往左移 - _offset = -addWidth - } else if (_index > index) { - // 右边的节点往右移 - _offset = addWidth - } - item.left += _offset + item.left += addWidth // 同步更新子节点的位置 if (item.children && item.children.length) { - this.updateChildren(item.children, 'left', _offset) + this.updateChildren(item.children, 'left', addWidth) } }) // 更新父节点的位置 diff --git a/simple-mind-map/src/layouts/Fishbone.js b/simple-mind-map/src/layouts/Fishbone.js new file mode 100644 index 00000000..a5ca82c4 --- /dev/null +++ b/simple-mind-map/src/layouts/Fishbone.js @@ -0,0 +1,373 @@ +import Base from './Base' +import { walk, asyncRun, degToRad } from '../utils' +import { CONSTANTS } from '../utils/constant' +import utils from './fishboneUtils' + +// 鱼骨图 +class Fishbone extends Base { + // 构造函数 + constructor(opt = {}) { + super(opt) + } + + // 布局 + doLayout(callback) { + let task = [ + () => { + this.computedBaseValue() + }, + () => { + this.computedLeftTopValue() + }, + () => { + this.adjustLeftTopValue() + }, + () => { + callback(this.root) + } + ] + asyncRun(task) + } + + // 遍历数据创建节点、计算根节点的位置,计算根节点的子节点的top值 + computedBaseValue() { + walk( + this.renderer.renderTree, + null, + (node, parent, isRoot, layerIndex, index) => { + // 创建节点 + let newNode = this.createNode(node, parent, isRoot, layerIndex) + // 根节点定位在画布中心位置 + if (isRoot) { + this.setNodeCenter(newNode) + } else { + // 非根节点 + // 三级及以下节点以上级方向为准 + if (parent._node.dir) { + newNode.dir = parent._node.dir + } else { + // 节点生长方向 + newNode.dir = + index % 2 === 0 + ? CONSTANTS.TIMELINE_DIR.TOP + : CONSTANTS.TIMELINE_DIR.BOTTOM + } + // 计算二级节点的top值 + if (parent._node.isRoot) { + if (this.checkIsTop(newNode)) { + newNode.top = parent._node.top - newNode.height + } else { + newNode.top = parent._node.top + parent._node.height + } + } + } + if (!node.data.expand) { + return true + } + }, + null, + true, + 0 + ) + } + + // 遍历节点树计算节点的left、top + computedLeftTopValue() { + walk( + this.root, + null, + (node, parent, isRoot, layerIndex) => { + if (node.isRoot) { + let topTotalLeft = node.left + node.width + node.height + let bottomTotalLeft = node.left + node.width + node.height + node.children.forEach(item => { + if (this.checkIsTop(item)) { + item.left = topTotalLeft + topTotalLeft += item.width + } else { + item.left = bottomTotalLeft + 20 + bottomTotalLeft += item.width + } + }) + } + let params = { layerIndex, node, ctx: this } + if (this.checkIsTop(node)) { + utils.top.computedLeftTopValue(params) + } else { + utils.bottom.computedLeftTopValue(params) + } + }, + null, + true + ) + } + + // 调整节点left、top + adjustLeftTopValue() { + walk( + this.root, + null, + (node, parent, isRoot, layerIndex) => { + if (!node.nodeData.data.expand) { + return + } + let params = { node, parent, layerIndex, ctx: this } + if (this.checkIsTop(node)) { + utils.top.adjustLeftTopValueBefore(params) + } else { + utils.bottom.adjustLeftTopValueBefore(params) + } + }, + (node, parent) => { + let params = { parent, node, ctx: this } + if (this.checkIsTop(node)) { + utils.top.adjustLeftTopValueAfter(params) + } else { + utils.bottom.adjustLeftTopValueAfter(params) + } + // 调整二级节点的子节点的left值 + if (node.isRoot) { + let topTotalLeft = 0 + let bottomTotalLeft = 0 + node.children.forEach(item => { + if (this.checkIsTop(item)) { + item.left += topTotalLeft + this.updateChildren(item.children, 'left', topTotalLeft) + let { left, right } = this.getNodeBoundaries(item, 'h') + topTotalLeft += right - left + } else { + item.left += bottomTotalLeft + this.updateChildren(item.children, 'left', bottomTotalLeft) + let { left, right } = this.getNodeBoundaries(item, 'h') + bottomTotalLeft += right - left + } + }) + } + }, + true + ) + } + + // 递归计算节点的宽度 + getNodeAreaHeight(node) { + let totalHeight = 0 + let loop = node => { + totalHeight += + node.height + + (this.getNodeActChildrenLength(node) > 0 ? node.expandBtnSize : 0) + if (node.children.length) { + node.children.forEach(item => { + loop(item) + }) + } + } + loop(node) + return totalHeight + } + + // 调整兄弟节点的left + updateBrothersLeft(node) { + let childrenList = node.children + let totalAddWidth = 0 + childrenList.forEach(item => { + item.left += totalAddWidth + if (item.children && item.children.length) { + this.updateChildren(item.children, 'left', totalAddWidth) + } + let { left, right } = this.getNodeBoundaries(item, 'h') + let areaWidth = right - left + let difference = areaWidth - item.width + if (difference > 0) { + totalAddWidth += difference + } + }) + } + + // 调整兄弟节点的top + updateBrothersTop(node, addHeight) { + if (node.parent && !node.parent.isRoot) { + let childrenList = node.parent.children + let index = childrenList.findIndex(item => { + return item === node + }) + childrenList.forEach((item, _index) => { + if (item.hasCustomPosition()) { + // 适配自定义位置 + return + } + let _offset = 0 + // 下面的节点往下移 + if (_index > index) { + _offset = addHeight + } + item.top += _offset + // 同步更新子节点的位置 + if (item.children && item.children.length) { + this.updateChildren(item.children, 'top', _offset) + } + }) + // 更新父节点的位置 + if (this.checkIsTop(node)) { + this.updateBrothersTop(node.parent, addHeight) + } else { + this.updateBrothersTop( + node.parent, + node.layerIndex === 3 ? 0 : addHeight + ) + } + } + } + + // 检查节点是否是上方节点 + checkIsTop(node) { + return node.dir === CONSTANTS.TIMELINE_DIR.TOP + } + + // 绘制连线,连接该节点到其子节点 + renderLine(node, lines, style) { + if (node.layerIndex !== 1 && node.children.length <= 0) { + return [] + } + let { top, height, expandBtnSize } = node + let len = node.children.length + if (node.isRoot) { + // 当前节点是根节点 + // 根节点的子节点是和根节点同一水平线排列 + let maxx = -Infinity + node.children.forEach((item) => { + if (item.left > maxx) { + maxx = item.left + } + // 水平线段到二级节点的连线 + let nodeLineX = item.left + item.width * 0.3 + let offset = item.height + node.height / 2 + let offsetX = offset / Math.tan(degToRad(this.mindMap.opt.fishboneDeg)) + let line = this.draw.path() + if (this.checkIsTop(item)) { + line.plot( + `M ${nodeLineX - offsetX},${item.top + offset} L ${nodeLineX},${ + item.top + }` + ) + } else { + line.plot( + `M ${nodeLineX - offsetX},${ + item.top + item.height - offset + } L ${nodeLineX},${item.top + item.height}` + ) + } + node.style.line(line) + node._lines.push(line) + style && style(line, node) + }) + // 从根节点出发的水平线 + let nodeHalfTop = node.top + node.height / 2 + let offset = node.height / 2 + let line = this.draw.path() + line.plot( + `M ${node.left + node.width},${nodeHalfTop} L ${ + maxx - offset / Math.tan(degToRad(this.mindMap.opt.fishboneDeg)) + },${nodeHalfTop}` + ) + node.style.line(line) + node._lines.push(line) + style && style(line, node) + } else { + // 当前节点为非根节点 + let maxy = -Infinity + let miny = Infinity + let maxx = -Infinity + let x = node.left + node.width * 0.3 + node.children.forEach((item, index) => { + if (item.left > maxx) { + maxx = item.left + } + let y = item.top + item.height / 2 + if (y > maxy) { + maxy = y + } + if (y < miny) { + miny = y + } + // 水平线 + if (node.layerIndex > 1) { + let path = `M ${x},${y} L ${item.left},${y}` + lines[index].plot(path) + style && style(lines[index], item) + } + }) + // 斜线 + if (len >= 0) { + let line = this.draw.path() + expandBtnSize = len > 0 ? expandBtnSize : 0 + let lineLength = maxx - node.left - node.width * 0.3 + lineLength = Math.max(lineLength, 0) + let params = { + node, + line, + top, + x, + lineLength, + height, + expandBtnSize, + maxy, + miny, + ctx: this + } + if (this.checkIsTop(node)) { + utils.top.renderLine(params) + } else { + utils.bottom.renderLine(params) + } + node.style.line(line) + node._lines.push(line) + style && style(line, node) + } + } + } + + // 渲染按钮 + renderExpandBtn(node, btn) { + let { width, height, expandBtnSize, isRoot } = node + if (!isRoot) { + let { translateX, translateY } = btn.transform() + let params = { + node, + btn, + expandBtnSize, + translateX, + translateY, + width, + height + } + if (this.checkIsTop(node)) { + utils.top.renderExpandBtn(params) + } else { + utils.bottom.renderExpandBtn(params) + } + } + } + + // 创建概要节点 + renderGeneralization(node, gLine, gNode) { + let { + top, + bottom, + right, + generalizationLineMargin, + generalizationNodeMargin + } = this.getNodeBoundaries(node, 'h') + let x1 = right + generalizationLineMargin + let y1 = top + let x2 = right + generalizationLineMargin + let y2 = bottom + let cx = x1 + 20 + let cy = y1 + (y2 - y1) / 2 + let path = `M ${x1},${y1} Q ${cx},${cy} ${x2},${y2}` + gLine.plot(path) + gNode.left = right + generalizationNodeMargin + gNode.top = top + (bottom - top - gNode.height) / 2 + } +} + +export default Fishbone diff --git a/simple-mind-map/src/layouts/FishboneBottom.js b/simple-mind-map/src/layouts/FishboneBottom.js new file mode 100644 index 00000000..25adc205 --- /dev/null +++ b/simple-mind-map/src/layouts/FishboneBottom.js @@ -0,0 +1,369 @@ +import Base from './Base' +import { walk, asyncRun } from '../utils' +import { CONSTANTS } from '../utils/constant' + +const degToRad = deg => { + return (Math.PI / 180) * deg +} + +// 下方鱼骨图 +class Fishbone extends Base { + // 构造函数 + constructor(opt = {}) { + super(opt) + } + + // 布局 + doLayout(callback) { + let task = [ + () => { + this.computedBaseValue() + }, + () => { + this.computedLeftTopValue() + }, + () => { + this.adjustLeftTopValue() + }, + () => { + callback(this.root) + } + ] + asyncRun(task) + } + + // 遍历数据创建节点、计算根节点的位置,计算根节点的子节点的top值 + computedBaseValue() { + walk( + this.renderer.renderTree, + null, + (node, parent, isRoot, layerIndex, index) => { + // 创建节点 + let newNode = this.createNode(node, parent, isRoot, layerIndex) + // 根节点定位在画布中心位置 + if (isRoot) { + this.setNodeCenter(newNode) + } else { + // 非根节点 + // 三级及以下节点以上级方向为准 + if (parent._node.dir) { + newNode.dir = parent._node.dir + } else { + // 节点生长方向 + newNode.dir = + index % 2 === 0 + ? CONSTANTS.TIMELINE_DIR.TOP + : CONSTANTS.TIMELINE_DIR.BOTTOM + } + // 计算二级节点的top值 + if (parent._node.isRoot) { + newNode.top = parent._node.top + parent._node.height + } + } + if (!node.data.expand) { + return true + } + }, + null, + true, + 0 + ) + } + + // 遍历节点树计算节点的left、top + computedLeftTopValue() { + walk( + this.root, + null, + (node, parent, isRoot, layerIndex, index) => { + if (node.isRoot) { + let totalLeft = node.left + node.width + node.children.forEach(item => { + item.left = totalLeft + totalLeft += item.width + }) + } + if (layerIndex === 1 && node.children) { + // 遍历二级节点的子节点 + let startLeft = node.left + node.width * 0.5 + let totalTop = + node.top + + node.height + + (this.getNodeActChildrenLength(node) > 0 ? node.expandBtnSize : 0) + + node.children.forEach(item => { + item.left = startLeft + item.top = + totalTop + + (this.getNodeActChildrenLength(item) > 0 ? item.expandBtnSize : 0) + totalTop += + item.height + + (this.getNodeActChildrenLength(item) > 0 ? item.expandBtnSize : 0) + }) + } + if (layerIndex > 1 && node.children) { + // 遍历三级及以下节点的子节点 + let startLeft = node.left + node.width * 0.5 + let totalTop = + node.top - + (this.getNodeActChildrenLength(node) > 0 ? node.expandBtnSize : 0) + node.children.forEach(item => { + item.left = startLeft + item.top = totalTop - item.height + totalTop -= + item.height + + (this.getNodeActChildrenLength(item) > 0 ? item.expandBtnSize : 0) + }) + } + }, + null, + true + ) + } + + // 调整节点left、top + adjustLeftTopValue() { + walk( + this.root, + null, + (node, parent, isRoot, layerIndex) => { + if (!node.nodeData.data.expand) { + return + } + // 调整top + let len = node.children.length + // 调整三级节点的top + // if (layerIndex === 2 && len > 0) { + // let totalHeight = node.children.reduce((h, item) => { + // return h + item.height + // }, 0) + // this.updateBrothersTop(node, totalHeight) + // } + if (layerIndex > 2 && len > 0) { + let totalHeight = node.children.reduce((h, item) => { + return ( + h + + item.height + + (this.getNodeActChildrenLength(item) > 0 ? item.expandBtnSize : 0) + ) + }, 0) + this.updateBrothersTop(node, -totalHeight) + } + }, + (node, parent) => { + // 将二级节点的子节点移到上方 + if (parent && parent.isRoot) { + // 遍历二级节点的子节点 + let totalHeight = 0 + let totalHeight2 = 0 + node.children.forEach(item => { + // 调整top + let hasChildren = this.getNodeActChildrenLength(item) > 0 + let nodeTotalHeight = this.getNodeAreaHeight(item) + let offset = + hasChildren > 0 + ? nodeTotalHeight - + item.height - + (hasChildren ? item.expandBtnSize : 0) + : 0 + let _top = totalHeight + offset + item.top += _top + // 调整left + let offsetLeft = + (totalHeight2 + nodeTotalHeight) / Math.tan(degToRad(this.mindMap.opt.fishboneDeg)) + item.left += offsetLeft + totalHeight += offset + totalHeight2 += nodeTotalHeight + // 同步更新后代节点 + this.updateChildrenPro(item.children, { + top: _top, + left: offsetLeft + }) + }) + } + // 调整二级节点的子节点的left值 + if (node.isRoot) { + let totalLeft = 0 + node.children.forEach(item => { + item.left += totalLeft + this.updateChildren(item.children, 'left', totalLeft) + let { left, right } = this.getNodeBoundaries(item, 'h') + totalLeft += right - left + }) + } + }, + true + ) + } + + // 递归计算节点的宽度 + getNodeAreaHeight(node) { + let totalHeight = 0 + let loop = node => { + totalHeight += + node.height + + (this.getNodeActChildrenLength(node) > 0 ? node.expandBtnSize : 0) + if (node.children.length) { + node.children.forEach(item => { + loop(item) + }) + } + } + loop(node) + return totalHeight + } + + // 调整兄弟节点的left + updateBrothersLeft(node) { + let childrenList = node.children + let totalAddWidth = 0 + childrenList.forEach(item => { + item.left += totalAddWidth + if (item.children && item.children.length) { + this.updateChildren(item.children, 'left', totalAddWidth) + } + // let areaWidth = this.getNodeAreaWidth(item) + let { left, right } = this.getNodeBoundaries(item, 'h') + let areaWidth = right - left + let difference = areaWidth - item.width + if (difference > 0) { + totalAddWidth += difference + } + }) + } + + // 调整兄弟节点的top + updateBrothersTop(node, addHeight) { + if (node.parent && !node.parent.isRoot) { + let childrenList = node.parent.children + let index = childrenList.findIndex(item => { + return item === node + }) + childrenList.forEach((item, _index) => { + if (item.hasCustomPosition()) { + // 适配自定义位置 + return + } + let _offset = 0 + // 下面的节点往下移 + if (_index > index) { + _offset = addHeight + } + item.top += _offset + // 同步更新子节点的位置 + if (item.children && item.children.length) { + this.updateChildren(item.children, 'top', _offset) + } + }) + // 更新父节点的位置 + this.updateBrothersTop(node.parent, node.layerIndex === 3 ? 0 : addHeight) + } + } + + // 绘制连线,连接该节点到其子节点 + renderLine(node, lines, style) { + if (node.children.length <= 0) { + return [] + } + let { left, top, width, height, expandBtnSize } = node + let len = node.children.length + if (node.isRoot) { + // 当前节点是根节点 + let prevBother = node + // 根节点的子节点是和根节点同一水平线排列 + node.children.forEach((item, index) => { + let x1 = prevBother.left + prevBother.width + let x2 = item.left + let y = node.top + node.height / 2 + let path = `M ${x1},${y} L ${x2},${y}` + lines[index].plot(path) + style && style(lines[index], item) + prevBother = item + }) + } else { + // 当前节点为非根节点 + let maxy = -Infinity + let miny = Infinity + let maxx = -Infinity + let x = node.left + node.width * 0.3 + node.children.forEach((item, index) => { + if (item.left > maxx) { + maxx = item.left + } + let y = item.top + item.height / 2 + if (y > maxy) { + maxy = y + } + if (y < miny) { + miny = y + } + // 水平线 + if (node.layerIndex > 1) { + let path = `M ${x},${y} L ${item.left},${y}` + lines[index].plot(path) + style && style(lines[index], item) + } + }) + // 竖线 + if (len > 0) { + let line = this.draw.path() + expandBtnSize = len > 0 ? expandBtnSize : 0 + let lineLength = maxx - node.left - node.width * 0.3 + if (node.parent && node.parent.isRoot) { + line.plot( + `M ${x},${top + height} L ${x + lineLength},${ + top + height + Math.tan(degToRad(this.mindMap.opt.fishboneDeg)) * lineLength + }` + ) + } else { + line.plot(`M ${x},${top} L ${x},${miny}`) + } + node.style.line(line) + node._lines.push(line) + style && style(line, node) + } + } + } + + // 渲染按钮 + renderExpandBtn(node, btn) { + let { width, height, expandBtnSize, isRoot } = node + if (!isRoot) { + let { translateX, translateY } = btn.transform() + if (node.parent && node.parent.isRoot) { + btn.translate( + width * 0.3 - expandBtnSize / 2 - translateX, + height + expandBtnSize / 2 - translateY + ) + } else { + btn.translate( + width * 0.3 - expandBtnSize / 2 - translateX, + -expandBtnSize / 2 - translateY + ) + } + } + } + + // 创建概要节点 + renderGeneralization(node, gLine, gNode) { + let { + top, + bottom, + right, + generalizationLineMargin, + generalizationNodeMargin + } = this.getNodeBoundaries(node, 'h') + let x1 = right + generalizationLineMargin + let y1 = top + let x2 = right + generalizationLineMargin + let y2 = bottom + let cx = x1 + 20 + let cy = y1 + (y2 - y1) / 2 + let path = `M ${x1},${y1} Q ${cx},${cy} ${x2},${y2}` + gLine.plot(path) + gNode.left = right + generalizationNodeMargin + gNode.top = top + (bottom - top - gNode.height) / 2 + } +} + +export default Fishbone diff --git a/simple-mind-map/src/layouts/FishboneTop.js b/simple-mind-map/src/layouts/FishboneTop.js new file mode 100644 index 00000000..f7799027 --- /dev/null +++ b/simple-mind-map/src/layouts/FishboneTop.js @@ -0,0 +1,350 @@ +import Base from './Base' +import { walk, asyncRun } from '../utils' +import { CONSTANTS } from '../utils/constant' + +const degToRad = deg => { + return (Math.PI / 180) * deg +} + +// 上方鱼骨图 +class Fishbone extends Base { + // 构造函数 + constructor(opt = {}) { + super(opt) + } + + // 布局 + doLayout(callback) { + let task = [ + () => { + this.computedBaseValue() + }, + () => { + this.computedLeftTopValue() + }, + () => { + this.adjustLeftTopValue() + }, + () => { + callback(this.root) + } + ] + asyncRun(task) + } + + // 遍历数据创建节点、计算根节点的位置,计算根节点的子节点的top值 + computedBaseValue() { + walk( + this.renderer.renderTree, + null, + (node, parent, isRoot, layerIndex, index) => { + // 创建节点 + let newNode = this.createNode(node, parent, isRoot, layerIndex) + // 根节点定位在画布中心位置 + if (isRoot) { + this.setNodeCenter(newNode) + } else { + // 非根节点 + // 三级及以下节点以上级方向为准 + if (parent._node.dir) { + newNode.dir = parent._node.dir + } else { + // 节点生长方向 + newNode.dir = + index % 2 === 0 + ? CONSTANTS.TIMELINE_DIR.TOP + : CONSTANTS.TIMELINE_DIR.BOTTOM + } + // 计算二级节点的top值 + if (parent._node.isRoot) { + newNode.top = parent._node.top - newNode.height + } + } + if (!node.data.expand) { + return true + } + }, + null, + true, + 0 + ) + } + + // 遍历节点树计算节点的left、top + computedLeftTopValue() { + walk( + this.root, + null, + (node, parent, isRoot, layerIndex, index) => { + if (node.isRoot) { + let totalLeft = node.left + node.width + node.children.forEach(item => { + item.left = totalLeft + totalLeft += item.width + }) + } + if (layerIndex >= 1 && node.children) { + // 遍历三级及以下节点的子节点 + let startLeft = node.left + node.width * 0.5 + let totalTop = + node.top + + node.height + + (this.getNodeActChildrenLength(node) > 0 ? node.expandBtnSize : 0) + node.children.forEach(item => { + item.left = startLeft + item.top += totalTop + totalTop += + item.height + + (this.getNodeActChildrenLength(item) > 0 ? item.expandBtnSize : 0) + }) + } + }, + null, + true + ) + } + + // 调整节点left、top + adjustLeftTopValue() { + walk( + this.root, + null, + (node, parent, isRoot, layerIndex) => { + if (!node.nodeData.data.expand) { + return + } + // 调整top + let len = node.children.length + // 调整三级及以下节点的top + if (parent && !parent.isRoot && len > 0) { + let totalHeight = node.children.reduce((h, item) => { + return ( + h + + item.height + + (this.getNodeActChildrenLength(item) > 0 ? item.expandBtnSize : 0) + ) + }, 0) + this.updateBrothersTop(node, totalHeight) + } + }, + (node, parent) => { + // 将二级节点的子节点移到上方 + if (parent && parent.isRoot) { + // 遍历二级节点的子节点 + let totalHeight = 0 + node.children.forEach(item => { + // 调整top + let nodeTotalHeight = this.getNodeAreaHeight(item) + let _top = item.top + item.top = + node.top - (item.top - node.top) - nodeTotalHeight + node.height + // 调整left + let offsetLeft = + (nodeTotalHeight + totalHeight) / Math.tan(degToRad(this.mindMap.opt.fishboneDeg)) + item.left += offsetLeft + totalHeight += nodeTotalHeight + // 同步更新后代节点 + this.updateChildrenPro(item.children, { + top: item.top - _top, + left: offsetLeft + }) + }) + } + // 调整二级节点的子节点的left值 + if (node.isRoot) { + let totalLeft = 0 + node.children.forEach(item => { + item.left += totalLeft + this.updateChildren(item.children, 'left', totalLeft) + let { left, right } = this.getNodeBoundaries(item, 'h') + totalLeft += right - left + }) + } + }, + true + ) + } + + // 递归计算节点的宽度 + getNodeAreaHeight(node) { + let totalHeight = 0 + let loop = node => { + totalHeight += + node.height + + (this.getNodeActChildrenLength(node) > 0 ? node.expandBtnSize : 0) + if (node.children.length) { + node.children.forEach(item => { + loop(item) + }) + } + } + loop(node) + return totalHeight + } + + // 调整兄弟节点的left + updateBrothersLeft(node) { + let childrenList = node.children + let totalAddWidth = 0 + childrenList.forEach(item => { + item.left += totalAddWidth + if (item.children && item.children.length) { + this.updateChildren(item.children, 'left', totalAddWidth) + } + // let areaWidth = this.getNodeAreaWidth(item) + let { left, right } = this.getNodeBoundaries(item, 'h') + let areaWidth = right - left + let difference = areaWidth - item.width + if (difference > 0) { + totalAddWidth += difference + } + }) + } + + // 调整兄弟节点的top + updateBrothersTop(node, addHeight) { + if (node.parent && !node.parent.isRoot) { + let childrenList = node.parent.children + let index = childrenList.findIndex(item => { + return item === node + }) + childrenList.forEach((item, _index) => { + if (item.hasCustomPosition()) { + // 适配自定义位置 + return + } + let _offset = 0 + // 下面的节点往下移 + if (_index > index) { + _offset = addHeight + } + item.top += _offset + // 同步更新子节点的位置 + if (item.children && item.children.length) { + this.updateChildren(item.children, 'top', _offset) + } + }) + // 更新父节点的位置 + this.updateBrothersTop(node.parent, addHeight) + } + } + + // 绘制连线,连接该节点到其子节点 + renderLine(node, lines, style) { + if (node.children.length <= 0) { + return [] + } + let { left, top, width, height, expandBtnSize } = node + let len = node.children.length + if (node.isRoot) { + // 当前节点是根节点 + let prevBother = node + // 根节点的子节点是和根节点同一水平线排列 + node.children.forEach((item, index) => { + let x1 = prevBother.left + prevBother.width + let x2 = item.left + let y = node.top + node.height / 2 + let path = `M ${x1},${y} L ${x2},${y}` + lines[index].plot(path) + style && style(lines[index], item) + prevBother = item + }) + } else { + // 当前节点为非根节点 + let maxy = -Infinity + let miny = Infinity + let maxx = -Infinity + let x = node.left + node.width * 0.3 + node.children.forEach((item, index) => { + if (item.left > maxx) { + maxx = item.left + } + let y = item.top + item.height / 2 + if (y > maxy) { + maxy = y + } + if (y < miny) { + miny = y + } + // 水平线 + if (node.layerIndex > 1) { + let path = `M ${x},${y} L ${item.left},${y}` + lines[index].plot(path) + style && style(lines[index], item) + } + }) + // 竖线 + if (len > 0) { + let line = this.draw.path() + expandBtnSize = len > 0 ? expandBtnSize : 0 + let lineLength = maxx - node.left - node.width * 0.3 + if ( + node.parent && + node.parent.isRoot && + node.dir === CONSTANTS.TIMELINE_DIR.TOP + ) { + line.plot( + `M ${x},${top} L ${x + lineLength},${ + top - Math.tan(degToRad(this.mindMap.opt.fishboneDeg)) * lineLength + }` + ) + } else { + if (node.parent && node.parent.isRoot) { + line.plot( + `M ${x},${top} L ${x + lineLength},${ + top - Math.tan(degToRad(this.mindMap.opt.fishboneDeg)) * lineLength + }` + ) + } else { + line.plot(`M ${x},${top + height + expandBtnSize} L ${x},${maxy}`) + } + } + node.style.line(line) + node._lines.push(line) + style && style(line, node) + } + } + } + + // 渲染按钮 + renderExpandBtn(node, btn) { + let { width, height, expandBtnSize, isRoot } = node + if (!isRoot) { + let { translateX, translateY } = btn.transform() + if (node.parent && node.parent.isRoot) { + btn.translate( + width * 0.3 - expandBtnSize / 2 - translateX, + -expandBtnSize / 2 - translateY + ) + } else { + btn.translate( + width * 0.3 - expandBtnSize / 2 - translateX, + height + expandBtnSize / 2 - translateY + ) + } + } + } + + // 创建概要节点 + renderGeneralization(node, gLine, gNode) { + let { + top, + bottom, + right, + generalizationLineMargin, + generalizationNodeMargin + } = this.getNodeBoundaries(node, 'h') + let x1 = right + generalizationLineMargin + let y1 = top + let x2 = right + generalizationLineMargin + let y2 = bottom + let cx = x1 + 20 + let cy = y1 + (y2 - y1) / 2 + let path = `M ${x1},${y1} Q ${cx},${cy} ${x2},${y2}` + gLine.plot(path) + gNode.left = right + generalizationNodeMargin + gNode.top = top + (bottom - top - gNode.height) / 2 + } +} + +export default Fishbone diff --git a/simple-mind-map/src/layouts/Timeline.js b/simple-mind-map/src/layouts/Timeline.js new file mode 100644 index 00000000..6b792f7d --- /dev/null +++ b/simple-mind-map/src/layouts/Timeline.js @@ -0,0 +1,338 @@ +import Base from './Base' +import { walk, asyncRun } from '../utils' +import { CONSTANTS } from '../utils/constant' + +// 时间轴 +class Timeline extends Base { + // 构造函数 + constructor(opt = {}, layout) { + super(opt) + this.layout = layout + } + + // 布局 + doLayout(callback) { + let task = [ + () => { + this.computedBaseValue() + }, + () => { + this.computedLeftTopValue() + }, + () => { + this.adjustLeftTopValue() + }, + () => { + callback(this.root) + } + ] + asyncRun(task) + } + + // 遍历数据创建节点、计算根节点的位置,计算根节点的子节点的top值 + computedBaseValue() { + walk( + this.renderer.renderTree, + null, + (cur, parent, isRoot, layerIndex, index) => { + let newNode = this.createNode(cur, parent, isRoot, layerIndex) + // 根节点定位在画布中心位置 + if (isRoot) { + this.setNodeCenter(newNode) + } else { + // 非根节点 + // 时间轴2类型需要交替显示 + if (this.layout === CONSTANTS.LAYOUT.TIMELINE2) { + // 三级及以下节点以上级为准 + if (parent._node.dir) { + newNode.dir = parent._node.dir + } else { + // 节点生长方向 + newNode.dir = + index % 2 === 0 + ? CONSTANTS.TIMELINE_DIR.BOTTOM + : CONSTANTS.TIMELINE_DIR.TOP + } + } else { + newNode.dir = '' + } + if (parent._node.isRoot) { + newNode.top = + parent._node.top + + (cur._node.height > parent._node.height + ? -(cur._node.height - parent._node.height) / 2 + : (parent._node.height - cur._node.height) / 2) + } + } + if (!cur.data.expand) { + return true + } + }, + null, + true, + 0 + ) + } + + // 遍历节点树计算节点的left、top + computedLeftTopValue() { + walk( + this.root, + null, + (node, parent, isRoot, layerIndex, index) => { + if ( + node.nodeData.data.expand && + node.children && + node.children.length + ) { + let marginX = this.getMarginX(layerIndex + 1) + let marginY = this.getMarginY(layerIndex + 1) + if (isRoot) { + let left = node.left + node.width + let totalLeft = left + marginX + node.children.forEach(cur => { + cur.left = totalLeft + totalLeft += cur.width + marginX + }) + } else { + let totalTop = + node.top + + node.height + + marginY + + (this.getNodeActChildrenLength(node) > 0 ? node.expandBtnSize : 0) + node.children.forEach(cur => { + cur.left = node.left + node.width * 0.5 + cur.top = totalTop + totalTop += + cur.height + + marginY + + (this.getNodeActChildrenLength(cur) > 0 ? cur.expandBtnSize : 0) + }) + } + } + }, + null, + true + ) + } + + // 调整节点left、top + adjustLeftTopValue() { + walk( + this.root, + null, + (node, parent, isRoot, layerIndex) => { + if (!node.nodeData.data.expand) { + return + } + // 调整left + if (node.isRoot) { + this.updateBrothersLeft(node) + } + // 调整top + let len = node.children.length + if (parent && !parent.isRoot && len > 0) { + let marginY = this.getMarginY(layerIndex + 1) + let totalHeight = + node.children.reduce((h, item) => { + return ( + h + + item.height + + (this.getNodeActChildrenLength(item) > 0 + ? item.expandBtnSize + : 0) + ) + }, 0) + + len * marginY + this.updateBrothersTop(node, totalHeight) + } + }, + (node, parent, isRoot, layerIndex) => { + if ( + parent && + parent.isRoot && + node.dir === CONSTANTS.TIMELINE_DIR.TOP + ) { + // 遍历二级节点的子节点 + node.children.forEach(item => { + let totalHeight = this.getNodeAreaHeight(item) + let _top = item.top + item.top = + node.top - (item.top - node.top) - totalHeight + node.height + this.updateChildren(item.children, 'top', item.top - _top) + }) + } + }, + true + ) + } + + // 递归计算节点的宽度 + getNodeAreaHeight(node) { + let totalHeight = 0 + let loop = node => { + totalHeight += + node.height + + (this.getNodeActChildrenLength(node) > 0 ? node.expandBtnSize : 0) + + this.getMarginY(node.layerIndex) + if (node.children.length) { + node.children.forEach(item => { + loop(item) + }) + } + } + loop(node) + return totalHeight + } + + // 调整兄弟节点的left + updateBrothersLeft(node) { + let childrenList = node.children + let totalAddWidth = 0 + childrenList.forEach(item => { + item.left += totalAddWidth + if (item.children && item.children.length) { + this.updateChildren(item.children, 'left', totalAddWidth) + } + // let areaWidth = this.getNodeAreaWidth(item) + let { left, right } = this.getNodeBoundaries(item, 'h') + let areaWidth = right - left + let difference = areaWidth - item.width + if (difference > 0) { + totalAddWidth += difference + } + }) + } + + // 调整兄弟节点的top + updateBrothersTop(node, addHeight) { + if (node.parent && !node.parent.isRoot) { + let childrenList = node.parent.children + let index = childrenList.findIndex(item => { + return item === node + }) + childrenList.forEach((item, _index) => { + if (item.hasCustomPosition()) { + // 适配自定义位置 + return + } + let _offset = 0 + // 下面的节点往下移 + if (_index > index) { + _offset = addHeight + } + item.top += _offset + // 同步更新子节点的位置 + if (item.children && item.children.length) { + this.updateChildren(item.children, 'top', _offset) + } + }) + // 更新父节点的位置 + this.updateBrothersTop(node.parent, addHeight) + } + } + + // 绘制连线,连接该节点到其子节点 + renderLine(node, lines, style) { + if (node.children.length <= 0) { + return [] + } + let { left, top, width, height, expandBtnSize } = node + let len = node.children.length + if (node.isRoot) { + // 当前节点是根节点 + let prevBother = node + // 根节点的子节点是和根节点同一水平线排列 + node.children.forEach((item, index) => { + let x1 = prevBother.left + prevBother.width + let x2 = item.left + let y = node.top + node.height / 2 + let path = `M ${x1},${y} L ${x2},${y}` + lines[index].plot(path) + style && style(lines[index], item) + prevBother = item + }) + } else { + // 当前节点为非根节点 + let maxy = -Infinity + let miny = Infinity + let x = node.left + node.width * 0.3 + node.children.forEach((item, index) => { + let y = item.top + item.height / 2 + if (y > maxy) { + maxy = y + } + if (y < miny) { + miny = y + } + // 水平线 + let path = `M ${x},${y} L ${item.left},${y}` + lines[index].plot(path) + style && style(lines[index], item) + }) + // 竖线 + if (len > 0) { + let line = this.draw.path() + expandBtnSize = len > 0 ? expandBtnSize : 0 + if ( + node.parent && + node.parent.isRoot && + node.dir === CONSTANTS.TIMELINE_DIR.TOP + ) { + line.plot(`M ${x},${top} L ${x},${miny}`) + } else { + line.plot(`M ${x},${top + height + expandBtnSize} L ${x},${maxy}`) + } + node.style.line(line) + node._lines.push(line) + style && style(line, node) + } + } + } + + // 渲染按钮 + renderExpandBtn(node, btn) { + let { width, height, expandBtnSize, isRoot } = node + if (!isRoot) { + let { translateX, translateY } = btn.transform() + if ( + node.parent && + node.parent.isRoot && + node.dir === CONSTANTS.TIMELINE_DIR.TOP + ) { + btn.translate( + width * 0.3 - expandBtnSize / 2 - translateX, + -expandBtnSize / 2 - translateY + ) + } else { + btn.translate( + width * 0.3 - expandBtnSize / 2 - translateX, + height + expandBtnSize / 2 - translateY + ) + } + } + } + + // 创建概要节点 + renderGeneralization(node, gLine, gNode) { + let { + top, + bottom, + right, + generalizationLineMargin, + generalizationNodeMargin + } = this.getNodeBoundaries(node, 'h') + let x1 = right + generalizationLineMargin + let y1 = top + let x2 = right + generalizationLineMargin + let y2 = bottom + let cx = x1 + 20 + let cy = y1 + (y2 - y1) / 2 + let path = `M ${x1},${y1} Q ${cx},${cy} ${x2},${y2}` + gLine.plot(path) + gNode.left = right + generalizationNodeMargin + gNode.top = top + (bottom - top - gNode.height) / 2 + } +} + +export default Timeline diff --git a/simple-mind-map/src/layouts/fishboneUtils.js b/simple-mind-map/src/layouts/fishboneUtils.js new file mode 100644 index 00000000..eece33df --- /dev/null +++ b/simple-mind-map/src/layouts/fishboneUtils.js @@ -0,0 +1,218 @@ +import { degToRad } from '../utils/' + +export default { + top: { + renderExpandBtn({ + node, + btn, + expandBtnSize, + translateX, + translateY, + width, + height + }) { + if (node.parent && node.parent.isRoot) { + btn.translate( + width * 0.3 - expandBtnSize / 2 - translateX, + -expandBtnSize / 2 - translateY + ) + } else { + btn.translate( + width * 0.3 - expandBtnSize / 2 - translateX, + height + expandBtnSize / 2 - translateY + ) + } + }, + renderLine({ + node, + line, + top, + x, + lineLength, + height, + expandBtnSize, + maxy, + ctx + }) { + if (node.parent && node.parent.isRoot) { + line.plot( + `M ${x},${top} L ${x + lineLength},${ + top - Math.tan(degToRad(ctx.mindMap.opt.fishboneDeg)) * lineLength + }` + ) + } else { + line.plot(`M ${x},${top + height + expandBtnSize} L ${x},${maxy}`) + } + }, + computedLeftTopValue({ layerIndex, node, ctx }) { + if (layerIndex >= 1 && node.children) { + // 遍历三级及以下节点的子节点 + let startLeft = node.left + node.width * 0.5 + let totalTop = + node.top + + node.height + + (ctx.getNodeActChildrenLength(node) > 0 ? node.expandBtnSize : 0) + node.children.forEach(item => { + item.left = startLeft + item.top += totalTop + totalTop += + item.height + + (ctx.getNodeActChildrenLength(item) > 0 ? item.expandBtnSize : 0) + }) + } + }, + adjustLeftTopValueBefore({ node, parent, ctx }) { + // 调整top + let len = node.children.length + // 调整三级及以下节点的top + if (parent && !parent.isRoot && len > 0) { + let totalHeight = node.children.reduce((h, item) => { + return ( + h + + item.height + + (ctx.getNodeActChildrenLength(item) > 0 ? item.expandBtnSize : 0) + ) + }, 0) + ctx.updateBrothersTop(node, totalHeight) + } + }, + adjustLeftTopValueAfter({ parent, node, ctx }) { + // 将二级节点的子节点移到上方 + if (parent && parent.isRoot) { + // 遍历二级节点的子节点 + let totalHeight = 0 + node.children.forEach(item => { + // 调整top + let nodeTotalHeight = ctx.getNodeAreaHeight(item) + let _top = item.top + item.top = + node.top - (item.top - node.top) - nodeTotalHeight + node.height + // 调整left + let offsetLeft = + (nodeTotalHeight + totalHeight) / Math.tan(degToRad(ctx.mindMap.opt.fishboneDeg)) + item.left += offsetLeft + totalHeight += nodeTotalHeight + // 同步更新后代节点 + ctx.updateChildrenPro(item.children, { + top: item.top - _top, + left: offsetLeft + }) + }) + } + } + }, + bottom: { + renderExpandBtn({ + node, + btn, + expandBtnSize, + translateX, + translateY, + width, + height + }) { + if (node.parent && node.parent.isRoot) { + btn.translate( + width * 0.3 - expandBtnSize / 2 - translateX, + height + expandBtnSize / 2 - translateY + ) + } else { + btn.translate( + width * 0.3 - expandBtnSize / 2 - translateX, + -expandBtnSize / 2 - translateY + ) + } + }, + renderLine({ node, line, top, x, lineLength, height, miny, ctx }) { + if (node.parent && node.parent.isRoot) { + line.plot( + `M ${x},${top + height} L ${x + lineLength},${ + top + height + Math.tan(degToRad(ctx.mindMap.opt.fishboneDeg)) * lineLength + }` + ) + } else { + line.plot(`M ${x},${top} L ${x},${miny}`) + } + }, + computedLeftTopValue({ layerIndex, node, ctx }) { + if (layerIndex === 1 && node.children) { + // 遍历二级节点的子节点 + let startLeft = node.left + node.width * 0.5 + let totalTop = + node.top + + node.height + + (ctx.getNodeActChildrenLength(node) > 0 ? node.expandBtnSize : 0) + + node.children.forEach(item => { + item.left = startLeft + item.top = + totalTop + + (ctx.getNodeActChildrenLength(item) > 0 ? item.expandBtnSize : 0) + totalTop += + item.height + + (ctx.getNodeActChildrenLength(item) > 0 ? item.expandBtnSize : 0) + }) + } + if (layerIndex > 1 && node.children) { + // 遍历三级及以下节点的子节点 + let startLeft = node.left + node.width * 0.5 + let totalTop = + node.top - + (ctx.getNodeActChildrenLength(node) > 0 ? node.expandBtnSize : 0) + node.children.forEach(item => { + item.left = startLeft + item.top = totalTop - item.height + totalTop -= + item.height + + (ctx.getNodeActChildrenLength(item) > 0 ? item.expandBtnSize : 0) + }) + } + }, + adjustLeftTopValueBefore({ node, ctx, layerIndex }) { + // 调整top + let len = node.children.length + if (layerIndex > 2 && len > 0) { + let totalHeight = node.children.reduce((h, item) => { + return ( + h + + item.height + + (ctx.getNodeActChildrenLength(item) > 0 ? item.expandBtnSize : 0) + ) + }, 0) + ctx.updateBrothersTop(node, -totalHeight) + } + }, + adjustLeftTopValueAfter({ parent, node, ctx }) { + // 将二级节点的子节点移到上方 + if (parent && parent.isRoot) { + // 遍历二级节点的子节点 + let totalHeight = 0 + let totalHeight2 = 0 + node.children.forEach(item => { + // 调整top + let hasChildren = ctx.getNodeActChildrenLength(item) > 0 + let nodeTotalHeight = ctx.getNodeAreaHeight(item) + let offset = + hasChildren > 0 + ? nodeTotalHeight - + item.height - + (hasChildren ? item.expandBtnSize : 0) + : 0 + let _top = totalHeight + offset + item.top += _top + // 调整left + let offsetLeft = + (totalHeight2 + nodeTotalHeight) / Math.tan(degToRad(ctx.mindMap.opt.fishboneDeg)) + item.left += offsetLeft + totalHeight += offset + totalHeight2 += nodeTotalHeight + // 同步更新后代节点 + ctx.updateChildrenPro(item.children, { + top: _top, + left: offsetLeft + }) + }) + } + } + } +} diff --git a/simple-mind-map/src/utils/constant.js b/simple-mind-map/src/utils/constant.js index da156f31..66633a83 100644 --- a/simple-mind-map/src/utils/constant.js +++ b/simple-mind-map/src/utils/constant.js @@ -154,7 +154,10 @@ export const CONSTANTS = { LOGICAL_STRUCTURE: 'logicalStructure', MIND_MAP: 'mindMap', ORGANIZATION_STRUCTURE: 'organizationStructure', - CATALOG_ORGANIZATION: 'catalogOrganization' + CATALOG_ORGANIZATION: 'catalogOrganization', + TIMELINE: 'timeline', + TIMELINE2: 'timeline2', + FISHBONE: 'fishbone' }, DIR: { UP: 'up', @@ -189,6 +192,10 @@ export const CONSTANTS = { RIGHT: 'right', BOTTOM: 'bottom', CENTER: 'center' + }, + TIMELINE_DIR: { + TOP: 'top', + BOTTOM: 'bottom' } } @@ -217,11 +224,26 @@ export const layoutList = [ { name: '目录组织图', value: CONSTANTS.LAYOUT.CATALOG_ORGANIZATION, + }, + { + name: '时间轴', + value: CONSTANTS.LAYOUT.TIMELINE, + }, + { + name: '时间轴2', + value: CONSTANTS.LAYOUT.TIMELINE2, + }, + { + name: '鱼骨图', + value: CONSTANTS.LAYOUT.FISHBONE, } ] export const layoutValueList = [ CONSTANTS.LAYOUT.LOGICAL_STRUCTURE, CONSTANTS.LAYOUT.MIND_MAP, CONSTANTS.LAYOUT.CATALOG_ORGANIZATION, - CONSTANTS.LAYOUT.ORGANIZATION_STRUCTURE + CONSTANTS.LAYOUT.ORGANIZATION_STRUCTURE, + CONSTANTS.LAYOUT.TIMELINE, + CONSTANTS.LAYOUT.TIMELINE2, + CONSTANTS.LAYOUT.FISHBONE ] \ No newline at end of file diff --git a/web/src/assets/.DS_Store b/web/src/assets/.DS_Store index 96b41531..5ab405d2 100644 Binary files a/web/src/assets/.DS_Store and b/web/src/assets/.DS_Store differ diff --git a/web/src/assets/img/fishbone.jpg b/web/src/assets/img/fishbone.jpg new file mode 100644 index 00000000..a52bd6ec Binary files /dev/null and b/web/src/assets/img/fishbone.jpg differ diff --git a/web/src/assets/img/timeline.jpg b/web/src/assets/img/timeline.jpg new file mode 100644 index 00000000..06cd682a Binary files /dev/null and b/web/src/assets/img/timeline.jpg differ diff --git a/web/src/assets/img/timeline2.jpg b/web/src/assets/img/timeline2.jpg new file mode 100644 index 00000000..4d0019e5 Binary files /dev/null and b/web/src/assets/img/timeline2.jpg differ diff --git a/web/src/config/constant.js b/web/src/config/constant.js index 9532e57f..8cd50454 100644 --- a/web/src/config/constant.js +++ b/web/src/config/constant.js @@ -3,7 +3,10 @@ export const layoutImgMap = { logicalStructure: require('../assets/img/logicalStructure.jpg'), mindMap: require('../assets/img/mindMap.jpg'), organizationStructure: require('../assets/img/organizationStructure.jpg'), - catalogOrganization: require('../assets/img/catalogOrganization.jpg') + catalogOrganization: require('../assets/img/catalogOrganization.jpg'), + timeline: require('../assets/img/timeline.jpg'), + timeline2: require('../assets/img/timeline2.jpg'), + fishbone: require('../assets/img/fishbone.jpg'), } // 主题图片映射 diff --git a/web/src/pages/Doc/en/changelog/index.md b/web/src/pages/Doc/en/changelog/index.md index 5ec7984c..651e9fb7 100644 --- a/web/src/pages/Doc/en/changelog/index.md +++ b/web/src/pages/Doc/en/changelog/index.md @@ -2,7 +2,11 @@ ## 0.5.4 -New: 1.Add new themes. +New: 1.Add new themes. 2.Added timeline and fishbone structure. + +Fix: 1.Fix the conflict issue between node right-click and canvas right-click. 2.Fix the bug that the line segment is not hidden when dragging nodes such as organizational chart and directory organization chart. + +optimization: 1.Optimize the layout of organizational chart. 2.Optimize the layout of the directory organization chart. ## 0.5.3 diff --git a/web/src/pages/Doc/en/changelog/index.vue b/web/src/pages/Doc/en/changelog/index.vue index c076a0d1..83b64dad 100644 --- a/web/src/pages/Doc/en/changelog/index.vue +++ b/web/src/pages/Doc/en/changelog/index.vue @@ -2,7 +2,9 @@

Changelog

0.5.4

-

New: 1.Add new themes.

+

New: 1.Add new themes. 2.Added timeline and fishbone structure.

+

Fix: 1.Fix the conflict issue between node right-click and canvas right-click. 2.Fix the bug that the line segment is not hidden when dragging nodes such as organizational chart and directory organization chart.

+

optimization: 1.Optimize the layout of organizational chart. 2.Optimize the layout of the directory organization chart.

0.5.3

Fix: 1.Fixed the issue of setting the text style when multiple nodes were selected in rich text mode, which would change the text of all selected nodes to the text of the last selected node.

New: 1.Support setting the position of the initial central node.

diff --git a/web/src/pages/Doc/en/constructor/index.md b/web/src/pages/Doc/en/constructor/index.md index 0a84f272..05d38c83 100644 --- a/web/src/pages/Doc/en/constructor/index.md +++ b/web/src/pages/Doc/en/constructor/index.md @@ -26,7 +26,8 @@ const mindMap = new MindMap({ | -------------------------------- | ------- | ---------------- | ------------------------------------------------------------ | -------- | | el | Element | | Container element, must be a DOM element | Yes | | data | Object | {} | Mind map data, refer to: https://github.com/wanglin2/mind-map/blob/main/simple-mind-map/example/exampleData.js | | -| layout | String | logicalStructure | Layout type, options: logicalStructure (logical structure diagram), mindMap (mind map), catalogOrganization (catalog organization diagram), organizationStructure (organization structure diagram) | | +| layout | String | logicalStructure | Layout type, options: logicalStructure (logical structure diagram), mindMap (mind map), catalogOrganization (catalog organization diagram), organizationStructure (organization structure diagram)、timeline(v0.5.4+, timeline)、timeline2(v0.5.4+, up down alternating timeline)、fishbone(v0.5.4+, fishbone diagram) | | +| fishboneDeg(v0.5.4+) | Number | 45 | Set the diagonal angle of the fishbone structure diagram | | | theme | String | default | Theme, options: default, classic, minions, pinkGrape, mint, gold, vitalityOrange, greenLeaf, dark2, skyGreen, classic2, classic3, classic4(v0.2.0+), classicGreen, classicBlue, blueSky, brainImpairedPink, dark, earthYellow, freshGreen, freshRed, romanticPurple, simpleBlack(v0.5.4+), courseGreen(v0.5.4+), coffee(v0.5.4+), redSpirit(v0.5.4+), blackHumour(v0.5.4+), lateNightOffice(v0.5.4+), blackGold(v0.5.4+) | | | themeConfig | Object | {} | Theme configuration, will be merged with the selected theme, available fields refer to: https://github.com/wanglin2/mind-map/blob/main/simple-mind-map/src/themes/default.js | | | scaleRatio | Number | 0.1 | The incremental scaling ratio | | diff --git a/web/src/pages/Doc/en/constructor/index.vue b/web/src/pages/Doc/en/constructor/index.vue index a407759d..b345d449 100644 --- a/web/src/pages/Doc/en/constructor/index.vue +++ b/web/src/pages/Doc/en/constructor/index.vue @@ -46,7 +46,14 @@ layout String logicalStructure -Layout type, options: logicalStructure (logical structure diagram), mindMap (mind map), catalogOrganization (catalog organization diagram), organizationStructure (organization structure diagram) +Layout type, options: logicalStructure (logical structure diagram), mindMap (mind map), catalogOrganization (catalog organization diagram), organizationStructure (organization structure diagram)、timeline(v0.5.4+, timeline)、timeline2(v0.5.4+, up down alternating timeline)、fishbone(v0.5.4+, fishbone diagram) + + + +fishboneDeg(v0.5.4+) +Number +45 +Set the diagonal angle of the fishbone structure diagram diff --git a/web/src/pages/Doc/en/introduction/index.md b/web/src/pages/Doc/en/introduction/index.md index 30db3092..faa22eb8 100644 --- a/web/src/pages/Doc/en/introduction/index.md +++ b/web/src/pages/Doc/en/introduction/index.md @@ -7,8 +7,8 @@ ## Features - [x] Plugin architecture. In addition to core functions, other functions are provided as plugins, which can be used as needed to reduce the overall volume -- [x] Supports four types of structures: logical structure diagrams, mind maps, - organizational structure diagrams, and directory organization diagrams +- [x] Supports six types of structures: logical structure diagrams, mind maps, + organizational structure diagrams, directory organization diagrams, timeline, and fishbone diagrams - [x] Built-in multiple themes and allows for highly customized styles, and support register new themes - [x] Supports shortcuts - [x] Node content supports images, icons, hyperlinks, notes, tags, and diff --git a/web/src/pages/Doc/en/introduction/index.vue b/web/src/pages/Doc/en/introduction/index.vue index dfa906e1..84f0d5fb 100644 --- a/web/src/pages/Doc/en/introduction/index.vue +++ b/web/src/pages/Doc/en/introduction/index.vue @@ -8,8 +8,8 @@

Features