├── .npmignore ├── docs ├── _config.yml ├── groups.md ├── config.md ├── container.md ├── edge.md └── node.md ├── .gitignore ├── config ├── lib.config.js └── index.js ├── LICENSE ├── package.json ├── webpack.config.js ├── README.md ├── src └── index.js └── dist └── ctopo.js /.npmignore: -------------------------------------------------------------------------------- 1 | config/ 2 | yarn.lock -------------------------------------------------------------------------------- /docs/_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-cayman -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | npm-debug.log* 4 | yarn-debug.log* 5 | yarn-error.log* 6 | 7 | # Editor directories and files 8 | .idea 9 | .vscode 10 | *.suo 11 | *.ntvs* 12 | *.njsproj 13 | *.sln 14 | -------------------------------------------------------------------------------- /docs/groups.md: -------------------------------------------------------------------------------- 1 | ## groups 为对象 2 | 3 | 1. 键名为group的名称,必须和node中的group值一致,只会匹配与当前键名相同的node来配置样式 4 | 2. 键值为一个对象,表示样式配置,除去label、group和size等选项其余可选键与nodes一致。 5 | 3. 主要包括背景色、字体、边框、告警、透明度、形变和可配置项。 6 | 4. shape只能选择‘image’并且必须同时设置image属性,可以设置节点为图片 -------------------------------------------------------------------------------- /docs/config.md: -------------------------------------------------------------------------------- 1 | ## config 为对象,格式如下: 2 | 3 | | key | description |required| default | 4 | |:-----------:|:----------------------------------------:|:------:|:-------:| 5 | | eagleEye | `[Boolean]`控制鹰眼的显示 | no | false | 6 | | disableWheelZoom | `[Boolean]`是否禁用滚轮缩放整个画布,默认可以滚轮缩放 | no | false | -------------------------------------------------------------------------------- /docs/container.md: -------------------------------------------------------------------------------- 1 | ## containers 数组项为对象,格式如下: 2 | 3 | | key | description |required| default | 4 | |:-----------:|:----------------------------------------:|:------:|:-------:| 5 | | nodes | `[Array]`节点组包含的节点id | yes | [] | 6 | | name | `[String]`节点组名称 | no | '' | 7 | 8 | > 可选键还包含color、font、border和可配置项,用法与nodes一致。主要包括背景色、字体、边框和可配置项。 -------------------------------------------------------------------------------- /config/lib.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module 配置package.json的参数和webpack.config的参数。 3 | * 4 | */ 5 | module.exports = { 6 | libraryName: 'ctopo', // npm包名,首字母不允许大写,支持驼峰和短杆写法 7 | bundleName: 'ctopo', // 打包后文件名,也是UMD script直接引入挂在windows对象的key名 8 | version: '0.1.0', // 版本号 9 | description: 'A tool to show topo canvas with improved JTopo', // 包描述 10 | keywords: ['JTopo', 'canvas', 'topo'], // 关键词 11 | author: 'Alan Chen', // 作者 12 | repository: { // 仓库地址和首页地址 13 | type: 'git', 14 | url: 'https://github.com/alanchenchen/CTopo' 15 | } 16 | } -------------------------------------------------------------------------------- /docs/edge.md: -------------------------------------------------------------------------------- 1 | ## edges 数组项为对象,格式如下: 2 | 3 | | key | description |required| default | 4 | |:-----------:|:----------------------------------------:|:------:|:-------:| 5 | | from | `[Number]或[String]`起始节点的id | yes | / | 6 | | to | `[Number]或[String]`终止节点的id | yes | / | 7 | | title | `[String]`连线名称,会一直显示 | no | '' | 8 | | tips | `[String]`连线名称,只有hover才会显示,会覆盖掉title | no | '' | 9 | | style | `[Object]`连线的样式设置,具体配置见下方| no | {} | 10 | 11 | > 可配置为对象的详细参数 12 | 13 | *style* 14 | 15 | | key | description |required| default | 16 | |:-----------:|:----------------------------------------:|:------:|:-------:| 17 | | color | `[RGB]`连线颜色,必须为RGB格式的字符串| no | '22,124,255' | 18 | | fontColor | `[RGB]`字体颜色,必须为RGB格式的字符串| no | '0,0,0' | 19 | | arrow | `[Number]`箭头,数字越大,箭头越大| no | null | 20 | | arrowEnabled | `[Number]`箭头方向是否正确显示| no | false | 21 | | dashed | `[Number]`箭头虚线的间隔,数字越大,箭头虚线间隔越大| no | 0 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Alan Chen 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ctopo", 3 | "version": "0.1.0", 4 | "description": "A tool to show topo canvas with improved JTopo", 5 | "main": "dist/ctopo.js", 6 | "scripts": { 7 | "config": "node config/index.js", 8 | "build": "webpack --progress" 9 | }, 10 | "keywords": [ 11 | "JTopo", 12 | "canvas", 13 | "topo" 14 | ], 15 | "author": "Alan Chen", 16 | "license": "MIT", 17 | "private": false, 18 | "repository": { 19 | "type": "git", 20 | "url": "https://github.com/alanchenchen/CTopo" 21 | }, 22 | "homepage": "https://github.com/alanchenchen/CTopo", 23 | "dependencies": { 24 | "babel-runtime": "^6.26.0" 25 | }, 26 | "devDependencies": { 27 | "babel-core": "^6.26.0", 28 | "babel-loader": "^7.1.2", 29 | "babel-plugin-transform-runtime": "^6.23.0", 30 | "babel-preset-env": "^1.6.1", 31 | "babel-preset-stage-3": "^6.24.1", 32 | "chalk": "^2.4.1", 33 | "clean-webpack-plugin": "^0.1.19", 34 | "css-loader": "^0.28.7", 35 | "extract-text-webpack-plugin": "^3.0.2", 36 | "file-loader": "^1.1.4", 37 | "inquirer": "^6.1.0", 38 | "uglifyjs-webpack-plugin": "^1.0.1", 39 | "vue-loader": "^13.0.5", 40 | "vue-template-compiler": "^2.4.4", 41 | "webpack": "^3.8.1" 42 | }, 43 | "babel": { 44 | "presets": [ 45 | "env", 46 | "stage-3" 47 | ], 48 | "plugins": [ 49 | [ 50 | "transform-runtime", 51 | { 52 | "polyfill": false, 53 | "regenerator": true, 54 | "moduleName": "babel-runtime" 55 | } 56 | ] 57 | ] 58 | } 59 | } -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const ExtractTextPlugin = require('extract-text-webpack-plugin') 3 | const UglifyJSPlugin = require('uglifyjs-webpack-plugin')//压缩混淆 4 | const CleanWebpackPlugin = require('clean-webpack-plugin')//清除打包后的重复chunk 5 | const ROOTPATH = process.cwd() 6 | const bundleName = require('./config/lib.config.js').bundleName 7 | 8 | module.exports = { 9 | entry: { 10 | [bundleName]: path.resolve(__dirname, './src/index.js') 11 | }, 12 | output: { 13 | path: path.resolve(__dirname, 'dist'), 14 | filename: '[name].js', 15 | library: bundleName, 16 | libraryTarget: 'umd', 17 | umdNamedDefine: true 18 | }, 19 | module: { 20 | rules: [ 21 | { 22 | test: /\.js$/, //打包js,转码ES6 23 | exclude: /(node_modules|bower_components)/, 24 | include: path.join(__dirname, 'src'), 25 | use: { 26 | loader: 'babel-loader', 27 | options: { 28 | presets: ['env', 'stage-3'] 29 | } 30 | } 31 | }, 32 | { 33 | test: /\.vue$/, //打包vue 34 | loader: 'vue-loader', 35 | options: { 36 | loaders: { 37 | css: ExtractTextPlugin.extract({ 38 | fallback: 'vue-style-loader', 39 | use: [ 40 | { 41 | loader: 'css-loader', 42 | options: { 43 | minimize: true 44 | } 45 | } 46 | ] 47 | }) 48 | } 49 | } 50 | } 51 | ] 52 | }, 53 | plugins: [ 54 | new ExtractTextPlugin({ 55 | filename: '[name].css', 56 | allChunks: true 57 | }), 58 | new CleanWebpackPlugin('dist', { root: ROOTPATH, verbose: false }), //每次打包都会清除dist目录 59 | new UglifyJSPlugin({//压缩混淆代码,并且生成sourceMap调试 60 | uglifyOptions: { 61 | ecma: 8,//支持ECMA 8语法 62 | warnings: false//去掉警告 63 | }, 64 | sourceMap: false 65 | }) 66 | ] 67 | } -------------------------------------------------------------------------------- /docs/node.md: -------------------------------------------------------------------------------- 1 | ## nodes 数组项为对象,格式如下: 2 | 3 | | key | description |required| default | 4 | |:-----------:|:----------------------------------------:|:------:|:-------:| 5 | | id | `[Number]或[String]`节点唯一标识 | yes | / | 6 | | x | `[Number]`节点的x坐标,不填会随机生成一个在canvas容器范围内的值 | no | 随机数 | 7 | | y | `[Number]`节点的y坐标,不填会随机生成一个在canvas容器范围内的值 | no | 随机数 | 8 | | label | `[String]`节点名称 | no | '' | 9 | | shape | `[String]`节点形状,只能选择‘circle’(圆形)和‘image’(图片),默认为正方形| no | '' | 10 | | group | `[String]`多个节点放入一个样式组的组名,这里group表示多个节点同时调用option里对应group的配置| no | '' | 11 | | image | `[URL String]`节点形状,如果选择shape为image,这里必须为图片的路径| no | '' | 12 | | size | `[Array]`节点大小,数组项为数字,分别标识宽和高,如果节点为圆形,则表示半径,如果为图片,不设置size,则为原图大小| no | [32, 32] | 13 | | color | `[RGB]`节点颜色,必须为RGB格式的字符串| no | '22,124,255' | 14 | | opacity | `[Number]`节点透明度,可选0~1| no | 1 | 15 | | font | `[Object]`节点内字体样式,具体配置见下方| no | {} | 16 | | border | `[Object]`节点边框样式,具体配置见下方| no | {} | 17 | | transform | `[Object]`节点形变,具体配置见下方| no | {} | 18 | | alarm | `[Object]`节点告警,会在节点上方显示告警内容,具体配置见下方| no | {} | 19 | | visible | `[Boolean]`节点是否可见| no | true | 20 | | dragable | `[Boolean]`节点是否可拖动| no | true | 21 | | selected | `[Boolean]`节点是否一加载被选中| no | false | 22 | | editable | `[Boolean]`节点是否可编辑| no | false | 23 | > 可配置为对象的详细参数 24 | 25 | *font* 26 | 27 | | key | description |required| default | 28 | |:-----------:|:----------------------------------------:|:------:|:-------:| 29 | | color | `[RGB]`字体颜色,必须为RGB格式的字符串| no | '0,0,0' | 30 | | size | `[String]`字体字号和样式,必须带上fontfamliy| no | '16px 微软雅黑' | 31 | | position | `[String]`字体布局,只能选择‘Top’(上方),‘Middle’(中间)和‘Bottom’(下方)| no | 'Bottom' | 32 | 33 | *border* 34 | 35 | | key | description |required| default | 36 | |:-----------:|:----------------------------------------:|:------:|:-------:| 37 | | width | `[Number]`边框宽度| no | 0 | 38 | | radius | `[Number]`边框弧度| no | null | 39 | | color | `[RGB]`边框颜色,必须为RGB格式的字符串| no | '255,255,255' | 40 | 41 | *transform* 42 | 43 | | key | description |required| default | 44 | |:-----------:|:----------------------------------------:|:------:|:-------:| 45 | | rotate | `[Number]`节点旋转角度| no | 0 | 46 | | scale | `[Number]或[Array]`节点缩放, 默认不缩放,参数若为数字,表示x和y缩放一致,若为数组,分别表示x和y方向缩放 | no | 1 | 47 | 48 | *alarm* 49 | 50 | | key | description |required| default | 51 | |:-----------:|:----------------------------------------:|:------:|:-------:| 52 | | name | `[String]`告警内容| no | '' | 53 | | color | `[RGB]`告警背景颜色,必须为RGB格式的字符串| no | / | 54 | | opacity | `[Number]`告警透明度| no | 1 | -------------------------------------------------------------------------------- /config/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module 调用npm命令的主要逻辑模块 3 | * @param {String} g 生成一个package.json缓存文件 4 | * @param {String} rm 删除已有的package.json缓存文件 5 | * @param {String} d 将缓存文件覆盖源package.json 6 | * @param {String} h 查看命令参数帮助 7 | */ 8 | const fs = require('fs') 9 | const path = require('path') 10 | const chalk = require('chalk') 11 | const inquirer = require('inquirer') 12 | 13 | 14 | const ROOTPATH = process.cwd() 15 | const config = require('./lib.config') 16 | const sourcePath = path.join(ROOTPATH, 'package.json') 17 | const copyPath = path.join(__dirname, 'temporary.json') 18 | 19 | const CommandParam = process.argv[2] 20 | 21 | // 显示帮助log 22 | const showHelper = () => { 23 | console.log(chalk` 24 | {blue Usage:} 25 | 26 | {green g} {yellow generate the temporary.json, if there is already one it will be overlapped} 27 | {green rm} {yellow remove the temporary.json} 28 | {green d} {yellow apply the temporary.json to root path} 29 | {green h} {yellow show the config command help} 30 | `) 31 | } 32 | 33 | // 读取源package.json,然后修改,写入新的json到一个缓存文件 34 | const modifyAndWriteJSON = () => { 35 | const version = config.version || '0.0.1' 36 | const keywords = config.keywords || [] 37 | const author = config.author || 'Alan Chen' 38 | 39 | fs.readFile(sourcePath, 'utf8', (err, data) => { 40 | if(err) { 41 | // console.log(chalk.red(err)) 42 | console.log(chalk`{yellow package.json源文件读取失败}`) 43 | } 44 | let copyPackageJson = JSON.parse(data) 45 | 46 | copyPackageJson.name = config.libraryName 47 | copyPackageJson.version = version 48 | copyPackageJson.description = config.description 49 | copyPackageJson.main = `dist/${config.bundleName}.js` 50 | copyPackageJson.keywords = keywords 51 | copyPackageJson.author = author 52 | copyPackageJson.repository = config.repository 53 | copyPackageJson.homepage = config.repository.url 54 | 55 | const newJson = JSON.stringify(copyPackageJson, null, 2) //格式化输出json文件 56 | 57 | fs.writeFile(copyPath, newJson, err => { 58 | if(err) { 59 | // console.log(chalk.red(err)) 60 | console.log(chalk`{yellow package.json缓存文件写入失败}`) 61 | } 62 | else { 63 | console.log(chalk.green(`package.json缓存文件写入成功!`)) 64 | } 65 | }) 66 | }) 67 | } 68 | 69 | // 调用unlink删除文件和remae改变路径之前必须先用access检测文件是否存在 70 | const checkJSONExist = path => { 71 | return new Promise((resolve, reject) => { 72 | fs.access(path, fs.constants.F_OK, err => { 73 | if(err) { 74 | // console.log(chalk.red(err)) 75 | console.log(chalk`{yellow 没有发现package.json缓存文件,请先yarn/npm run config g 生成缓存文件}`) 76 | } 77 | else { 78 | resolve() 79 | } 80 | }) 81 | }) 82 | } 83 | 84 | // 删除缓存文件 85 | const removeJSON = () => { 86 | checkJSONExist(copyPath) 87 | .then(() => { 88 | fs.unlink(copyPath, err => { 89 | if(err) { 90 | // console.log(chalk.red(err)) 91 | console.log(chalk`{yellow package.json缓存文件删除失败}`) 92 | } 93 | else { 94 | console.log(chalk`{green package.json缓存文件删除成功!}`) 95 | } 96 | }) 97 | }) 98 | } 99 | 100 | // 覆盖源package.json 101 | const deposit = () => { 102 | inquirer.prompt([{ 103 | type: 'list', 104 | name: 'deposit', 105 | message: chalk.green('此操作将会用缓存文件覆盖根目录下的package.json'), 106 | choices: [ 107 | {name: '执行', value: true, short: chalk.green('覆盖源package.json')}, 108 | {name: '取消', value: false, short: chalk.red('取消')} 109 | ] 110 | }]) 111 | .then(answers => { 112 | if(answers.deposit) { 113 | checkJSONExist(copyPath) 114 | .then(() => { 115 | fs.rename(copyPath, sourcePath, err => { 116 | if(err) { 117 | // console.log(chalk.red(err)) 118 | console.log(chalk`{yellow package.json文件覆盖失败}`) 119 | } 120 | else { 121 | console.log(chalk.bgBlue(`package.json文件覆盖成功! 可以使用npm run buil打包,然后npm publish发布`)) 122 | } 123 | }) 124 | }) 125 | } 126 | 127 | }) 128 | } 129 | 130 | switch (CommandParam) { 131 | case 'g': 132 | modifyAndWriteJSON() 133 | break 134 | case 'rm': 135 | removeJSON() 136 | break 137 | case 'd': 138 | deposit() 139 | break 140 | case 'h': 141 | showHelper() 142 | break 143 | default: 144 | showHelper() 145 | break 146 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CTopo 2 | 3 | ![](https://img.shields.io/npm/v/ctopo.svg) 4 | ![](https://img.shields.io/npm/dt/ctopo.svg) 5 | ![](https://img.shields.io/github/license/alanchenchen/CTopo.svg) 6 | 7 | > 基于JTopo完全二次封装,模仿vis库的network模块的api使用方法,实现topo图更加简单 8 | 9 | > version: 0.1.0 10 | 11 | > Author: Alan Chen 12 | 13 | ## Features 14 | 1. 完全封装JTopo基础api,模仿现有vis库network的api使用方法。 15 | 2. 解决鼠标cursor报错和滚轮缩放方向bug,修改源码中callee,现支持es5严格格式不报错。 16 | 3. 只处理topo图逻辑,省略了JTopo中表格部分。 17 | 18 | ## Why 19 | * 目前能用的topo图插件库是比较少的,国内外专门做这方面的可视化插件库大多需要收费,像国内的*Qunee*和*Hightopo*非常适合公司项目,但是需要收费,大部分国外插件库也需要收费。我所能查到的免费插件库推荐以下几个。 20 | * 免费使用的topo图插件库: 21 | 1. *vis* github开源,APACHE-2.0的license。自带很多模块,network主要用来处理topo图,但是在我使用过程中存在一个问题,无法自动处理两个节点之间重复的连线,默认开启物理特效模式可以展示多连线,但是一旦关闭特效就会覆盖所有连线只显示一条(相关issue有很多,作者表示目前无法解决)。这个问题可以通过自带的api取巧解决,但是操作起来十分麻烦,需要写一个以数字0隔开两边互为相反数的数列算法来实现,这样两侧线条弧度一致只是弯曲方向相反。数列类似于:`-6,-4,-2,0,2,4,6`。详情见我提过的[issue](https://github.com/almende/vis/issues/3905)。vis还有一个问题就是不支持节点组,目前只支持多个节点合成一个集群,但是一旦集群分散后就无法重新聚合。这个问题也有[issue](https://github.com/almende/vis/issues/3293)提过,我在里面也提了问题。这个问题无解,所以我只好放弃了,*JTopo*,*Qunee*和*Hightopo*都原生支持节点组功能。 22 | 2. *sigma* gitub开源,MIT的license。它和*vis*最大的区别在于*vis*是直接整合了所有功能,但是*sgima*本身只有一个绘制canvas的简易功能,*sgima*有很多插件可以使用,使用这些丰富的插件可以实现topo图,缺点是文档少的可怜,github的wiki也是介绍的摸棱两可,甚至在StackOverflow上也很难找到一些与我项目契合的demo。只能说库很好,对于新手入门门槛太高。另说一句,*vis*文档相当齐全,虽然是全英文。 23 | 3. *d3* github开源,BSD 3-Clause的license。*d3*是个非常强悍的插件库,甚至可以说前端可视化通过d3可以完全做到。*d3*的力导向图可以用来做topo图。但是我没选择d3的原因是d3在v5版本前一直以svg为主,最新版本虽然增加了canvas,但是大部分api还是只兼容svg。需要强调一句的是,*d3*本身不处理svg,它是一个以数据驱动的类似于jq的链式调用的插件。所以svg应该怎么显示需要开发者掌握svg基础,这对我而言临时增加了学习成本,所以放弃了。但是不得不说的是,如果你想自由开发你想的任何东西,请学习*d3*。 24 | 4. *JTopo* github没有找到官方仓库,目前不知道是哪种开源协议。目前官网已经挂掉,只能通过百度才能去看部分api文档!这是一个大牛在2013年开源,2014年停止维护的插件。插件直接在作者的网站即可下载,插件实现了很多功能,其中解决了我在*vis*中遇到的两个问题。但是*JTopo*由于长时间没有维护导致代码停留在es3时代。没有实现模块化,并且其中大量使用的callee语法在es5的strict模式下报错,拓扑图的滚轮缩放方向也是相反方向。 25 | 5. *g6* gitub开源,MIT的license。阿里蚂蚁金服基于自研的G引擎衍生出的专做关系图的插件库。功能非常丰富,大部分api与vis像,可定制程度高,后期ctopo可能会丢弃过时的JTopo而重新基于g6封装一层api。 26 | ## CTopo做了哪些东西 27 | 首先,CTopo更改了部分JTopo源码,JTopo代码书写格式让我很难受。。。因为作者并没有开源,我仅仅是把压缩过的代码美化后拿来修改,去除了代码中的所有callle语法,现在webpack不再报错,可以正确被转译为es5的strict模式。然后CTopo重新写了一个接口层,基本模仿vis的network使用方法。你会看到一个新的JTopo。哈哈 28 | 29 | ## Usage Help 30 | 1. `npm install ctopo --save` or `yarn add ctopo`。 或者直接script引入即可(ctopo直接挂载在windows对象下) 31 | 2. 使用插件必须要保证html中存在一个canvas标签,例如: 32 | ``` javascript 33 | import CTopo from 'ctopo' 34 | 35 | const network = new CTopo('.canvas') // 构造器只接受一个参数,必须是canvas标签的class名、id名或tag名 36 | ``` 37 | ## Options 38 | * 构造函数,construtor必须接收一个参数,当前绑定topo的canvas类名或id名,与jq选择器一致,必须是canvas标签。设置canvas宽高建议直接通过setAtrribute。 39 | * 实例方法: 40 | 1. `setData(dataset)` 设置topo图的节点、连线和节点组等数据。dataset是个对象,包含3个可选键 41 | * [nodes(Array)节点](./docs/node.md) 42 | * [edges(Array)连线](./docs/edge.md) 43 | * [containers(Array)节点组](./docs/container.md) 44 | 2. `setOption(options)` 设置画布舞台和group等配置。options是个对象,包含2个可选键 45 | * [groups(Object)多个节点的重复配置](./docs/groups.md) 46 | * [config(Object)画布舞台的整体配置](./docs/config.md) 47 | 3. `update(dataset)` 更新topo图的节点和连线数据。dataset是个对象,包含2个可选键 48 | > update只会更新对应id的节点或连线,不会重新刷新canvas。 49 | * nodes(Array)节点, id必选 同setData()中nodes的可选键,除了没有image,shape和group,其余完全一致 50 | * edges(Array)连线, id必选 同setData()中edges的可选键完全一致 51 | * node和edge的id建议从监听事件返回参数获取(target.data.id)。node的id是创建节点时开发者手动输入,而edge的id是插件自动生成(Symbol类型) 52 | 4. `add(dataset)` 添加一个或多个topo图的节点、连线和节点组等数据。dataset是个对象,包含3个可选键 53 | > 参数和用法与`setData()`完全一致。 54 | 5. `remove(dataset)` 删除一个或多个topo图的节点和连线等数据。dataset是个对象,包含2个可选键 55 | * nodes(Array)节点 nodes的数组项为String或Number类型,只有一个值,为node创建时手动传入的id 56 | * edges(Array)连线 edges的数组项为Symbol类型,为edge创建时插件生成的id。由于Symbol的唯一性,所以edge的id只能从监听事件返回参数中获取(target.data.id) 57 | * 例: 58 | ```js 59 | remove({ 60 | nodes: ['1', '2'], 61 | edges: [Symbol(0), Symbol(1)] 62 | }) 63 | ``` 64 | 6. `getPosition()` 获取当前所有节点的位置信息 return一个数组,无参数 65 | 7. `setPosition(positions)` 设置对应id的节点坐标位置。 positions为数组,数组项必须包含id、x和y3个键值。x和y必须是数字 66 | 8. `on(eventType, cb)` 绑定事件,有2个参数: 67 | * eventType目前支持 68 | * 'click'(单击) 69 | * 'dbclcik'(双击) 70 | * 'contextmenu'(右键) 71 | * 'mousemove'(鼠标移动) 72 | * 'drag'(拖动) 73 | * dragEnd'(拖动节点结束,只有节点位置发生改变才会触发) 74 | * 回调函数return 一个对象。包含: 75 | * `DOM` --> 鼠标在body DOM元素上的page位移坐标(pageX和pageY) 76 | * `canvas` --> 鼠标在canvas画布上的x和y坐标 77 | * `target` --> 鼠标当前选中的node或edge或container等数据,包含选中的所有canvas属性以及自定义传入的数据(挂载在data属性上) 78 | * `type` --> 当前选中的是哪种类型,如:node或link或container 79 | 9. `off(eventType)` 解绑定事件,只有一个参数, 参数和`on(eventType, cb)`第一个参数可选范围一致 80 | 81 | ## Attentions 82 | 1. setOption必须在setData之后调用。setData可以连续调用,数据不会叠加,而setOption调用一次就会保存当前的配置 83 | 2. dragEnd事件只有在鼠标移动后,发现node节点有位置移动才触发 84 | 3. 可以在nodes或者edges的单个对象里传入别的数据,这些数据存在nodes或者edges实例的data属性里 85 | 4. 本文档并没有写全JTopo里的所有api用法,例如,可以单独修改stage、node或者edge的属性。这些api需要开发者自行去百度JTopo。 86 | > 目前JTopo作者也没给出完善的文档,入坑需谨慎。 87 | 88 | ## DEMO 89 | [代码](https://runjs.cn/code/s1ycvhqr) 90 | [展示](https://sandbox.runjs.cn/show/s1ycvhqr) 91 | 92 | 93 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @name Topo图插件二次封装,基于JTopo 0.4.8 3 | * @description 完全封装JTopo基础api,解决鼠标cursor报错和滚轮缩放方向bug,修改源码中callee,现支持es5严格格式不报错 4 | * @author Alan Chen 5 | * @version 2019/3/27 6 | * 7 | * @constructor 8 | * @param {selector|String} DOM 必选 构造函数接受一个参数,当前绑定topo的canvas类名或id名,与jq选择器一致,必须是canvas标签 9 | * @instance 实例方法 10 | * @method setData(dataset) 设置topo图的节点、连线和节点组等数据 11 | * dataset包含3个可选键 nodes(Array)节点 edges(Array)连线 containers(Array)节点组 12 | * 13 | * nodes 数组项为对象,必选id(Number | String 节点唯一标识) 14 | * 可选键如下: 15 | * x (Number)--节点的x坐标,不填会随机生成一个在canvas容器范围内的值 16 | * y (Number)-- 节点的y坐标,不填会随机生成一个在canvas容器范围内的值 17 | * label (String)--节点名称 18 | * shape (String)--节点形状,只能选择‘circle’(圆形)和‘image’(图片),默认为正方形 19 | * group (String) -- 多个节点放入一个样式组的组名,这里group表示多个节点同时调用option里对应group的配置 20 | * image (String URL)--如果选择shape为image,这里必须为图片的路径 21 | * size (Array)--节点大小,数组项为数字,分别标识宽和高,默认为32.如果节点为圆形,则表示半径,如果为图片,不设置size,则为原图大小 22 | * color (RGB)--节点颜色,默认为‘22,124,255’,必须为RGB格式的字符串 23 | * opacity (Number)--节点透明度,默认为1 24 | * font (Object)--节点内字体样式 25 | * color(RGB)--字体颜色,默认为‘0,0,0’,必须为RGB格式的字符串 26 | * size(String)--字体字号和样式,必须带上fontfamliy,例:‘16px 微软雅黑’ 27 | * position(String)--字体布局,只能选择‘Top’(上方),‘Middle’(中间)和‘Bottom’(下方),默认‘Bottom’ 28 | * border (Object)--节点边框样式 29 | * width(Number)--边框宽度,默认为0 30 | * radius(Number)--边框弧度,默认为null 31 | * color(RGB)--边框颜色,默认为‘255,255,255’,必须为RGB格式的字符串 32 | * transform (Object)--节点形变 33 | * rotate(Number)--节点旋转角度,默认为0 34 | * scale(Number | Array)--节点缩放,默认不缩放,参数若为数字,表示x和y缩放一致,若为数组,分别表示x和y方向缩放 35 | * alarm (Object)--节点告警,会在节点上方显示告警内容 36 | * name(String)--告警内容 37 | * color(RGB)--告警背景颜色,必须为RGB格式的字符串 38 | * opacity(Number)--告警透明度,默认为1 39 | * visible (Boolean)--节点是否可见,默认true 40 | * dragable (Boolean)--节点是否可拖动,默认true 41 | * selected (Boolean)--节点是否一加载被选中,默认false 42 | * editable (Boolean)--节点是否可编辑,默认false 43 | * 44 | * edges 数组项为对象,必选from(Number | String 起始节点的id)和to(Number | String 终止节点的id) 45 | * 可选键如下: 46 | * title (String)--连线名称,会一直显示 47 | * tips (String)--连线名称,只有hover才会显示,会覆盖掉title 48 | * style (Object)--连线的样式设置 49 | * arrow (Number)--箭头,默认为null,数字越大,箭头越大 50 | * arrowEnabled (Boolean)--箭头方向是否正确显示 51 | * color (RGB)--连线颜色,默认为‘22,124,255’,必须为RGB格式的字符串 52 | * fontColor (RGB)--字体颜色,默认为‘0,0,0’,必须为RGB格式的字符串 53 | * dashed (Number)--箭头虚线的间隔,默认为0,数字越大,箭头虚线间隔越大 54 | * 55 | * containers 数组项为对象,必选nodes(Array | 节点组包含的节点id) 56 | * 可选键如下: 57 | * name (String)--节点组名称 58 | * 可选键还包含color、font、border和可配置项,用法与nodes一致。 59 | * 主要包括背景色、字体、边框和可配置项。 60 | * 61 | * @method setOption(options) 设置画布舞台和group等配置 62 | * options包含2个可选键 groups(Object)多个节点的重复配置 config(Object)画布舞台的整体配置 63 | * 64 | * groups 对象, 65 | * 键名为group的名称,必须和node中的group值一致,只会匹配与当前键名相同的node来配置样式 66 | * 键值为一个对象,表示样式配置,除去label、group和size等选项其余可选键与nodes一致。 67 | * 主要包括背景色、字体、边框、告警、透明度、形变和可配置项。 68 | * shape只能选择‘image’并且必须同时设置image属性,可以设置节点为图片 69 | * config 对象 70 | * 可选键如下: 71 | * eagleEye (Boolean)--控制鹰眼的显示。默认为false,关闭鹰眼 72 | * disableWheelZoom (Boolean)--是否禁用滚轮缩放整个画布。默认为false,可以滚轮缩放 73 | * 74 | * @method update(dataset) 更新topo图的节点和连线等数据 75 | * dataset包含2个可选键 nodes(Array)节点 edges(Array)连线 76 | * 77 | * nodes 数组项为对象 78 | * 必选id(String或Number类型)。id为node创建时手动传入 79 | * 可选键:同setData()中nodes的可选键,除了没有image,shape和group,其余完全一致 80 | * 81 | * edges 数组项为对象 82 | * 必选id(Symbol类型) 83 | * id为edge创建时插件生成。由于Symbol的唯一性,所以edge的id只能从监听事件返回参数中获取(target.data.id) 84 | * 可选键:同setData()中edges的可选键完全一致 85 | * 86 | * @method add(dataset) 添加一个或多个topo图的节点、连线和节点组等数据 87 | * dataset包含3个可选键 nodes(Array)节点 edges(Array)连线 containers(Array)节点组 88 | * 注:参数和用法与setData()完全一致 89 | * 90 | * @method remove(dataset) 删除一个或多个topo图的节点和连线等数据 91 | * dataset包含2个可选键 nodes(Array)节点 edges(Array)连线 92 | * nodes的数组项为String或Number类型,只有一个值,为node创建时手动传入的id 93 | * edges的数组项为Symbol类型,为edge创建时插件生成的id。由于Symbol的唯一性,所以edge的id只能从监听事件返回参数中获取(target.data.id) 94 | * 例:remove({ 95 | * nodes: ['1', '2'], 96 | * edges: [Symbol(0), Symbol(1)] 97 | * }) 98 | * 99 | * @method getPosition() 获取当前所有节点的位置信息 return一个数组。 100 | * 101 | * @method setPosition(positions) 设置对应id的节点坐标位置。 102 | * positions为数组,数组项必须包含id、x和y3个键值。x和y必须是数字 103 | * 104 | * @method on(eventType,cb) 绑定事件,有2个参数 105 | * 目前支持 106 | * 'click'(单击),'dbclcik'(双击) 107 | * 'contextmenu'(右键) 108 | * 'mousemove'(鼠标移动) 109 | * 'drag'(拖动)和dragEnd'(拖动节点结束) 110 | * 回调函数return 一个对象。包含 111 | * DOM --> 鼠标在body DOM元素上的page位移坐标(pageX和pageY) 112 | * canvas --> 鼠标在canvas画布上的x和y坐标 113 | * target --> 鼠标当前选中的node或edge或container等数据,包含选中的所有canvas属性以及自定义传入的数据(挂载在data属性上) 114 | * type --> 当前选中的是哪种类型,如:node或link或container 115 | * 116 | * @method off(eventType) 解绑定事件,只有一个参数,目前支持 117 | * 'click'(单击),'dbclcik'(双击) 118 | * 'contextmenu'(右键) 119 | * 'mousemove'(鼠标移动) 120 | * 'drag'(拖动)和dragEnd'(拖动节点结束) 121 | * 122 | * @summary 123 | (1) setOption必须在setData之后调用,setData可以连续调用,数据不会叠加 124 | (2) dragEnd事件只有在鼠标移动后,发现node节点有位置移动才触发 125 | (3) 可以在nodes或者edges的单个对象里传入别的数据,这些数据存在nodes或者edges实例的data属性里 126 | * */ 127 | 128 | 129 | import {JTopo} from'./JTopoCode' 130 | class Topo { 131 | constructor(DOM) { 132 | this.version = require('../package.json').version 133 | this.baseVersion = `Based on JTopo-${JTopo.version}` 134 | this.eventLoop = [] 135 | this.init(DOM) 136 | } 137 | init(i) { 138 | this.canvas = document.querySelector(i) 139 | this.stage = new JTopo.Stage(this.canvas) 140 | this.stage.wheelZoom = 0.85 141 | this.scene = new JTopo.Scene(this.stage) 142 | } 143 | setNodeConfig(node, config) { 144 | const isValueExist = v => { 145 | const type = typeof v 146 | if(type == 'string') { 147 | return true 148 | } 149 | else if(type == 'number') { 150 | return true 151 | } 152 | else if(type == 'undefined') { 153 | return false 154 | } 155 | else if(type == 'object') { 156 | return v === null? false: true 157 | } 158 | } 159 | //node节点文本 160 | if(isValueExist(config.label)) { 161 | node.text = config.label 162 | } 163 | //node节点颜色 164 | if(isValueExist(config.color)) { 165 | node.fillColor = config.color 166 | } 167 | //node节点透明度 168 | if(isValueExist(config.opacity)) { 169 | node.alpha = config.opacity 170 | } 171 | //node节点内字体 172 | if(isValueExist(config.font)) { 173 | //字体颜色 174 | if(isValueExist(config.font.color)) { 175 | node.fontColor = config.font.color 176 | } 177 | //字体大小和格式 178 | if(isValueExist(config.font.size)) { 179 | node.font = config.font.size 180 | } 181 | //字体位置 182 | if(isValueExist(config.font.position)) { 183 | node.textPosition = config.font.position + '_Center' 184 | } 185 | } 186 | //node节点边框 187 | if(isValueExist(config.border)) { 188 | //边框宽度 189 | if(isValueExist(config.border.width)) { 190 | node.borderWidth = config.border.width 191 | } 192 | //边框弧度 193 | if(isValueExist(config.border.radius)) { 194 | node.borderRadius = config.border.radius 195 | } 196 | //边框颜色 197 | if(isValueExist(config.border.color)) { 198 | node.borderColor = config.border.color 199 | } 200 | } 201 | //node节点形变 202 | if(isValueExist(config.transform)) { 203 | //z轴旋转 204 | if(isValueExist(config.transform.rotate)) { 205 | node.rotate = config.transform.rotate 206 | } 207 | //x和y放向缩放 208 | if(typeof config.transform.scale == 'number') { 209 | node.scaleX = config.transform.scale 210 | node.scaleY = config.transform.scale 211 | } 212 | else if(Array.isArray(config.transform.scale)) { 213 | node.scaleX = config.transform.scale[0] 214 | node.scaleY = config.transform.scale[1] 215 | } 216 | } 217 | //node告警配置 218 | if(isValueExist(config.alarm)) { 219 | if(isValueExist(config.alarm.name)) { 220 | node.alarm = config.alarm.name 221 | } 222 | if(isValueExist(config.alarm.color)) { 223 | node.alarmColor = config.alarm.color 224 | } 225 | if(isValueExist(config.alarm.opacity)) { 226 | node.alarmAlpha = config.alarm.opacity 227 | } 228 | } 229 | //node的可配置性 230 | node.visible = config.visible == undefined?true:config.visible 231 | node.dragable = config.dragable == undefined?true:config.dragable 232 | node.selected = config.selected || false 233 | node.editAble = config.editable || false 234 | } 235 | setData({nodes, edges, containers}, clear=true) { 236 | if(Boolean(clear)) { 237 | this.scene.clear() //每次先清除画布,避免数据重复 238 | } 239 | //ndoe节点 240 | nodes && nodes.forEach( (item, index) => { 241 | let node 242 | const x = Number.parseFloat(item.x) 243 | const y = Number.parseFloat(item.y) 244 | //node的形状 245 | if(item.shape == 'circle') { 246 | node = new JTopo.CircleNode(item.label) 247 | if(item.size) node.radius = Math.max(...item.size) 248 | } 249 | else if(item.shape == 'image') { 250 | node = new JTopo.Node(item.label) 251 | if(item.size) { 252 | node.setImage(item.image) 253 | node.setBound(x,y,...item.size) 254 | } 255 | else { 256 | node.setImage(item.image, true) 257 | } 258 | } 259 | else{ 260 | node = new JTopo.Node(item.label) 261 | if(item.size) { 262 | node.setSize(...item.size) 263 | } 264 | } 265 | node.fontColor = '0,0,0' //所有node默认字体为黑色 266 | this.setNodeConfig(node, item) 267 | //如果没有传入坐标值,默认给一个随机坐标 268 | const nodeX = x || Math.random()*this.canvas.width 269 | const nodeY = y || Math.random()*this.canvas.height 270 | node.setLocation(nodeX,nodeY) 271 | node.data = item 272 | node._index = index //给每个node添加一个数字索引,用于排序连线的两个节点顺序 273 | this.scene.add(node) 274 | }) 275 | this.nodes = this.scene.childs.filter(child => child.elementType == 'node') 276 | 277 | //edge连线 278 | edges && edges.forEach((item, index) => { 279 | const fromNode = this.nodes.find(child => item.from !=undefined &&child.data.id == item.from) 280 | const toNode = this.nodes.find(child => item.to !=undefined && child.data.id == item.to) 281 | //将连线的两个节点排序,永远使from的_index小于to的_index。避免相反方向连线重叠! 282 | const sortNodes = [fromNode, toNode].sort((a,b) => a._index - b._index) 283 | // 通过style.arrowEnabled来判断箭头方向是正确显示还是排序显示避免连线重叠 284 | const isArrowRight = item.style && item.style.arrowEnabled 285 | const source = isArrowRight? fromNode: sortNodes[0] 286 | const destinate = isArrowRight? toNode: sortNodes[1] 287 | if(fromNode && toNode) { 288 | let link = new JTopo.Link(source, destinate, item.title) 289 | link.bundleOffset = 35 290 | link.bundleGap = 20 291 | link.textOffsetY = 5 292 | 293 | if(item.tips) { //tips显示文本,hover效果,会覆盖掉title 294 | link.mouseover(() =>{ 295 | link.text = item.tips 296 | }) 297 | link.mouseout(() =>{ 298 | link.text = '' 299 | }) 300 | } 301 | if(item.style) { 302 | link.arrowsRadius = item.style.arrow 303 | link.dashedPattern = item.style.dashed 304 | link.strokeColor = item.style.color 305 | } 306 | link.fontColor = (item.style && item.style.fontColor) || '0,0,0' 307 | 308 | link.data = item 309 | // 生成一个不会重复的值作为link的唯一id 310 | // link.data.id = link._id 311 | link.data.id = Symbol(index) 312 | this.scene.add(link) 313 | } 314 | }) 315 | this.edges = this.scene.childs.filter(child => child.elementType == 'link') 316 | 317 | //container节点组 318 | containers && containers.forEach( item => { 319 | const wrap = new JTopo.Container(item.name) 320 | wrap.fontColor = '0,0,0' 321 | this.setNodeConfig(wrap,item) 322 | const wrapNodes = this.nodes.filter(a => { 323 | return item.nodes.includes(a.data.id) 324 | }) 325 | wrapNodes.forEach(a => { 326 | wrap.add(a) 327 | }) 328 | this.scene.add(wrap) 329 | }) 330 | this.containers = this.scene.childs.filter(child => child.elementType == 'container') 331 | //每次渲染完数据后都判断是否配置规则 332 | this.options && this.setOption(this.options) 333 | } 334 | setOption(options) { 335 | this.options = options 336 | const groups = options.groups 337 | const config = options.config 338 | //多个node相同的配置成为group组 339 | if(groups) { 340 | const entries = Object.entries(groups) 341 | let groupNodes = [] 342 | entries.forEach(item => { 343 | const nodes = this.nodes.filter(a => { 344 | return a.data.group == item[0] 345 | }) 346 | groupNodes.push({node: nodes, option: item[1]}) 347 | }) 348 | groupNodes.forEach(a => { 349 | a.node.forEach(b => { 350 | // //设置node为图片 351 | if(a.option.shape == 'image' && a.option.image) { 352 | b.setImage(a.option.image, true) 353 | } 354 | //通过config来匹配操作 355 | this.setNodeConfig(b, a.option) 356 | }) 357 | }) 358 | } 359 | //舞台画布的整体配置 360 | this.stage.eagleEye.visible = config && config.eagleEye //鹰眼默认关闭 361 | this.stage.wheelZoom = config && Boolean(config.disableWheelZoom) 362 | ? false 363 | : 0.85 //默认开启滚轮缩放 364 | 365 | } 366 | update({nodes, edges}) { 367 | //ndoe节点 368 | nodes && nodes.forEach( item => { 369 | let targetNode = this.nodes.find(child => child.data.id == item.id) 370 | if(Boolean(targetNode)) { 371 | this.setNodeConfig(targetNode, item) 372 | if(item.x && item.y) { 373 | targetNode.setLocation(item.x, item.y) 374 | } 375 | if(item.size) { 376 | targetNode.setSize(...item.size) 377 | } 378 | 379 | // 合并额外传入的值data 380 | targetNode.data = {...targetNode.data, ...item} 381 | } 382 | else { 383 | console.warn(`没有对应id为${item.id}的节点,所以无法更新`) 384 | } 385 | }) 386 | 387 | //edge连线 388 | edges && edges.forEach( item => { 389 | let targetLink = this.edges.find(child => child.data.id == item.id) 390 | if(Boolean(targetLink)) { 391 | targetLink.text = String(item.title) 392 | if(item.tips) { //tips显示文本,hover效果,会覆盖掉title 393 | targetLink.removeEventListener('mouseover') 394 | targetLink.removeEventListener('mouseout') 395 | targetLink.mouseover(() =>{ 396 | targetLink.text = String(item.tips) 397 | }) 398 | targetLink.mouseout(() =>{ 399 | targetLink.text = '' 400 | }) 401 | } 402 | 403 | if(item.style) { 404 | if(item.style.color) { 405 | targetLink.strokeColor = item.style.color 406 | } 407 | if(item.style.fontColor) { 408 | targetLink.fontColor = item.style.fontColor 409 | } 410 | if(typeof item.style.arrow == 'number' || item.style.arrow === null) { 411 | targetLink.arrowsRadius = item.style.arrow 412 | } 413 | if(typeof item.style.dashed == 'number') { 414 | targetLink.dashedPattern = item.style.dashed 415 | } 416 | } 417 | 418 | // 合并额外传入的值data 419 | targetLink.data = {...targetLink.data, ...item} 420 | } 421 | else { 422 | console.warn(`没有对应id为${item.id}的连线,所以无法更新`) 423 | } 424 | }) 425 | } 426 | add({nodes, edges, containers}) { 427 | this.setData({nodes, edges, containers}, false) 428 | } 429 | remove({nodes, edges}) { 430 | //ndoe节点 431 | nodes && nodes.forEach( item => { 432 | let targetNode = this.nodes.find(child => child.data.id == item) 433 | if(Boolean(targetNode)) { 434 | this.scene.remove(targetNode) 435 | } 436 | else { 437 | console.warn(`没有对应id为${item.id}的节点,所以无法删除`) 438 | } 439 | }) 440 | 441 | //edge连线 442 | edges && edges.forEach( item => { 443 | let targetLink = this.edges.find(child => child.data.id == item) 444 | if(Boolean(targetLink)) { 445 | this.scene.remove(targetLink) 446 | } 447 | else { 448 | console.warn(`没有对应id为${item.id}的连线,所以无法删除`) 449 | } 450 | }) 451 | } 452 | eventHandler(name, cb, flag) { 453 | const eventHandler = { 454 | // tag是JTopo封装的事件名,raw是js原生事件名 455 | click: { 456 | tag: 'click', 457 | raw: 'click' 458 | }, 459 | dbclick: { 460 | tag: 'dbclick', 461 | raw: 'click' 462 | }, 463 | mousemove: { 464 | tag: 'mousemove', 465 | raw: 'mousemove' 466 | }, 467 | contextmenu: { 468 | tag: 'mouseup', 469 | raw: 'mouseup' 470 | }, 471 | drag: { 472 | tag: 'mousedrag', 473 | raw: 'mousemove' 474 | }, 475 | dragEnd: { 476 | tag: 'mouseup', 477 | raw: 'mouseup' 478 | } 479 | } 480 | const targetEvent = eventHandler[name].tag 481 | 482 | if(targetEvent) { 483 | if(flag) {//绑定事件 484 | this.eventLoop.push(name) 485 | 486 | let preNodesLocations = null 487 | this.scene.addEventListener('mousedown', e => { 488 | preNodesLocations = this.getPosition() //指定事件触发前的所有nodes节点的位置 489 | }) 490 | 491 | this.scene.addEventListener(targetEvent, e => { 492 | const type = e.target? e.target.elementType: null 493 | const result = { 494 | DOM: {pageX: e.pageX, pageY: e.pageY}, 495 | canvas: {x: e.x, y: e.y}, 496 | target: e.target, 497 | type 498 | } 499 | 500 | const latterNodesLocations = this.getPosition() //指定事件触发后的所有nodes节点的位置 501 | //判断所有节点是否都未移动坐标 502 | const didEveryNodesNotMoved = name == 'dragEnd' && latterNodesLocations.every(a => { 503 | const relativeNode = preNodesLocations.find(b => b.id == a.id) 504 | return (a.x == relativeNode.x && a.y == relativeNode.y) 505 | }) 506 | 507 | const contextmenuTrigger = (name == 'contextmenu' && e.button == 2) 508 | const dragEndTrigger = (name == 'dragEnd' && e.button == 0 && !didEveryNodesNotMoved) 509 | const dragTrigger = (name == 'drag') 510 | const clickTrigger = (name == 'click') 511 | const dbclickTrigger = (name == 'dbclick') 512 | const mousemoveTrigger = (name == 'mousemove') 513 | const eventTrigger = (contextmenuTrigger || dragEndTrigger || dragTrigger || clickTrigger || dbclickTrigger || mousemoveTrigger) 514 | 515 | if(eventTrigger) { 516 | cb && cb(result) 517 | } 518 | }) 519 | } 520 | else { //解绑事件 521 | this.scene.removeEventListener(targetEvent) 522 | const index = this.eventLoop.findIndex(a => a == name) 523 | this.eventLoop.splice(index, 1) 524 | } 525 | } 526 | else { 527 | throw new Error('The event name is invalid, only clcik, dbclick and contextmenu is valid!') 528 | } 529 | } 530 | on(eType, cb) { 531 | this.eventHandler(eType, cb, true) 532 | } 533 | off(eType) { 534 | this.eventHandler(eType) 535 | } 536 | getPosition() { 537 | const allNodesPosition = this.nodes.map(a => { 538 | const id = a.data.id 539 | const x = a.x 540 | const y = a.y 541 | return {id, x, y} 542 | }) 543 | return allNodesPosition 544 | } 545 | setPosition(positions) { 546 | const isArrayNotNull = Array.isArray(positions) && positions.length> 0 547 | isArrayNotNull && positions.forEach( a => { 548 | const moveNodes = this.nodes.find(b => b.data.id == a.id) 549 | if(moveNodes) { 550 | const x = Number.parseFloat(a.x) 551 | const y = Number.parseFloat(a.y) 552 | moveNodes.setLocation(x, y) 553 | } 554 | else { 555 | console.warn(`没有对应id为${a.id}的节点,所以无法设置坐标`) 556 | } 557 | }) 558 | } 559 | } 560 | 561 | export default Topo -------------------------------------------------------------------------------- /dist/ctopo.js: -------------------------------------------------------------------------------- 1 | !function(t,e){"object"==typeof exports&&"object"==typeof module?module.exports=e():"function"==typeof define&&define.amd?define("ctopo",[],e):"object"==typeof exports?exports.ctopo=e():t.ctopo=e()}("undefined"!=typeof self?self:this,function(){return function(t){var e={};function i(n){if(e[n])return e[n].exports;var s=e[n]={i:n,l:!1,exports:{}};return t[n].call(s.exports,s,s.exports,i),s.l=!0,s.exports}return i.m=t,i.c=e,i.d=function(t,e,n){i.o(t,e)||Object.defineProperty(t,e,{configurable:!1,enumerable:!0,get:n})},i.n=function(t){var e=t&&t.__esModule?function(){return t.default}:function(){return t};return i.d(e,"a",e),e},i.o=function(t,e){return Object.prototype.hasOwnProperty.call(t,e)},i.p="",i(i.s=41)}([function(t,e,i){var n=i(23)("wks"),s=i(16),o=i(1).Symbol,r="function"==typeof o;(t.exports=function(t){return n[t]||(n[t]=r&&o[t]||(r?o:s)("Symbol."+t))}).store=n},function(t,e){var i=t.exports="undefined"!=typeof window&&window.Math==Math?window:"undefined"!=typeof self&&self.Math==Math?self:Function("return this")();"number"==typeof __g&&(__g=i)},function(t,e){var i=t.exports={version:"2.5.7"};"number"==typeof __e&&(__e=i)},function(t,e,i){var n=i(9),s=i(31),o=i(18),r=Object.defineProperty;e.f=i(4)?Object.defineProperty:function(t,e,i){if(n(t),e=o(e,!0),n(i),s)try{return r(t,e,i)}catch(t){}if("get"in i||"set"in i)throw TypeError("Accessors not supported!");return"value"in i&&(t[e]=i.value),t}},function(t,e,i){t.exports=!i(11)(function(){return 7!=Object.defineProperty({},"a",{get:function(){return 7}}).a})},function(t,e){var i={}.hasOwnProperty;t.exports=function(t,e){return i.call(t,e)}},function(t,e,i){var n=i(3),s=i(12);t.exports=i(4)?function(t,e,i){return n.f(t,e,s(1,i))}:function(t,e,i){return t[e]=i,t}},function(t,e,i){var n=i(34),s=i(20);t.exports=function(t){return n(s(t))}},function(t,e,i){var n=i(1),s=i(2),o=i(30),r=i(6),a=i(5),h=function(t,e,i){var l,u,c,d=t&h.F,f=t&h.G,p=t&h.S,v=t&h.P,g=t&h.B,m=t&h.W,y=f?s:s[e]||(s[e]={}),x=y.prototype,w=f?n:p?n[e]:(n[e]||{}).prototype;for(l in f&&(i=e),i)(u=!d&&w&&void 0!==w[l])&&a(y,l)||(c=u?w[l]:i[l],y[l]=f&&"function"!=typeof w[l]?i[l]:g&&u?o(c,n):m&&w[l]==c?function(t){var e=function(e,i,n){if(this instanceof t){switch(arguments.length){case 0:return new t;case 1:return new t(e);case 2:return new t(e,i)}return new t(e,i,n)}return t.apply(this,arguments)};return e.prototype=t.prototype,e}(c):v&&"function"==typeof c?o(Function.call,c):c,v&&((y.virtual||(y.virtual={}))[l]=c,t&h.R&&x&&!x[l]&&r(x,l,c)))};h.F=1,h.G=2,h.S=4,h.P=8,h.B=16,h.W=32,h.U=64,h.R=128,t.exports=h},function(t,e,i){var n=i(10);t.exports=function(t){if(!n(t))throw TypeError(t+" is not an object!");return t}},function(t,e){t.exports=function(t){return"object"==typeof t?null!==t:"function"==typeof t}},function(t,e){t.exports=function(t){try{return!!t()}catch(t){return!0}}},function(t,e){t.exports=function(t,e){return{enumerable:!(1&t),configurable:!(2&t),writable:!(4&t),value:e}}},function(t,e){t.exports={}},function(t,e,i){var n=i(33),s=i(24);t.exports=Object.keys||function(t){return n(t,s)}},function(t,e){t.exports=!0},function(t,e){var i=0,n=Math.random();t.exports=function(t){return"Symbol(".concat(void 0===t?"":t,")_",(++i+n).toString(36))}},function(t,e){e.f={}.propertyIsEnumerable},function(t,e,i){var n=i(10);t.exports=function(t,e){if(!n(t))return t;var i,s;if(e&&"function"==typeof(i=t.toString)&&!n(s=i.call(t)))return s;if("function"==typeof(i=t.valueOf)&&!n(s=i.call(t)))return s;if(!e&&"function"==typeof(i=t.toString)&&!n(s=i.call(t)))return s;throw TypeError("Can't convert object to primitive value")}},function(t,e){var i={}.toString;t.exports=function(t){return i.call(t).slice(8,-1)}},function(t,e){t.exports=function(t){if(void 0==t)throw TypeError("Can't call method on "+t);return t}},function(t,e){var i=Math.ceil,n=Math.floor;t.exports=function(t){return isNaN(t=+t)?0:(t>0?n:i)(t)}},function(t,e,i){var n=i(23)("keys"),s=i(16);t.exports=function(t){return n[t]||(n[t]=s(t))}},function(t,e,i){var n=i(2),s=i(1),o=s["__core-js_shared__"]||(s["__core-js_shared__"]={});(t.exports=function(t,e){return o[t]||(o[t]=void 0!==e?e:{})})("versions",[]).push({version:n.version,mode:i(15)?"pure":"global",copyright:"© 2018 Denis Pushkarev (zloirock.ru)"})},function(t,e){t.exports="constructor,hasOwnProperty,isPrototypeOf,propertyIsEnumerable,toLocaleString,toString,valueOf".split(",")},function(t,e){e.f=Object.getOwnPropertySymbols},function(t,e,i){var n=i(20);t.exports=function(t){return Object(n(t))}},function(t,e,i){var n=i(3).f,s=i(5),o=i(0)("toStringTag");t.exports=function(t,e,i){t&&!s(t=i?t:t.prototype,o)&&n(t,o,{configurable:!0,value:e})}},function(t,e,i){e.f=i(0)},function(t,e,i){var n=i(1),s=i(2),o=i(15),r=i(28),a=i(3).f;t.exports=function(t){var e=s.Symbol||(s.Symbol=o?{}:n.Symbol||{});"_"==t.charAt(0)||t in e||a(e,t,{value:r.f(t)})}},function(t,e,i){var n=i(46);t.exports=function(t,e,i){if(n(t),void 0===e)return t;switch(i){case 1:return function(i){return t.call(e,i)};case 2:return function(i,n){return t.call(e,i,n)};case 3:return function(i,n,s){return t.call(e,i,n,s)}}return function(){return t.apply(e,arguments)}}},function(t,e,i){t.exports=!i(4)&&!i(11)(function(){return 7!=Object.defineProperty(i(32)("div"),"a",{get:function(){return 7}}).a})},function(t,e,i){var n=i(10),s=i(1).document,o=n(s)&&n(s.createElement);t.exports=function(t){return o?s.createElement(t):{}}},function(t,e,i){var n=i(5),s=i(7),o=i(48)(!1),r=i(22)("IE_PROTO");t.exports=function(t,e){var i,a=s(t),h=0,l=[];for(i in a)i!=r&&n(a,i)&&l.push(i);for(;e.length>h;)n(a,i=e[h++])&&(~o(l,i)||l.push(i));return l}},function(t,e,i){var n=i(19);t.exports=Object("z").propertyIsEnumerable(0)?Object:function(t){return"String"==n(t)?t.split(""):Object(t)}},function(t,e,i){var n=i(21),s=Math.min;t.exports=function(t){return t>0?s(n(t),9007199254740991):0}},function(t,e,i){"use strict";var n=i(53)(!0);i(37)(String,"String",function(t){this._t=String(t),this._i=0},function(){var t,e=this._t,i=this._i;return i>=e.length?{value:void 0,done:!0}:(t=n(e,i),this._i+=t.length,{value:t,done:!1})})},function(t,e,i){"use strict";var n=i(15),s=i(8),o=i(38),r=i(6),a=i(13),h=i(54),l=i(27),u=i(57),c=i(0)("iterator"),d=!([].keys&&"next"in[].keys()),f=function(){return this};t.exports=function(t,e,i,p,v,g,m){h(i,e,p);var y,x,w,b=function(t){if(!d&&t in P)return P[t];switch(t){case"keys":case"values":return function(){return new i(this,t)}}return function(){return new i(this,t)}},E=e+" Iterator",T="values"==v,S=!1,P=t.prototype,M=P[c]||P["@@iterator"]||v&&P[v],k=M||b(v),L=v?T?b("entries"):k:void 0,I="Array"==e&&P.entries||M;if(I&&(w=u(I.call(new t)))!==Object.prototype&&w.next&&(l(w,E,!0),n||"function"==typeof w[c]||r(w,c,f)),T&&M&&"values"!==M.name&&(S=!0,k=function(){return M.call(this)}),n&&!m||!d&&!S&&P[c]||r(P,c,k),a[e]=k,a[E]=f,v)if(y={values:T?k:b("values"),keys:g?k:b("keys"),entries:L},m)for(x in y)x in P||o(P,x,y[x]);else s(s.P+s.F*(d||S),e,y);return y}},function(t,e,i){t.exports=i(6)},function(t,e,i){var n=i(9),s=i(55),o=i(24),r=i(22)("IE_PROTO"),a=function(){},h=function(){var t,e=i(32)("iframe"),n=o.length;for(e.style.display="none",i(56).appendChild(e),e.src="javascript:",(t=e.contentWindow.document).open(),t.write("