🚀 在线实例
如果急用, 可先cv走代码修改, 哪里没明白再回来看哪里.
https://github.com/any86/any-...
d3 是一个大而全的图形库, 集成了svg 元素操作和常见图表(图形)的数据结构.
本文基于v5 版本的 d3 编写, d3 的功能都是拆分成独立包的, 我们这里只需要引入d3-hierarch和d3-shape生成拓扑的数据结构.
// hierarchy 用来生成d3的树形对象, 同时挂载一些方法, 比如获取子元素descendants // tree 用来给节点分配x/y坐标 // linkHorizontal用来生成水平连接线 import {hierarchy, tree} from 'd3-hierarchy'; import {linkHorizontal} from 'd3-shape';
下面代码看起来长, 但是主要都是注释.
// 测试数据 // 普通树形 const dataset = { name: '第1级', children: [ { name: '第2级', children: [ { name: '第3级A', children: [ { name: '第4级A', }, { name: '第4级B', }, ], }, ], }, ], };
下面代码看起来很长, 但实际只有4个方法总20几行代码,剩余都是注释.
{ /** * 把普通树形变成d3需要的树形 */ genTreeData(data) { const width = 1000; const height = 1000; // hierarchy把普通的树形数据变成d3的tree结构, // 这样tree就有了d3的方法, 可以通过方法获取子节点(tree.descendants)/父节点/节点数等信息 const root = hierarchy(data); // 遍历子节点,descendants是后代的意思, // 但是其实也会包含当前节点本身. // 给节点增加hidden字段用来控制当前节点显示/隐藏. root.descendants().forEach((node) => { node.hidden = false; }); // d3.tree运行后会返回一个函数, // 通过函数可以设置图形的一些尺寸(nodeSize)/位置间距(separation)信息 // 这样在返回的函数中传入刚才输入的d3.tree结构数据, 比如上面的root, // 那么拓扑所需的数据就都全了. return ( tree() .separation(function(a, b) { // 同级元素调整间隙比例 // 一般就用2:1就好 return (a.parent == b.parent ? 2 : 1) / a.depth; }) // 节点尺寸 .nodeSize([110, width / (root.height + 1)])(root) ); }, /** * 生成节点数组 => [Node, Node] * 用来给模板渲染元素 */ updateNodes() { this.nodes = this.tree.descendants(); }, /** * 生成线 */ updateLinks() { // tree.links会根据节点数据生成连线数据 this.linkPaths = this.tree.links().map((link) => { // d.linkHorizontal和上面的d3.tree一样, // 可以当做构造函数, // 其返回一个函数 // 可以用函数上的x/y方法指定 // 由于默认生成tree数据是上下结构的拓扑数据, // 所以为了生成左至右的线需要把X/Y数据颠倒 // 最终生成线数据结构类似这样:{source:{},target:{}} if (!link.target.hidden) { return linkHorizontal() .x((d) => d.y) .y((d) => d.x)(link); } }); }, /** * 生成所需数据 */ renderTree() { this.tree = this.genTreeData(dataset); this.updateLinks(); this.updateNodes(); }, }
dom 结构看起来太长了, 但实际只做了 2 件事:
<foreignObject>
元素实现 svg 内部可以嵌套普通 html 元素.<svg xmlns="http://www.w3.org/2000/svg" width="1000" height="1000" style="width:100%"> <g transform="translate(100, 100)"> <template v-for="(linkPath, index) in linkPaths"> <path v-if="linkPath" :key="index" :d="linkPath" class="line" /> </template> </g> <g transform="translate(100, 100)"> <foreignObject v-for="(node,index) in nodes" v-show="!node.hidden" :class="{[`at-${action}`]:activeNode===node}" :key="'foreignObject'+index" :width="itemWidth" :height="itemHeight" :x="node.y - itemWidth/2" :y="node.x - itemHeight/2" @panstart="onPanstart(index,$event)" @panmove="onPanmove(index,$event)" @panend="onPanend" @pancancel="onPanend" @tap="onTap(index)" > <body xmlns="http://www.w3.org/1999/xhtml"> <div class="text"> <p>节点层级: {{node.depth}}</p> <p>节点顺序: {{index}}</p> </div> </body> </foreignObject> </g> </svg>
{ /** * 拖拽开始, 记录当前节点 */ onPanstart(index, e) { const [item] = this.nodes.splice(index, 1); this.nodes.push(item); this.activeNode = item; }, /** * 拖拽中 * 变化节点坐标 * 重新生成连线数据 */ onPanmove(index, e) { this.action = e.type; const { deltaX, deltaY } = e; const { length } = this.nodes; this.activeNode.x += deltaY; this.activeNode.y += deltaX; this.updateLinks(); }, /** * 取消当前节点激活 */ onPanend() { this.activeNode = null; }, /** * 收起/展开子节点 */ onTap(index) { this.activeNode = this.nodes[index]; // 当前节点记录是否收起/展开 if (void 0 === this.activeNode.collapse) { this.$set(this.activeNode, 'collapse', true); } else { this.activeNode.collapse = !this.activeNode.collapse; } const { x, y, collapse } = this.activeNode; // descendants返回的子节点包含自己, 所以排除自己 const [a, ...childNodes] = this.activeNode.descendants(); // 根据节点折叠状态来展开/折叠子节点显示 childNodes.forEach((node) => { if (collapse) { const x1 = node.x; const y1 = node.y; // 存储展开时候的位置, // 下次复原位置用 node._x = x1; node._y = y1; animate(1, 0, 200, (value, isDone) => { node.x = x - (x - x1) * value; node.y = y - (y - y1) * value; if (isDone) { node.hidden = true; } this.updateLinks(); }); } else { node.hidden = false; // 此处让value从0 - 1在200ms内不停变化 // 从而让节点位置变化实现展开收缩动画 animate(0, 1, 200, (value) => { node.x = x + (node._x - x) * value; node.y = y + (node._y - y) * value; this.updateLinks(); }); } }); } }
源码: https://github.com/any86/any-...
animate函数实现其实很简单, 主要说下easeInOut
函数, 他其实就是一个"时间为x轴, 值为y轴的曲线", 是我百度搜的, 其实还有很多类似的曲线函数, 大家可自行搜索. 大家可以自己尝试写一个, 比如借助Math.sin
.
动画在本例就是锦上添花, 逻辑也很简单不展开讲解, 如果有兴趣可留言讨论.
/** * t 时间 * b 起始值 * c 目标值 * d 所需时间 * */ function easeInOut(t, b, c, d) { if ((t /= d / 2) < 1) return c / 2 * t * t + b; return -c / 2 * ((--t) * (t - 2) - 1) + b; } /** * 用requestAnimationFrame不断执行easeInOut * */ export function animate(from = 0, to = 0, duration = 1000, callback = () => void 0) { const startTime = window.performance.now(); function run() { const timeDiff = window.performance.now() - startTime; const value = easeInOut(timeDiff, from, to - from, duration); if (timeDiff <= duration) { callback(value); requestAnimationFrame(run); } else { // 修正超出边界 callback(to, true); } } run(); };
全部代码: https://github.com/any86/any-...
计划封装成vue组件并开源, 一切看大家反馈, 如果支持这个计划请下方留言☎️.
基础教程从这里开始
第一课, 体验typescript
第二课, 基础类型和入门高级类型
第三课, 泛型
第四课, 解读高级类型
第五课, 命名空间(namespace)是什么
特别篇, 在vue3🔥源码中学会typescript🦕 - "is"
第六课, 什么是声明文件(declare)? 🦕 - 全局声明篇
新手前端学🔥typescript - 实战篇, 实现浏览器全屏(59行)
🔥常用正则大全2020
🚆新手前端不要慌! 给你✊10根救命稻草🍃
真.1px边框, 🚀 支持任意数量边和圆角, 1 个万金油的方法
🚀揭秘vue/react组件库中🤚5个"作者不造的轮子"
vue / react的UI库都在用的几个DOM API🚀
刚玩微博, 咱们可以互相关注, 嘿嘿
感谢大家的阅读, 如有疑问可以加我微信, 我拉你进入微信群(由于腾讯对微信群的100人限制, 超过100人后必须由群成员拉入)