├── .github └── workflows │ ├── build.yml │ ├── deploy.yml │ └── release.yml ├── .gitignore ├── LICENSE.md ├── README-CN.md ├── README.md ├── docs ├── canvas-tree-chart-CN.md ├── canvas-tree-chart.md ├── react-tree-chart.md ├── tech-underneath.md ├── vue-tree-chart-CN.md └── vue-tree-chart.md ├── lerna.json ├── package-lock.json ├── package.json ├── packages ├── react-tree-chart │ ├── .babelrc │ ├── .gitignore │ ├── README.md │ ├── package-lock.json │ ├── package.json │ ├── public │ │ ├── favicon.ico │ │ ├── index.html │ │ ├── logo192.png │ │ ├── logo512.png │ │ ├── manifest.json │ │ └── robots.txt │ ├── rollup.config.js │ ├── src │ │ ├── demo │ │ │ ├── App.css │ │ │ ├── App.tsx │ │ │ ├── index.css │ │ │ ├── logo.svg │ │ │ └── react-app-env.d.ts │ │ ├── index.css │ │ ├── index.tsx │ │ ├── react-app-env.d.ts │ │ ├── setupTests.ts │ │ └── tree-chart │ │ │ ├── react-tree-chart.scss │ │ │ ├── react-tree-chart.tsx │ │ │ └── typings.d.ts │ └── tsconfig.json ├── tree-chart-core │ ├── .eslintrc.js │ ├── .gitignore │ ├── package-lock.json │ ├── package.json │ ├── src │ │ ├── base │ │ │ └── uuid.ts │ │ ├── index.ts │ │ └── tree-chart │ │ │ ├── constant.ts │ │ │ ├── index.ts │ │ │ ├── tree-chart.ts │ │ │ └── util.ts │ └── tsconfig.json ├── tree-chart-demo │ ├── build │ │ ├── webpack.config.base.js │ │ ├── webpack.config.dev.js │ │ └── webpack.config.prod.js │ ├── package-lock.json │ ├── package.json │ ├── src │ │ ├── base │ │ │ ├── color-util.ts │ │ │ ├── data-generator.ts │ │ │ ├── utils.ts │ │ │ └── uuid.ts │ │ ├── components │ │ │ ├── CanvasTree.vue │ │ │ ├── VueTreeDemo.vue │ │ │ └── org-chart.ts │ │ ├── demo │ │ │ ├── App.vue │ │ │ ├── google-icon.css │ │ │ ├── main.ts │ │ │ └── router │ │ │ │ ├── constant.ts │ │ │ │ └── index.ts │ │ └── vue.shims.d.ts │ ├── template │ │ └── index.html │ └── tsconfig.json ├── vue-tree-chart │ ├── .babelrc │ ├── .editorconfig │ ├── .eslintignore │ ├── .eslintrc.js │ ├── .postcssrc.js │ ├── .prettierignore │ ├── README.md │ ├── build │ │ ├── webpack.config.base.js │ │ ├── webpack.config.dev.js │ │ └── webpack.config.library.js │ ├── library │ │ ├── .gitkeep │ │ └── vue-tree-chart.js │ ├── package-lock.json │ ├── package.json │ ├── src │ │ ├── base │ │ │ └── uuid.ts │ │ ├── demo │ │ │ ├── App.vue │ │ │ └── main.ts │ │ ├── vue-tree │ │ │ ├── VueTree.vue │ │ │ └── index.ts │ │ └── vue.shims.d.ts │ ├── template │ │ ├── favicon.ico │ │ └── index.html │ └── tsconfig.json └── vue3-tree-chart │ ├── .gitignore │ ├── README.md │ ├── babel.config.js │ ├── package-lock.json │ ├── package.json │ ├── public │ ├── favicon.ico │ └── index.html │ ├── src │ ├── App.vue │ ├── assets │ │ └── logo.png │ ├── base │ │ └── uuid.js │ ├── components │ │ └── HelloWorld.vue │ ├── main.js │ └── vue-tree │ │ ├── VueTree.vue │ │ └── index.js │ └── vite.config.js └── screenshots ├── demo.jpeg ├── org-chart.gif └── unique_color.png /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: Check Build 5 | 6 | on: 7 | push: 8 | branches: [master] 9 | pull_request: 10 | branches: [master] 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | 16 | strategy: 17 | matrix: 18 | node-version: [14.x] 19 | 20 | steps: 21 | - uses: actions/checkout@v2 22 | - name: Use Node.js ${{ matrix.node-version }} 23 | uses: actions/setup-node@v1 24 | with: 25 | node-version: ${{ matrix.node-version }} 26 | - name: install dependencies 27 | run: npm run install:deps 28 | - name: (tree-chart-core)Build 29 | working-directory: packages/tree-chart-core 30 | run: npm run build 31 | - name: (vue-tree-chart)Build 32 | working-directory: packages/vue-tree-chart 33 | run: npm run build:component 34 | - name: (tree-chart-demo)Build 35 | working-directory: packages/tree-chart-demo 36 | run: npm run build 37 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: Deploy Github Page 5 | 6 | on: 7 | push: 8 | branches: [ master ] 9 | 10 | jobs: 11 | build: 12 | 13 | runs-on: ubuntu-latest 14 | 15 | strategy: 16 | matrix: 17 | node-version: [14.x] 18 | 19 | steps: 20 | - uses: actions/checkout@v2 21 | - name: Use Node.js ${{ matrix.node-version }} 22 | uses: actions/setup-node@v1 23 | with: 24 | node-version: ${{ matrix.node-version }} 25 | - name: install dependencies 26 | run: npm run install:deps 27 | - name: Build 28 | working-directory: packages/tree-chart-demo 29 | run: | 30 | npm run build 31 | - name: Deploy 🚀 32 | uses: JamesIves/github-pages-deploy-action@releases/v3 33 | with: 34 | ACCESS_TOKEN: ${{ secrets.ACCESS_TOKEN }} 35 | BRANCH: gh-pages # The branch the action should deploy to. 36 | FOLDER: packages/tree-chart-demo/dist # The folder the action should deploy. 37 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: Release 5 | 6 | on: 7 | push: 8 | tags: 9 | - "*" 10 | 11 | jobs: 12 | build: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v2 16 | - uses: actions/setup-node@v2 17 | with: 18 | node-version: "12.x" 19 | registry-url: "https://registry.npmjs.org" 20 | - name: install dependencies 21 | run: npm run install:deps 22 | - run: npm run build:component 23 | working-directory: packages/vue-tree-chart 24 | - run: npm publish 25 | working-directory: packages/vue-tree-chart 26 | env: 27 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | dist 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | built/ # Ignore out dir in tsconfig.json 8 | es 9 | 10 | 11 | # Editor directories and files 12 | .idea 13 | .vscode 14 | *.suo 15 | *.ntvs* 16 | *.njsproj 17 | *.sln 18 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Shen Shuntian 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 | -------------------------------------------------------------------------------- /README-CN.md: -------------------------------------------------------------------------------- 1 | ## [English](./README.md) | [中文](./README-CN.md) 2 | 3 | ## Demo 页面 4 | 5 | https://ssthouse.github.io/tree-chart/#/svgTree 6 | 7 | ## Demo 动图 8 | 9 | ![demo gif](https://raw.githubusercontent.com/ssthouse/organization-chart/master/screenshots/org-chart.gif) 10 | 11 | 12 | ## Vue Tree Chart (同时支持 Vue2 和 Vue3) 13 | 14 | 请参考: [vue-tree-chart](./docs/vue-tree-chart.md) 15 | 16 | ## Canvas Tree Chart 17 | 18 | 请参考: [canvas tree chart](./docs/canvas-tree-chart.md) 19 | 20 | ## 开始开发 21 | 22 | ```bash 23 | npm install 24 | 25 | # serve with hot reload at localhost 26 | npm run dev 27 | 28 | # build for production with minification (build to ./docs folder, which can be auto servered by github page 🤓) 29 | npm run build 30 | ``` 31 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## [English](./README.md) | [中文](./README-CN.md) 2 | 3 | 4 | ## Demo 5 | 6 | https://ssthouse.github.io/tree-chart/#/svgTree 7 | 8 | ## Demo Gif 9 | 10 | ![demo gif](https://raw.githubusercontent.com/ssthouse/organization-chart/master/screenshots/org-chart.gif) 11 | 12 | 13 | ## Vue Tree Chart (support both Vue2 and Vue3) 14 | 15 | please refer [vue-tree-chart](./docs/vue-tree-chart.md) 16 | 17 | ## React Tree Chart 18 | 19 | please refer [react-tree-chart](./docs/react-tree-chart.md) 20 | 21 | ## Canvas Tree Chart 22 | 23 | please refer [canvas tree chart](./docs/canvas-tree-chart.md) 24 | 25 | ## Build Setup 26 | 27 | ```bash 28 | # install dependencies 29 | npm install 30 | 31 | # serve with hot reload at localhost 32 | npm run dev 33 | 34 | # build for production with minification (build to ./docs folder, which can be auto servered by github page 🤓) 35 | npm run build 36 | ``` 37 | -------------------------------------------------------------------------------- /docs/canvas-tree-chart-CN.md: -------------------------------------------------------------------------------- 1 | # Canvas Tree Chart 2 | 3 | ## Canvas 版本 使用到的技术点 4 | 5 | - 将 D3.js 和 Canvas 一起使用,提升绘制效率(其中 D3.js 使用虚拟 DOM 就行渲染,Canvas 取虚拟 DOM 节点坐标进行绘制) 6 | - 使用 `唯一颜色值`的方案,实现 Canvas 上点击事件的监听 (你也可以参考这篇文档了解其详细实现: https://medium.com/@lverspohl/how-to-turn-d3-and-canvas-into-good-friends-b7a240a32915) 7 | 8 | ## Canavs API 9 | 10 | canvas 的版本, 因为其绘制过程较难抽象, 且仅仅在数据量较大的情况下才有意义,所以没有发布为 npm module. 11 | 12 | 如果你希望使用 canvas 版本的 tree-chart,可以将源代码下载下来,并进行一下步骤替换为自己的数据集: 13 | 14 | - 将 `packages/tree-chart-demo/src/base/data-generator.js`文件中的数据替换为你自己的数据. 15 | - 在 `packages/tree-chart-demo/src/components/org-chart.js`文件中,修改`drawShowCanvas`函数的绘制逻辑. 16 | -------------------------------------------------------------------------------- /docs/canvas-tree-chart.md: -------------------------------------------------------------------------------- 1 | # Canvas Tree Chart 2 | 3 | ## Canvas version Using Tech 4 | 5 | - use D3.js with Canvas to draw organizationChart more efficiently. 6 | - Use `unique-color` manner to identify mouse click event in Canvas (you can refer to https://medium.com/@lverspohl/how-to-turn-d3-and-canvas-into-good-friends-b7a240a32915 to see detail) 7 | 8 | 9 | ### Canvas version API 10 | 11 | the canvas version is not published with npm module. 12 | 13 | if you want to use this project's canvas version, please download the source code and edit with the following steps: 14 | 15 | - replace the data in `packages/tree-chart-demo/src/base/data-generator.js` with your own nested data. 16 | - add your data drawing logic in `packages/tree-chart-demo/src/components/org-chart.js #drawShowCanvas` -------------------------------------------------------------------------------- /docs/react-tree-chart.md: -------------------------------------------------------------------------------- 1 | ## Demo page 2 | 3 | https://codesandbox.io/s/react-tree-chart-544i2?file=/src/App.tsx 4 | 5 | ## How to use? 6 | 7 | #### 1. install npm module 8 | 9 | install **Vue2 version** 10 | 11 | ```shell 12 | npm install @ssthouse/react-tree-chart 13 | ``` 14 | 15 | #### 2. import component 16 | 17 | ```javascript 18 | import TreeChart from "@ssthouse/react-tree-chart"; 19 | import "@ssthouse/react-tree-chart/lib/react-tree-chart.css"; 20 | ``` 21 | 22 | you can also check this [codesanbox example](https://codesandbox.io/s/react-tree-chart-544i2?file=/src/App.tsx) 23 | 24 | #### 3. use component 25 | -------------------------------------------------------------------------------- /docs/tech-underneath.md: -------------------------------------------------------------------------------- 1 | ## Using Tech 2 | 3 | ### Svg version 4 | 5 | - use D3 to calculate node & link positon 6 | - use Vue to handle dom element entring and leaving 7 | - use Vue slot to let user easily use with their own data -------------------------------------------------------------------------------- /docs/vue-tree-chart-CN.md: -------------------------------------------------------------------------------- 1 | ## [English](./vue-tree-chart.md) 2 | 3 | ## Demo 页面 4 | 5 | https://ssthouse.github.io/tree-chart/#/svgTree 6 | 7 | ## Demo 动图 8 | 9 | ![demo gif](https://raw.githubusercontent.com/ssthouse/organization-chart/master/screenshots/org-chart.gif) 10 | 11 | ## 使用到的技术点 12 | 13 | ### Svg 版本 14 | 15 | - 使用 D3.js 计算**节点**和**链接线**的坐标 16 | - 使用 Vue 控制 DOM 节点的变更 17 | - 使用 Vue slot 抽象节点渲染流程, 让使用者可以高度定制化节点绘制 18 | 19 | ### Canvas 版本 20 | 21 | - 将 D3.js 和 Canvas 一起使用,提升绘制效率(其中 D3.js 使用虚拟 DOM 就行渲染,Canvas 取虚拟 DOM 节点坐标进行绘制) 22 | - 使用 `唯一颜色值`的方案,实现 Canvas 上点击事件的监听 (你也可以参考这篇文档了解其详细实现: https://medium.com/@lverspohl/how-to-turn-d3-and-canvas-into-good-friends-b7a240a32915) 23 | 24 | ## 如何将图中数据替换为我的数据? 25 | 26 | ### Svg version 27 | 28 | Svg 版本通过 Vue 进行了良好的封装,使用起来非常方便且灵活. 29 | 30 | #### 1.安装 31 | 32 | 执行下面的命令安装 Svg 版本的 tree-chart 33 | 34 | `npm install @ssthouse/vue-tree-chart` 35 | 36 | #### 2. 注册 `vue-tree` 组件 37 | 38 | ```javascript 39 | import VueTree from "@ssthouse/vue-tree-chart"; 40 | import Vue from "vue"; 41 | Vue.component("vue-tree", VueTree); 42 | ``` 43 | 44 | #### 3. 使用组件 45 | 46 | **3.1 基本用法** 47 | 48 |
49 | See Code 50 | 51 | ```vue 52 | 62 | 63 | 80 | 81 | 88 | ``` 89 | 90 |
91 | 92 | ![](https://tva1.sinaimg.cn/large/007S8ZIlly1geprw1syiaj30na0hk0sl.jpg) 93 | 94 | **3.2 使用 vue-slot 异化展示折叠节点** 95 | 96 | 97 |
98 | See Code 99 | 100 | ```vue 101 | 118 | 119 | 136 | 137 | 154 | ``` 155 | 156 |
157 | 158 | 159 | ![](https://tva1.sinaimg.cn/large/007S8ZIlly1geprwtbw6sj30oc0hrq2t.jpg) 160 | 161 | **3.3 自定义渲染富媒体节点** 162 | 163 | 164 |
165 | See Code 166 | 167 | 168 | ```vue 169 | 193 | 194 | 258 | 259 | 278 | ``` 279 |
280 | 281 | 282 | ![](https://tva1.sinaimg.cn/large/007S8ZIlly1geprx8a8zgj30sh0hdglq.jpg) 283 | 284 | #### 4. API 285 | 286 | **4.1 props 参数** 287 | 288 | | | type | default | description | 289 | | --------- | ------ | ------------------------------------------------------------------------- | ------------------------------------------------------------------------------------ | 290 | | dataset | [Object, Array] | null | nested tree data or an array of nested tree (multi root tree) | 291 | | config | Object | {
nodeWidth: 100,
nodeHeight: 100,
levelHeight: 200
} | nodeWidth 和 nodeHeight 用于配置树状图节点大小. levelHeight 用于配置树状图一层的高度 | 292 | | linkStyle | String | 'curve' | 控制连接线样式, 可选项: 'curve' 或 'straight' | 293 | | direction | string | 'vertical' | 控制树状图方向, 可选项: 'vertical' 或 'horizontal' | 294 | 295 | **4.2 slot 参数** 296 | 297 | 该组件仅支持 **默认 slot**. 298 | 299 | 基本用法如下所示: 300 | 301 | ```vue 302 | 309 | ``` 310 | 311 | slot 提供两个参数用于渲染树状图节点内容。 312 | 313 | | slot param | type | description | 314 | | ---------- | ------- | -------------------------------- | 315 | | node | Object | current node data to be rendered | 316 | | collapsed | Boolean | current node collapse status | 317 | 318 | **4.3 API > 缩放** 319 | 320 | 通过Vue ref,可以调用组件的缩放接口 321 | 322 | 支持的接口有: 323 | 324 | 缩小: `this.$refs.tree.zoomIn()` 325 | 326 | 放大: `this.$refs.tree.zoomOut()` 327 | 328 | 恢复原始大小: `this.$refs.tree.restoreScale()` 329 | 330 | ### Canavs 版本 331 | 332 | canvas 的版本, 因为其绘制过程较难抽象, 且仅仅在数据量较大的情况下才有意义,所以没有发布为 npm module. 333 | 334 | 如果你希望使用 canvas 版本的 tree-chart,可以将源代码下载下来,并进行一下步骤替换为自己的数据集: 335 | 336 | - 将 `/src/base/data-generator.js`文件中的数据替换为你自己的数据. 337 | - 在 `/src/components/org-chart.js`文件中,修改`drawShowCanvas`函数的绘制逻辑. 338 | 339 | ## 开始开发 340 | 341 | ```bash 342 | # install dependencies 343 | npm install 344 | 345 | # serve with hot reload at localhost 346 | npm run dev 347 | 348 | # build for production with minification (build to ./docs folder, which can be auto servered by github page 🤓) 349 | npm run build 350 | ``` 351 | -------------------------------------------------------------------------------- /docs/vue-tree-chart.md: -------------------------------------------------------------------------------- 1 | ## [中文](./vue-tree-chart-CN.md) 2 | 3 | ![Node.js CI](https://github.com/ssthouse/vue-tree-chart/workflows/Node.js%20CI/badge.svg?branch=master) 4 | 5 | ## Demo page 6 | 7 | https://ssthouse.github.io/tree-chart/#/svgTree 8 | 9 | ## Demo Gif 10 | 11 | ![demo gif](https://raw.githubusercontent.com/ssthouse/organization-chart/master/screenshots/org-chart.gif) 12 | 13 | ## Using Tech 14 | 15 | ### Svg version 16 | 17 | - use D3 to calculate node & link positon 18 | - use Vue to handle dom element entring and leaving 19 | - use Vue slot to let user easily use with their own data 20 | 21 | ## How to use? 22 | 23 | ### Svg version 24 | 25 | #### 1. install npm module 26 | 27 | install **Vue2 version** 28 | 29 | ```shell 30 | npm install @ssthouse/vue-tree-chart 31 | ``` 32 | 33 | install **Vue3 version** 34 | 35 | ```shell 36 | npm install @ssthouse/vue3-tree-chart 37 | ``` 38 | 39 | #### 2. register `vue-tree` component 40 | 41 | For Vue2 42 | 43 | ```javascript 44 | import VueTree from '@ssthouse/vue-tree-chart' 45 | import Vue from 'vue' 46 | Vue.component('vue-tree', VueTree) 47 | ``` 48 | 49 | For Vue3 50 | 51 | ```javascript 52 | import VueTree from "@ssthouse/vue3-tree-chart"; 53 | import "@ssthouse/vue3-tree-chart/dist/vue3-tree-chart.css"; 54 | ``` 55 | 56 | you can also check this [codesanbox example](https://codesandbox.io/s/vue3-tree-chart-demo-j11uj?file=/src/App.vue) 57 | 58 | #### 3. use component 59 | 60 | **3.1 basic usage** 61 | 62 |
63 | See Code 64 | 65 | ```vue 66 | 76 | 77 | 94 | 95 | 102 | ``` 103 | 104 |
105 | 106 | ![](https://tva1.sinaimg.cn/large/007S8ZIlly1geprw1syiaj30na0hk0sl.jpg) 107 | 108 | **3.2 show collapsed node in different style** 109 | 110 |
111 | See Code 112 | 113 | ```vue 114 | 131 | 132 | 149 | 150 | 167 | ``` 168 | 169 |
170 | 171 | ![](https://tva1.sinaimg.cn/large/007S8ZIlly1geprwtbw6sj30oc0hrq2t.jpg) 172 | 173 | **3.3 render rich media data** 174 | 175 |
176 | See Code 177 | 178 | 179 | ```vue 180 | 204 | 205 | 269 | 270 | 289 | ``` 290 |
291 | 292 | 293 | ![](https://tva1.sinaimg.cn/large/007S8ZIlly1geprx8a8zgj30sh0hdglq.jpg) 294 | 295 | **3.4 render tree with multiple parents** 296 | 297 |
298 | See Code 299 | 300 | ```vue 301 | 322 | 376 | 377 | 396 | ``` 397 |
398 | 399 | ![](https://github.com/Maxim-Durand/scrapcalculator/blob/143ef85f15aaca1b4044faa6fbfc920922aa5ec2/src/assets/multipleParents.png?raw=true) 400 | 401 | #### 4. API 402 | 403 | **4.1 props** 404 | 405 | | | type | default | description | 406 | | --------- | ------ | ------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- | 407 | | dataset | [Object,Array] | null | nested tree data or an array of nested tree (multi root tree) | 408 | | config | Object | {
nodeWidth: 100,
nodeHeight: 100,
levelHeight: 200
} | nodeWidth and nodeHeight config the tree node size. levelHeight is tree row height | 409 | | linkStyle | String | 'curve' | control link style, options: 'curve' or 'straight' | 410 | | direction | string | 'vertical' | control tree chart direction, options: 'vertical' or 'horizontal' | 411 | | collapse-enabled | Boolean | true | Control whether when clicking on a node it collapses its children | 412 | 413 | **4.2 slot** 414 | 415 | this component only support **default slot**. 416 | 417 | a sample usage like this: 418 | 419 | ```vue 420 | 427 | ``` 428 | 429 | there are two slot params provided to render slot content: 430 | 431 | | slot param | type | description | 432 | | ---------- | ------- | -------------------------------- | 433 | | node | Object | current node data to be rendered | 434 | | collapsed | Boolean | current node collapse status | 435 | 436 | 437 | **4.3 API > zoom** 438 | 439 | use vue ref to call zoom api. 440 | 441 | support methods: 442 | 443 | zoom in: `this.$refs.tree.zoomIn()` 444 | 445 | zoom out: `this.$refs.tree.zoomOut()` 446 | 447 | restore initial scale: `this.$refs.tree.restoreScale()` 448 | 449 | 450 | 451 | ## Build Setup 452 | 453 | ```bash 454 | # install dependencies 455 | npm install 456 | 457 | # serve with hot reload at localhost 458 | npm run dev 459 | 460 | # build for production with minification (build to ./docs folder, which can be auto servered by github page 🤓) 461 | npm run build 462 | ``` 463 | -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "packages": [ 3 | "packages/*" 4 | ], 5 | "version": "independent" 6 | } 7 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@ssthouse/vue-tree-chart", 3 | "private": true, 4 | "author": "ssthouse", 5 | "scripts": { 6 | "install:deps": "lerna bootstrap", 7 | "build:component": "lerna run build:component --scope @ssthouse/vue-tree-chart", 8 | "build:demo": "lerna run build --scope @ssthouse/vue-tree-chart", 9 | "release:all": "lerna run release" 10 | }, 11 | "devDependencies": { 12 | "lerna": "^4.0.0" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /packages/react-tree-chart/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["@babel/preset-typescript", { 4 | "allExtensions": true, 5 | "isTsx": true 6 | }], 7 | "@babel/react" 8 | ] 9 | 10 | } -------------------------------------------------------------------------------- /packages/react-tree-chart/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | /lib 14 | 15 | # misc 16 | .DS_Store 17 | .env.local 18 | .env.development.local 19 | .env.test.local 20 | .env.production.local 21 | 22 | npm-debug.log* 23 | yarn-debug.log* 24 | yarn-error.log* 25 | -------------------------------------------------------------------------------- /packages/react-tree-chart/README.md: -------------------------------------------------------------------------------- 1 | # Getting Started with Create React App 2 | 3 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 4 | 5 | ## Available Scripts 6 | 7 | In the project directory, you can run: 8 | 9 | ### `yarn start` 10 | 11 | Runs the app in the development mode.\ 12 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 13 | 14 | The page will reload if you make edits.\ 15 | You will also see any lint errors in the console. 16 | 17 | ### `yarn test` 18 | 19 | Launches the test runner in the interactive watch mode.\ 20 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. 21 | 22 | ### `yarn build` 23 | 24 | Builds the app for production to the `build` folder.\ 25 | It correctly bundles React in production mode and optimizes the build for the best performance. 26 | 27 | The build is minified and the filenames include the hashes.\ 28 | Your app is ready to be deployed! 29 | 30 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. 31 | 32 | ### `yarn eject` 33 | 34 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!** 35 | 36 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. 37 | 38 | Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. 39 | 40 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. 41 | 42 | ## Learn More 43 | 44 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). 45 | 46 | To learn React, check out the [React documentation](https://reactjs.org/). 47 | -------------------------------------------------------------------------------- /packages/react-tree-chart/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@ssthouse/react-tree-chart", 3 | "version": "0.1.2", 4 | "dependencies": { 5 | "@ssthouse/tree-chart-core": "1.1.2", 6 | "react": "^17.0.2", 7 | "react-dom": "^17.0.2", 8 | "react-transition-group": "^1.2.1" 9 | }, 10 | "main": "lib/react-tree-chart.js", 11 | "module": "es/react-tree-chart.js", 12 | "scripts": { 13 | "start": "BROWSER=none react-scripts start", 14 | "build": "react-scripts build", 15 | "test": "react-scripts test", 16 | "eject": "react-scripts eject", 17 | "build:component": "rm -rf lib es && rollup -c", 18 | "release": "npm run build:component && npm publish" 19 | }, 20 | "eslintConfig": { 21 | "extends": [ 22 | "react-app", 23 | "react-app/jest" 24 | ] 25 | }, 26 | "browserslist": { 27 | "production": [ 28 | ">0.2%", 29 | "not dead", 30 | "not op_mini all" 31 | ], 32 | "development": [ 33 | "last 1 chrome version", 34 | "last 1 firefox version", 35 | "last 1 safari version" 36 | ] 37 | }, 38 | "devDependencies": { 39 | "@babel/cli": "^7.16.0", 40 | "@babel/preset-react": "^7.16.0", 41 | "@babel/preset-typescript": "^7.16.0", 42 | "@rollup/plugin-commonjs": "^21.0.1", 43 | "@testing-library/jest-dom": "^5.11.4", 44 | "@testing-library/react": "^11.1.0", 45 | "@testing-library/user-event": "^12.1.10", 46 | "@types/jest": "^26.0.15", 47 | "@types/node": "^12.0.0", 48 | "@types/react": "^17.0.0", 49 | "@types/react-dom": "^17.0.0", 50 | "node-sass": "^7.0.0", 51 | "react-scripts": "4.0.3", 52 | "rollup": "^2.61.1", 53 | "rollup-plugin-scss": "3", 54 | "rollup-plugin-typescript2": "^0.31.1", 55 | "sass": "^1.45.0", 56 | "typescript": "^4.1.2", 57 | "web-vitals": "^1.0.1" 58 | }, 59 | "publishConfig": { 60 | "registry": "https://registry.npmjs.org/" 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /packages/react-tree-chart/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ssthouse/tree-chart/948a4d2bcd2415ed1a8a34e78ee54e74c3c40c54/packages/react-tree-chart/public/favicon.ico -------------------------------------------------------------------------------- /packages/react-tree-chart/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /packages/react-tree-chart/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ssthouse/tree-chart/948a4d2bcd2415ed1a8a34e78ee54e74c3c40c54/packages/react-tree-chart/public/logo192.png -------------------------------------------------------------------------------- /packages/react-tree-chart/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ssthouse/tree-chart/948a4d2bcd2415ed1a8a34e78ee54e74c3c40c54/packages/react-tree-chart/public/logo512.png -------------------------------------------------------------------------------- /packages/react-tree-chart/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /packages/react-tree-chart/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /packages/react-tree-chart/rollup.config.js: -------------------------------------------------------------------------------- 1 | import commonjs from "@rollup/plugin-commonjs"; 2 | import typescript from "rollup-plugin-typescript2"; 3 | import scss from 'rollup-plugin-scss' 4 | 5 | const packageJson = require("./package.json"); 6 | 7 | const rollupConfig = { 8 | input: "./src/tree-chart/react-tree-chart.tsx", 9 | output: [ 10 | { 11 | file: packageJson.main, 12 | format: "cjs", 13 | sourcemap: true 14 | }, 15 | { 16 | file: packageJson.module, 17 | format: "es", 18 | sourcemap: true 19 | } 20 | ], 21 | plugins: [ 22 | typescript({ 23 | rollupCommonJSResolveHack: false, 24 | clean: true, 25 | }), 26 | scss(), 27 | // peerDepsExternal(), 28 | // resolve(), 29 | commonjs(), 30 | ] 31 | }; 32 | 33 | export default rollupConfig; 34 | -------------------------------------------------------------------------------- /packages/react-tree-chart/src/demo/App.css: -------------------------------------------------------------------------------- 1 | .App { 2 | text-align: center; 3 | } 4 | 5 | .App-logo { 6 | height: 40vmin; 7 | pointer-events: none; 8 | } 9 | 10 | @media (prefers-reduced-motion: no-preference) { 11 | .App-logo { 12 | animation: App-logo-spin infinite 20s linear; 13 | } 14 | } 15 | 16 | .App-header { 17 | background-color: #282c34; 18 | min-height: 100vh; 19 | display: flex; 20 | flex-direction: column; 21 | align-items: center; 22 | justify-content: center; 23 | font-size: calc(10px + 2vmin); 24 | color: white; 25 | } 26 | 27 | .App-link { 28 | color: #61dafb; 29 | } 30 | 31 | @keyframes App-logo-spin { 32 | from { 33 | transform: rotate(0deg); 34 | } 35 | to { 36 | transform: rotate(360deg); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /packages/react-tree-chart/src/demo/App.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import logo from './logo.svg'; 3 | import './App.css'; 4 | 5 | function App() { 6 | return ( 7 |
8 |
9 | logo 10 |

11 | Edit src/App.tsx and save to reload. 12 |

13 | 19 | Learn React 20 | 21 |
22 |
23 | ); 24 | } 25 | 26 | export default App; 27 | -------------------------------------------------------------------------------- /packages/react-tree-chart/src/demo/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 12 | monospace; 13 | } 14 | -------------------------------------------------------------------------------- /packages/react-tree-chart/src/demo/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/react-tree-chart/src/demo/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /packages/react-tree-chart/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 12 | monospace; 13 | } 14 | -------------------------------------------------------------------------------- /packages/react-tree-chart/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useRef, useState } from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import './index.css'; 4 | import TreeChart from './tree-chart/react-tree-chart'; 5 | 6 | const sampleData = { 7 | value: "1", 8 | children: [ 9 | { value: "2", children: [{ value: "4" }, { value: "5" }] }, 10 | { value: "3" }, 11 | ], 12 | }; 13 | 14 | const sampleDataTwo = { 15 | value: "100", 16 | children: [ 17 | { value: "3" }, 18 | { value: "2", children: [{ value: "555" }, { value: "444" }] }, 19 | ], 20 | }; 21 | 22 | const Demo = () => { 23 | const [dataSet, setDataset] = useState(sampleData); 24 | const [enableCollapse, setEnableCollapse] = useState(true); 25 | const treeChartRef = useRef(null); 26 | 27 | return
28 | 29 | 33 | 34 | 35 |
45 | {(data as any).value + (collapsed ? 'yes' : 'no')} 46 |
} 47 | /> 48 |
49 | } 50 | 51 | ReactDOM.render( 52 | 53 | 54 | , 55 | document.getElementById('root') 56 | ); 57 | -------------------------------------------------------------------------------- /packages/react-tree-chart/src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /packages/react-tree-chart/src/setupTests.ts: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom'; 6 | -------------------------------------------------------------------------------- /packages/react-tree-chart/src/tree-chart/react-tree-chart.scss: -------------------------------------------------------------------------------- 1 | .tree-node-item-enter { 2 | opacity: 0.01; 3 | } 4 | 5 | .tree-node-item-enter.tree-node-item-enter-active { 6 | opacity: 1; 7 | transition: opacity 800md ease-in-out; 8 | } 9 | 10 | .tree-node-item-leave { 11 | opacity: 1; 12 | } 13 | 14 | .tree-node-item-leave.tree-node-item-leave-active { 15 | opacity: 0.01; 16 | transition: opacity 800ms ease-in-out; 17 | } 18 | 19 | .tree-container { 20 | position: relative; 21 | overflow: hidden; 22 | 23 | .vue-tree { 24 | position: relative; 25 | } 26 | 27 | > svg, 28 | .dom-container { 29 | width: 100%; 30 | height: 100%; 31 | position: absolute; 32 | left: 0; 33 | top: 0; 34 | overflow: visible; 35 | transform-origin: 0 50%; 36 | } 37 | 38 | .dom-container { 39 | z-index: 1; 40 | pointer-events: none; 41 | } 42 | } 43 | 44 | .node-slot { 45 | cursor: pointer; 46 | pointer-events: all; 47 | position: absolute; 48 | background-color: transparent; 49 | box-sizing: border-box; 50 | transform: translate(-50%, -50%); 51 | display: flex; 52 | align-items: center; 53 | justify-content: center; 54 | box-sizing: content-box; 55 | transition: all 0.8s; 56 | transition-timing-function: ease-in-out; 57 | } 58 | 59 | .tree-container { 60 | .node { 61 | fill: grey !important; 62 | } 63 | 64 | .link { 65 | stroke-width: 2px !important; 66 | fill: transparent !important; 67 | stroke: #cecece !important; 68 | } 69 | } -------------------------------------------------------------------------------- /packages/react-tree-chart/src/tree-chart/react-tree-chart.tsx: -------------------------------------------------------------------------------- 1 | import React, { forwardRef, useEffect, useImperativeHandle, useRef, useState } from 'react'; 2 | import TreeChartCore, { DEFAULT_LEVEL_HEIGHT, DEFAULT_NODE_HEIGHT, DEFAULT_NODE_WIDTH, Direction, TreeLinkStyle } from '@ssthouse/tree-chart-core'; 3 | import { CSSTransitionGroup } from 'react-transition-group' 4 | import './react-tree-chart.scss'; 5 | 6 | const formatDimension = (dimension: number | string) => { 7 | if (typeof dimension === "number") return `${dimension}px`; 8 | if (dimension.indexOf("px") !== -1) { 9 | return dimension; 10 | } else { 11 | return `${dimension}px`; 12 | } 13 | }; 14 | 15 | export interface TreeChartNodeProps { 16 | data: unknown; 17 | collapsed: boolean; 18 | } 19 | 20 | interface TreeChartConfig { 21 | nodeWidth: number; 22 | nodeHeight: number; 23 | levelHeight: number; 24 | } 25 | 26 | interface TreeChartProps { 27 | /** basic tree node size config */ 28 | config?: TreeChartConfig; 29 | /** link node with straight line or curve line */ 30 | linkStyle?: TreeLinkStyle; 31 | /** chart direction, vertical or horizontal */ 32 | direction?: Direction; 33 | /** if chart node can be click to toggle collapse status */ 34 | collapseEnabled?: boolean; 35 | dataset: Object | Object[]; 36 | /** css style for contaienr Div */ 37 | style?: React.CSSProperties; 38 | /** 39 | * custom tree node component 40 | * default node is {value} 41 | * */ 42 | renderCustomNode?: React.FC; 43 | } 44 | 45 | const DEFAULT_CONFIG = { 46 | nodeWidth: DEFAULT_NODE_WIDTH, 47 | nodeHeight: DEFAULT_NODE_HEIGHT, 48 | levelHeight: DEFAULT_LEVEL_HEIGHT, 49 | }; 50 | 51 | const TreeChart = forwardRef((props: TreeChartProps, ref) => { 52 | const { 53 | direction = Direction.VERTICAL, 54 | config = DEFAULT_CONFIG, 55 | dataset, 56 | collapseEnabled = true, 57 | style = {}, 58 | renderCustomNode: customNode 59 | } = props; 60 | const [treeChartCore, setTreeChartCore] = useState(); 61 | const [initialTransformStyle, setInitialTransformStyle] = useState({}) 62 | const [nodeDataList, setNodeDataList] = useState([]); 63 | 64 | // refs 65 | const containerRef = useRef(null); 66 | const domContainerRef = useRef(null); 67 | const svgRef = useRef(null); 68 | 69 | // init tree chart core with refs 70 | useEffect(() => { 71 | console.log('update collapse', collapseEnabled) 72 | const treeChartCore = new TreeChartCore({ 73 | svgElement: svgRef.current as SVGElement, 74 | domElement: domContainerRef.current as HTMLDivElement, 75 | treeContainer: containerRef.current as HTMLDivElement, 76 | dataset, 77 | collapseEnabled, 78 | treeConfig: config, 79 | }); 80 | 81 | setTreeChartCore(treeChartCore); 82 | }, [collapseEnabled, dataset, config]) 83 | 84 | useEffect(() => { 85 | if (!treeChartCore) return; 86 | treeChartCore.init(); 87 | const nodeDataList = treeChartCore.getNodeDataList(); 88 | const initialTransformStyle = treeChartCore.getInitialTransformStyle(); 89 | setInitialTransformStyle(initialTransformStyle) 90 | setNodeDataList(nodeDataList); 91 | }, [treeChartCore]); 92 | 93 | const onClickNode = (index: number) => { 94 | if (!treeChartCore) return; 95 | treeChartCore.onClickNode(index); 96 | setNodeDataList(treeChartCore.getNodeDataList()); 97 | } 98 | 99 | useImperativeHandle(ref, () => { 100 | return { 101 | zoomIn() { 102 | treeChartCore?.zoomIn(); 103 | }, 104 | zoomOut() { 105 | treeChartCore?.zoomOut(); 106 | }, 107 | restoreScale() { 108 | treeChartCore?.setScale(1); 109 | }, 110 | } 111 | }) 112 | return
114 | 115 |
120 | 121 | 125 | { 126 | nodeDataList.map((node, index) => { 127 | return
onClickNode(index)} 130 | key={node.data._key} 131 | style={{ 132 | left: formatDimension( 133 | direction === Direction.VERTICAL ? node.x : node.y 134 | ), 135 | top: formatDimension( 136 | direction === Direction.VERTICAL ? node.y : node.x 137 | ), 138 | width: formatDimension(config.nodeWidth), 139 | height: formatDimension(config.nodeHeight), 140 | }} 141 | > 142 | { 143 | customNode 144 | ? customNode({ collapsed: node.data._collapsed, data: node.data }) 145 | : {node.data.value} 146 | } 147 |
148 | }) 149 | } 150 |
151 |
152 |
153 | }); 154 | 155 | export default TreeChart; 156 | -------------------------------------------------------------------------------- /packages/react-tree-chart/src/tree-chart/typings.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'react-transition-group' -------------------------------------------------------------------------------- /packages/react-tree-chart/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "declaration": true, 5 | "lib": [ 6 | "dom", 7 | "dom.iterable", 8 | "esnext" 9 | ], 10 | "allowJs": true, 11 | "skipLibCheck": true, 12 | "esModuleInterop": true, 13 | "allowSyntheticDefaultImports": true, 14 | "strict": true, 15 | "forceConsistentCasingInFileNames": true, 16 | "noFallthroughCasesInSwitch": true, 17 | "module": "esnext", 18 | "moduleResolution": "node", 19 | "resolveJsonModule": true, 20 | "isolatedModules": true, 21 | "jsx": "react-jsx" 22 | }, 23 | "include": [ 24 | "src/tree-chart" 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /packages/tree-chart-core/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "extends": "eslint:recommended", 3 | "parserOptions": { 4 | "sourceType": "module", 5 | ecmaVersion: 2015 6 | } 7 | } -------------------------------------------------------------------------------- /packages/tree-chart-core/.gitignore: -------------------------------------------------------------------------------- 1 | build -------------------------------------------------------------------------------- /packages/tree-chart-core/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@ssthouse/tree-chart-core", 3 | "version": "1.1.2", 4 | "description": "tree chart's core logic (shared by Vue2,Vue3,React)", 5 | "main": "build/index.js", 6 | "module": "build/index.js", 7 | "scripts": { 8 | "build": "rm -rf build && npx tsc --declaration true", 9 | "release": "npm run build && npm publish" 10 | }, 11 | "author": "ssthouse", 12 | "license": "ISC", 13 | "dependencies": { 14 | "d3": "^7.2.0" 15 | }, 16 | "devDependencies": { 17 | "eslint": "^8.4.1", 18 | "typescript": "^4.5.2" 19 | }, 20 | "publishConfig": { 21 | "registry": "https://registry.npmjs.org/" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /packages/tree-chart-core/src/base/uuid.ts: -------------------------------------------------------------------------------- 1 | 2 | export function uuid(): string { 3 | const s = [] 4 | const hexDigits = '0123456789abcdef' 5 | for (let i = 0; i < 36; i++) { 6 | s[i] = hexDigits.substr(Math.floor(Math.random() * 0x10), 1) 7 | } 8 | s[14] = '4' 9 | s[19] = hexDigits.substr((s[19] & 0x3) | 0x8, 1) 10 | s[8] = s[13] = s[18] = s[23] = '-' 11 | return s.join('') 12 | } 13 | -------------------------------------------------------------------------------- /packages/tree-chart-core/src/index.ts: -------------------------------------------------------------------------------- 1 | import TreeChartCore from "./tree-chart/index"; 2 | 3 | export default TreeChartCore; 4 | 5 | export * from './tree-chart/constant'; 6 | export * from './tree-chart/tree-chart'; -------------------------------------------------------------------------------- /packages/tree-chart-core/src/tree-chart/constant.ts: -------------------------------------------------------------------------------- 1 | export const DEFAULT_NODE_WIDTH = 100; 2 | export const DEFAULT_NODE_HEIGHT = 100; 3 | export const DEFAULT_LEVEL_HEIGHT = 200; 4 | 5 | /** 6 | * Used to decrement the height of the 'initTransformY' to center diagrams. 7 | * This is only a hotfix caused by the addition of '__invisible_root' node 8 | * for multi root purposes. 9 | */ 10 | export const DEFAULT_HEIGHT_DECREMENT = 200; 11 | 12 | export const ANIMATION_DURATION = 800; 13 | 14 | export const MATCH_TRANSLATE_REGEX = /translate\((-?\d+)px, ?(-?\d+)px\)/i; 15 | export const MATCH_SCALE_REGEX = /scale\((\S*)\)/i; -------------------------------------------------------------------------------- /packages/tree-chart-core/src/tree-chart/index.ts: -------------------------------------------------------------------------------- 1 | import * as d3 from 'd3'; 2 | import { ANIMATION_DURATION, DEFAULT_HEIGHT_DECREMENT, DEFAULT_LEVEL_HEIGHT, DEFAULT_NODE_HEIGHT, DEFAULT_NODE_WIDTH, MATCH_SCALE_REGEX, MATCH_TRANSLATE_REGEX } from './constant'; 3 | import { TreeDataset, Direction, TreeLinkStyle } from './tree-chart'; 4 | import { deepCopy, rotatePoint } from './util'; 5 | 6 | 7 | interface TreeConfig { 8 | nodeWidth: number; 9 | nodeHeight: number; 10 | levelHeight: number; 11 | } 12 | 13 | 14 | interface TreeChartCoreParams { 15 | treeConfig?: TreeConfig; 16 | linkStyle?: TreeLinkStyle; 17 | direction?: Direction; 18 | collapseEnabled: boolean; 19 | dataset: TreeDataset; 20 | svgElement: SVGElement; 21 | domElement: HTMLDivElement; 22 | treeContainer: HTMLDivElement; 23 | } 24 | 25 | export default class TreeChartCore { 26 | treeConfig: TreeConfig = { 27 | nodeWidth: DEFAULT_NODE_WIDTH, 28 | nodeHeight: DEFAULT_NODE_HEIGHT, 29 | levelHeight: DEFAULT_LEVEL_HEIGHT, 30 | }; 31 | linkStyle: TreeLinkStyle = TreeLinkStyle.CURVE; 32 | direction: Direction = Direction.VERTICAL; 33 | collapseEnabled: boolean = true 34 | 35 | dataset: TreeDataset; 36 | 37 | svgElement: SVGElement; 38 | svgSelection: any; 39 | domElement: HTMLDivElement; 40 | treeContainer: HTMLDivElement; 41 | 42 | nodeDataList: any[]; 43 | linkDataList: any[]; 44 | initTransformX: number; 45 | initTransformY: number; 46 | 47 | currentScale: number = 1; 48 | 49 | constructor(params: TreeChartCoreParams) { 50 | if (params.treeConfig) { 51 | this.treeConfig = params.treeConfig; 52 | } 53 | this.collapseEnabled = params.collapseEnabled 54 | this.svgElement = params.svgElement; 55 | this.domElement = params.domElement; 56 | this.treeContainer = params.treeContainer; 57 | this.dataset = this.updatedInternalData(params.dataset); 58 | if (params.direction) this.direction = params.direction; 59 | if (params.linkStyle) this.linkStyle = params.linkStyle 60 | } 61 | 62 | init() { 63 | this.draw(); 64 | this.enableDrag(); 65 | this.initTransform(); 66 | } 67 | 68 | getNodeDataList() { 69 | return this.nodeDataList; 70 | } 71 | 72 | getInitialTransformStyle(): Record { 73 | return { 74 | transform: `scale(1) translate(${this.initTransformX}px, ${this.initTransformY}px)`, 75 | transformOrigin: "center", 76 | }; 77 | } 78 | 79 | zoomIn() { 80 | const originTransformStr = this.domElement.style.transform; 81 | // 如果已有scale属性, 在原基础上修改 82 | let targetScale = 1 * 1.2; 83 | const scaleMatchResult = originTransformStr.match(MATCH_SCALE_REGEX); 84 | if (scaleMatchResult && scaleMatchResult.length > 0) { 85 | const originScale = parseFloat(scaleMatchResult[1]); 86 | targetScale *= originScale; 87 | } 88 | this.setScale(targetScale); 89 | } 90 | 91 | zoomOut() { 92 | const originTransformStr = this.domElement.style.transform; 93 | // 如果已有scale属性, 在原基础上修改 94 | let targetScale = 1 / 1.2; 95 | const scaleMatchResult = originTransformStr.match(MATCH_SCALE_REGEX); 96 | if (scaleMatchResult && scaleMatchResult.length > 0) { 97 | const originScale = parseFloat(scaleMatchResult[1]); 98 | targetScale = originScale / 1.2; 99 | } 100 | this.setScale(targetScale); 101 | } 102 | 103 | restoreScale() { 104 | this.setScale(1); 105 | } 106 | 107 | setScale(scaleNum) { 108 | if (typeof scaleNum !== "number") return; 109 | let pos = this.getTranslate(); 110 | let translateString = `translate(${pos[0]}px, ${pos[1]}px)`; 111 | this.svgElement.style.transform = `scale(${scaleNum}) ` + translateString; 112 | this.domElement.style.transform = 113 | `scale(${scaleNum}) ` + translateString; 114 | this.currentScale = scaleNum; 115 | } 116 | getTranslate() { 117 | let string = this.svgElement.style.transform; 118 | let match = string.match(MATCH_TRANSLATE_REGEX); 119 | if (match === null) { 120 | return [null, null]; 121 | } 122 | let x = parseInt(match[1]); 123 | let y = parseInt(match[2]); 124 | return [x, y]; 125 | } 126 | 127 | 128 | isVertical() { 129 | return this.direction === Direction.VERTICAL; 130 | } 131 | /** 132 | * 根据link数据,生成svg path data 133 | */ 134 | private generateLinkPath(d) { 135 | const self = this; 136 | if (this.linkStyle === TreeLinkStyle.CURVE) { 137 | return this.generateCurceLinkPath(self, d); 138 | } 139 | if (this.linkStyle === TreeLinkStyle.STRAIGHT) { 140 | // the link path is: source -> secondPoint -> thirdPoint -> target 141 | return this.generateStraightLinkPath(d); 142 | } 143 | } 144 | 145 | private generateCurceLinkPath(self: this, d: any) { 146 | const linkPath = this.isVertical() 147 | ? d3.linkVertical() 148 | : d3.linkHorizontal(); 149 | linkPath 150 | .x(function (d) { 151 | return d.x; 152 | }) 153 | .y(function (d) { 154 | return d.y; 155 | }) 156 | .source(function (d) { 157 | const sourcePoint = { 158 | x: d.source.x, 159 | y: d.source.y, 160 | }; 161 | return self.direction === Direction.VERTICAL 162 | ? sourcePoint 163 | : rotatePoint(sourcePoint); 164 | }) 165 | .target(function (d) { 166 | const targetPoint = { 167 | x: d.target.x, 168 | y: d.target.y, 169 | }; 170 | return self.direction === Direction.VERTICAL 171 | ? targetPoint 172 | : rotatePoint(targetPoint); 173 | }); 174 | return linkPath(d); 175 | } 176 | 177 | private generateStraightLinkPath(d: any) { 178 | const linkPath = d3.path(); 179 | let sourcePoint = { x: d.source.x, y: d.source.y }; 180 | let targetPoint = { x: d.target.x, y: d.target.y }; 181 | if (!this.isVertical()) { 182 | sourcePoint = rotatePoint(sourcePoint); 183 | targetPoint = rotatePoint(targetPoint); 184 | } 185 | const xOffset = targetPoint.x - sourcePoint.x; 186 | const yOffset = targetPoint.y - sourcePoint.y; 187 | const secondPoint = this.isVertical() 188 | ? { x: sourcePoint.x, y: sourcePoint.y + yOffset / 2 } 189 | : { x: sourcePoint.x + xOffset / 2, y: sourcePoint.y }; 190 | const thirdPoint = this.isVertical() 191 | ? { x: targetPoint.x, y: sourcePoint.y + yOffset / 2 } 192 | : { x: sourcePoint.x + xOffset / 2, y: targetPoint.y }; 193 | linkPath.moveTo(sourcePoint.x, sourcePoint.y); 194 | linkPath.lineTo(secondPoint.x, secondPoint.y); 195 | linkPath.lineTo(thirdPoint.x, thirdPoint.y); 196 | linkPath.lineTo(targetPoint.x, targetPoint.y); 197 | return linkPath.toString(); 198 | } 199 | 200 | updateDataList() { 201 | let [nodeDataList, linkDataList] = this.buildTree() 202 | nodeDataList.splice(0, 1); 203 | linkDataList = linkDataList.filter( 204 | (x) => x.source.data.name !== "__invisible_root" 205 | ); 206 | this.linkDataList = linkDataList; 207 | this.nodeDataList = nodeDataList; 208 | } 209 | 210 | private draw() { 211 | this.updateDataList(); 212 | const identifier = this.dataset["identifier"]; 213 | const specialLinks = this.dataset["links"]; 214 | if (specialLinks && identifier) { 215 | for (const link of specialLinks) { 216 | let parent, 217 | children = undefined; 218 | if (identifier === "value") { 219 | parent = this.nodeDataList.find((d) => { 220 | return d[identifier] == link.parent; 221 | }); 222 | children = this.nodeDataList.filter((d) => { 223 | return d[identifier] == link.child; 224 | }); 225 | } else { 226 | parent = this.nodeDataList.find((d) => { 227 | return d["data"][identifier] == link.parent; 228 | }); 229 | children = this.nodeDataList.filter((d) => { 230 | return d["data"][identifier] == link.child; 231 | }); 232 | } 233 | if (parent && children) { 234 | for (const child of children) { 235 | const new_link = { 236 | source: parent, 237 | target: child, 238 | }; 239 | this.linkDataList.push(new_link); 240 | } 241 | } 242 | } 243 | } 244 | 245 | this.svgSelection = d3.select(this.svgElement); 246 | 247 | const self = this; 248 | const links = this.svgSelection 249 | .selectAll(".link") 250 | .data(this.linkDataList, (d) => { 251 | return `${d.source.data._key}-${d.target.data._key}`; 252 | }); 253 | 254 | links 255 | .enter() 256 | .append("path") 257 | .style("opacity", 0) 258 | .transition() 259 | .duration(ANIMATION_DURATION) 260 | .ease(d3.easeCubicInOut) 261 | .style("opacity", 1) 262 | .attr("class", "link") 263 | .attr("d", function (d) { 264 | return self.generateLinkPath(d); 265 | }); 266 | links 267 | .transition() 268 | .duration(ANIMATION_DURATION) 269 | .ease(d3.easeCubicInOut) 270 | .attr("d", function (d) { 271 | return self.generateLinkPath(d); 272 | }); 273 | links 274 | .exit() 275 | .transition() 276 | .duration(ANIMATION_DURATION / 2) 277 | .ease(d3.easeCubicInOut) 278 | .style("opacity", 0) 279 | .remove(); 280 | } 281 | 282 | /** 283 | * Returns updated dataset by deep copying every nodes from the externalData and adding unique '_key' attributes. 284 | **/ 285 | private updatedInternalData(externalData) { 286 | var data = { name: "__invisible_root", children: [] }; 287 | if (!externalData) return data; 288 | if (Array.isArray(externalData)) { 289 | for (var i = externalData.length - 1; i >= 0; i--) { 290 | data.children.push(deepCopy(externalData[i])); 291 | } 292 | } else { 293 | data.children.push(deepCopy(externalData)); 294 | } 295 | return data; 296 | } 297 | 298 | private buildTree() { 299 | const treeBuilder = d3 300 | .tree() 301 | .nodeSize([this.treeConfig.nodeWidth, this.treeConfig.levelHeight]); 302 | const tree = treeBuilder(d3.hierarchy(this.dataset)); 303 | return [tree.descendants(), tree.links()]; 304 | } 305 | 306 | private enableDrag() { 307 | let startX = 0; 308 | let startY = 0; 309 | let isDrag = false; 310 | // 保存鼠标点下时的位移 311 | let mouseDownTransform = ""; 312 | this.treeContainer.onmousedown = (event) => { 313 | mouseDownTransform = this.svgElement.style.transform; 314 | startX = event.clientX; 315 | startY = event.clientY; 316 | isDrag = true; 317 | }; 318 | this.treeContainer.onmousemove = (event) => { 319 | if (!isDrag) return; 320 | const originTransform = mouseDownTransform; 321 | let originOffsetX = 0; 322 | let originOffsetY = 0; 323 | if (originTransform) { 324 | const result = originTransform.match(MATCH_TRANSLATE_REGEX); 325 | if (result !== null && result.length !== 0) { 326 | const [offsetX, offsetY] = result.slice(1); 327 | originOffsetX = parseInt(offsetX); 328 | originOffsetY = parseInt(offsetY); 329 | } 330 | } 331 | let newX = 332 | Math.floor((event.clientX - startX) / this.currentScale) + 333 | originOffsetX; 334 | let newY = 335 | Math.floor((event.clientY - startY) / this.currentScale) + 336 | originOffsetY; 337 | let transformStr = `translate(${newX}px, ${newY}px)`; 338 | if (originTransform) { 339 | transformStr = originTransform.replace( 340 | MATCH_TRANSLATE_REGEX, 341 | transformStr 342 | ); 343 | } 344 | this.svgElement.style.transform = transformStr; 345 | this.domElement.style.transform = transformStr; 346 | }; 347 | 348 | this.treeContainer.onmouseup = () => { 349 | startX = 0; 350 | startY = 0; 351 | isDrag = false; 352 | }; 353 | } 354 | 355 | initTransform() { 356 | const containerWidth = this.domElement.offsetWidth; 357 | const containerHeight = this.domElement.offsetHeight; 358 | if (this.isVertical()) { 359 | this.initTransformX = Math.floor(containerWidth / 2); 360 | this.initTransformY = Math.floor( 361 | this.treeConfig.nodeHeight - DEFAULT_HEIGHT_DECREMENT 362 | ); 363 | } else { 364 | this.initTransformX = Math.floor( 365 | this.treeConfig.nodeWidth - DEFAULT_HEIGHT_DECREMENT 366 | ); 367 | this.initTransformY = Math.floor(containerHeight / 2); 368 | } 369 | } 370 | 371 | onClickNode(index: number) { 372 | if (this.collapseEnabled) { 373 | const curNode = this.nodeDataList[index]; 374 | if (curNode.data.children) { 375 | curNode.data._children = curNode.data.children; 376 | curNode.data.children = null; 377 | curNode.data._collapsed = true; 378 | } else { 379 | curNode.data.children = curNode.data._children; 380 | curNode.data._children = null; 381 | curNode.data._collapsed = false; 382 | } 383 | this.draw(); 384 | } 385 | } 386 | 387 | /** 388 | * call this function to update dataset 389 | * notice : you need to update the view rendered by `nodeDataList` too 390 | * @param dataset the new dataset to show in chart 391 | */ 392 | updateDataset(dataset: TreeDataset) { 393 | this.dataset = this.updatedInternalData(dataset); 394 | this.draw(); 395 | } 396 | 397 | /** 398 | * release all dom reference 399 | */ 400 | destroy() { 401 | this.svgElement = null; 402 | this.domElement = null; 403 | this.treeContainer = null; 404 | } 405 | } -------------------------------------------------------------------------------- /packages/tree-chart-core/src/tree-chart/tree-chart.ts: -------------------------------------------------------------------------------- 1 | 2 | export enum Direction { 3 | VERTICAL = "vertical", 4 | HORIZONTAL = "horizontal", 5 | } 6 | 7 | export enum TreeLinkStyle { 8 | CURVE = "curve", 9 | STRAIGHT = "straight", 10 | } 11 | 12 | export type TreeDataset = Object | Object[]; 13 | -------------------------------------------------------------------------------- /packages/tree-chart-core/src/tree-chart/util.ts: -------------------------------------------------------------------------------- 1 | import { uuid } from "../base/uuid"; 2 | 3 | export function rotatePoint({ x, y }: { x: number; y: number }) { 4 | return { 5 | x: y, 6 | y: x, 7 | }; 8 | } 9 | 10 | 11 | /** 12 | * Returns a deep copy of selected node (copy of itself and it's children). 13 | * If selected node or it's children have no '_key' attribute it will assign a new one. 14 | **/ 15 | export function deepCopy(node) { 16 | let obj = { _key: uuid() }; 17 | for (var key in node) { 18 | if (node[key] === null) { 19 | obj[key] = null; 20 | } else if (Array.isArray(node[key])) { 21 | obj[key] = node[key].map((x) => deepCopy(x)); 22 | } else if (typeof node[key] === "object") { 23 | obj[key] = deepCopy(node[key]); 24 | } else { 25 | obj[key] = node[key]; 26 | } 27 | } 28 | return obj; 29 | } 30 | -------------------------------------------------------------------------------- /packages/tree-chart-core/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "./build/", 4 | "sourceMap": false, 5 | "strict": false, 6 | "noImplicitReturns": true, 7 | "module": "es2015", 8 | "lib": ["es2015", "DOM"], 9 | "moduleResolution": "node", 10 | "target": "es5" 11 | }, 12 | "include": ["src/**/*"] 13 | } 14 | -------------------------------------------------------------------------------- /packages/tree-chart-demo/build/webpack.config.base.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const { VueLoaderPlugin } = require('vue-loader') 3 | const HtmlWebpackPlugin = require('html-webpack-plugin') 4 | 5 | module.exports = { 6 | entry: './src/demo/main.ts', 7 | output: { 8 | path: path.resolve(__dirname, '../dist'), 9 | filename: 'bundle.js' 10 | }, 11 | module: { 12 | rules: [ 13 | { 14 | test: /\.css$/, 15 | use: ['style-loader', 'css-loader'] 16 | }, 17 | { 18 | test: /\.less$/, 19 | use: ['style-loader', 'css-loader', 'less-loader'] 20 | }, 21 | { 22 | test: /\.(png|svg|jpg|gif)$/, 23 | use: ['file-loader'] 24 | }, 25 | { 26 | test: /\.vue$/, 27 | use: ['vue-loader'] 28 | }, 29 | { 30 | test: /\.ts$/, 31 | loader: 'ts-loader', 32 | exclude: /node_modules/, 33 | options: { 34 | appendTsSuffixTo: [/\.vue$/] 35 | } 36 | } 37 | ] 38 | }, 39 | plugins: [ 40 | new VueLoaderPlugin(), 41 | new HtmlWebpackPlugin({ 42 | filename: 'index.html', 43 | template: './template/index.html', 44 | inject: true 45 | }) 46 | ], 47 | resolve: { 48 | extensions: ['.ts', '.js', '.vue', '.json'], 49 | alias: { 50 | vue$: 'vue/dist/vue.esm.js' 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /packages/tree-chart-demo/build/webpack.config.dev.js: -------------------------------------------------------------------------------- 1 | const merge = require('webpack-merge') 2 | 3 | module.exports = merge(require('./webpack.config.base'), { 4 | mode: 'development', 5 | output: { 6 | publicPath: '/' 7 | }, 8 | devtool: 'source-map' 9 | }) 10 | -------------------------------------------------------------------------------- /packages/tree-chart-demo/build/webpack.config.prod.js: -------------------------------------------------------------------------------- 1 | const merge = require('webpack-merge') 2 | const webpack = require('webpack') 3 | 4 | module.exports = merge(require('./webpack.config.base'), { 5 | mode: 'production', 6 | devtool: 'source-map', 7 | output: { 8 | publicPath: './' 9 | }, 10 | optimization: { 11 | minimize: true 12 | } 13 | }) 14 | -------------------------------------------------------------------------------- /packages/tree-chart-demo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tree-chart-demo", 3 | "version": "0.0.1", 4 | "description": "tree chart demo page", 5 | "main": "index.js", 6 | "scripts": { 7 | "lint": "eslint --ext .js,.vue,.ts src", 8 | "dev": "webpack-dev-server --progress --config ./build/webpack.config.dev.js", 9 | "build": "webpack --config ./build/webpack.config.prod.js" 10 | }, 11 | "author": "ssthouse", 12 | "license": "ISC", 13 | "dependencies": { 14 | "@ssthouse/tree-chart-core": "^1.0.0", 15 | "@ssthouse/vue-tree-chart": "^0.5.4", 16 | "d3": "^7.1.1", 17 | "vue": "2.6.8", 18 | "vue-router": "^3.5.3", 19 | "vuetify": "^1.0.18" 20 | }, 21 | "devDependencies": { 22 | "@typescript-eslint/eslint-plugin": "^2.34.0", 23 | "@typescript-eslint/parser": "^2.34.0", 24 | "@vue/eslint-config-prettier": "^6.0.0", 25 | "@vue/eslint-config-typescript": "^5.0.2", 26 | "autoprefixer": "^7.1.2", 27 | "babel-core": "^6.22.1", 28 | "babel-eslint": "^8.2.1", 29 | "babel-helper-vue-jsx-merge-props": "^2.0.3", 30 | "babel-loader": "^7.1.1", 31 | "babel-plugin-syntax-jsx": "^6.18.0", 32 | "babel-plugin-transform-runtime": "^6.22.0", 33 | "babel-plugin-transform-vue-jsx": "^3.5.0", 34 | "babel-preset-env": "^1.3.2", 35 | "babel-preset-stage-2": "^6.22.0", 36 | "chalk": "^2.0.1", 37 | "copy-webpack-plugin": "^9.0.1", 38 | "css-loader": "^6.5.0", 39 | "eslint": "^8.1.0", 40 | "eslint-config-standard": "^10.2.1", 41 | "eslint-friendly-formatter": "^3.0.0", 42 | "eslint-loader": "^1.7.1", 43 | "eslint-plugin-import": "^2.7.0", 44 | "eslint-plugin-node": "^5.2.0", 45 | "eslint-plugin-prettier": "^3.1.4", 46 | "eslint-plugin-promise": "^3.4.0", 47 | "eslint-plugin-standard": "^3.0.1", 48 | "eslint-plugin-vue": "^6.0.0", 49 | "extract-text-webpack-plugin": "^3.0.0", 50 | "file-loader": "^1.1.4", 51 | "html-webpack-plugin": "^5.5.0", 52 | "husky": "^7.0.4", 53 | "lerna": "^4.0.0", 54 | "less": "^4.1.2", 55 | "less-loader": "^10.2.0", 56 | "lint-staged": "^10.2.11", 57 | "node-notifier": "^8.0.1", 58 | "optimize-css-assets-webpack-plugin": "^6.0.1", 59 | "ora": "^1.2.0", 60 | "portfinder": "^1.0.13", 61 | "postcss-import": "^11.0.0", 62 | "postcss-loader": "^6.2.0", 63 | "postcss-url": "^7.2.1", 64 | "prettier": "^2.0.5", 65 | "rimraf": "^2.6.0", 66 | "semver": "^5.3.0", 67 | "shelljs": "^0.8.5", 68 | "style-loader": "^3.3.1", 69 | "ts-loader": "^9.2.6", 70 | "typescript": "^3.3.3333", 71 | "uglifyjs-webpack-plugin": "^2.2.0", 72 | "url-loader": "^4.1.1", 73 | "vue-class-component": "^7.0.1", 74 | "vue-loader": "^15.7.0", 75 | "vue-property-decorator": "^8.0.0", 76 | "vue-style-loader": "^4.1.3", 77 | "vue-template-compiler": "2.6.8", 78 | "webpack": "^5.61.0", 79 | "webpack-bundle-analyzer": "^3.3.2", 80 | "webpack-cli": "^4.9.1", 81 | "webpack-dev-server": "^4.4.0", 82 | "webpack-merge": "^4.1.0" 83 | }, 84 | "browserslist": [ 85 | "> 1%", 86 | "last 2 versions", 87 | "not ie <= 8" 88 | ], 89 | "prettier": { 90 | "#": "prettier config in here :)", 91 | "singleQuote": true, 92 | "semi": false, 93 | "arrowParens": "always", 94 | "space-before-function-paren": "off", 95 | "trailingComma": "none" 96 | }, 97 | "husky": { 98 | "hooks": { 99 | "pre-commit": "lint-staged" 100 | } 101 | }, 102 | "lint-staged": { 103 | "*.js": [ 104 | "eslint", 105 | "prettier --write" 106 | ], 107 | "*.vue": [ 108 | "eslint", 109 | "prettier --write" 110 | ] 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /packages/tree-chart-demo/src/base/color-util.ts: -------------------------------------------------------------------------------- 1 | export function randomColor(): string { 2 | const letters = [ 3 | '0', 4 | '1', 5 | '2', 6 | '3', 7 | '4', 8 | '5', 9 | '6', 10 | '7', 11 | '8', 12 | '9', 13 | 'a', 14 | 'b', 15 | 'c', 16 | 'd', 17 | 'e', 18 | 'f' 19 | ] 20 | let color = '#' 21 | for (let i = 0; i < 6; i++) { 22 | color += letters[Math.floor(Math.random() * 16)] 23 | } 24 | return color 25 | } 26 | 27 | function appendFront0(numStr: string): string { 28 | return numStr.padStart(2, '0') 29 | } 30 | 31 | export function getColorStrFromCanvas( 32 | context: any, 33 | xIndex: number, 34 | yIndex: number 35 | ): string { 36 | const pixelData = context.getImageData(xIndex, yIndex, 1, 1).data 37 | const [r, g, b] = pixelData 38 | return ( 39 | '#' + 40 | appendFront0(r.toString(16)) + 41 | appendFront0(g.toString(16)) + 42 | appendFront0(b.toString(16)) 43 | ) 44 | } 45 | -------------------------------------------------------------------------------- /packages/tree-chart-demo/src/base/data-generator.ts: -------------------------------------------------------------------------------- 1 | export interface Data { 2 | name: string 3 | title: string 4 | children: any 5 | } 6 | 7 | export function generateOrgChartData(depth: number) { 8 | const data: Data = { 9 | name: 'Lao Lao', 10 | title: 'general manager', 11 | children: [ 12 | { name: 'Bo Miao', title: 'department manager' }, 13 | { 14 | name: 'Su Miao', 15 | title: 'department manager', 16 | children: [ 17 | { name: 'Tie Hua', title: 'senior engineer' }, 18 | { 19 | name: 'Hei Hei', 20 | title: 'senior engineer', 21 | children: [ 22 | { name: 'Pang Pang', title: 'engineer' }, 23 | { name: 'Xiang Xiang', title: 'UE engineer' } 24 | ] 25 | } 26 | ] 27 | }, 28 | { name: 'Hong Miao', title: 'department manager' } 29 | ] 30 | } 31 | 32 | for (let i = 0; i < 3; i++) { 33 | data['children'].push({ 34 | name: 'Lao Lao', 35 | title: 'general manager', 36 | children: [ 37 | { name: 'Bo Miao', title: 'department manager' }, 38 | { 39 | name: 'Su Miao', 40 | title: 'department manager', 41 | children: [{ name: 'Tie Hua', title: 'senior engineer' }] 42 | } 43 | ] 44 | }) 45 | } 46 | 47 | let temp = data 48 | for (let i = 0; i < depth; i++) { 49 | if (!temp.children) { 50 | temp.children = [] 51 | } 52 | temp.children.push({ 53 | name: 'Lao Lao', 54 | title: 'general manager', 55 | children: [ 56 | { name: 'Bo Miao', title: 'department manager' }, 57 | { 58 | name: 'Su Miao', 59 | title: 'department manager', 60 | children: [ 61 | { name: 'Tie Hua', title: 'senior engineer' }, 62 | { 63 | name: 'Hei Hei', 64 | title: 'senior engineer', 65 | children: [{ name: 'Xiang Xiang', title: 'UE engineer' }] 66 | } 67 | ] 68 | } 69 | ] 70 | }) 71 | temp = temp.children[0] 72 | } 73 | return data 74 | } 75 | 76 | export function generateOrgChartDataFolded(depth: any, foldDepth: number) { 77 | const data = this.generateOrgChartData(depth) 78 | let tempNode = data 79 | for (let i = 0; i < foldDepth && tempNode.children; i++) { 80 | tempNode = tempNode.children[0] 81 | } 82 | tempNode._children = tempNode.children 83 | tempNode.children = null 84 | return data 85 | } 86 | -------------------------------------------------------------------------------- /packages/tree-chart-demo/src/base/utils.ts: -------------------------------------------------------------------------------- 1 | function text(ctx, text, x: number, y: number, fontSize, fontColor: string) { 2 | ctx.font = '14px Arial' 3 | ctx.fillStyle = fontColor 4 | ctx.fillText(text, x, y) 5 | } 6 | 7 | function wrapText( 8 | context, 9 | text: string, 10 | x: number, 11 | y: number, 12 | maxWidth: number, 13 | lineHeight: number, 14 | fontColor: string 15 | ) { 16 | context.fillStyle = fontColor 17 | const words = text.split(' ') 18 | let line = '' 19 | for (let n = 0; n < words.length; n++) { 20 | let testLine = `${line + words[n]} ` 21 | let metrics = context.measureText(testLine) 22 | let testWidth = metrics.width 23 | if (testWidth > maxWidth && n > 0) { 24 | context.fillText(line, x, y) 25 | line = `${words[n]} ` 26 | y += lineHeight 27 | } else { 28 | line = testLine 29 | } 30 | } 31 | context.fillText(line, x, y) 32 | } 33 | 34 | function roundRect( 35 | context: CanvasRenderingContext2D, 36 | x: number, 37 | y: number, 38 | width: number, 39 | height: number, 40 | radius: any, 41 | fill: boolean, 42 | stroke: boolean 43 | ): void { 44 | if (typeof stroke === 'undefined') { 45 | stroke = true 46 | } 47 | if (typeof radius === 'undefined') { 48 | radius = 5 49 | } 50 | if (typeof radius === 'number') { 51 | radius = { tl: radius, tr: radius, br: radius, bl: radius } 52 | } else { 53 | let defaultRadius = { tl: 0, tr: 0, br: 0, bl: 0 } 54 | for (let side in defaultRadius) { 55 | radius[side] = radius[side] || defaultRadius[side] 56 | } 57 | } 58 | context.beginPath() 59 | context.moveTo(x + radius.tl, y) 60 | context.lineTo(x + width - radius.tr, y) 61 | context.quadraticCurveTo(x + width, y, x + width, y + radius.tr) 62 | context.lineTo(x + width, y + height - radius.br) 63 | context.quadraticCurveTo( 64 | x + width, 65 | y + height, 66 | x + width - radius.br, 67 | y + height 68 | ) 69 | context.lineTo(x + radius.bl, y + height) 70 | context.quadraticCurveTo(x, y + height, x, y + height - radius.bl) 71 | context.lineTo(x, y + radius.tl) 72 | context.quadraticCurveTo(x, y, x + radius.tl, y) 73 | context.closePath() 74 | if (fill) { 75 | context.fill() 76 | } 77 | if (stroke) { 78 | context.stroke() 79 | } 80 | } 81 | 82 | export default { 83 | text, 84 | wrapText, 85 | roundRect 86 | } 87 | -------------------------------------------------------------------------------- /packages/tree-chart-demo/src/base/uuid.ts: -------------------------------------------------------------------------------- 1 | 2 | export function uuid(): string { 3 | const s = [] 4 | const hexDigits = '0123456789abcdef' 5 | for (let i = 0; i < 36; i++) { 6 | s[i] = hexDigits.substr(Math.floor(Math.random() * 0x10), 1) 7 | } 8 | s[14] = '4' 9 | s[19] = hexDigits.substr((s[19] & 0x3) | 0x8, 1) 10 | s[8] = s[13] = s[18] = s[23] = '-' 11 | return s.join('') 12 | } 13 | -------------------------------------------------------------------------------- /packages/tree-chart-demo/src/components/CanvasTree.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 66 | 67 | 104 | -------------------------------------------------------------------------------- /packages/tree-chart-demo/src/components/VueTreeDemo.vue: -------------------------------------------------------------------------------- 1 | 173 | 174 | 352 | 353 | 401 | -------------------------------------------------------------------------------- /packages/tree-chart-demo/src/components/org-chart.ts: -------------------------------------------------------------------------------- 1 | import * as d3 from 'd3' 2 | import Util from '../base/utils' 3 | import { randomColor, getColorStrFromCanvas } from '../base/color-util' 4 | 5 | class OrgChart { 6 | d3: any 7 | width: number 8 | height: number 9 | padding: number 10 | nodeWidth: number 11 | nodeHeight: number 12 | unitPadding: number 13 | unitWidth: number 14 | unitHeight: number 15 | duration: number 16 | scale: number 17 | data: any 18 | treeGenerator: any 19 | treeData: any 20 | virtualContainerNode: any 21 | container: any 22 | canvasNode: any 23 | hiddenCanvasNode: any 24 | context: any 25 | hiddenContext: any 26 | colorNodeMap: {} 27 | onDrag_: boolean 28 | dragStartPoint_: { x: number; y: number } 29 | 30 | constructor() { 31 | this.d3 = d3 32 | this.init() 33 | } 34 | 35 | init() { 36 | this.initVariables() 37 | this.initCanvas() 38 | this.initVirtualNode() 39 | this.setCanvasListener() 40 | } 41 | 42 | initVariables() { 43 | this.width = window.innerWidth 44 | this.height = window.innerHeight 45 | this.padding = 20 46 | // tree node size 47 | this.nodeWidth = 180 48 | this.nodeHeight = 280 49 | // org unit size 50 | this.unitPadding = 20 51 | this.unitWidth = 140 52 | this.unitHeight = 100 53 | // animation duration 54 | this.duration = 600 55 | this.scale = 1.0 56 | } 57 | 58 | draw(data) { 59 | this.data = this.d3.hierarchy(data) 60 | this.treeGenerator = this.d3 61 | .tree() 62 | .nodeSize([this.nodeWidth, this.nodeHeight]) 63 | this.update(null) 64 | 65 | const self = this 66 | this.d3.timer(function () { 67 | self.drawCanvas() 68 | }) 69 | } 70 | 71 | update(targetTreeNode) { 72 | this.treeData = this.treeGenerator(this.data) 73 | const nodes = this.treeData.descendants() 74 | const links = this.treeData.links() 75 | 76 | let animatedStartX = 0 77 | let animatedStartY = 0 78 | let animatedEndX = 0 79 | let animatedEndY = 0 80 | if (targetTreeNode) { 81 | animatedStartX = targetTreeNode.x0 82 | animatedStartY = targetTreeNode.y0 83 | animatedEndX = targetTreeNode.x 84 | animatedEndY = targetTreeNode.y 85 | } 86 | 87 | this.updateOrgUnits( 88 | nodes, 89 | animatedStartX, 90 | animatedStartY, 91 | animatedEndX, 92 | animatedEndY 93 | ) 94 | this.updateLinks( 95 | links, 96 | animatedStartX, 97 | animatedStartY, 98 | animatedEndX, 99 | animatedEndY 100 | ) 101 | 102 | this.addColorKey() 103 | this.bindNodeToTreeData() 104 | } 105 | 106 | updateOrgUnits( 107 | nodes, 108 | animatedStartX, 109 | animatedStartY, 110 | animatedEndX, 111 | animatedEndY 112 | ) { 113 | let orgUnitSelection = this.virtualContainerNode 114 | .selectAll('.orgUnit') 115 | .data(nodes, (d) => d['colorKey']) 116 | 117 | orgUnitSelection 118 | .attr('class', 'orgUnit') 119 | .attr('x', function (data) { 120 | return data.x0 121 | }) 122 | .attr('y', function (data) { 123 | return data.y0 124 | }) 125 | .transition() 126 | .duration(this.duration) 127 | .attr('x', function (data) { 128 | return data.x 129 | }) 130 | .attr('y', function (data) { 131 | return data.y 132 | }) 133 | .attr('fillStyle', '#ff0000') 134 | 135 | orgUnitSelection 136 | .enter() 137 | .append('orgUnit') 138 | .attr('class', 'orgUnit') 139 | .attr('x', animatedStartX) 140 | .attr('y', animatedStartY) 141 | .transition() 142 | .duration(this.duration) 143 | .attr('x', function (data) { 144 | return data.x 145 | }) 146 | .attr('y', function (data) { 147 | return data.y 148 | }) 149 | .attr('fillStyle', '#ff0000') 150 | 151 | orgUnitSelection 152 | .exit() 153 | .transition() 154 | .duration(this.duration) 155 | .attr('x', animatedEndX) 156 | .attr('y', animatedEndY) 157 | .remove() 158 | 159 | // record origin index for animation 160 | nodes.forEach((treeNode) => { 161 | treeNode['x0'] = treeNode.x 162 | treeNode['y0'] = treeNode.y 163 | }) 164 | 165 | orgUnitSelection = null 166 | } 167 | 168 | updateLinks( 169 | links, 170 | animatedStartX, 171 | animatedStartY, 172 | animatedEndX, 173 | animatedEndY 174 | ) { 175 | let linkSelection = this.virtualContainerNode 176 | .selectAll('.link') 177 | .data(links, function (d) { 178 | return d.source['colorKey'] + ':' + d.target['colorKey'] 179 | }) 180 | 181 | linkSelection 182 | .attr('class', 'link') 183 | .attr('sourceX', function (linkData) { 184 | return linkData.source['x00'] 185 | }) 186 | .attr('sourceY', function (linkData) { 187 | return linkData.source['y00'] 188 | }) 189 | .attr('targetX', function (linkData) { 190 | return linkData.target['x00'] 191 | }) 192 | .attr('targetY', function (linkData) { 193 | return linkData.target['y00'] 194 | }) 195 | .transition() 196 | .duration(this.duration) 197 | .attr('sourceX', function (linkData) { 198 | return linkData.source.x 199 | }) 200 | .attr('sourceY', function (linkData) { 201 | return linkData.source.y 202 | }) 203 | .attr('targetX', function (linkData) { 204 | return linkData.target.x 205 | }) 206 | .attr('targetY', function (linkData) { 207 | return linkData.target.y 208 | }) 209 | 210 | linkSelection 211 | .enter() 212 | .append('link') 213 | .attr('class', 'link') 214 | .attr('sourceX', animatedStartX) 215 | .attr('sourceY', animatedStartY) 216 | .attr('targetX', animatedStartX) 217 | .attr('targetY', animatedStartY) 218 | .transition() 219 | .duration(this.duration) 220 | .attr('sourceX', function (link) { 221 | return link.source.x 222 | }) 223 | .attr('sourceY', function (link) { 224 | return link.source.y 225 | }) 226 | .attr('targetX', function (link) { 227 | return link.target.x 228 | }) 229 | .attr('targetY', function (link) { 230 | return link.target.y 231 | }) 232 | 233 | linkSelection 234 | .exit() 235 | .transition() 236 | .duration(this.duration) 237 | .attr('sourceX', animatedEndX) 238 | .attr('sourceY', animatedEndY) 239 | .attr('targetX', animatedEndX) 240 | .attr('targetY', animatedEndY) 241 | .remove() 242 | 243 | // record origin data for animation 244 | links.forEach((treeNode) => { 245 | treeNode.source['x00'] = treeNode.source.x 246 | treeNode.source['y00'] = treeNode.source.y 247 | treeNode.target['x00'] = treeNode.target.x 248 | treeNode.target['y00'] = treeNode.target.y 249 | }) 250 | linkSelection = null 251 | } 252 | 253 | initCanvas() { 254 | this.container = this.d3.select('#org-chart-container') 255 | this.canvasNode = this.container 256 | .append('canvas') 257 | .attr('class', 'orgChart') 258 | .attr('width', this.width) 259 | .attr('height', this.height) 260 | this.hiddenCanvasNode = this.container 261 | .append('canvas') 262 | .attr('class', 'orgChart') 263 | .attr('width', this.width) 264 | .attr('height', this.height) 265 | .style('display', '') 266 | this.context = this.canvasNode.node().getContext('2d') 267 | this.context.translate(this.width / 2, this.padding) 268 | this.hiddenContext = this.hiddenCanvasNode.node().getContext('2d') 269 | this.hiddenContext.translate(this.width / 2, this.padding) 270 | } 271 | 272 | initVirtualNode() { 273 | let virtualContainer = document.createElement('root') 274 | this.virtualContainerNode = this.d3.select(virtualContainer) 275 | this.colorNodeMap = {} 276 | } 277 | 278 | addColorKey() { 279 | // give each node a unique color 280 | const self = this 281 | this.virtualContainerNode.selectAll('.orgUnit').each(function () { 282 | const node = self.d3.select(this) 283 | let newColor = randomColor() 284 | while (self.colorNodeMap[newColor]) { 285 | newColor = randomColor() 286 | } 287 | node.attr('colorKey', newColor) 288 | node.data()[0]['colorKey'] = newColor 289 | self.colorNodeMap[newColor] = node 290 | }) 291 | } 292 | 293 | bindNodeToTreeData() { 294 | // give each node a unique color 295 | const self = this 296 | this.virtualContainerNode.selectAll('.orgUnit').each(function () { 297 | const node = self.d3.select(this) 298 | const data = node.data()[0] 299 | data.node = node 300 | }) 301 | } 302 | 303 | drawCanvas() { 304 | this.drawShowCanvas() 305 | this.drawHiddenCanvas() 306 | } 307 | 308 | drawShowCanvas() { 309 | this.context.clearRect(-50000, -10000, 100000, 100000) 310 | 311 | const self = this 312 | // draw links 313 | this.virtualContainerNode.selectAll('.link').each(function () { 314 | const node = self.d3.select(this) 315 | const linkPath = self.d3 316 | .linkVertical() 317 | .x(function (d) { 318 | return d.x 319 | }) 320 | .y(function (d) { 321 | return d.y 322 | }) 323 | .source(function () { 324 | return { x: node.attr('sourceX'), y: node.attr('sourceY') } 325 | }) 326 | .target(function () { 327 | return { x: node.attr('targetX'), y: node.attr('targetY') } 328 | }) 329 | const path = new Path2D(linkPath()) 330 | self.context.stroke(path) 331 | }) 332 | 333 | this.virtualContainerNode.selectAll('.orgUnit').each(function () { 334 | const node = self.d3.select(this) 335 | const treeNode = node.data()[0] 336 | const data = treeNode.data 337 | self.context.fillStyle = '#3ca0ff' 338 | const indexX = Number(node.attr('x')) - self.unitWidth / 2 339 | const indexY = Number(node.attr('y')) - self.unitHeight / 2 340 | 341 | // draw unit outline rect (if you want to modify this line ===> please modify the same line in `drawHiddenCanvas`) 342 | Util.roundRect( 343 | self.context, 344 | indexX, 345 | indexY, 346 | self.unitWidth, 347 | self.unitHeight, 348 | 4, 349 | true, 350 | false 351 | ) 352 | 353 | Util.text( 354 | self.context, 355 | data.name, 356 | indexX + self.unitPadding, 357 | indexY + self.unitPadding, 358 | '20px', 359 | '#ffffff' 360 | ) 361 | // Util.text(self.context, data.title, indexX + self.unitPadding, indexY + self.unitPadding + 30, '20px', '#000000') 362 | const maxWidth = self.unitWidth - 2 * self.unitPadding 363 | Util.wrapText( 364 | self.context, 365 | data.title, 366 | indexX + self.unitPadding, 367 | indexY + self.unitPadding + 24, 368 | maxWidth, 369 | 20, 370 | '#000000' 371 | ) 372 | }) 373 | } 374 | 375 | /** 376 | * fill the node outline with colorKey color 377 | */ 378 | drawHiddenCanvas() { 379 | this.hiddenContext.clearRect(-50000, -10000, 100000, 100000) 380 | 381 | const self = this 382 | this.virtualContainerNode.selectAll('.orgUnit').each(function () { 383 | const node = self.d3.select(this) 384 | self.hiddenContext.fillStyle = node.attr('colorKey') 385 | Util.roundRect( 386 | self.hiddenContext, 387 | Number(node.attr('x')) - self.unitWidth / 2, 388 | Number(node.attr('y')) - self.unitHeight / 2, 389 | self.unitWidth, 390 | self.unitHeight, 391 | 4, 392 | true, 393 | false 394 | ) 395 | }) 396 | } 397 | 398 | setCanvasListener() { 399 | this.setClickListener() 400 | this.setDragListener() 401 | this.setMouseWheelZoomListener() 402 | } 403 | 404 | setClickListener() { 405 | const self = this 406 | this.canvasNode.node().addEventListener('click', (e) => { 407 | const colorStr = getColorStrFromCanvas( 408 | self.hiddenContext, 409 | e.layerX, 410 | e.layerY 411 | ) 412 | const node = self.colorNodeMap[colorStr] 413 | if (node) { 414 | self.toggleTreeNode(node.data()[0]) 415 | self.update(node.data()[0]) 416 | } 417 | }) 418 | } 419 | 420 | setMouseWheelZoomListener() { 421 | const self = this 422 | this.canvasNode.node().addEventListener('mousewheel', (event) => { 423 | event.preventDefault() 424 | if (event.deltaY < 0) { 425 | self.bigger() 426 | } else { 427 | self.smaller() 428 | } 429 | }) 430 | } 431 | 432 | setDragListener() { 433 | this.onDrag_ = false 434 | this.dragStartPoint_ = { x: 0, y: 0 } 435 | const self = this 436 | this.canvasNode.node().onmousedown = function (e) { 437 | self.dragStartPoint_.x = e.x 438 | self.dragStartPoint_.y = e.y 439 | self.onDrag_ = true 440 | } 441 | 442 | this.canvasNode.node().onmousemove = function (e) { 443 | if (!self.onDrag_) { 444 | return 445 | } 446 | self.context.translate( 447 | (e.x - self.dragStartPoint_.x) / self.scale, 448 | (e.y - self.dragStartPoint_.y) / self.scale 449 | ) 450 | self.hiddenContext.translate( 451 | (e.x - self.dragStartPoint_.x) / self.scale, 452 | (e.y - self.dragStartPoint_.y) / self.scale 453 | ) 454 | self.dragStartPoint_.x = e.x 455 | self.dragStartPoint_.y = e.y 456 | } 457 | 458 | this.canvasNode.node().onmouseout = function (e) { 459 | if (self.onDrag_) { 460 | self.onDrag_ = false 461 | } 462 | } 463 | 464 | this.canvasNode.node().onmouseup = function (e) { 465 | if (self.onDrag_) { 466 | self.onDrag_ = false 467 | } 468 | } 469 | } 470 | 471 | toggleTreeNode(treeNode) { 472 | if (treeNode.children) { 473 | treeNode._children = treeNode.children 474 | treeNode.children = null 475 | } else { 476 | treeNode.children = treeNode._children 477 | treeNode._children = null 478 | } 479 | } 480 | 481 | bigger() { 482 | if (this.scale > 7) return 483 | this.clearCanvas_() 484 | this.scale = (this.scale * 5) / 4 485 | this.context.scale(5 / 4, 5 / 4) 486 | this.hiddenContext.scale(5 / 4, 5 / 4) 487 | } 488 | 489 | smaller() { 490 | if (this.scale < 0.2) return 491 | this.clearCanvas_() 492 | 493 | this.scale = (this.scale * 4) / 5 494 | this.context.scale(4 / 5, 4 / 5) 495 | this.hiddenContext.scale(4 / 5, 4 / 5) 496 | } 497 | 498 | clearCanvas_() { 499 | this.context.clearRect(-1000000, -10000, 2000000, 2000000) 500 | this.hiddenContext.clearRect(-1000000, -10000, 2000000, 2000000) 501 | } 502 | } 503 | 504 | export default OrgChart 505 | -------------------------------------------------------------------------------- /packages/tree-chart-demo/src/demo/App.vue: -------------------------------------------------------------------------------- 1 | 48 | 49 | 85 | 86 | 94 | -------------------------------------------------------------------------------- /packages/tree-chart-demo/src/demo/google-icon.css: -------------------------------------------------------------------------------- 1 | /* fallback */ 2 | @font-face { 3 | font-family: 'Material Icons'; 4 | font-style: normal; 5 | font-weight: 400; 6 | src: url(https://fonts.gstatic.com/s/materialicons/v46/flUhRq6tzZclQEJ-Vdg-IuiaDsNcIhQ8tQ.woff2) format('woff2'); 7 | } 8 | /* cyrillic-ext */ 9 | @font-face { 10 | font-family: 'Roboto'; 11 | font-style: normal; 12 | font-weight: 300; 13 | src: local('Roboto Light'), local('Roboto-Light'), url(https://fonts.gstatic.com/s/roboto/v18/KFOlCnqEu92Fr1MmSU5fCRc4AMP6lbBP.woff2) format('woff2'); 14 | unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F; 15 | } 16 | /* cyrillic */ 17 | @font-face { 18 | font-family: 'Roboto'; 19 | font-style: normal; 20 | font-weight: 300; 21 | src: local('Roboto Light'), local('Roboto-Light'), url(https://fonts.gstatic.com/s/roboto/v18/KFOlCnqEu92Fr1MmSU5fABc4AMP6lbBP.woff2) format('woff2'); 22 | unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116; 23 | } 24 | /* greek-ext */ 25 | @font-face { 26 | font-family: 'Roboto'; 27 | font-style: normal; 28 | font-weight: 300; 29 | src: local('Roboto Light'), local('Roboto-Light'), url(https://fonts.gstatic.com/s/roboto/v18/KFOlCnqEu92Fr1MmSU5fCBc4AMP6lbBP.woff2) format('woff2'); 30 | unicode-range: U+1F00-1FFF; 31 | } 32 | /* greek */ 33 | @font-face { 34 | font-family: 'Roboto'; 35 | font-style: normal; 36 | font-weight: 300; 37 | src: local('Roboto Light'), local('Roboto-Light'), url(https://fonts.gstatic.com/s/roboto/v18/KFOlCnqEu92Fr1MmSU5fBxc4AMP6lbBP.woff2) format('woff2'); 38 | unicode-range: U+0370-03FF; 39 | } 40 | /* vietnamese */ 41 | @font-face { 42 | font-family: 'Roboto'; 43 | font-style: normal; 44 | font-weight: 300; 45 | src: local('Roboto Light'), local('Roboto-Light'), url(https://fonts.gstatic.com/s/roboto/v18/KFOlCnqEu92Fr1MmSU5fCxc4AMP6lbBP.woff2) format('woff2'); 46 | unicode-range: U+0102-0103, U+0110-0111, U+1EA0-1EF9, U+20AB; 47 | } 48 | /* latin-ext */ 49 | @font-face { 50 | font-family: 'Roboto'; 51 | font-style: normal; 52 | font-weight: 300; 53 | src: local('Roboto Light'), local('Roboto-Light'), url(https://fonts.gstatic.com/s/roboto/v18/KFOlCnqEu92Fr1MmSU5fChc4AMP6lbBP.woff2) format('woff2'); 54 | unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF; 55 | } 56 | /* latin */ 57 | @font-face { 58 | font-family: 'Roboto'; 59 | font-style: normal; 60 | font-weight: 300; 61 | src: local('Roboto Light'), local('Roboto-Light'), url(https://fonts.gstatic.com/s/roboto/v18/KFOlCnqEu92Fr1MmSU5fBBc4AMP6lQ.woff2) format('woff2'); 62 | unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; 63 | } 64 | /* cyrillic-ext */ 65 | @font-face { 66 | font-family: 'Roboto'; 67 | font-style: normal; 68 | font-weight: 400; 69 | src: local('Roboto'), local('Roboto-Regular'), url(https://fonts.gstatic.com/s/roboto/v18/KFOmCnqEu92Fr1Mu72xKKTU1Kvnz.woff2) format('woff2'); 70 | unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F; 71 | } 72 | /* cyrillic */ 73 | @font-face { 74 | font-family: 'Roboto'; 75 | font-style: normal; 76 | font-weight: 400; 77 | src: local('Roboto'), local('Roboto-Regular'), url(https://fonts.gstatic.com/s/roboto/v18/KFOmCnqEu92Fr1Mu5mxKKTU1Kvnz.woff2) format('woff2'); 78 | unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116; 79 | } 80 | /* greek-ext */ 81 | @font-face { 82 | font-family: 'Roboto'; 83 | font-style: normal; 84 | font-weight: 400; 85 | src: local('Roboto'), local('Roboto-Regular'), url(https://fonts.gstatic.com/s/roboto/v18/KFOmCnqEu92Fr1Mu7mxKKTU1Kvnz.woff2) format('woff2'); 86 | unicode-range: U+1F00-1FFF; 87 | } 88 | /* greek */ 89 | @font-face { 90 | font-family: 'Roboto'; 91 | font-style: normal; 92 | font-weight: 400; 93 | src: local('Roboto'), local('Roboto-Regular'), url(https://fonts.gstatic.com/s/roboto/v18/KFOmCnqEu92Fr1Mu4WxKKTU1Kvnz.woff2) format('woff2'); 94 | unicode-range: U+0370-03FF; 95 | } 96 | /* vietnamese */ 97 | @font-face { 98 | font-family: 'Roboto'; 99 | font-style: normal; 100 | font-weight: 400; 101 | src: local('Roboto'), local('Roboto-Regular'), url(https://fonts.gstatic.com/s/roboto/v18/KFOmCnqEu92Fr1Mu7WxKKTU1Kvnz.woff2) format('woff2'); 102 | unicode-range: U+0102-0103, U+0110-0111, U+1EA0-1EF9, U+20AB; 103 | } 104 | /* latin-ext */ 105 | @font-face { 106 | font-family: 'Roboto'; 107 | font-style: normal; 108 | font-weight: 400; 109 | src: local('Roboto'), local('Roboto-Regular'), url(https://fonts.gstatic.com/s/roboto/v18/KFOmCnqEu92Fr1Mu7GxKKTU1Kvnz.woff2) format('woff2'); 110 | unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF; 111 | } 112 | /* latin */ 113 | @font-face { 114 | font-family: 'Roboto'; 115 | font-style: normal; 116 | font-weight: 400; 117 | src: local('Roboto'), local('Roboto-Regular'), url(https://fonts.gstatic.com/s/roboto/v18/KFOmCnqEu92Fr1Mu4mxKKTU1Kg.woff2) format('woff2'); 118 | unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; 119 | } 120 | /* cyrillic-ext */ 121 | @font-face { 122 | font-family: 'Roboto'; 123 | font-style: normal; 124 | font-weight: 500; 125 | src: local('Roboto Medium'), local('Roboto-Medium'), url(https://fonts.gstatic.com/s/roboto/v18/KFOlCnqEu92Fr1MmEU9fCRc4AMP6lbBP.woff2) format('woff2'); 126 | unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F; 127 | } 128 | /* cyrillic */ 129 | @font-face { 130 | font-family: 'Roboto'; 131 | font-style: normal; 132 | font-weight: 500; 133 | src: local('Roboto Medium'), local('Roboto-Medium'), url(https://fonts.gstatic.com/s/roboto/v18/KFOlCnqEu92Fr1MmEU9fABc4AMP6lbBP.woff2) format('woff2'); 134 | unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116; 135 | } 136 | /* greek-ext */ 137 | @font-face { 138 | font-family: 'Roboto'; 139 | font-style: normal; 140 | font-weight: 500; 141 | src: local('Roboto Medium'), local('Roboto-Medium'), url(https://fonts.gstatic.com/s/roboto/v18/KFOlCnqEu92Fr1MmEU9fCBc4AMP6lbBP.woff2) format('woff2'); 142 | unicode-range: U+1F00-1FFF; 143 | } 144 | /* greek */ 145 | @font-face { 146 | font-family: 'Roboto'; 147 | font-style: normal; 148 | font-weight: 500; 149 | src: local('Roboto Medium'), local('Roboto-Medium'), url(https://fonts.gstatic.com/s/roboto/v18/KFOlCnqEu92Fr1MmEU9fBxc4AMP6lbBP.woff2) format('woff2'); 150 | unicode-range: U+0370-03FF; 151 | } 152 | /* vietnamese */ 153 | @font-face { 154 | font-family: 'Roboto'; 155 | font-style: normal; 156 | font-weight: 500; 157 | src: local('Roboto Medium'), local('Roboto-Medium'), url(https://fonts.gstatic.com/s/roboto/v18/KFOlCnqEu92Fr1MmEU9fCxc4AMP6lbBP.woff2) format('woff2'); 158 | unicode-range: U+0102-0103, U+0110-0111, U+1EA0-1EF9, U+20AB; 159 | } 160 | /* latin-ext */ 161 | @font-face { 162 | font-family: 'Roboto'; 163 | font-style: normal; 164 | font-weight: 500; 165 | src: local('Roboto Medium'), local('Roboto-Medium'), url(https://fonts.gstatic.com/s/roboto/v18/KFOlCnqEu92Fr1MmEU9fChc4AMP6lbBP.woff2) format('woff2'); 166 | unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF; 167 | } 168 | /* latin */ 169 | @font-face { 170 | font-family: 'Roboto'; 171 | font-style: normal; 172 | font-weight: 500; 173 | src: local('Roboto Medium'), local('Roboto-Medium'), url(https://fonts.gstatic.com/s/roboto/v18/KFOlCnqEu92Fr1MmEU9fBBc4AMP6lQ.woff2) format('woff2'); 174 | unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; 175 | } 176 | /* cyrillic-ext */ 177 | @font-face { 178 | font-family: 'Roboto'; 179 | font-style: normal; 180 | font-weight: 700; 181 | src: local('Roboto Bold'), local('Roboto-Bold'), url(https://fonts.gstatic.com/s/roboto/v18/KFOlCnqEu92Fr1MmWUlfCRc4AMP6lbBP.woff2) format('woff2'); 182 | unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F; 183 | } 184 | /* cyrillic */ 185 | @font-face { 186 | font-family: 'Roboto'; 187 | font-style: normal; 188 | font-weight: 700; 189 | src: local('Roboto Bold'), local('Roboto-Bold'), url(https://fonts.gstatic.com/s/roboto/v18/KFOlCnqEu92Fr1MmWUlfABc4AMP6lbBP.woff2) format('woff2'); 190 | unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116; 191 | } 192 | /* greek-ext */ 193 | @font-face { 194 | font-family: 'Roboto'; 195 | font-style: normal; 196 | font-weight: 700; 197 | src: local('Roboto Bold'), local('Roboto-Bold'), url(https://fonts.gstatic.com/s/roboto/v18/KFOlCnqEu92Fr1MmWUlfCBc4AMP6lbBP.woff2) format('woff2'); 198 | unicode-range: U+1F00-1FFF; 199 | } 200 | /* greek */ 201 | @font-face { 202 | font-family: 'Roboto'; 203 | font-style: normal; 204 | font-weight: 700; 205 | src: local('Roboto Bold'), local('Roboto-Bold'), url(https://fonts.gstatic.com/s/roboto/v18/KFOlCnqEu92Fr1MmWUlfBxc4AMP6lbBP.woff2) format('woff2'); 206 | unicode-range: U+0370-03FF; 207 | } 208 | /* vietnamese */ 209 | @font-face { 210 | font-family: 'Roboto'; 211 | font-style: normal; 212 | font-weight: 700; 213 | src: local('Roboto Bold'), local('Roboto-Bold'), url(https://fonts.gstatic.com/s/roboto/v18/KFOlCnqEu92Fr1MmWUlfCxc4AMP6lbBP.woff2) format('woff2'); 214 | unicode-range: U+0102-0103, U+0110-0111, U+1EA0-1EF9, U+20AB; 215 | } 216 | /* latin-ext */ 217 | @font-face { 218 | font-family: 'Roboto'; 219 | font-style: normal; 220 | font-weight: 700; 221 | src: local('Roboto Bold'), local('Roboto-Bold'), url(https://fonts.gstatic.com/s/roboto/v18/KFOlCnqEu92Fr1MmWUlfChc4AMP6lbBP.woff2) format('woff2'); 222 | unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF; 223 | } 224 | /* latin */ 225 | @font-face { 226 | font-family: 'Roboto'; 227 | font-style: normal; 228 | font-weight: 700; 229 | src: local('Roboto Bold'), local('Roboto-Bold'), url(https://fonts.gstatic.com/s/roboto/v18/KFOlCnqEu92Fr1MmWUlfBBc4AMP6lQ.woff2) format('woff2'); 230 | unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; 231 | } 232 | 233 | .material-icons { 234 | font-family: 'Material Icons'; 235 | font-weight: normal; 236 | font-style: normal; 237 | font-size: 24px; 238 | line-height: 1; 239 | letter-spacing: normal; 240 | text-transform: none; 241 | display: inline-block; 242 | white-space: nowrap; 243 | word-wrap: normal; 244 | direction: ltr; 245 | -webkit-font-feature-settings: 'liga'; 246 | -webkit-font-smoothing: antialiased; 247 | } 248 | -------------------------------------------------------------------------------- /packages/tree-chart-demo/src/demo/main.ts: -------------------------------------------------------------------------------- 1 | // The Vue build version to load with the `import` command 2 | // (runtime-only or standalone) has been set in webpack.base.conf with an alias. 3 | import Vue from 'vue' 4 | import App from './App.vue' 5 | import router from './router' 6 | import Vuetify from 'vuetify' 7 | import 'vuetify/dist/vuetify.min.css' 8 | import './google-icon.css' 9 | 10 | Vue.use(Vuetify) 11 | Vue.config.productionTip = false 12 | 13 | /* eslint-disable no-new */ 14 | new Vue({ 15 | el: '#app', 16 | router, 17 | components: { App }, 18 | template: '' 19 | }) 20 | -------------------------------------------------------------------------------- /packages/tree-chart-demo/src/demo/router/constant.ts: -------------------------------------------------------------------------------- 1 | export const SVG_TREE = 'svgTree' 2 | export const CANVAS_TREE = 'canvasTree' 3 | export const JSON_VIEWER = 'jsonViewer' 4 | -------------------------------------------------------------------------------- /packages/tree-chart-demo/src/demo/router/index.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Router from 'vue-router' 3 | import CanvasTree from '../../components/CanvasTree.vue' 4 | import VueTree from '../../components/VueTreeDemo.vue' 5 | import * as Cons from './constant' 6 | 7 | Vue.use(Router) 8 | 9 | export default new Router({ 10 | routes: [ 11 | { 12 | path: '/', 13 | redirect: Cons.SVG_TREE 14 | }, 15 | { 16 | path: '/' + Cons.CANVAS_TREE, 17 | name: Cons.CANVAS_TREE, 18 | component: CanvasTree 19 | }, 20 | { 21 | path: '/' + Cons.SVG_TREE, 22 | name: Cons.SVG_TREE, 23 | component: VueTree 24 | } 25 | ] 26 | }) 27 | -------------------------------------------------------------------------------- /packages/tree-chart-demo/src/vue.shims.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.vue' { 2 | import Vue from 'vue' 3 | export default Vue 4 | } 5 | -------------------------------------------------------------------------------- /packages/tree-chart-demo/template/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | organization-chart 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /packages/tree-chart-demo/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "./build/", 4 | "sourceMap": true, 5 | "strict": false, 6 | "noImplicitReturns": true, 7 | "module": "es2015", 8 | "moduleResolution": "node", 9 | "target": "es5" 10 | }, 11 | "include": ["src/**/*"] 12 | } 13 | -------------------------------------------------------------------------------- /packages/vue-tree-chart/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["env", { 4 | "modules": false, 5 | "targets": { 6 | "browsers": ["> 1%", "last 2 versions", "not ie <= 8"] 7 | } 8 | }], 9 | "stage-2" 10 | ], 11 | "plugins": ["transform-vue-jsx", "transform-runtime"] 12 | } 13 | -------------------------------------------------------------------------------- /packages/vue-tree-chart/.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /packages/vue-tree-chart/.eslintignore: -------------------------------------------------------------------------------- 1 | /build/ 2 | /config/ 3 | /dist/ 4 | /*.js 5 | library/*.js 6 | src/**/*d.ts 7 | -------------------------------------------------------------------------------- /packages/vue-tree-chart/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | 'node': true 5 | }, 6 | extends: [ 7 | 'plugin:vue/essential', 8 | '@vue/typescript', 9 | '@vue/prettier', 10 | ], 11 | parserOptions: { 12 | ecmaVersion: 2020, 13 | }, 14 | rules: { 15 | 'prettier/prettier': [ 16 | 'warn', 17 | { 18 | '#': 'prettier config in here :)', 19 | 'singleQuote': true, 20 | 'semi': false, 21 | 'trailingComma': 'none', 22 | 'space-before-function-paren': 'off' 23 | } 24 | ] 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /packages/vue-tree-chart/.postcssrc.js: -------------------------------------------------------------------------------- 1 | // https://github.com/michael-ciniawsky/postcss-load-config 2 | 3 | module.exports = { 4 | "plugins": { 5 | "postcss-import": {}, 6 | "postcss-url": {}, 7 | // to edit target browsers: use "browserslist" field in package.json 8 | "autoprefixer": {} 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /packages/vue-tree-chart/.prettierignore: -------------------------------------------------------------------------------- 1 | /build/ 2 | /config/ 3 | /dist/ 4 | /*.js 5 | library/*.js 6 | -------------------------------------------------------------------------------- /packages/vue-tree-chart/README.md: -------------------------------------------------------------------------------- 1 | # @ssthouse/vue-tree-chart 2 | 3 | ## Docs 4 | 5 | (https://github.com/ssthouse/vue-tree-chart)[https://github.com/ssthouse/vue-tree-chart] 6 | 7 | ## 文档 8 | 9 | (https://github.com/ssthouse/vue-tree-chart)[https://github.com/ssthouse/vue-tree-chart] -------------------------------------------------------------------------------- /packages/vue-tree-chart/build/webpack.config.base.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const { VueLoaderPlugin } = require('vue-loader') 3 | const HtmlWebpackPlugin = require('html-webpack-plugin') 4 | 5 | module.exports = { 6 | entry: './src/demo/main.ts', 7 | output: { 8 | path: path.resolve(__dirname, '../dist'), 9 | filename: 'bundle.js' 10 | }, 11 | module: { 12 | rules: [ 13 | { 14 | test: /\.css$/, 15 | use: ['style-loader', 'css-loader'] 16 | }, 17 | { 18 | test: /\.less$/, 19 | use: ['style-loader', 'css-loader', 'less-loader'] 20 | }, 21 | { 22 | test: /\.(png|svg|jpg|gif)$/, 23 | use: ['file-loader'] 24 | }, 25 | { 26 | test: /\.vue$/, 27 | use: ['vue-loader'] 28 | }, 29 | { 30 | test: /\.ts$/, 31 | loader: 'ts-loader', 32 | exclude: /node_modules/, 33 | options: { 34 | appendTsSuffixTo: [/\.vue$/] 35 | } 36 | } 37 | ] 38 | }, 39 | plugins: [ 40 | new VueLoaderPlugin(), 41 | new HtmlWebpackPlugin({ 42 | filename: 'index.html', 43 | template: './template/index.html', 44 | inject: true 45 | }) 46 | ], 47 | resolve: { 48 | extensions: ['.ts', '.js', '.vue', '.json'], 49 | alias: { 50 | vue$: 'vue/dist/vue.esm.js' 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /packages/vue-tree-chart/build/webpack.config.dev.js: -------------------------------------------------------------------------------- 1 | const merge = require('webpack-merge') 2 | 3 | module.exports = merge(require('./webpack.config.base'), { 4 | mode: 'development', 5 | output: { 6 | publicPath: '/' 7 | }, 8 | devtool: 'source-map' 9 | }) 10 | -------------------------------------------------------------------------------- /packages/vue-tree-chart/build/webpack.config.library.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const { VueLoaderPlugin } = require('vue-loader') 3 | 4 | module.exports = { 5 | mode: 'development', 6 | entry: './src/vue-tree/index.ts', 7 | output: { 8 | path: path.resolve(__dirname, '..', 'library'), 9 | filename: 'vue-tree-chart.js', 10 | library: 'vue-tree-chart', 11 | libraryTarget: 'umd' 12 | }, 13 | module: { 14 | rules: [ 15 | { 16 | test: /\.css$/, 17 | use: ['style-loader', 'css-loader'] 18 | }, 19 | { 20 | test: /\.less$/, 21 | use: ['style-loader', 'css-loader', 'less-loader'] 22 | }, 23 | { 24 | test: /\.(png|svg|jpg|gif)$/, 25 | use: ['file-loader'] 26 | }, 27 | { 28 | test: /\.vue$/, 29 | use: ['vue-loader'] 30 | }, 31 | { 32 | test: /\.ts$/, 33 | loader: 'ts-loader', 34 | exclude: /node_modules/, 35 | options: { 36 | appendTsSuffixTo: [/\.vue$/] 37 | } 38 | } 39 | ] 40 | }, 41 | plugins: [new VueLoaderPlugin()], 42 | resolve: { 43 | extensions: ['.ts', '.js', '.vue', '.json'], 44 | alias: { 45 | vue$: 'vue/dist/vue.esm.js' 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /packages/vue-tree-chart/library/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ssthouse/tree-chart/948a4d2bcd2415ed1a8a34e78ee54e74c3c40c54/packages/vue-tree-chart/library/.gitkeep -------------------------------------------------------------------------------- /packages/vue-tree-chart/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@ssthouse/vue-tree-chart", 3 | "version": "0.6.8", 4 | "description": "Tree Component for Vue2 powered by D3.js & SVG", 5 | "author": "ssthouse", 6 | "main": "library/vue-tree-chart.js", 7 | "scripts": { 8 | "lint": "eslint --ext .js,.vue,.ts src", 9 | "dev": "webpack serve --config ./build/webpack.config.dev.js", 10 | "build:component": "webpack --config ./build/webpack.config.library.js", 11 | "release": "npm run build:component && npm publish" 12 | }, 13 | "dependencies": { 14 | "@ssthouse/tree-chart-core": "1.1.2" 15 | }, 16 | "peerDependencies": { 17 | "vue": "^2.6.8" 18 | }, 19 | "devDependencies": { 20 | "@types/d3": "^7.1.0", 21 | "@typescript-eslint/eslint-plugin": "^2.34.0", 22 | "@typescript-eslint/parser": "^2.34.0", 23 | "@vue/eslint-config-prettier": "^6.0.0", 24 | "@vue/eslint-config-typescript": "^5.0.2", 25 | "autoprefixer": "^7.1.2", 26 | "babel-core": "^6.22.1", 27 | "babel-eslint": "^8.2.1", 28 | "babel-helper-vue-jsx-merge-props": "^2.0.3", 29 | "babel-loader": "^7.1.1", 30 | "babel-plugin-syntax-jsx": "^6.18.0", 31 | "babel-plugin-transform-runtime": "^6.22.0", 32 | "babel-plugin-transform-vue-jsx": "^3.5.0", 33 | "babel-preset-env": "^1.3.2", 34 | "babel-preset-stage-2": "^6.22.0", 35 | "chalk": "^2.0.1", 36 | "copy-webpack-plugin": "^9.0.1", 37 | "css-loader": "^6.5.0", 38 | "eslint": "^8.1.0", 39 | "eslint-config-standard": "^10.2.1", 40 | "eslint-friendly-formatter": "^3.0.0", 41 | "eslint-loader": "^1.7.1", 42 | "eslint-plugin-import": "^2.7.0", 43 | "eslint-plugin-node": "^5.2.0", 44 | "eslint-plugin-prettier": "^3.1.4", 45 | "eslint-plugin-promise": "^3.4.0", 46 | "eslint-plugin-standard": "^3.0.1", 47 | "eslint-plugin-vue": "^6.0.0", 48 | "extract-text-webpack-plugin": "^3.0.0", 49 | "file-loader": "^1.1.4", 50 | "html-webpack-plugin": "^5.5.0", 51 | "husky": "^7.0.4", 52 | "lerna": "^4.0.0", 53 | "less": "^4.1.2", 54 | "less-loader": "^10.2.0", 55 | "lint-staged": "^10.2.11", 56 | "node-notifier": "^8.0.1", 57 | "optimize-css-assets-webpack-plugin": "^6.0.1", 58 | "ora": "^1.2.0", 59 | "portfinder": "^1.0.13", 60 | "postcss-import": "^11.0.0", 61 | "postcss-loader": "^6.2.0", 62 | "postcss-url": "^7.2.1", 63 | "prettier": "^2.0.5", 64 | "rimraf": "^2.6.0", 65 | "semver": "^5.3.0", 66 | "shelljs": "^0.8.5", 67 | "style-loader": "^3.3.1", 68 | "ts-loader": "^9.2.6", 69 | "typescript": "^3.3.3333", 70 | "uglifyjs-webpack-plugin": "^2.2.0", 71 | "url-loader": "^4.1.1", 72 | "vue": "2.6.8", 73 | "vue-class-component": "^7.0.1", 74 | "vue-loader": "^15.7.0", 75 | "vue-property-decorator": "^8.0.0", 76 | "vue-style-loader": "^4.1.3", 77 | "vue-template-compiler": "2.6.8", 78 | "webpack": "^5.61.0", 79 | "webpack-bundle-analyzer": "^3.3.2", 80 | "webpack-cli": "^4.9.1", 81 | "webpack-dev-server": "^4.4.0", 82 | "webpack-merge": "^4.1.0" 83 | }, 84 | "engines": { 85 | "node": ">= 6.0.0", 86 | "npm": ">= 3.0.0" 87 | }, 88 | "browserslist": [ 89 | "> 1%", 90 | "last 2 versions", 91 | "not ie <= 8" 92 | ], 93 | "prettier": { 94 | "#": "prettier config in here :)", 95 | "singleQuote": true, 96 | "semi": false, 97 | "arrowParens": "always", 98 | "space-before-function-paren": "off", 99 | "trailingComma": "none" 100 | }, 101 | "husky": { 102 | "hooks": { 103 | "pre-commit": "lint-staged" 104 | } 105 | }, 106 | "lint-staged": { 107 | "*.js": [ 108 | "eslint", 109 | "prettier --write" 110 | ], 111 | "*.vue": [ 112 | "eslint", 113 | "prettier --write" 114 | ] 115 | }, 116 | "publishConfig": { 117 | "registry": "https://registry.npmjs.org/" 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /packages/vue-tree-chart/src/base/uuid.ts: -------------------------------------------------------------------------------- 1 | 2 | export function uuid(): string { 3 | const s = [] 4 | const hexDigits = '0123456789abcdef' 5 | for (let i = 0; i < 36; i++) { 6 | s[i] = hexDigits.substr(Math.floor(Math.random() * 0x10), 1) 7 | } 8 | s[14] = '4' 9 | s[19] = hexDigits.substr((s[19] & 0x3) | 0x8, 1) 10 | s[8] = s[13] = s[18] = s[23] = '-' 11 | return s.join('') 12 | } 13 | -------------------------------------------------------------------------------- /packages/vue-tree-chart/src/demo/App.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 49 | 50 | 60 | -------------------------------------------------------------------------------- /packages/vue-tree-chart/src/demo/main.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import App from './App.vue' 3 | 4 | new Vue({ 5 | el: '#app', 6 | components: { App }, 7 | template: '' 8 | }) 9 | -------------------------------------------------------------------------------- /packages/vue-tree-chart/src/vue-tree/VueTree.vue: -------------------------------------------------------------------------------- 1 | 39 | 40 | 147 | 148 | 161 | 162 | 216 | -------------------------------------------------------------------------------- /packages/vue-tree-chart/src/vue-tree/index.ts: -------------------------------------------------------------------------------- 1 | import VueTree from './VueTree.vue' 2 | 3 | export default VueTree 4 | -------------------------------------------------------------------------------- /packages/vue-tree-chart/src/vue.shims.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.vue' { 2 | import Vue from 'vue' 3 | export default Vue 4 | } 5 | -------------------------------------------------------------------------------- /packages/vue-tree-chart/template/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ssthouse/tree-chart/948a4d2bcd2415ed1a8a34e78ee54e74c3c40c54/packages/vue-tree-chart/template/favicon.ico -------------------------------------------------------------------------------- /packages/vue-tree-chart/template/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | <%= htmlWebpackPlugin.options.title %> 8 | 9 | 10 | 13 |
14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /packages/vue-tree-chart/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "./build/", 4 | "sourceMap": true, 5 | "strict": false, 6 | "noImplicitReturns": true, 7 | "module": "es2015", 8 | "moduleResolution": "node", 9 | "target": "es5" 10 | }, 11 | "include": ["src/**/*"] 12 | } 13 | -------------------------------------------------------------------------------- /packages/vue3-tree-chart/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | 5 | 6 | # local env files 7 | .env.local 8 | .env.*.local 9 | 10 | # Log files 11 | npm-debug.log* 12 | yarn-debug.log* 13 | yarn-error.log* 14 | pnpm-debug.log* 15 | 16 | # Editor directories and files 17 | .idea 18 | .vscode 19 | *.suo 20 | *.ntvs* 21 | *.njsproj 22 | *.sln 23 | *.sw? 24 | -------------------------------------------------------------------------------- /packages/vue3-tree-chart/README.md: -------------------------------------------------------------------------------- 1 | # @ssthouse/vue3-tree-chart 2 | 3 | ## Docs 4 | 5 | [https://github.com/ssthouse/vue-tree-chart](https://github.com/ssthouse/vue-tree-chart) 6 | 7 | ## 文档 8 | 9 | [https://github.com/ssthouse/vue-tree-chart](https://github.com/ssthouse/vue-tree-chart) -------------------------------------------------------------------------------- /packages/vue3-tree-chart/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@vue/cli-plugin-babel/preset' 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /packages/vue3-tree-chart/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@ssthouse/vue3-tree-chart", 3 | "author": "ssthouse", 4 | "version": "0.2.6", 5 | "description": "Tree Component for Vue3 powered by D3.js & SVG", 6 | "scripts": { 7 | "serve": "vue-cli-service serve", 8 | "build": "vue-cli-service build", 9 | "lint": "vue-cli-service lint", 10 | "release": "npm run build:component && npm publish", 11 | "build:component": "vue-cli-service build --target lib --name vue3-tree-chart src/vue-tree/index.js" 12 | }, 13 | "main": "dist/vue3-tree-chart.umd.js", 14 | "dependencies": { 15 | "@ssthouse/tree-chart-core": "^1.1.0" 16 | }, 17 | "devDependencies": { 18 | "@vue/cli-plugin-babel": "~4.5.0", 19 | "@vue/cli-plugin-eslint": "~4.5.0", 20 | "@vue/cli-service": "~4.5.0", 21 | "@vue/compiler-sfc": "^3.0.0", 22 | "babel-eslint": "^10.1.0", 23 | "eslint": "^6.7.2", 24 | "eslint-plugin-vue": "^7.0.0", 25 | "less": "^4.1.2", 26 | "less-loader": "^6.0.0", 27 | "vue": "^3.0.0" 28 | }, 29 | "eslintConfig": { 30 | "root": true, 31 | "env": { 32 | "node": true 33 | }, 34 | "extends": [ 35 | "plugin:vue/vue3-essential", 36 | "eslint:recommended" 37 | ], 38 | "parserOptions": { 39 | "parser": "babel-eslint" 40 | }, 41 | "rules": {} 42 | }, 43 | "browserslist": [ 44 | "> 1%", 45 | "last 2 versions", 46 | "not dead" 47 | ], 48 | "publishConfig": { 49 | "registry": "https://registry.npmjs.org/" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /packages/vue3-tree-chart/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ssthouse/tree-chart/948a4d2bcd2415ed1a8a34e78ee54e74c3c40c54/packages/vue3-tree-chart/public/favicon.ico -------------------------------------------------------------------------------- /packages/vue3-tree-chart/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | <%= htmlWebpackPlugin.options.title %> 9 | 10 | 11 | 14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /packages/vue3-tree-chart/src/App.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 49 | 50 | 60 | -------------------------------------------------------------------------------- /packages/vue3-tree-chart/src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ssthouse/tree-chart/948a4d2bcd2415ed1a8a34e78ee54e74c3c40c54/packages/vue3-tree-chart/src/assets/logo.png -------------------------------------------------------------------------------- /packages/vue3-tree-chart/src/base/uuid.js: -------------------------------------------------------------------------------- 1 | 2 | export function uuid() { 3 | const s = [] 4 | const hexDigits = '0123456789abcdef' 5 | for (let i = 0; i < 36; i++) { 6 | s[i] = hexDigits.substr(Math.floor(Math.random() * 0x10), 1) 7 | } 8 | s[14] = '4' 9 | s[19] = hexDigits.substr((s[19] & 0x3) | 0x8, 1) 10 | s[8] = s[13] = s[18] = s[23] = '-' 11 | return s.join('') 12 | } 13 | -------------------------------------------------------------------------------- /packages/vue3-tree-chart/src/components/HelloWorld.vue: -------------------------------------------------------------------------------- 1 | 32 | 33 | 41 | 42 | 43 | 59 | -------------------------------------------------------------------------------- /packages/vue3-tree-chart/src/main.js: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue' 2 | import App from './App.vue' 3 | 4 | createApp(App).mount('#app') 5 | -------------------------------------------------------------------------------- /packages/vue3-tree-chart/src/vue-tree/VueTree.vue: -------------------------------------------------------------------------------- 1 | 39 | 40 | 147 | 148 | 161 | 162 | 216 | -------------------------------------------------------------------------------- /packages/vue3-tree-chart/src/vue-tree/index.js: -------------------------------------------------------------------------------- 1 | // @ts-ignore 2 | import VueTree from './VueTree.vue' 3 | 4 | export default VueTree 5 | -------------------------------------------------------------------------------- /packages/vue3-tree-chart/vite.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | css: { extract: false } 3 | } -------------------------------------------------------------------------------- /screenshots/demo.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ssthouse/tree-chart/948a4d2bcd2415ed1a8a34e78ee54e74c3c40c54/screenshots/demo.jpeg -------------------------------------------------------------------------------- /screenshots/org-chart.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ssthouse/tree-chart/948a4d2bcd2415ed1a8a34e78ee54e74c3c40c54/screenshots/org-chart.gif -------------------------------------------------------------------------------- /screenshots/unique_color.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ssthouse/tree-chart/948a4d2bcd2415ed1a8a34e78ee54e74c3c40c54/screenshots/unique_color.png --------------------------------------------------------------------------------