├── CONTRIBUTING.md ├── src ├── utils │ ├── dagre │ │ ├── version.js │ │ ├── position │ │ │ └── index.js │ │ ├── order │ │ │ ├── barycenter.js │ │ │ ├── init-order.js │ │ │ ├── add-subgraph-constraints.js │ │ │ ├── sort.js │ │ │ ├── cross-count.js │ │ │ ├── index.js │ │ │ ├── sort-subgraph.js │ │ │ ├── build-layer-graph.js │ │ │ └── resolve-conflicts.js │ │ ├── debug.js │ │ ├── add-border-segments.js │ │ ├── rank │ │ │ ├── index.js │ │ │ ├── util.js │ │ │ ├── feasible-tree.js │ │ │ └── network-simplex.js │ │ ├── index.js │ │ ├── data │ │ │ └── list.js │ │ ├── acyclic.js │ │ ├── lodash.js │ │ ├── coordinate-system.js │ │ ├── parent-dummy-chains.js │ │ ├── normalize.js │ │ ├── greedy-fas.js │ │ ├── nesting-graph.js │ │ └── util.js │ ├── graphlib │ │ ├── version.js │ │ ├── alg │ │ │ ├── postorder.js │ │ │ ├── preorder.js │ │ │ ├── find-cycles.js │ │ │ ├── dijkstra-all.js │ │ │ ├── is-acyclic.js │ │ │ ├── components.js │ │ │ ├── topsort.js │ │ │ ├── index.js │ │ │ ├── tarjan.js │ │ │ ├── dfs.js │ │ │ ├── prim.js │ │ │ ├── floyd-warshall.js │ │ │ └── dijkstra.js │ │ ├── index.js │ │ ├── lodash.js │ │ ├── json.js │ │ └── data │ │ │ └── priority-queue.js │ ├── prevent-default-drop.js │ ├── empty-drag-image.js │ ├── device-pixel-ratio.js │ └── intersect-helper.js ├── index.less ├── components │ ├── grid │ │ ├── GridShapeBase.vue │ │ ├── LineGrid.vue │ │ ├── DotGrid.vue │ │ └── TileGrid.vue │ ├── DragonflyScale.vue │ ├── shift-strategies.js │ ├── DragonflyLinkingEdge.vue │ ├── edge │ │ ├── LineShapeBase.vue │ │ ├── StraightLine.vue │ │ ├── SCurveLine.vue │ │ ├── ZigZagLine.vue │ │ ├── LRoundedCornerLine.vue │ │ └── LBrokenLine.vue │ ├── canvas-wheeling-behavior-handlers.js │ ├── DragonflyEndpoints.vue │ ├── DragonflySelectionProvider.vue │ ├── DragonflyGrid.vue │ ├── DragonflyZoneResizeHandler.vue │ ├── history-traveller.js │ ├── canvas-dragging-behavior-handlers.js │ ├── node-dragging-behavior-handlers.js │ ├── DragonflyCanvasTools.vue │ ├── DragonflyCanvasEdgesLayer.vue │ ├── DragonflyZone.vue │ ├── DragonflyEdge.vue │ ├── DragonflyNode.vue │ ├── DragonflyEndpoint.vue │ ├── DragonflyMinimap.vue │ └── dragonfly-dag.less ├── main.js ├── layout │ └── dagre-layout.js ├── CanvasData.vue ├── index.js ├── CanvasConfig.vue └── App.vue ├── public └── favicon.ico ├── .gitignore ├── index.html ├── vite.config.js ├── interops └── es-browser │ ├── index.js │ ├── index.html │ └── index.css ├── .github └── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md ├── LICENSE ├── package.json └── CODE_OF_CONDUCT.md /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/utils/dagre/version.js: -------------------------------------------------------------------------------- 1 | export default "0.8.6-pre"; 2 | -------------------------------------------------------------------------------- /src/utils/graphlib/version.js: -------------------------------------------------------------------------------- 1 | export default '2.1.9-pre'; 2 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lchrennew/dragonfly-dag/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /src/utils/graphlib/alg/postorder.js: -------------------------------------------------------------------------------- 1 | import dfs from "./dfs.js"; 2 | 3 | export default (g, vs) => dfs(g, vs, "post") 4 | -------------------------------------------------------------------------------- /src/utils/graphlib/alg/preorder.js: -------------------------------------------------------------------------------- 1 | import dfs from "./dfs.js"; 2 | 3 | export default (g, vs) => dfs(g, vs, "pre") 4 | -------------------------------------------------------------------------------- /src/utils/prevent-default-drop.js: -------------------------------------------------------------------------------- 1 | const preventDefaultDrop = event => event.preventDefault() 2 | export default preventDefaultDrop 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | dist 4 | *.local 5 | *.iws 6 | *.patch 7 | package-lock.json 8 | yarn.lock 9 | *.iml 10 | *.ipr 11 | .idea 12 | /yarn-error.log 13 | -------------------------------------------------------------------------------- /src/utils/empty-drag-image.js: -------------------------------------------------------------------------------- 1 | const img = document.createElement("img") 2 | img.src = 'data:image/svg+xml,' 3 | 4 | export default img 5 | -------------------------------------------------------------------------------- /src/index.less: -------------------------------------------------------------------------------- 1 | html { 2 | height: 100%; 3 | } 4 | 5 | body { 6 | height: 100%; 7 | margin: 0; 8 | } 9 | 10 | #app { 11 | width: 100%; 12 | height: 100%; 13 | } 14 | -------------------------------------------------------------------------------- /src/components/grid/GridShapeBase.vue: -------------------------------------------------------------------------------- 1 | 9 | -------------------------------------------------------------------------------- /src/utils/graphlib/index.js: -------------------------------------------------------------------------------- 1 | // Includes only the "core" of graphlib 2 | import _Graph from "./graph.js"; 3 | import _version from "./version.js"; 4 | 5 | export const Graph = _Graph 6 | export const version = _version 7 | -------------------------------------------------------------------------------- /src/utils/graphlib/alg/find-cycles.js: -------------------------------------------------------------------------------- 1 | import _ from "../lodash.js"; 2 | 3 | import tarjan from "./tarjan.js"; 4 | 5 | export default g => _.filter(tarjan(g), cmpt => cmpt.length > 1 || (cmpt.length === 1 && g.hasEdge(cmpt[0], cmpt[0]))); 6 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue' 2 | import App from './App.vue' 3 | import './index.less' 4 | import Antdv from 'ant-design-vue' 5 | 6 | const app = createApp(App) 7 | app.config.unwrapInjectedRef = true 8 | app.use(Antdv).mount('#app') 9 | -------------------------------------------------------------------------------- /src/components/DragonflyScale.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 10 | -------------------------------------------------------------------------------- /src/utils/graphlib/alg/dijkstra-all.js: -------------------------------------------------------------------------------- 1 | import dijkstra from "./dijkstra.js"; 2 | import _ from "../lodash.js"; 3 | 4 | 5 | export default (g, weightFunc, edgeFunc) => _.transform(g.nodes(), (acc, v) => { 6 | acc[v] = dijkstra(g, v, weightFunc, edgeFunc); 7 | }, {}) 8 | -------------------------------------------------------------------------------- /src/components/shift-strategies.js: -------------------------------------------------------------------------------- 1 | const shiftStrategies = { 2 | disabled: (selected, selecting) => selecting, 3 | continue: (selected, selecting) => selected || selecting, 4 | reverse: (selected, selecting) => selecting ? !selected : selected 5 | } 6 | export default shiftStrategies 7 | -------------------------------------------------------------------------------- /src/utils/graphlib/alg/is-acyclic.js: -------------------------------------------------------------------------------- 1 | import topsort from "./topsort.js"; 2 | 3 | export default g => { 4 | try { 5 | topsort(g); 6 | } catch (e) { 7 | if (e instanceof topsort.CycleException) { 8 | return false; 9 | } 10 | throw e; 11 | } 12 | return true; 13 | } 14 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Dragonfly 💗 Butterfly 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /vite.config.js: -------------------------------------------------------------------------------- 1 | import vue from '@vitejs/plugin-vue' 2 | import { defineConfig } from 'vite' 3 | 4 | export default defineConfig({ 5 | plugins: [ vue() ], 6 | server: { port: 3003, host: '0.0.0.0' }, 7 | build: { 8 | rollupOptions: { 9 | external: [ 'vue' ], 10 | output: { 11 | globals: { 12 | vue: 'Vue' 13 | } 14 | } 15 | } 16 | } 17 | }) 18 | -------------------------------------------------------------------------------- /src/components/DragonflyLinkingEdge.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 17 | -------------------------------------------------------------------------------- /interops/es-browser/index.js: -------------------------------------------------------------------------------- 1 | let feed = 1 2 | let nodes = [] 3 | const dag = dragonfly.createDag('#app', 4 | { 5 | nodes, 6 | onUpdateNodes(newNodes) { 7 | nodes = newNodes 8 | }, 9 | nodeRenderer(node) { 10 | const {h} = dragonfly 11 | return h('div', {class: 'node'}, `Hi, ${node.id}`) 12 | } 13 | }) 14 | 15 | 16 | const addNode = () => { 17 | nodes.push({id: feed++}) 18 | dag.setNodes([...nodes]) 19 | } 20 | 21 | -------------------------------------------------------------------------------- /src/utils/device-pixel-ratio.js: -------------------------------------------------------------------------------- 1 | import { computed, ref } from "vue"; 2 | 3 | const innerWidth = ref(window.innerWidth) 4 | const outerWidth = ref(window.outerWidth) 5 | const dpr = ref(window.devicePixelRatio) 6 | window.addEventListener('resize', () => { 7 | innerWidth.value = window.innerWidth 8 | outerWidth.value = window.outerWidth 9 | dpr.value = window.devicePixelRatio 10 | }) 11 | export const zoomLevel = computed(() => outerWidth.value / innerWidth.value) 12 | export const pixelRatio = computed(() => dpr.value / zoomLevel.value) 13 | 14 | -------------------------------------------------------------------------------- /src/components/grid/LineGrid.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 16 | -------------------------------------------------------------------------------- /src/components/grid/DotGrid.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 16 | -------------------------------------------------------------------------------- /src/utils/graphlib/alg/components.js: -------------------------------------------------------------------------------- 1 | import _ from "../lodash.js"; 2 | 3 | export default g => { 4 | const visited = {}; 5 | const cmpts = []; 6 | let cmpt; 7 | 8 | const dfs = v => { 9 | if (_.has(visited, v)) return; 10 | visited[v] = true; 11 | cmpt.push(v); 12 | _.each(g.successors(v), dfs); 13 | _.each(g.predecessors(v), dfs); 14 | }; 15 | 16 | _.each(g.nodes(), v => { 17 | cmpt = []; 18 | dfs(v); 19 | if (cmpt.length) { 20 | cmpts.push(cmpt); 21 | } 22 | }); 23 | 24 | return cmpts; 25 | } 26 | -------------------------------------------------------------------------------- /src/layout/dagre-layout.js: -------------------------------------------------------------------------------- 1 | import dagre from '../utils/dagre/index.js' 2 | 3 | const dagreLayout = (nodes = [], positions = {}, edges = [], config = {}) => { 4 | const g = new dagre.graphlib.Graph({ multigraph: true }) 5 | g.setGraph({ ...config }) 6 | g.setDefaultEdgeLabel(() => ({})) 7 | nodes.forEach(node => g.setNode(node.id, { 8 | label: node.id, 9 | width: positions[node.id]?.width, 10 | height: positions[node.id]?.height, 11 | })) 12 | edges.forEach(edge => g.setEdge(edge.source, edge.target, { label: edge.id })) 13 | dagre.layout(g) 14 | return g 15 | } 16 | 17 | export default dagreLayout 18 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /src/utils/dagre/position/index.js: -------------------------------------------------------------------------------- 1 | import _ from "../lodash.js"; 2 | import * as util from "../util.js"; 3 | import bk from "./bk.js"; 4 | 5 | const { positionX } = bk; 6 | 7 | const positionY = g => { 8 | const layering = util.buildLayerMatrix(g); 9 | const rankSep = g.graph().ranksep; 10 | let prevY = 0; 11 | _.forEach(layering, layer => { 12 | const maxHeight = _.max(_.map(layer, v => g.node(v).height)); 13 | _.forEach(layer, v => g.node(v).y = prevY + maxHeight / 2); 14 | prevY += maxHeight + rankSep; 15 | }); 16 | }; 17 | 18 | 19 | export default g => { 20 | g = util.asNonCompoundGraph(g); 21 | 22 | positionY(g); 23 | _.forEach(positionX(g), (x, v) => g.node(v).x = x); 24 | } 25 | -------------------------------------------------------------------------------- /src/components/grid/TileGrid.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 28 | -------------------------------------------------------------------------------- /src/utils/dagre/order/barycenter.js: -------------------------------------------------------------------------------- 1 | import _ from "../lodash.js"; 2 | 3 | export default (g, movable) => _.map(movable, v => { 4 | const inV = g.inEdges(v); 5 | if (!inV.length) { 6 | return { v: v }; 7 | } else { 8 | const result = _.reduce(inV, (acc, e) => { 9 | const edge = g.edge(e), 10 | nodeU = g.node(e.v); 11 | return { 12 | sum: acc.sum + (edge.weight * nodeU.order), 13 | weight: acc.weight + edge.weight 14 | }; 15 | }, { sum: 0, weight: 0 }); 16 | 17 | return { 18 | v: v, 19 | barycenter: result.sum / result.weight, 20 | weight: result.weight 21 | }; 22 | } 23 | }) 24 | -------------------------------------------------------------------------------- /src/CanvasData.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 22 | 23 | 32 | -------------------------------------------------------------------------------- /src/utils/dagre/debug.js: -------------------------------------------------------------------------------- 1 | import _ from "./lodash.js"; 2 | import * as util from "./util.js"; 3 | import { Graph } from "../graphlib/index.js"; 4 | 5 | /* istanbul ignore next */ 6 | export const debugOrdering = g => { 7 | const layerMatrix = util.buildLayerMatrix(g); 8 | 9 | const h = new Graph({ compound: true, multigraph: true }).setGraph({}); 10 | 11 | _.forEach(g.nodes(), v => { 12 | h.setNode(v, { label: v }); 13 | h.setParent(v, "layer" + g.node(v).rank); 14 | }); 15 | 16 | _.forEach(g.edges(), e => h.setEdge(e.v, e.w, {}, e.name)); 17 | 18 | _.forEach(layerMatrix, (layer, i) => { 19 | const layerV = "layer" + i; 20 | h.setNode(layerV, { rank: "same" }); 21 | _.reduce(layer, (u, v) => { 22 | h.setEdge(u, v, { style: "invis" }); 23 | return v; 24 | }); 25 | }); 26 | 27 | return h; 28 | }; 29 | -------------------------------------------------------------------------------- /src/components/edge/LineShapeBase.vue: -------------------------------------------------------------------------------- 1 | 34 | 35 | 38 | -------------------------------------------------------------------------------- /src/utils/graphlib/alg/topsort.js: -------------------------------------------------------------------------------- 1 | import _ from "../lodash.js"; 2 | 3 | const topsort = g => { 4 | const visited = {}; 5 | const stack = {}; 6 | const results = []; 7 | 8 | const visit = node => { 9 | if (_.has(stack, node)) { 10 | throw new CycleException(); 11 | } 12 | 13 | if (!_.has(visited, node)) { 14 | stack[node] = true; 15 | visited[node] = true; 16 | _.each(g.predecessors(node), visit); 17 | delete stack[node]; 18 | results.push(node); 19 | } 20 | }; 21 | 22 | _.each(g.sinks(), visit); 23 | 24 | if (_.size(visited) !== g.nodeCount()) { 25 | throw new CycleException(); 26 | } 27 | 28 | return results; 29 | }; 30 | 31 | class CycleException extends Error { 32 | constructor() { 33 | super(); 34 | } 35 | } 36 | 37 | topsort.CycleException = CycleException; 38 | 39 | export default topsort 40 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /interops/es-browser/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 原生JS 6 | 7 | 32 | 33 | 34 | 35 |
36 | 37 | 38 | 39 |
40 |
41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /src/utils/dagre/order/init-order.js: -------------------------------------------------------------------------------- 1 | import _ from "../lodash.js"; 2 | 3 | /* 4 | * Assigns an initial order value for each node by performing a DFS search 5 | * starting from nodes in the first rank. Nodes are assigned an order in their 6 | * rank as they are first visited. 7 | * 8 | * This approach comes from Gansner, et al., "A Technique for Drawing Directed 9 | * Graphs." 10 | * 11 | * Returns a layering matrix with an array per layer and each layer sorted by 12 | * the order of its nodes. 13 | */ 14 | export default g => { 15 | const visited = {}; 16 | const simpleNodes = _.filter(g.nodes(), v => !g.children(v).length); 17 | const maxRank = _.max(_.map(simpleNodes, v => g.node(v).rank)); 18 | const layers = _.map(_.range(maxRank + 1), () => []); 19 | 20 | const dfs = v => { 21 | if (_.has(visited, v)) return; 22 | visited[v] = true; 23 | const node = g.node(v); 24 | layers[node.rank].push(v); 25 | _.forEach(g.successors(v), dfs); 26 | }; 27 | 28 | const orderedVs = _.sortBy(simpleNodes, v => g.node(v).rank); 29 | _.forEach(orderedVs, dfs); 30 | 31 | return layers; 32 | } 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 李淳 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dragonfly-dag", 3 | "version": "1.0.18", 4 | "license": "MIT", 5 | "type": "module", 6 | "author": { 7 | "name": "李淳", 8 | "email": "lchrennew@126.com" 9 | }, 10 | "description": "基于Vue3的DAG流程图组件。", 11 | "keywords": [ 12 | "dag", 13 | "vue3", 14 | "workflow", 15 | "mindmap", 16 | "visualization", 17 | "process", 18 | "流程图" 19 | ], 20 | "homepage": "https://github.com/lchrennew/dragonfly-dag", 21 | "files": [ 22 | "src/index.js", 23 | "src/utils", 24 | "src/layout", 25 | "src/components", 26 | "vite.config.js" 27 | ], 28 | "repository": { 29 | "url": "https://github.com/lchrennew/dragonfly-dag.git" 30 | }, 31 | "bugs": { 32 | "url": "https://github.com/lchrennew/dragonfly-dag/issues" 33 | }, 34 | "scripts": { 35 | "dev": "vite", 36 | "build": "vite build" 37 | }, 38 | "dependencies": { 39 | "lodash-es": "^4.17.21", 40 | "vue": "^3.2.45" 41 | }, 42 | "devDependencies": { 43 | "@vitejs/plugin-vue": "^4.0.0-alpha.1", 44 | "less": "^4.1.3", 45 | "vite": "^4.0.0-alpha.5" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/utils/graphlib/lodash.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line no-redeclare 2 | /* global window */ 3 | 4 | import clone from "lodash-es/clone.js"; 5 | import constant from "lodash-es/constant.js"; 6 | import each from "lodash-es/each.js"; 7 | import filter from "lodash-es/filter.js"; 8 | import has from "lodash-es/has.js"; 9 | import isArray from "lodash-es/isArray.js"; 10 | import isEmpty from "lodash-es/isEmpty.js"; 11 | import isFunction from "lodash-es/isFunction.js"; 12 | import isUndefined from "lodash-es/isUndefined.js"; 13 | import keys from "lodash-es/keys.js"; 14 | import map from "lodash-es/map.js"; 15 | import reduce from "lodash-es/reduce.js"; 16 | import size from "lodash-es/size.js"; 17 | import transform from "lodash-es/transform.js"; 18 | import union from "lodash-es/union.js"; 19 | import values from "lodash-es/values.js"; 20 | 21 | export default { 22 | clone, 23 | constant, 24 | each, 25 | filter, 26 | has, 27 | isArray, 28 | isEmpty, 29 | isFunction, 30 | isUndefined, 31 | keys, 32 | map, 33 | reduce, 34 | size, 35 | transform, 36 | union, 37 | values 38 | }; 39 | -------------------------------------------------------------------------------- /src/utils/graphlib/alg/index.js: -------------------------------------------------------------------------------- 1 | import _components from "./components.js" 2 | import _dijkstra from "./dijkstra.js" 3 | import _dijkstraAll from "./dijkstra-all.js" 4 | import _findCycles from "./find-cycles.js" 5 | import _floydWarshall from "./floyd-warshall.js" 6 | import _isAcyclic from "./is-acyclic.js" 7 | import _postorder from "./postorder.js" 8 | import _preorder from "./preorder.js" 9 | import _prim from "./prim.js" 10 | import _tarjan from "./tarjan.js" 11 | import _topsort from "./topsort.js" 12 | 13 | export const components = _components 14 | export const dijkstra = _dijkstra 15 | export const dijkstraAll = _dijkstraAll 16 | export const findCycles = _findCycles 17 | export const floydWarshall = _floydWarshall 18 | export const isAcyclic = _isAcyclic 19 | export const postorder = _postorder 20 | export const preorder = _preorder 21 | export const prim = _prim 22 | export const tarjan = _tarjan 23 | export const topsort = _topsort 24 | 25 | export default { 26 | components, 27 | dijkstra, 28 | dijkstraAll, 29 | findCycles, 30 | floydWarshall, 31 | isAcyclic, 32 | postorder, 33 | preorder, 34 | prim, 35 | tarjan, 36 | topsort 37 | }; 38 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import DragonflyCanvas from "./components/DragonflyCanvas.vue"; 2 | import DragonflyEdge from "./components/DragonflyEdge.vue"; 3 | import DragonflyEndpoint from "./components/DragonflyEndpoint.vue"; 4 | import StraightLine from "./components/edge/StraightLine.vue"; 5 | import ZigZagLine from "./components/edge/ZigZagLine.vue"; 6 | import SCurveLine from "./components/edge/SCurveLine.vue"; 7 | import LBrokenLine from "./components/edge/LBrokenLine.vue"; 8 | import LineShapeBase from "./components/edge/LineShapeBase.vue"; 9 | import LRoundedCornerLine from "./components/edge/LRoundedCornerLine.vue"; 10 | import DotGrid from "./components/grid/DotGrid.vue"; 11 | import LineGrid from "./components/grid/LineGrid.vue"; 12 | import TileGrid from "./components/grid/TileGrid.vue"; 13 | import DragonflySelectionProvider from './components/DragonflySelectionProvider.vue' 14 | import './components/dragonfly-dag.less' 15 | 16 | export { 17 | DragonflyCanvas, 18 | DragonflyEdge, 19 | DragonflyEndpoint, 20 | StraightLine, 21 | ZigZagLine, 22 | SCurveLine, 23 | LineShapeBase, 24 | DotGrid, 25 | LineGrid, 26 | TileGrid, 27 | LBrokenLine, 28 | LRoundedCornerLine, 29 | DragonflySelectionProvider, 30 | } 31 | -------------------------------------------------------------------------------- /src/utils/dagre/add-border-segments.js: -------------------------------------------------------------------------------- 1 | import _ from "./lodash.js"; 2 | import * as util from "./util.js"; 3 | 4 | const addBorderNode = (g, prop, prefix, sg, sgNode, rank) => { 5 | const label = { width: 0, height: 0, rank: rank, borderType: prop }; 6 | const prev = sgNode[prop][rank - 1]; 7 | const curr = util.addDummyNode(g, "border", label, prefix); 8 | sgNode[prop][rank] = curr; 9 | g.setParent(curr, sg); 10 | if (prev) { 11 | g.setEdge(prev, curr, { weight: 1 }); 12 | } 13 | }; 14 | 15 | export default g => { 16 | const dfs = v => { 17 | const children = g.children(v); 18 | const node = g.node(v); 19 | if (children.length) { 20 | _.forEach(children, dfs); 21 | } 22 | 23 | if (_.has(node, "minRank")) { 24 | node.borderLeft = []; 25 | node.borderRight = []; 26 | let rank = node.minRank, maxRank = node.maxRank + 1; 27 | for (; 28 | rank < maxRank; 29 | ++rank) { 30 | addBorderNode(g, "borderLeft", "_bl", v, node, rank); 31 | addBorderNode(g, "borderRight", "_br", v, node, rank); 32 | } 33 | } 34 | }; 35 | 36 | _.forEach(g.children(), dfs); 37 | } 38 | -------------------------------------------------------------------------------- /src/utils/graphlib/alg/tarjan.js: -------------------------------------------------------------------------------- 1 | import _ from "../lodash.js"; 2 | 3 | export default g => { 4 | let index = 0; 5 | const stack = []; 6 | const visited = {}; // node id -> { onStack, lowlink, index } 7 | const results = []; 8 | 9 | const dfs = v => { 10 | const entry = visited[v] = { 11 | onStack: true, 12 | lowlink: index, 13 | index: index++ 14 | }; 15 | stack.push(v); 16 | 17 | g.successors(v).forEach(w => { 18 | if (!_.has(visited, w)) { 19 | dfs(w); 20 | entry.lowlink = Math.min(entry.lowlink, visited[w].lowlink); 21 | } else if (visited[w].onStack) { 22 | entry.lowlink = Math.min(entry.lowlink, visited[w].index); 23 | } 24 | }); 25 | 26 | if (entry.lowlink === entry.index) { 27 | const cmpt = []; 28 | let w; 29 | do { 30 | w = stack.pop(); 31 | visited[w].onStack = false; 32 | cmpt.push(w); 33 | } while (v !== w); 34 | results.push(cmpt); 35 | } 36 | }; 37 | 38 | g.nodes().forEach(v => { 39 | if (!_.has(visited, v)) { 40 | dfs(v); 41 | } 42 | }); 43 | 44 | return results; 45 | } 46 | -------------------------------------------------------------------------------- /src/utils/graphlib/alg/dfs.js: -------------------------------------------------------------------------------- 1 | import _ from "../lodash.js"; 2 | 3 | const doDfs = (g, v, postorder, visited, navigation, acc) => { 4 | if (!_.has(visited, v)) { 5 | visited[v] = true; 6 | 7 | if (!postorder) { 8 | acc.push(v); 9 | } 10 | _.each(navigation(v), w => doDfs(g, w, postorder, visited, navigation, acc)); 11 | if (postorder) { 12 | acc.push(v); 13 | } 14 | } 15 | }; 16 | 17 | /* 18 | * A helper that preforms a pre- or post-order traversal on the input graph 19 | * and returns the nodes in the order they were visited. If the graph is 20 | * undirected then this algorithm will navigate using neighbors. If the graph 21 | * is directed then this algorithm will navigate using successors. 22 | * 23 | * Order must be one of "pre" or "post". 24 | */ 25 | export default (g, vs, order) => { 26 | if (!_.isArray(vs)) { 27 | vs = [ vs ]; 28 | } 29 | 30 | const navigation = (g.isDirected() ? g.successors : g.neighbors).bind(g); 31 | 32 | const acc = []; 33 | const visited = {}; 34 | _.each(vs, v => { 35 | if (!g.hasNode(v)) { 36 | throw new Error("Graph does not have node: " + v); 37 | } 38 | 39 | doDfs(g, v, order === "post", visited, navigation, acc); 40 | }); 41 | return acc; 42 | } 43 | -------------------------------------------------------------------------------- /src/components/canvas-wheeling-behavior-handlers.js: -------------------------------------------------------------------------------- 1 | const getCanvasWheelingHandlers = ({ data, props, computed, emit, methods }) => ({ 2 | zoom: { 3 | wheel(event) { 4 | event.preventDefault() 5 | if ((event.deltaY < 0 && computed.zoomScale <= props.minZoomScale) || (event.deltaY > 0 && computed.zoomScale >= props.maxZoomScale)) 6 | return 7 | 8 | let scale = computed.zoomScale + props.zoomSensitivity * event.deltaY 9 | if (scale > props.maxZoomScale) scale = props.maxZoomScale 10 | else if (scale < props.minZoomScale) scale = props.minZoomScale 11 | 12 | const delta = scale - computed.zoomScale 13 | const rect = methods.getEl().getBoundingClientRect() 14 | data.offsetX += (data.offsetX + rect.left - event.clientX) * delta / computed.zoomScale 15 | data.offsetY += (data.offsetY + rect.top - event.clientY) * delta / computed.zoomScale 16 | computed.zoomScale = scale 17 | emit('update:zoomScale', scale) 18 | } 19 | }, 20 | scroll: { 21 | wheel(event) { 22 | event.preventDefault() 23 | 24 | data.offsetX -= event.deltaX 25 | data.offsetY -= event.deltaY 26 | } 27 | }, 28 | off: {}, 29 | }); 30 | 31 | export default getCanvasWheelingHandlers 32 | -------------------------------------------------------------------------------- /src/components/DragonflyEndpoints.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 47 | -------------------------------------------------------------------------------- /src/utils/dagre/order/add-subgraph-constraints.js: -------------------------------------------------------------------------------- 1 | import _ from "../lodash.js"; 2 | 3 | export default (g, cg, vs) => { 4 | let prev = {}, 5 | rootPrev; 6 | 7 | _.forEach(vs, v => { 8 | let child = g.parent(v), 9 | parent, 10 | prevChild; 11 | while (child) { 12 | parent = g.parent(child); 13 | if (parent) { 14 | prevChild = prev[parent]; 15 | prev[parent] = child; 16 | } else { 17 | prevChild = rootPrev; 18 | rootPrev = child; 19 | } 20 | if (prevChild && prevChild !== child) { 21 | cg.setEdge(prevChild, child); 22 | return; 23 | } 24 | child = parent; 25 | } 26 | }); 27 | 28 | /* 29 | function dfs(v) { 30 | var children = v ? g.children(v) : g.children(); 31 | if (children.length) { 32 | var min = Number.POSITIVE_INFINITY, 33 | subgraphs = []; 34 | _.each(children, function(child) { 35 | var childMin = dfs(child); 36 | if (g.children(child).length) { 37 | subgraphs.push({ v: child, order: childMin }); 38 | } 39 | min = Math.min(min, childMin); 40 | }); 41 | _.reduce(_.sortBy(subgraphs, "order"), function(prev, curr) { 42 | cg.setEdge(prev.v, curr.v); 43 | return curr; 44 | }); 45 | return min; 46 | } 47 | return g.node(v).order; 48 | } 49 | dfs(undefined); 50 | */ 51 | }; 52 | -------------------------------------------------------------------------------- /src/utils/dagre/rank/index.js: -------------------------------------------------------------------------------- 1 | import { longestPath } from "./util.js"; 2 | import feasibleTree from "./feasible-tree.js"; 3 | 4 | import networkSimplex from "./network-simplex.js"; 5 | 6 | 7 | // A fast and simple ranker, but results are far from optimal. 8 | const longestPathRanker = longestPath; 9 | 10 | 11 | const tightTreeRanker = g => { 12 | longestPath(g); 13 | feasibleTree(g); 14 | }; 15 | 16 | const networkSimplexRanker = g => networkSimplex(g); 17 | 18 | 19 | /* 20 | * Assigns a rank to each node in the input graph that respects the "minlen" 21 | * constraint specified on edges between nodes. 22 | * 23 | * This basic structure is derived from Gansner, et al., "A Technique for 24 | * Drawing Directed Graphs." 25 | * 26 | * Pre-conditions: 27 | * 28 | * 1. Graph must be a connected DAG 29 | * 2. Graph nodes must be objects 30 | * 3. Graph edges must have "weight" and "minlen" attributes 31 | * 32 | * Post-conditions: 33 | * 34 | * 1. Graph nodes will have a "rank" attribute based on the results of the 35 | * algorithm. Ranks can start at any index (including negative), we'll 36 | * fix them up later. 37 | */ 38 | export default g => { 39 | switch(g.graph().ranker) { 40 | case "network-simplex": networkSimplexRanker(g); break; 41 | case "tight-tree": tightTreeRanker(g); break; 42 | case "longest-path": longestPathRanker(g); break; 43 | default: networkSimplexRanker(g); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/utils/graphlib/alg/prim.js: -------------------------------------------------------------------------------- 1 | import _ from "../lodash.js"; 2 | import Graph from "../graph.js"; 3 | import PriorityQueue from "../data/priority-queue.js"; 4 | 5 | export default (g, weightFunc) => { 6 | const result = new Graph(); 7 | const parents = {}; 8 | const pq = new PriorityQueue(); 9 | let v; 10 | 11 | const updateNeighbors = edge => { 12 | const w = edge.v === v ? edge.w : edge.v; 13 | const pri = pq.priority(w); 14 | if (pri !== undefined) { 15 | const edgeWeight = weightFunc(edge); 16 | if (edgeWeight < pri) { 17 | parents[w] = v; 18 | pq.decrease(w, edgeWeight); 19 | } 20 | } 21 | }; 22 | 23 | if (g.nodeCount() === 0) { 24 | return result; 25 | } 26 | 27 | _.each(g.nodes(), v => { 28 | pq.add(v, Number.POSITIVE_INFINITY); 29 | result.setNode(v); 30 | }); 31 | 32 | // Start from an arbitrary node 33 | pq.decrease(g.nodes()[0], 0); 34 | 35 | let init = false; 36 | while (pq.size() > 0) { 37 | v = pq.removeMin(); 38 | if (_.has(parents, v)) { 39 | result.setEdge(v, parents[v]); 40 | } else if (init) { 41 | throw new Error("Input graph is not connected: " + g); 42 | } else { 43 | init = true; 44 | } 45 | 46 | g.nodeEdges(v).forEach(updateNeighbors); 47 | } 48 | 49 | return result; 50 | }; 51 | -------------------------------------------------------------------------------- /src/components/DragonflySelectionProvider.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 49 | -------------------------------------------------------------------------------- /src/utils/graphlib/alg/floyd-warshall.js: -------------------------------------------------------------------------------- 1 | import _ from "../lodash.js"; 2 | 3 | const DEFAULT_WEIGHT_FUNC = _.constant(1); 4 | 5 | const runFloydWarshall = (g, weightFn, edgeFn) => { 6 | const results = {}; 7 | const nodes = g.nodes(); 8 | 9 | nodes.forEach(v => { 10 | results[v] = {}; 11 | results[v][v] = { distance: 0 }; 12 | nodes.forEach(w => { 13 | if (v !== w) { 14 | results[v][w] = { distance: Number.POSITIVE_INFINITY }; 15 | } 16 | }); 17 | edgeFn(v).forEach(edge => { 18 | const w = edge.v === v ? edge.w : edge.v; 19 | const d = weightFn(edge); 20 | results[v][w] = { distance: d, predecessor: v }; 21 | }); 22 | }); 23 | 24 | nodes.forEach(k => { 25 | const rowK = results[k]; 26 | nodes.forEach(i => { 27 | const rowI = results[i]; 28 | nodes.forEach(j => { 29 | const ik = rowI[k]; 30 | const kj = rowK[j]; 31 | const ij = rowI[j]; 32 | const altDistance = ik.distance + kj.distance; 33 | if (altDistance < ij.distance) { 34 | ij.distance = altDistance; 35 | ij.predecessor = kj.predecessor; 36 | } 37 | }); 38 | }); 39 | }); 40 | 41 | return results; 42 | }; 43 | 44 | export default (g, weightFn, edgeFn) => runFloydWarshall(g, 45 | weightFn || DEFAULT_WEIGHT_FUNC, 46 | edgeFn || (v => g.outEdges(v))); 47 | -------------------------------------------------------------------------------- /src/utils/dagre/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2012-2014 Chris Pettitt 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in 12 | all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | THE SOFTWARE. 21 | */ 22 | 23 | import * as graphlib from "../graphlib/index.js"; 24 | import layout from "./layout.js"; 25 | import * as debug from "./debug.js"; 26 | import { time, notime } from "./util.js"; 27 | import version from "./version.js"; 28 | 29 | export default { 30 | graphlib, 31 | layout, 32 | debug, 33 | util: { 34 | time, 35 | notime, 36 | }, 37 | version, 38 | }; 39 | -------------------------------------------------------------------------------- /src/utils/dagre/data/list.js: -------------------------------------------------------------------------------- 1 | const unlink = entry => { 2 | entry._prev._next = entry._next; 3 | entry._next._prev = entry._prev; 4 | delete entry._next; 5 | delete entry._prev; 6 | }; 7 | 8 | const filterOutLinks = (k, v) => { 9 | if (k !== "_next" && k !== "_prev") { 10 | return v; 11 | } 12 | }; 13 | 14 | /* 15 | * Simple doubly linked list implementation derived from Cormen, et al., 16 | * "Introduction to Algorithms". 17 | */ 18 | export default class List { 19 | constructor() { 20 | const sentinel = {}; 21 | sentinel._next = sentinel._prev = sentinel; 22 | this._sentinel = sentinel; 23 | } 24 | 25 | dequeue() { 26 | const sentinel = this._sentinel; 27 | const entry = sentinel._prev; 28 | if (entry !== sentinel) { 29 | unlink(entry); 30 | return entry; 31 | } 32 | } 33 | 34 | enqueue(entry) { 35 | const sentinel = this._sentinel; 36 | if (entry._prev && entry._next) { 37 | unlink(entry); 38 | } 39 | entry._next = sentinel._next; 40 | sentinel._next._prev = entry; 41 | sentinel._next = entry; 42 | entry._prev = sentinel; 43 | } 44 | 45 | toString() { 46 | const strs = []; 47 | const sentinel = this._sentinel; 48 | let curr = sentinel._prev; 49 | while (curr !== sentinel) { 50 | strs.push(JSON.stringify(curr, filterOutLinks)); 51 | curr = curr._prev; 52 | } 53 | return "[" + strs.join(", ") + "]"; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/utils/dagre/acyclic.js: -------------------------------------------------------------------------------- 1 | import _ from "./lodash.js"; 2 | import greedyFAS from "./greedy-fas.js"; 3 | 4 | const dfsFAS = g => { 5 | const fas = []; 6 | const stack = {}; 7 | const visited = {}; 8 | 9 | const dfs = v => { 10 | if (_.has(visited, v)) { 11 | return; 12 | } 13 | visited[v] = true; 14 | stack[v] = true; 15 | _.forEach(g.outEdges(v), e => { 16 | if (_.has(stack, e.w)) { 17 | fas.push(e); 18 | } else { 19 | dfs(e.w); 20 | } 21 | }); 22 | delete stack[v]; 23 | }; 24 | 25 | _.forEach(g.nodes(), dfs); 26 | return fas; 27 | }; 28 | 29 | const run = g => { 30 | const weightFn = g => e => g.edge(e).weight; 31 | 32 | const fas = (g.graph().acyclicer === "greedy" 33 | ? greedyFAS(g, weightFn(g)) 34 | : dfsFAS(g)); 35 | 36 | _.forEach(fas, e => { 37 | const label = g.edge(e); 38 | g.removeEdge(e); 39 | label.forwardName = e.name; 40 | label.reversed = true; 41 | g.setEdge(e.w, e.v, label, _.uniqueId("rev")); 42 | }); 43 | 44 | 45 | }; 46 | 47 | const undo = g => { 48 | _.forEach(g.edges(), e => { 49 | const label = g.edge(e); 50 | if (label.reversed) { 51 | g.removeEdge(e); 52 | 53 | const forwardName = label.forwardName; 54 | delete label.reversed; 55 | delete label.forwardName; 56 | g.setEdge(e.w, e.v, label, forwardName); 57 | } 58 | }); 59 | }; 60 | 61 | export default { 62 | run, 63 | undo 64 | }; 65 | -------------------------------------------------------------------------------- /src/utils/dagre/lodash.js: -------------------------------------------------------------------------------- 1 | import cloneDeep from "lodash-es/cloneDeep.js" 2 | import constant from "lodash-es/constant.js" 3 | import defaults from "lodash-es/defaults.js" 4 | import each from "lodash-es/each.js" 5 | import filter from "lodash-es/filter.js" 6 | import find from "lodash-es/find.js" 7 | import flatten from "lodash-es/flatten.js" 8 | import forEach from "lodash-es/forEach.js" 9 | import forIn from "lodash-es/forIn.js" 10 | import has from "lodash-es/has.js" 11 | import isUndefined from "lodash-es/isUndefined.js" 12 | import last from "lodash-es/last.js" 13 | import map from "lodash-es/map.js" 14 | import mapValues from "lodash-es/mapValues.js" 15 | import max from "lodash-es/max.js" 16 | import merge from "lodash-es/merge.js" 17 | import min from "lodash-es/min.js" 18 | import minBy from "lodash-es/minBy.js" 19 | import now from "lodash-es/now.js" 20 | import pick from "lodash-es/pick.js" 21 | import range from "lodash-es/range.js" 22 | import reduce from "lodash-es/reduce.js" 23 | import sortBy from "lodash-es/sortBy.js" 24 | import uniqueId from "lodash-es/uniqueId.js" 25 | import values from "lodash-es/values.js" 26 | import zipObject from "lodash-es/zipObject.js" 27 | 28 | export default { 29 | cloneDeep, 30 | constant, 31 | defaults, 32 | each, 33 | filter, 34 | find, 35 | flatten, 36 | forEach, 37 | forIn, 38 | has, 39 | isUndefined, 40 | last, 41 | map, 42 | mapValues, 43 | max, 44 | merge, 45 | min, 46 | minBy, 47 | now, 48 | pick, 49 | range, 50 | reduce, 51 | sortBy, 52 | uniqueId, 53 | values, 54 | zipObject, 55 | } 56 | -------------------------------------------------------------------------------- /src/utils/dagre/order/sort.js: -------------------------------------------------------------------------------- 1 | import _ from "../lodash.js"; 2 | 3 | import * as util from "../util.js"; 4 | 5 | const consumeUnsortable = (vs, unsortable, index) => { 6 | let last; 7 | while (unsortable.length && (last = _.last(unsortable)).i <= index) { 8 | unsortable.pop(); 9 | vs.push(last.vs); 10 | index++; 11 | } 12 | return index; 13 | }; 14 | 15 | const compareWithBias = bias => (entryV, entryW) => { 16 | if (entryV.barycenter < entryW.barycenter) { 17 | return -1; 18 | } else if (entryV.barycenter > entryW.barycenter) { 19 | return 1; 20 | } 21 | 22 | return !bias ? entryV.i - entryW.i : entryW.i - entryV.i; 23 | }; 24 | 25 | 26 | export default (entries, biasRight) => { 27 | const parts = util.partition(entries, entry => _.has(entry, "barycenter")); 28 | let sortable = parts.lhs, 29 | unsortable = _.sortBy(parts.rhs, entry => -entry.i), 30 | vs = [], 31 | sum = 0, 32 | weight = 0, 33 | vsIndex = 0; 34 | 35 | sortable.sort(compareWithBias(!!biasRight)); 36 | 37 | vsIndex = consumeUnsortable(vs, unsortable, vsIndex); 38 | 39 | _.forEach(sortable, entry => { 40 | vsIndex += entry.vs.length; 41 | vs.push(entry.vs); 42 | sum += entry.barycenter * entry.weight; 43 | weight += entry.weight; 44 | vsIndex = consumeUnsortable(vs, unsortable, vsIndex); 45 | }); 46 | 47 | const result = { vs: _.flatten(vs, true) }; 48 | if (weight) { 49 | result.barycenter = sum / weight; 50 | result.weight = weight; 51 | } 52 | return result; 53 | } 54 | -------------------------------------------------------------------------------- /src/utils/intersect-helper.js: -------------------------------------------------------------------------------- 1 | const getActualIntercepts = (intercept1, intercept2, blood) => ({ 2 | intercept1: Math.min(intercept1, intercept2) - blood, 3 | intercept2: Math.max(intercept1, intercept2) + blood, 4 | }) 5 | 6 | const calculateArea = rect => Math.abs((rect[0][0] - rect[1][0]) * (rect[0][1] - rect[1][1])) 7 | 8 | export const intersect = (rect1, rect2, blood = 0.5) => { 9 | const { intercept1: x1, intercept2: x2 } = getActualIntercepts(rect1[0][0], rect1[1][0], blood) 10 | const { intercept1: x3, intercept2: x4 } = getActualIntercepts(rect2[0][0], rect2[1][0], blood) 11 | const { intercept1: y1, intercept2: y2 } = getActualIntercepts(rect1[0][1], rect1[1][1], blood) 12 | const { intercept1: y3, intercept2: y4 } = getActualIntercepts(rect2[0][1], rect2[1][1], blood) 13 | 14 | const intersectedX = x1 <= x4 && x2 >= x3 15 | const intersectedY = y1 <= y4 && y2 >= y3 16 | if (!intersectedX || !intersectedY) return { r1: 0, r2: 0 } 17 | const [ , intersectionX1, intersectionX2, ] = [ rect1[0][0], rect1[1][0], rect2[0][0], rect2[1][0] ].sort((a, b) => a - b) 18 | const [ , intersectionY1, intersectionY2, ] = [ rect1[0][1], rect1[1][1], rect2[0][1], rect2[1][1] ].sort((a, b) => a - b) 19 | 20 | const area1 = calculateArea(rect1), 21 | area2 = calculateArea(rect2), 22 | areaIntersection = calculateArea([ [ intersectionX1, intersectionY1 ], [ intersectionX2, intersectionY2 ] ]) 23 | return { 24 | r1: areaIntersection / area1, 25 | r2: areaIntersection / area2, 26 | a1: area1, 27 | a2: area2, 28 | ai: areaIntersection, 29 | } 30 | }; 31 | -------------------------------------------------------------------------------- /src/utils/graphlib/alg/dijkstra.js: -------------------------------------------------------------------------------- 1 | import _ from "../lodash.js"; 2 | 3 | import PriorityQueue from "../data/priority-queue.js"; 4 | 5 | const DEFAULT_WEIGHT_FUNC = _.constant(1); 6 | 7 | const runDijkstra = (g, source, weightFn, edgeFn) => { 8 | const results = {}; 9 | const pq = new PriorityQueue(); 10 | let v, vEntry; 11 | 12 | const updateNeighbors = edge => { 13 | const w = edge.v !== v ? edge.v : edge.w; 14 | const wEntry = results[w]; 15 | const weight = weightFn(edge); 16 | const distance = vEntry.distance + weight; 17 | 18 | if (weight < 0) { 19 | throw new Error("dijkstra does not allow negative edge weights. " + 20 | "Bad edge: " + edge + " Weight: " + weight); 21 | } 22 | 23 | if (distance < wEntry.distance) { 24 | wEntry.distance = distance; 25 | wEntry.predecessor = v; 26 | pq.decrease(w, distance); 27 | } 28 | }; 29 | 30 | g.nodes().forEach(v => { 31 | const distance = v === source ? 0 : Number.POSITIVE_INFINITY; 32 | results[v] = { distance: distance }; 33 | pq.add(v, distance); 34 | }); 35 | 36 | while (pq.size() > 0) { 37 | v = pq.removeMin(); 38 | vEntry = results[v]; 39 | if (vEntry.distance === Number.POSITIVE_INFINITY) { 40 | break; 41 | } 42 | 43 | edgeFn(v).forEach(updateNeighbors); 44 | } 45 | 46 | return results; 47 | }; 48 | 49 | export default (g, source, weightFn, edgeFn) => runDijkstra(g, String(source), 50 | weightFn || DEFAULT_WEIGHT_FUNC, 51 | edgeFn || (v => g.outEdges(v))) 52 | -------------------------------------------------------------------------------- /src/utils/graphlib/json.js: -------------------------------------------------------------------------------- 1 | import _ from "./lodash.js"; 2 | import Graph from "./graph.js"; 3 | 4 | const writeNodes = g => _.map(g.nodes(), v => { 5 | const nodeValue = g.node(v); 6 | const parent = g.parent(v); 7 | const node = { v: v }; 8 | if (!_.isUndefined(nodeValue)) { 9 | node.value = nodeValue; 10 | } 11 | if (!_.isUndefined(parent)) { 12 | node.parent = parent; 13 | } 14 | return node; 15 | }); 16 | 17 | const writeEdges = g => _.map(g.edges(), e => { 18 | const edgeValue = g.edge(e); 19 | const edge = { v: e.v, w: e.w }; 20 | if (!_.isUndefined(e.name)) { 21 | edge.name = e.name; 22 | } 23 | if (!_.isUndefined(edgeValue)) { 24 | edge.value = edgeValue; 25 | } 26 | return edge; 27 | }); 28 | 29 | const write = g => { 30 | const json = { 31 | options: { 32 | directed: g.isDirected(), 33 | multigraph: g.isMultigraph(), 34 | compound: g.isCompound() 35 | }, 36 | nodes: writeNodes(g), 37 | edges: writeEdges(g) 38 | }; 39 | if (!_.isUndefined(g.graph())) { 40 | json.value = _.clone(g.graph()); 41 | } 42 | return json; 43 | }; 44 | 45 | 46 | const read = json => { 47 | const g = new Graph(json.options).setGraph(json.value); 48 | _.each(json.nodes, entry => { 49 | g.setNode(entry.v, entry.value); 50 | if (entry.parent) { 51 | g.setParent(entry.v, entry.parent); 52 | } 53 | }); 54 | _.each(json.edges, entry => g.setEdge({ v: entry.v, w: entry.w, name: entry.name }, entry.value)); 55 | return g; 56 | }; 57 | 58 | export default { 59 | write, 60 | read 61 | }; 62 | -------------------------------------------------------------------------------- /src/utils/dagre/coordinate-system.js: -------------------------------------------------------------------------------- 1 | import _ from "./lodash.js"; 2 | 3 | const swapXYOne = attrs => { 4 | const x = attrs.x; 5 | attrs.x = attrs.y; 6 | attrs.y = x; 7 | }; 8 | 9 | const reverseYOne = attrs => attrs.y = -attrs.y; 10 | 11 | const swapWidthHeightOne = attrs => { 12 | const w = attrs.width; 13 | attrs.width = attrs.height; 14 | attrs.height = w; 15 | }; 16 | 17 | const reverseY = g => { 18 | _.forEach(g.nodes(), v => reverseYOne(g.node(v))); 19 | 20 | _.forEach(g.edges(), e => { 21 | const edge = g.edge(e); 22 | _.forEach(edge.points, reverseYOne); 23 | if (_.has(edge, "y")) { 24 | reverseYOne(edge); 25 | } 26 | }); 27 | }; 28 | 29 | 30 | const swapXY = g => { 31 | _.forEach(g.nodes(), v => swapXYOne(g.node(v))); 32 | 33 | _.forEach(g.edges(), e => { 34 | const edge = g.edge(e); 35 | _.forEach(edge.points, swapXYOne); 36 | if (_.has(edge, "x")) { 37 | swapXYOne(edge); 38 | } 39 | }); 40 | }; 41 | const swapWidthHeight = g => { 42 | _.forEach(g.nodes(), v => swapWidthHeightOne(g.node(v))); 43 | _.forEach(g.edges(), e => swapWidthHeightOne(g.edge(e))); 44 | }; 45 | 46 | 47 | const adjust = g => { 48 | const rankDir = g.graph().rankdir.toLowerCase(); 49 | if (rankDir === "lr" || rankDir === "rl") { 50 | swapWidthHeight(g); 51 | } 52 | }; 53 | 54 | const undo = g => { 55 | const rankDir = g.graph().rankdir.toLowerCase(); 56 | if (rankDir === "bt" || rankDir === "rl") { 57 | reverseY(g); 58 | } 59 | 60 | if (rankDir === "lr" || rankDir === "rl") { 61 | swapXY(g); 62 | swapWidthHeight(g); 63 | } 64 | }; 65 | 66 | export default { 67 | adjust, 68 | undo 69 | }; 70 | -------------------------------------------------------------------------------- /src/utils/dagre/rank/util.js: -------------------------------------------------------------------------------- 1 | import _ from "../lodash.js"; 2 | 3 | /* 4 | * Initializes ranks for the input graph using the longest path algorithm. This 5 | * algorithm scales well and is fast in practice, it yields rather poor 6 | * solutions. Nodes are pushed to the lowest layer possible, leaving the bottom 7 | * ranks wide and leaving edges longer than necessary. However, due to its 8 | * speed, this algorithm is good for getting an initial ranking that can be fed 9 | * into other algorithms. 10 | * 11 | * This algorithm does not normalize layers because it will be used by other 12 | * algorithms in most cases. If using this algorithm directly, be sure to 13 | * run normalize at the end. 14 | * 15 | * Pre-conditions: 16 | * 17 | * 1. Input graph is a DAG. 18 | * 2. Input graph node labels can be assigned properties. 19 | * 20 | * Post-conditions: 21 | * 22 | * 1. Each node will be assign an (unnormalized) "rank" property. 23 | */ 24 | export const longestPath = g => { 25 | const visited = {}; 26 | 27 | const dfs = v => { 28 | const label = g.node(v); 29 | if (_.has(visited, v)) { 30 | return label.rank; 31 | } 32 | visited[v] = true; 33 | 34 | let rank = _.min(_.map(g.outEdges(v), e => dfs(e.w) - g.edge(e).minlen)); 35 | 36 | if (rank === Number.POSITIVE_INFINITY || // return value of _.map([]) for Lodash 3 37 | rank === undefined || // return value of _.map([]) for Lodash 4 38 | rank === null) { // return value of _.map([null]) 39 | rank = 0; 40 | } 41 | 42 | return (label.rank = rank); 43 | }; 44 | 45 | _.forEach(g.sources(), dfs); 46 | }; 47 | 48 | /* 49 | * Returns the amount of slack for the given edge. The slack is defined as the 50 | * difference between the length of the edge and its minimum length. 51 | */ 52 | export const slack = (g, e) => g.node(e.w).rank - g.node(e.v).rank - g.edge(e).minlen; 53 | -------------------------------------------------------------------------------- /src/components/DragonflyGrid.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 57 | -------------------------------------------------------------------------------- /src/components/edge/StraightLine.vue: -------------------------------------------------------------------------------- 1 | 61 | -------------------------------------------------------------------------------- /src/components/edge/SCurveLine.vue: -------------------------------------------------------------------------------- 1 | 63 | 64 | 67 | -------------------------------------------------------------------------------- /src/components/edge/ZigZagLine.vue: -------------------------------------------------------------------------------- 1 | 64 | -------------------------------------------------------------------------------- /src/utils/dagre/order/cross-count.js: -------------------------------------------------------------------------------- 1 | import _ from "../lodash.js"; 2 | 3 | const twoLayerCrossCount = (g, northLayer, southLayer) => { 4 | // Sort all of the edges between the north and south layers by their position 5 | // in the north layer and then the south. Map these edges to the position of 6 | // their head in the south layer. 7 | const southPos = _.zipObject(southLayer, 8 | _.map(southLayer, (v, i) => i)); 9 | const southEntries = _.flatten(_.map(northLayer, v => _.sortBy(_.map(g.outEdges(v), e => ({ 10 | pos: southPos[e.w], 11 | weight: g.edge(e).weight 12 | })), "pos")), true); 13 | 14 | // Build the accumulator tree 15 | let firstIndex = 1; 16 | while (firstIndex < southLayer.length) firstIndex <<= 1; 17 | const treeSize = 2 * firstIndex - 1; 18 | firstIndex -= 1; 19 | const tree = _.map(new Array(treeSize), () => 0); 20 | 21 | // Calculate the weighted crossings 22 | let cc = 0; 23 | _.forEach(southEntries.forEach(entry => { 24 | let index = entry.pos + firstIndex; 25 | tree[index] += entry.weight; 26 | let weightSum = 0; 27 | while (index > 0) { 28 | if (index % 2) { 29 | weightSum += tree[index + 1]; 30 | } 31 | index = (index - 1) >> 1; 32 | tree[index] += entry.weight; 33 | } 34 | cc += entry.weight * weightSum; 35 | })); 36 | 37 | return cc; 38 | }; 39 | 40 | /* 41 | * A function that takes a layering (an array of layers, each with an array of 42 | * ordererd nodes) and a graph and returns a weighted crossing count. 43 | * 44 | * Pre-conditions: 45 | * 46 | * 1. Input graph must be simple (not a multigraph), directed, and include 47 | * only simple edges. 48 | * 2. Edges in the input graph must have assigned weights. 49 | * 50 | * Post-conditions: 51 | * 52 | * 1. The graph and layering matrix are left unchanged. 53 | * 54 | * This algorithm is derived from Barth, et al., "Bilayer Cross Counting." 55 | */ 56 | export default (g, layering) => { 57 | let cc = 0; 58 | for (let i = 1; i < layering.length; ++i) { 59 | cc += twoLayerCrossCount(g, layering[i - 1], layering[i]); 60 | } 61 | return cc; 62 | } 63 | 64 | -------------------------------------------------------------------------------- /src/utils/dagre/parent-dummy-chains.js: -------------------------------------------------------------------------------- 1 | import _ from "./lodash.js"; 2 | 3 | // Find a path from v to w through the lowest common ancestor (LCA). Return the 4 | // full path and the LCA. 5 | const findPath = (g, postorderNums, v, w) => { 6 | const vPath = []; 7 | const wPath = []; 8 | const low = Math.min(postorderNums[v].low, postorderNums[w].low); 9 | const lim = Math.max(postorderNums[v].lim, postorderNums[w].lim); 10 | let parent; 11 | let lca; 12 | // Traverse up from v to find the LCA 13 | parent = v; 14 | do { 15 | parent = g.parent(parent); 16 | vPath.push(parent); 17 | } while (parent && 18 | (postorderNums[parent].low > low || lim > postorderNums[parent].lim)); 19 | lca = parent; 20 | 21 | // Traverse from w to LCA 22 | parent = w; 23 | while ((parent = g.parent(parent)) !== lca) { 24 | wPath.push(parent); 25 | } 26 | 27 | return { path: vPath.concat(wPath.reverse()), lca: lca }; 28 | }; 29 | 30 | const postorder = g => { 31 | const result = {}; 32 | let lim = 0; 33 | 34 | const dfs = v => { 35 | const low = lim; 36 | _.forEach(g.children(v), dfs); 37 | result[v] = { low: low, lim: lim++ }; 38 | }; 39 | _.forEach(g.children(), dfs); 40 | 41 | return result; 42 | }; 43 | 44 | export default g => { 45 | const postorderNums = postorder(g); 46 | 47 | _.forEach(g.graph().dummyChains, v => { 48 | let node = g.node(v); 49 | const edgeObj = node.edgeObj; 50 | const pathData = findPath(g, postorderNums, edgeObj.v, edgeObj.w); 51 | const path = pathData.path; 52 | const lca = pathData.lca; 53 | let pathIdx = 0; 54 | let pathV = path[pathIdx]; 55 | let ascending = true; 56 | while (v !== edgeObj.w) { 57 | node = g.node(v); 58 | 59 | if (ascending) { 60 | while ((pathV = path[pathIdx]) !== lca && 61 | g.node(pathV).maxRank < node.rank) { 62 | pathIdx++; 63 | } 64 | 65 | if (pathV === lca) { 66 | ascending = false; 67 | } 68 | } 69 | 70 | if (!ascending) { 71 | while (pathIdx < path.length - 1 && 72 | g.node(pathV = path[pathIdx + 1]).minRank <= node.rank) { 73 | pathIdx++; 74 | } 75 | pathV = path[pathIdx]; 76 | } 77 | 78 | g.setParent(v, pathV); 79 | v = g.successors(v)[0]; 80 | } 81 | }); 82 | } 83 | -------------------------------------------------------------------------------- /src/utils/dagre/order/index.js: -------------------------------------------------------------------------------- 1 | import _ from "../lodash.js"; 2 | import initOrder from "./init-order.js"; 3 | import crossCount from "./cross-count.js"; 4 | import sortSubgraph from "./sort-subgraph.js"; 5 | import buildLayerGraph from "./build-layer-graph.js"; 6 | import addSubgraphConstraints from "./add-subgraph-constraints.js"; 7 | import { Graph } from "../../graphlib/index.js"; 8 | import * as util from "../util.js"; 9 | 10 | 11 | const buildLayerGraphs = (g, ranks, relationship) => _.map(ranks, rank => buildLayerGraph(g, rank, relationship)); 12 | 13 | const sweepLayerGraphs = (layerGraphs, biasRight) => { 14 | const cg = new Graph(); 15 | _.forEach(layerGraphs, lg => { 16 | const root = lg.graph().root; 17 | const sorted = sortSubgraph(lg, root, cg, biasRight); 18 | _.forEach(sorted.vs, (v, i) => lg.node(v).order = i); 19 | addSubgraphConstraints(lg, cg, sorted.vs); 20 | }); 21 | }; 22 | 23 | const assignOrder = (g, layering) => _.forEach(layering, layer => _.forEach(layer, (v, i) => g.node(v).order = i)); 24 | 25 | /* 26 | * Applies heuristics to minimize edge crossings in the graph and sets the best 27 | * order solution as an order attribute on each node. 28 | * 29 | * Pre-conditions: 30 | * 31 | * 1. Graph must be DAG 32 | * 2. Graph nodes must be objects with a "rank" attribute 33 | * 3. Graph edges must have the "weight" attribute 34 | * 35 | * Post-conditions: 36 | * 37 | * 1. Graph nodes will have an "order" attribute based on the results of the 38 | * algorithm. 39 | */ 40 | export default g => { 41 | const maxRank = util.maxRank(g), 42 | downLayerGraphs = buildLayerGraphs(g, _.range(1, maxRank + 1), "inEdges"), 43 | upLayerGraphs = buildLayerGraphs(g, _.range(maxRank - 1, -1, -1), "outEdges"); 44 | 45 | let layering = initOrder(g); 46 | assignOrder(g, layering); 47 | 48 | let bestCC = Number.POSITIVE_INFINITY, 49 | best; 50 | 51 | let i = 0, lastBest = 0; 52 | for (; lastBest < 4; ++i, ++lastBest) { 53 | sweepLayerGraphs(i % 2 ? downLayerGraphs : upLayerGraphs, i % 4 >= 2); 54 | 55 | layering = util.buildLayerMatrix(g); 56 | const cc = crossCount(g, layering); 57 | if (cc < bestCC) { 58 | lastBest = 0; 59 | best = _.cloneDeep(layering); 60 | bestCC = cc; 61 | } 62 | } 63 | 64 | assignOrder(g, best); 65 | } 66 | -------------------------------------------------------------------------------- /src/components/edge/LRoundedCornerLine.vue: -------------------------------------------------------------------------------- 1 | 70 | -------------------------------------------------------------------------------- /src/components/DragonflyZoneResizeHandler.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 66 | -------------------------------------------------------------------------------- /src/utils/dagre/rank/feasible-tree.js: -------------------------------------------------------------------------------- 1 | import _ from "../lodash.js"; 2 | import { Graph } from "../../graphlib/index.js"; 3 | import { slack } from "./util.js"; 4 | 5 | /* 6 | * Finds a maximal tree of tight edges and returns the number of nodes in the 7 | * tree. 8 | */ 9 | const tightTree = (t, g) => { 10 | const dfs = v => { 11 | _.forEach(g.nodeEdges(v), e => { 12 | const edgeV = e.v, 13 | w = (v === edgeV) ? e.w : edgeV; 14 | if (!t.hasNode(w) && !slack(g, e)) { 15 | t.setNode(w, {}); 16 | t.setEdge(v, w, {}); 17 | dfs(w); 18 | } 19 | }); 20 | }; 21 | 22 | _.forEach(t.nodes(), dfs); 23 | return t.nodeCount(); 24 | }; 25 | 26 | /* 27 | * Finds the edge with the smallest slack that is incident on tree and returns 28 | * it. 29 | */ 30 | const findMinSlackEdge = (t, g) => _.minBy(g.edges(), e => { 31 | if (t.hasNode(e.v) !== t.hasNode(e.w)) { 32 | return slack(g, e); 33 | } 34 | }); 35 | 36 | const shiftRanks = (t, g, delta) => { 37 | _.forEach(t.nodes(), v => g.node(v).rank += delta); 38 | }; 39 | 40 | /* 41 | * Constructs a spanning tree with tight edges and adjusted the input node's 42 | * ranks to achieve this. A tight edge is one that is has a length that matches 43 | * its "minlen" attribute. 44 | * 45 | * The basic structure for this function is derived from Gansner, et al., "A 46 | * Technique for Drawing Directed Graphs." 47 | * 48 | * Pre-conditions: 49 | * 50 | * 1. Graph must be a DAG. 51 | * 2. Graph must be connected. 52 | * 3. Graph must have at least one node. 53 | * 5. Graph nodes must have been previously assigned a "rank" property that 54 | * respects the "minlen" property of incident edges. 55 | * 6. Graph edges must have a "minlen" property. 56 | * 57 | * Post-conditions: 58 | * 59 | * - Graph nodes will have their rank adjusted to ensure that all edges are 60 | * tight. 61 | * 62 | * Returns a tree (undirected graph) that is constructed using only "tight" 63 | * edges. 64 | */ 65 | export default g => { 66 | const t = new Graph({ directed: false }); 67 | 68 | // Choose arbitrary node from which to start our tree 69 | const start = g.nodes()[0]; 70 | const size = g.nodeCount(); 71 | t.setNode(start, {}); 72 | 73 | let edge, delta; 74 | while (tightTree(t, g) < size) { 75 | edge = findMinSlackEdge(t, g); 76 | delta = t.hasNode(edge.v) ? slack(g, edge) : -slack(g, edge); 77 | shiftRanks(t, g, delta); 78 | } 79 | 80 | return t; 81 | } 82 | -------------------------------------------------------------------------------- /src/utils/dagre/order/sort-subgraph.js: -------------------------------------------------------------------------------- 1 | import _ from "../lodash.js"; 2 | import barycenter from "./barycenter.js"; 3 | import resolveConflicts from "./resolve-conflicts.js"; 4 | import sort from "./sort.js"; 5 | 6 | const expandSubgraphs = (entries, subgraphs) => { 7 | _.forEach(entries, entry => { 8 | entry.vs = _.flatten(entry.vs.map(v => { 9 | if (subgraphs[v]) { 10 | return subgraphs[v].vs; 11 | } 12 | return v; 13 | }), true); 14 | }); 15 | }; 16 | 17 | const mergeBarycenters = (target, other) => { 18 | if (!_.isUndefined(target.barycenter)) { 19 | target.barycenter = (target.barycenter * target.weight + 20 | other.barycenter * other.weight) / 21 | (target.weight + other.weight); 22 | target.weight += other.weight; 23 | } else { 24 | target.barycenter = other.barycenter; 25 | target.weight = other.weight; 26 | } 27 | }; 28 | 29 | const sortSubgraph = (g, v, cg, biasRight) => { 30 | let movable = g.children(v); 31 | const node = g.node(v); 32 | const bl = node ? node.borderLeft : undefined; 33 | const br = node ? node.borderRight : undefined; 34 | const subgraphs = {}; 35 | 36 | if (bl) { 37 | movable = _.filter(movable, w => w !== bl && w !== br); 38 | } 39 | 40 | const barycenters = barycenter(g, movable); 41 | _.forEach(barycenters, entry => { 42 | if (g.children(entry.v).length) { 43 | const subgraphResult = sortSubgraph(g, entry.v, cg, biasRight); 44 | subgraphs[entry.v] = subgraphResult; 45 | if (_.has(subgraphResult, "barycenter")) { 46 | mergeBarycenters(entry, subgraphResult); 47 | } 48 | } 49 | }); 50 | 51 | const entries = resolveConflicts(barycenters, cg); 52 | expandSubgraphs(entries, subgraphs); 53 | 54 | const result = sort(entries, biasRight); 55 | 56 | if (bl) { 57 | result.vs = _.flatten([ bl, result.vs, br ], true); 58 | if (g.predecessors(bl).length) { 59 | const blPred = g.node(g.predecessors(bl)[0]), 60 | brPred = g.node(g.predecessors(br)[0]); 61 | if (!_.has(result, "barycenter")) { 62 | result.barycenter = 0; 63 | result.weight = 0; 64 | } 65 | result.barycenter = (result.barycenter * result.weight + 66 | blPred.order + brPred.order) / (result.weight + 2); 67 | result.weight += 2; 68 | } 69 | } 70 | 71 | return result; 72 | }; 73 | export default sortSubgraph 74 | 75 | -------------------------------------------------------------------------------- /src/components/history-traveller.js: -------------------------------------------------------------------------------- 1 | const historyTraveller = { 2 | 'nodes:added': { 3 | forward(added) { 4 | this.nodes = [ ...this.nodes, ...added ] 5 | }, 6 | back(added) { 7 | const hash = Object.fromEntries(added.map(node => { 8 | delete this.positions[node.id] 9 | return [ node.id, true ]; 10 | })) 11 | this.nodes = this.nodes.filter(node => !hash[node.id]) 12 | } 13 | }, 14 | 'nodes:deleted': { 15 | forward({ nodes, positions, edges }) { 16 | const hash = Object.fromEntries(nodes.map(node => [ node.id, true ])) 17 | const edgeHash = Object.fromEntries(edges.map(edge => [ edge.id, true ])) 18 | this.nodes = this.nodes.filter(node => !hash[node.id]) 19 | Object.keys(positions).forEach(id => delete this.positions[id]) 20 | this.edges = this.edges.filter(edge => !edgeHash[edge.id]) 21 | }, 22 | back({ nodes, positions, edges }) { 23 | this.nodes = [ ...this.nodes, ...nodes ] 24 | this.positions = { ...this.positions, ...positions } 25 | this.edges = [ ...this.edges, ...edges ] 26 | } 27 | }, 28 | 'edges:deleted': { 29 | forward(deleted) { 30 | const hash = Object.fromEntries(deleted.map(edge => [ edge.id, true ])) 31 | this.edges = this.edges.filter(edge => !hash[edge.id]) 32 | }, 33 | back(deleted) { 34 | this.edges = [ ...this.edges, ...deleted ] 35 | }, 36 | }, 37 | 'edges:added': { 38 | forward({ edge }) { 39 | this.edges = [ ...this.edges, edge ] 40 | }, 41 | back({ edge }) { 42 | this.edges = this.edges.filter(({ id }) => id !== edge.id) 43 | }, 44 | }, 45 | 'zones:deleted': { 46 | forward(deleted) { 47 | const hash = Object.fromEntries(deleted.map(zone => [ zone.id, true ])) 48 | this.zones = this.zones.filter(zone => !hash[zone.id]) 49 | }, 50 | back(deleted) { 51 | this.zones = [ ...this.zones, ...deleted ] 52 | }, 53 | }, 54 | 'zones:added': { 55 | forward(added) { 56 | this.zones = [ ...this.zones, ...added ] 57 | }, 58 | back(added) { 59 | const hash = Object.fromEntries(added.map(zone => [ zone.id, true ])) 60 | this.zones = this.zones.filter(zone => !hash[zone.id]) 61 | }, 62 | }, 63 | 'selected:moved': { 64 | forward({ target, source }) { 65 | this.positions = { ...this.positions, ...target } 66 | }, 67 | back({ target, source }) { 68 | this.positions = { ...this.positions, ...source } 69 | }, 70 | } 71 | } 72 | export default historyTraveller 73 | -------------------------------------------------------------------------------- /src/utils/dagre/normalize.js: -------------------------------------------------------------------------------- 1 | import _ from "./lodash.js"; 2 | import * as util from "./util.js"; 3 | 4 | 5 | const normalizeEdge = (g, e) => { 6 | let v = e.v; 7 | let vRank = g.node(v).rank; 8 | const w = e.w; 9 | const wRank = g.node(w).rank; 10 | const name = e.name; 11 | const edgeLabel = g.edge(e); 12 | const labelRank = edgeLabel.labelRank; 13 | 14 | if (wRank === vRank + 1) return; 15 | 16 | g.removeEdge(e); 17 | 18 | let dummy, attrs, i; 19 | for (i = 0, ++vRank; vRank < wRank; ++i, ++vRank) { 20 | edgeLabel.points = []; 21 | attrs = { 22 | width: 0, height: 0, 23 | edgeLabel: edgeLabel, edgeObj: e, 24 | rank: vRank 25 | }; 26 | dummy = util.addDummyNode(g, "edge", attrs, "_d"); 27 | if (vRank === labelRank) { 28 | attrs.width = edgeLabel.width; 29 | attrs.height = edgeLabel.height; 30 | attrs.dummy = "edge-label"; 31 | attrs.labelpos = edgeLabel.labelpos; 32 | } 33 | g.setEdge(v, dummy, { weight: edgeLabel.weight }, name); 34 | if (i === 0) { 35 | g.graph().dummyChains.push(dummy); 36 | } 37 | v = dummy; 38 | } 39 | 40 | g.setEdge(v, w, { weight: edgeLabel.weight }, name); 41 | }; 42 | const undo = g => { 43 | _.forEach(g.graph().dummyChains, v => { 44 | let node = g.node(v); 45 | const origLabel = node.edgeLabel; 46 | let w; 47 | g.setEdge(node.edgeObj, origLabel); 48 | while (node.dummy) { 49 | w = g.successors(v)[0]; 50 | g.removeNode(v); 51 | origLabel.points.push({ x: node.x, y: node.y }); 52 | if (node.dummy === "edge-label") { 53 | origLabel.x = node.x; 54 | origLabel.y = node.y; 55 | origLabel.width = node.width; 56 | origLabel.height = node.height; 57 | } 58 | v = w; 59 | node = g.node(v); 60 | } 61 | }); 62 | }; 63 | /* 64 | * Breaks any long edges in the graph into short segments that span 1 layer 65 | * each. This operation is undoable with the denormalize function. 66 | * 67 | * Pre-conditions: 68 | * 69 | * 1. The input graph is a DAG. 70 | * 2. Each node in the graph has a "rank" property. 71 | * 72 | * Post-condition: 73 | * 74 | * 1. All edges in the graph have a length of 1. 75 | * 2. Dummy nodes are added where edges have been split into segments. 76 | * 3. The graph is augmented with a "dummyChains" attribute which contains 77 | * the first dummy in each chain of dummy nodes produced. 78 | */ 79 | const run = g => { 80 | g.graph().dummyChains = []; 81 | _.forEach(g.edges(), edge => normalizeEdge(g, edge)); 82 | }; 83 | 84 | 85 | export default { 86 | run, 87 | undo 88 | }; 89 | -------------------------------------------------------------------------------- /src/components/canvas-dragging-behavior-handlers.js: -------------------------------------------------------------------------------- 1 | const getCanvasDraggingHandlers = ({ methods, data, props, computed }) => { 2 | const mouseupOutside = event => { 3 | if (data.dragging) { 4 | document.removeEventListener('mousemove', mousemoveOutsideHandler) 5 | document.removeEventListener('mouseup', mouseupOutsideHandler) 6 | mouseupOutsideHandler = null 7 | mousemoveOutsideHandler = null 8 | mouseup(event) 9 | } 10 | } 11 | 12 | let mouseupOutsideHandler 13 | let mousemoveOutsideHandler 14 | 15 | const mousedown = event => { 16 | if (!event.shiftKey) methods.clearSelection() 17 | const canvas = methods.getCanvas() 18 | const fromCanvas = event.target === canvas 19 | fromCanvas && canvas.focus({ preventScroll: true }) // hacking: 用这个方式来获取keydown事件,必须结合canvas的tabindex属性,并防止获得焦点时滚动屏幕 20 | const insideCanvas = !fromCanvas && event.composedPath().includes(canvas) 21 | if (!insideCanvas) { 22 | event.preventDefault() 23 | 24 | data.dragging = true 25 | // hacking: 如果在canvas内开始选择,就不再需要去掉canvas相对于viewport的偏移 26 | data.draggingSource.x = data.draggingTarget.x = event.offsetX / (fromCanvas ? 1 : computed.zoomScale) - (fromCanvas ? 0 : data.offsetX / data.scale) 27 | data.draggingSource.y = data.draggingTarget.y = event.offsetY / (fromCanvas ? 1 : computed.zoomScale) - (fromCanvas ? 0 : data.offsetY / data.scale) 28 | methods[`${props.canvasDragging}DraggingStart`]?.(event) 29 | } 30 | } 31 | 32 | const mouseleave = event => { 33 | if (data.dragging) { 34 | mouseupOutsideHandler = mouseupOutside 35 | mousemoveOutsideHandler = mousemove 36 | document.addEventListener('mousemove', mousemoveOutsideHandler, false) 37 | document.addEventListener('mouseup', mouseupOutsideHandler, { once: true }) 38 | } 39 | } 40 | 41 | const mouseenter = event => { 42 | if (data.dragging) { 43 | document.removeEventListener('mousemove', mousemoveOutsideHandler) 44 | document.removeEventListener('mouseup', mouseupOutsideHandler) 45 | mouseupOutsideHandler = null 46 | mousemoveOutsideHandler = null 47 | } 48 | } 49 | 50 | const mousemove = event => { 51 | if (data.dragging) { 52 | event.preventDefault() // hacking: 避免移动时选择外部文本 53 | methods[`${props.canvasDragging}Dragging`]?.(event) 54 | } 55 | } 56 | 57 | const mouseup = event => { 58 | if (data.dragging) { 59 | data.dragging = false 60 | methods[`${props.canvasDragging}DraggingEnd`]?.(event) 61 | } 62 | } 63 | return { mouseup, mousedown, mousemove, mouseleave, mouseenter }; 64 | }; 65 | 66 | 67 | export default getCanvasDraggingHandlers 68 | -------------------------------------------------------------------------------- /src/components/node-dragging-behavior-handlers.js: -------------------------------------------------------------------------------- 1 | import img from "../utils/empty-drag-image.js"; 2 | import preventDefaultDrop from "../utils/prevent-default-drop.js"; 3 | 4 | const getNodeDraggingHandlers = ({ data, props, computed, methods }) => ({ 5 | move: { 6 | dragstart(event) { 7 | if (computed.draggable.value) { 8 | event.dataTransfer.setDragImage(img, 0, 0) // hacking: 用空svg图片隐藏DragImage 9 | document.addEventListener("dragover", preventDefaultDrop, false) // hacking: 避免最后一次事件的坐标回到0,0 10 | methods.startNodeMoving() 11 | } 12 | }, 13 | drag(event) { 14 | if (!event.screenX && !event.screenY) return // hacking: 防止拖出窗口位置被置为(0,0) 15 | if (computed.draggable.value) { 16 | methods.nodeMoving( // hacking: 回调DragonflyCanvasCore, 修改所有选择节点输入的position信息(同时可以影响到edge) 17 | Math.round(event.offsetX - data.inDomOffset.x), 18 | Math.round(event.offsetY - data.inDomOffset.y), 19 | ) 20 | } 21 | }, 22 | dragend(event) { 23 | document.removeEventListener('dragover', preventDefaultDrop) 24 | methods.stopNodeMoving() 25 | }, 26 | }, 27 | link: { 28 | dragstart(event) { 29 | if (computed.draggable.value) { 30 | event.dataTransfer.setDragImage(img, 0, 0) // hacking: 用空svg图片隐藏DragImage 31 | computed.groupLinkOut.value(props.node) && methods.startNodeLinking({ 32 | source: props.node.id, 33 | sourceGroup: computed.groupName.value, 34 | }) 35 | document.addEventListener("dragover", preventDefaultDrop, false) // hacking: 避免最后一次事件的坐标回到0,0 36 | } 37 | }, 38 | drag(event) { 39 | if (!event.screenX && !event.screenY) return // hacking: 防止拖出窗口位置被置为(0,0) 40 | if (computed.draggable.value) { 41 | methods.nodeLinking( 42 | computed.position.value.x, 43 | computed.position.value.y, 44 | data.width, 45 | data.height, 46 | 'right', 47 | event.offsetX + computed.x.value, 48 | event.offsetY + computed.y.value, 49 | ) 50 | } 51 | }, 52 | dragenter(event) { 53 | if (event.composedPath().includes(event.toElement)) 54 | data.targeted = true 55 | }, 56 | dragleave(event) { 57 | if (!event.composedPath().includes(event.fromElement)) 58 | data.targeted = false 59 | }, 60 | dragend(event) { 61 | document.removeEventListener('dragover', preventDefaultDrop) 62 | methods.stopNodeLinking() 63 | }, 64 | }, 65 | }); 66 | 67 | export default getNodeDraggingHandlers 68 | -------------------------------------------------------------------------------- /src/utils/dagre/order/build-layer-graph.js: -------------------------------------------------------------------------------- 1 | import _ from "../lodash.js"; 2 | import { Graph } from "../../graphlib/index.js"; 3 | 4 | const createRootNode = g => { 5 | let v; 6 | while (g.hasNode((v = _.uniqueId("_root")))) ; 7 | return v; 8 | }; 9 | 10 | /* 11 | * Constructs a graph that can be used to sort a layer of nodes. The graph will 12 | * contain all base and subgraph nodes from the request layer in their original 13 | * hierarchy and any edges that are incident on these nodes and are of the type 14 | * requested by the "relationship" parameter. 15 | * 16 | * Nodes from the requested rank that do not have parents are assigned a root 17 | * node in the output graph, which is set in the root graph attribute. This 18 | * makes it easy to walk the hierarchy of movable nodes during ordering. 19 | * 20 | * Pre-conditions: 21 | * 22 | * 1. Input graph is a DAG 23 | * 2. Base nodes in the input graph have a rank attribute 24 | * 3. Subgraph nodes in the input graph has minRank and maxRank attributes 25 | * 4. Edges have an assigned weight 26 | * 27 | * Post-conditions: 28 | * 29 | * 1. Output graph has all nodes in the movable rank with preserved 30 | * hierarchy. 31 | * 2. Root nodes in the movable layer are made children of the node 32 | * indicated by the root attribute of the graph. 33 | * 3. Non-movable nodes incident on movable nodes, selected by the 34 | * relationship parameter, are included in the graph (without hierarchy). 35 | * 4. Edges incident on movable nodes, selected by the relationship 36 | * parameter, are added to the output graph. 37 | * 5. The weights for copied edges are aggregated as need, since the output 38 | * graph is not a multi-graph. 39 | */ 40 | export default (g, rank, relationship) => { 41 | const root = createRootNode(g), 42 | result = new Graph({ compound: true }).setGraph({ root: root }) 43 | .setDefaultNodeLabel(v => g.node(v)); 44 | 45 | _.forEach(g.nodes(), v => { 46 | const node = g.node(v), 47 | parent = g.parent(v); 48 | 49 | if (node.rank === rank || node.minRank <= rank && rank <= node.maxRank) { 50 | result.setNode(v); 51 | result.setParent(v, parent || root); 52 | 53 | // This assumes we have only short edges! 54 | _.forEach(g[relationship](v), e => { 55 | const u = e.v === v ? e.w : e.v, 56 | edge = result.edge(u, v), 57 | weight = !_.isUndefined(edge) ? edge.weight : 0; 58 | result.setEdge(u, v, { weight: g.edge(e).weight + weight }); 59 | }); 60 | 61 | if (_.has(node, "minRank")) { 62 | result.setNode(v, { 63 | borderLeft: node.borderLeft[rank], 64 | borderRight: node.borderRight[rank] 65 | }); 66 | } 67 | } 68 | }); 69 | 70 | return result; 71 | } 72 | 73 | -------------------------------------------------------------------------------- /src/components/DragonflyCanvasTools.vue: -------------------------------------------------------------------------------- 1 | 39 | 40 | 80 | -------------------------------------------------------------------------------- /src/components/DragonflyCanvasEdgesLayer.vue: -------------------------------------------------------------------------------- 1 | 54 | 55 | 75 | 80 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at lchrennew@126.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /src/utils/dagre/order/resolve-conflicts.js: -------------------------------------------------------------------------------- 1 | import _ from "../lodash.js"; 2 | 3 | const mergeEntries = (target, source) => { 4 | let sum = 0; 5 | let weight = 0; 6 | 7 | if (target.weight) { 8 | sum += target.barycenter * target.weight; 9 | weight += target.weight; 10 | } 11 | 12 | if (source.weight) { 13 | sum += source.barycenter * source.weight; 14 | weight += source.weight; 15 | } 16 | 17 | target.vs = source.vs.concat(target.vs); 18 | target.barycenter = sum / weight; 19 | target.weight = weight; 20 | target.i = Math.min(source.i, target.i); 21 | source.merged = true; 22 | }; 23 | 24 | const doResolveConflicts = sourceSet => { 25 | const entries = []; 26 | 27 | const handleIn = vEntry => uEntry => { 28 | if (uEntry.merged) { 29 | return; 30 | } 31 | if (_.isUndefined(uEntry.barycenter) || 32 | _.isUndefined(vEntry.barycenter) || 33 | uEntry.barycenter >= vEntry.barycenter) { 34 | mergeEntries(vEntry, uEntry); 35 | } 36 | }; 37 | 38 | const handleOut = vEntry => wEntry => { 39 | wEntry["in"].push(vEntry); 40 | if (--wEntry.indegree === 0) { 41 | sourceSet.push(wEntry); 42 | } 43 | }; 44 | 45 | while (sourceSet.length) { 46 | const entry = sourceSet.pop(); 47 | entries.push(entry); 48 | _.forEach(entry["in"].reverse(), handleIn(entry)); 49 | _.forEach(entry.out, handleOut(entry)); 50 | } 51 | 52 | return _.map(_.filter(entries, entry => !entry.merged), 53 | entry => _.pick(entry, [ "vs", "i", "barycenter", "weight" ])); 54 | 55 | }; 56 | 57 | /* 58 | * Given a list of entries of the form {v, barycenter, weight} and a 59 | * constraint graph this function will resolve any conflicts between the 60 | * constraint graph and the barycenters for the entries. If the barycenters for 61 | * an entry would violate a constraint in the constraint graph then we coalesce 62 | * the nodes in the conflict into a new node that respects the contraint and 63 | * aggregates barycenter and weight information. 64 | * 65 | * This implementation is based on the description in Forster, "A Fast and 66 | * Simple Hueristic for Constrained Two-Level Crossing Reduction," thought it 67 | * differs in some specific details. 68 | * 69 | * Pre-conditions: 70 | * 71 | * 1. Each entry has the form {v, barycenter, weight}, or if the node has 72 | * no barycenter, then {v}. 73 | * 74 | * Returns: 75 | * 76 | * A new list of entries of the form {vs, i, barycenter, weight}. The list 77 | * `vs` may either be a singleton or it may be an aggregation of nodes 78 | * ordered such that they do not violate constraints from the constraint 79 | * graph. The property `i` is the lowest original index of any of the 80 | * elements in `vs`. 81 | */ 82 | export default (entries, cg) => { 83 | const mappedEntries = {}; 84 | _.forEach(entries, (entry, i) => { 85 | const tmp = mappedEntries[entry.v] = { 86 | indegree: 0, 87 | "in": [], 88 | out: [], 89 | vs: [ entry.v ], 90 | i: i 91 | }; 92 | if (!_.isUndefined(entry.barycenter)) { 93 | tmp.barycenter = entry.barycenter; 94 | tmp.weight = entry.weight; 95 | } 96 | }); 97 | 98 | _.forEach(cg.edges(), e => { 99 | const entryV = mappedEntries[e.v]; 100 | const entryW = mappedEntries[e.w]; 101 | if (!_.isUndefined(entryV) && !_.isUndefined(entryW)) { 102 | entryW.indegree++; 103 | entryV.out.push(mappedEntries[e.w]); 104 | } 105 | }); 106 | 107 | const sourceSet = _.filter(mappedEntries, entry => !entry.indegree); 108 | 109 | return doResolveConflicts(sourceSet); 110 | } 111 | 112 | -------------------------------------------------------------------------------- /src/CanvasConfig.vue: -------------------------------------------------------------------------------- 1 | 86 | 87 | 93 | 94 | 105 | -------------------------------------------------------------------------------- /src/components/edge/LBrokenLine.vue: -------------------------------------------------------------------------------- 1 | 96 | 97 | 100 | -------------------------------------------------------------------------------- /src/utils/dagre/greedy-fas.js: -------------------------------------------------------------------------------- 1 | import _ from "./lodash.js"; 2 | import { Graph } from "../graphlib/index.js"; 3 | import List from "./data/list.js"; 4 | 5 | const DEFAULT_WEIGHT_FN = _.constant(1); 6 | 7 | const assignBucket = (buckets, zeroIdx, entry) => { 8 | if (!entry.out) { 9 | buckets[0].enqueue(entry); 10 | } else if (!entry["in"]) { 11 | buckets[buckets.length - 1].enqueue(entry); 12 | } else { 13 | buckets[entry.out - entry["in"] + zeroIdx].enqueue(entry); 14 | } 15 | }; 16 | 17 | const removeNode = (g, buckets, zeroIdx, entry, collectPredecessors) => { 18 | const results = collectPredecessors ? [] : undefined; 19 | 20 | _.forEach(g.inEdges(entry.v), edge => { 21 | const weight = g.edge(edge); 22 | const uEntry = g.node(edge.v); 23 | 24 | if (collectPredecessors) { 25 | results.push({ v: edge.v, w: edge.w }); 26 | } 27 | 28 | uEntry.out -= weight; 29 | assignBucket(buckets, zeroIdx, uEntry); 30 | }); 31 | 32 | _.forEach(g.outEdges(entry.v), 33 | edge => { 34 | const weight = g.edge(edge); 35 | const w = edge.w; 36 | const wEntry = g.node(w); 37 | wEntry["in"] -= weight; 38 | assignBucket(buckets, zeroIdx, wEntry); 39 | }); 40 | 41 | g.removeNode(entry.v); 42 | 43 | return results; 44 | }; 45 | 46 | const buildState = (g, weightFn) => { 47 | const fasGraph = new Graph(); 48 | let maxIn = 0; 49 | let maxOut = 0; 50 | _.forEach(g.nodes(), v => fasGraph.setNode(v, { v: v, "in": 0, out: 0 })); 51 | 52 | // Aggregate weights on nodes, but also sum the weights across multi-edges 53 | // into a single edge for the fasGraph. 54 | _.forEach(g.edges(), e => { 55 | const prevWeight = fasGraph.edge(e.v, e.w) || 0; 56 | const weight = weightFn(e); 57 | const edgeWeight = prevWeight + weight; 58 | fasGraph.setEdge(e.v, e.w, edgeWeight); 59 | maxOut = Math.max(maxOut, fasGraph.node(e.v).out += weight); 60 | maxIn = Math.max(maxIn, fasGraph.node(e.w)["in"] += weight); 61 | }); 62 | 63 | const buckets = _.range(maxOut + maxIn + 3).map(() => new List()); 64 | const zeroIdx = maxIn + 1; 65 | 66 | _.forEach(fasGraph.nodes(), v => assignBucket(buckets, zeroIdx, fasGraph.node(v))); 67 | 68 | return { graph: fasGraph, buckets: buckets, zeroIdx: zeroIdx }; 69 | }; 70 | 71 | const greedyFAS = (g, weightFn) => { 72 | if (g.nodeCount() <= 1) { 73 | return []; 74 | } 75 | const state = buildState(g, weightFn || DEFAULT_WEIGHT_FN); 76 | const results = doGreedyFAS(state.graph, state.buckets, state.zeroIdx); 77 | 78 | // Expand multi-edges 79 | return _.flatten(_.map(results, e => g.outEdges(e.v, e.w)), true); 80 | }; 81 | const doGreedyFAS = (g, buckets, zeroIdx) => { 82 | let results = []; 83 | const sources = buckets[buckets.length - 1]; 84 | const sinks = buckets[0]; 85 | 86 | let entry; 87 | while (g.nodeCount()) { 88 | while ((entry = sinks.dequeue())) { 89 | removeNode(g, buckets, zeroIdx, entry); 90 | } 91 | while ((entry = sources.dequeue())) { 92 | removeNode(g, buckets, zeroIdx, entry); 93 | } 94 | if (g.nodeCount()) { 95 | for (let i = buckets.length - 2; i > 0; --i) { 96 | entry = buckets[i].dequeue(); 97 | if (entry) { 98 | results = results.concat(removeNode(g, buckets, zeroIdx, entry, true)); 99 | break; 100 | } 101 | } 102 | } 103 | } 104 | 105 | return results; 106 | }; 107 | 108 | 109 | /* 110 | * A greedy heuristic for finding a feedback arc set for a graph. A feedback 111 | * arc set is a set of edges that can be removed to make a graph acyclic. 112 | * The algorithm comes from: P. Eades, X. Lin, and W. F. Smyth, "A fast and 113 | * effective heuristic for the feedback arc set problem." This implementation 114 | * adjusts that from the paper to allow for weighted edges. 115 | */ 116 | export default greedyFAS; 117 | 118 | -------------------------------------------------------------------------------- /src/utils/dagre/nesting-graph.js: -------------------------------------------------------------------------------- 1 | import _ from "./lodash.js"; 2 | import * as util from "./util.js"; 3 | 4 | const dfs = (g, root, nodeSep, weight, height, depths, v) => { 5 | const children = g.children(v); 6 | if (!children.length) { 7 | if (v !== root) { 8 | g.setEdge(root, v, { weight: 0, minlen: nodeSep }); 9 | } 10 | return; 11 | } 12 | 13 | const top = util.addBorderNode(g, "_bt"); 14 | const bottom = util.addBorderNode(g, "_bb"); 15 | const label = g.node(v); 16 | 17 | g.setParent(top, v); 18 | label.borderTop = top; 19 | g.setParent(bottom, v); 20 | label.borderBottom = bottom; 21 | 22 | _.forEach(children, child => { 23 | dfs(g, root, nodeSep, weight, height, depths, child); 24 | const childNode = g.node(child); 25 | const childTop = childNode.borderTop ? childNode.borderTop : child; 26 | const childBottom = childNode.borderBottom ? childNode.borderBottom : child; 27 | const thisWeight = childNode.borderTop ? weight : 2 * weight; 28 | const minlen = childTop !== childBottom ? 1 : height - depths[v] + 1; 29 | g.setEdge(top, childTop, { 30 | weight: thisWeight, 31 | minlen: minlen, 32 | nestingEdge: true 33 | }); 34 | 35 | g.setEdge(childBottom, bottom, { 36 | weight: thisWeight, 37 | minlen: minlen, 38 | nestingEdge: true 39 | }); 40 | }); 41 | 42 | if (!g.parent(v)) { 43 | g.setEdge(root, top, { weight: 0, minlen: height + depths[v] }); 44 | } 45 | }; 46 | 47 | const treeDepths = g => { 48 | const depths = {}; 49 | 50 | const dfs = (v, depth) => { 51 | const children = g.children(v); 52 | if (children && children.length) { 53 | _.forEach(children, child => dfs(child, depth + 1)); 54 | } 55 | depths[v] = depth; 56 | }; 57 | _.forEach(g.children(), v => dfs(v, 1)); 58 | return depths; 59 | }; 60 | 61 | const sumWeights = g => _.reduce(g.edges(), (acc, e) => acc + g.edge(e).weight, 0); 62 | 63 | /* 64 | * A nesting graph creates dummy nodes for the tops and bottoms of subgraphs, 65 | * adds appropriate edges to ensure that all cluster nodes are placed between 66 | * these boundries, and ensures that the graph is connected. 67 | * 68 | * In addition we ensure, through the use of the minlen property, that nodes 69 | * and subgraph border nodes to not end up on the same rank. 70 | * 71 | * Preconditions: 72 | * 73 | * 1. Input graph is a DAG 74 | * 2. Nodes in the input graph has a minlen attribute 75 | * 76 | * Postconditions: 77 | * 78 | * 1. Input graph is connected. 79 | * 2. Dummy nodes are added for the tops and bottoms of subgraphs. 80 | * 3. The minlen attribute for nodes is adjusted to ensure nodes do not 81 | * get placed on the same rank as subgraph border nodes. 82 | * 83 | * The nesting graph idea comes from Sander, "Layout of Compound Directed 84 | * Graphs." 85 | */ 86 | const run = g => { 87 | // Note: depths is an Object not an array 88 | const root = util.addDummyNode(g, "root", {}, "_root"); 89 | const depths = treeDepths(g); 90 | const height = _.max(_.values(depths)) - 1; 91 | const nodeSep = 2 * height + 1; 92 | 93 | g.graph().nestingRoot = root; 94 | 95 | // Multiply minlen by nodeSep to align nodes on non-border ranks. 96 | _.forEach(g.edges(), e => g.edge(e).minlen *= nodeSep); 97 | 98 | // Calculate a weight that is sufficient to keep subgraphs vertically compact 99 | const weight = sumWeights(g) + 1; 100 | 101 | // Create border nodes and link them up 102 | _.forEach(g.children(), child => dfs(g, root, nodeSep, weight, height, depths, child)); 103 | 104 | // Save the multiplier for node layers for later removal of empty border 105 | // layers. 106 | g.graph().nodeRankFactor = nodeSep; 107 | }; 108 | 109 | 110 | const cleanup = g => { 111 | const graphLabel = g.graph(); 112 | g.removeNode(graphLabel.nestingRoot); 113 | delete graphLabel.nestingRoot; 114 | _.forEach(g.edges(), e => { 115 | const edge = g.edge(e); 116 | if (edge.nestingEdge) { 117 | g.removeEdge(e); 118 | } 119 | }); 120 | }; 121 | 122 | export default { 123 | run, 124 | cleanup 125 | }; 126 | -------------------------------------------------------------------------------- /src/components/DragonflyZone.vue: -------------------------------------------------------------------------------- 1 | 30 | 31 | 123 | -------------------------------------------------------------------------------- /src/utils/graphlib/data/priority-queue.js: -------------------------------------------------------------------------------- 1 | import _ from "../lodash.js"; 2 | 3 | export default class PriorityQueue { 4 | /** 5 | * A min-priority queue data structure. This algorithm is derived from Cormen, 6 | * et al., "Introduction to Algorithms". The basic idea of a min-priority 7 | * queue is that you can efficiently (in O(1) time) get the smallest key in 8 | * the queue. Adding and removing elements takes O(log n) time. A key can 9 | * have its priority decreased in O(log n) time. 10 | */ 11 | constructor() { 12 | this._arr = []; 13 | this._keyIndices = {}; 14 | } 15 | 16 | /** 17 | * Returns the number of elements in the queue. Takes `O(1)` time. 18 | */ 19 | size() { 20 | return this._arr.length; 21 | } 22 | 23 | /** 24 | * Returns the keys that are in the queue. Takes `O(n)` time. 25 | */ 26 | keys() { 27 | return this._arr.map(x => x.key); 28 | } 29 | 30 | /** 31 | * Returns `true` if **key** is in the queue and `false` if not. 32 | */ 33 | has(key) { 34 | return _.has(this._keyIndices, key); 35 | } 36 | 37 | /** 38 | * Returns the priority for **key**. If **key** is not present in the queue 39 | * then this function returns `undefined`. Takes `O(1)` time. 40 | * 41 | * @param {Object} key 42 | */ 43 | priority(key) { 44 | const index = this._keyIndices[key]; 45 | if (index !== undefined) { 46 | return this._arr[index].priority; 47 | } 48 | } 49 | 50 | /** 51 | * Returns the key for the minimum element in this queue. If the queue is 52 | * empty this function throws an Error. Takes `O(1)` time. 53 | */ 54 | min() { 55 | if (this.size() === 0) { 56 | throw new Error("Queue underflow"); 57 | } 58 | return this._arr[0].key; 59 | } 60 | 61 | /** 62 | * Inserts a new key into the priority queue. If the key already exists in 63 | * the queue this function returns `false`; otherwise it will return `true`. 64 | * Takes `O(n)` time. 65 | * 66 | * @param {Object} key the key to add 67 | * @param {Number} priority the initial priority for the key 68 | */ 69 | add(key, priority) { 70 | const keyIndices = this._keyIndices; 71 | key = String(key); 72 | if (!_.has(keyIndices, key)) { 73 | const arr = this._arr; 74 | const index = arr.length; 75 | keyIndices[key] = index; 76 | arr.push({ key: key, priority: priority }); 77 | this._decrease(index); 78 | return true; 79 | } 80 | return false; 81 | } 82 | 83 | /** 84 | * Removes and returns the smallest key in the queue. Takes `O(log n)` time. 85 | */ 86 | removeMin() { 87 | this._swap(0, this._arr.length - 1); 88 | const min = this._arr.pop(); 89 | delete this._keyIndices[min.key]; 90 | this._heapify(0); 91 | return min.key; 92 | } 93 | 94 | /** 95 | * Decreases the priority for **key** to **priority**. If the new priority is 96 | * greater than the previous priority, this function will throw an Error. 97 | * 98 | * @param {Object} key the key for which to raise priority 99 | * @param {Number} priority the new priority for the key 100 | */ 101 | decrease(key, priority) { 102 | const index = this._keyIndices[key]; 103 | if (priority > this._arr[index].priority) { 104 | throw new Error("New priority is greater than current priority. " + 105 | "Key: " + key + " Old: " + this._arr[index].priority + " New: " + priority); 106 | } 107 | this._arr[index].priority = priority; 108 | this._decrease(index); 109 | } 110 | 111 | _heapify(i) { 112 | const arr = this._arr; 113 | const l = 2 * i; 114 | const r = l + 1; 115 | let largest = i; 116 | if (l < arr.length) { 117 | largest = arr[l].priority < arr[largest].priority ? l : largest; 118 | if (r < arr.length) { 119 | largest = arr[r].priority < arr[largest].priority ? r : largest; 120 | } 121 | if (largest !== i) { 122 | this._swap(i, largest); 123 | this._heapify(largest); 124 | } 125 | } 126 | } 127 | 128 | _decrease(index) { 129 | const arr = this._arr; 130 | const priority = arr[index].priority; 131 | let parent; 132 | while (index !== 0) { 133 | parent = index >> 1; 134 | if (arr[parent].priority < priority) { 135 | break; 136 | } 137 | this._swap(index, parent); 138 | index = parent; 139 | } 140 | } 141 | 142 | _swap(i, j) { 143 | const arr = this._arr; 144 | const keyIndices = this._keyIndices; 145 | const origArrI = arr[i]; 146 | const origArrJ = arr[j]; 147 | arr[i] = origArrJ; 148 | arr[j] = origArrI; 149 | keyIndices[origArrJ.key] = i; 150 | keyIndices[origArrI.key] = j; 151 | } 152 | } 153 | 154 | -------------------------------------------------------------------------------- /src/components/DragonflyEdge.vue: -------------------------------------------------------------------------------- 1 | 56 | 178 | -------------------------------------------------------------------------------- /src/utils/dagre/util.js: -------------------------------------------------------------------------------- 1 | import _ from "./lodash.js"; 2 | import { Graph } from "../graphlib/index.js"; 3 | 4 | export const notime = (name, fn) => fn(); 5 | 6 | export const maxRank = g => _.max(_.map(g.nodes(), v => { 7 | const { rank } = g.node(v); 8 | if (!_.isUndefined(rank)) { 9 | return rank; 10 | } 11 | })); 12 | 13 | /* 14 | * Adds a dummy node to the graph and return v. 15 | */ 16 | export const addDummyNode = (g, type, attrs, name) => { 17 | let v; 18 | do { 19 | v = _.uniqueId(name); 20 | } while (g.hasNode(v)); 21 | 22 | attrs.dummy = type; 23 | g.setNode(v, attrs); 24 | return v; 25 | }; 26 | 27 | /* 28 | * Returns a new graph with only simple edges. Handles aggregation of data 29 | * associated with multi-edges. 30 | */ 31 | export const simplify = g => { 32 | const simplified = new Graph().setGraph(g.graph()); 33 | _.forEach(g.nodes(), v => simplified.setNode(v, g.node(v))); 34 | _.forEach(g.edges(), e => { 35 | const simpleLabel = simplified.edge(e.v, e.w) || { weight: 0, minlen: 1 }; 36 | const label = g.edge(e); 37 | simplified.setEdge(e.v, e.w, { 38 | weight: simpleLabel.weight + label.weight, 39 | minlen: Math.max(simpleLabel.minlen, label.minlen) 40 | }); 41 | }); 42 | return simplified; 43 | }; 44 | 45 | export const asNonCompoundGraph = g => { 46 | const simplified = new Graph({ multigraph: g.isMultigraph() }).setGraph(g.graph()); 47 | _.forEach(g.nodes(), v => { 48 | if (!g.children(v).length) { 49 | simplified.setNode(v, g.node(v)); 50 | } 51 | }); 52 | _.forEach(g.edges(), e => simplified.setEdge(e, g.edge(e))); 53 | return simplified; 54 | }; 55 | 56 | export const successorWeights = g => { 57 | const weightMap = _.map(g.nodes(), v => { 58 | const sucs = {}; 59 | _.forEach(g.outEdges(v), e => sucs[e.w] = (sucs[e.w] || 0) + g.edge(e).weight); 60 | return sucs; 61 | }); 62 | return _.zipObject(g.nodes(), weightMap); 63 | }; 64 | 65 | export const predecessorWeights = g => { 66 | const weightMap = _.map(g.nodes(), v => { 67 | const preds = {}; 68 | _.forEach(g.inEdges(v), e => preds[e.v] = (preds[e.v] || 0) + g.edge(e).weight); 69 | return preds; 70 | }); 71 | return _.zipObject(g.nodes(), weightMap); 72 | }; 73 | 74 | /* 75 | * Finds where a line starting at point ({x, y}) would intersect a rectangle 76 | * ({x, y, width, height}) if it were pointing at the rectangle's center. 77 | */ 78 | export const intersectRect = (rect, point) => { 79 | // Rectangle intersection algorithm from: 80 | // http://math.stackexchange.com/questions/108113/find-edge-between-two-boxes 81 | const x = rect.x; 82 | const y = rect.y; 83 | const dx = point.x - x; 84 | const dy = point.y - y; 85 | let w = rect.width / 2; 86 | let h = rect.height / 2; 87 | 88 | if (!dx && !dy) { 89 | throw new Error("Not possible to find intersection inside of the rectangle"); 90 | } 91 | 92 | let sx, sy; 93 | if (Math.abs(dy) * w > Math.abs(dx) * h) { 94 | // Intersection is top or bottom of rect. 95 | if (dy < 0) { 96 | h = -h; 97 | } 98 | sx = h * dx / dy; 99 | sy = h; 100 | } else { 101 | // Intersection is left or right of rect. 102 | if (dx < 0) { 103 | w = -w; 104 | } 105 | sx = w; 106 | sy = w * dy / dx; 107 | } 108 | 109 | return { x: x + sx, y: y + sy }; 110 | }; 111 | 112 | /* 113 | * Given a DAG with each node assigned "rank" and "order" properties, this 114 | * function will produce a matrix with the ids of each node. 115 | */ 116 | export const buildLayerMatrix = g => { 117 | const layering = _.map(_.range(maxRank(g) + 1), () => []); 118 | _.forEach(g.nodes(), v => { 119 | const node = g.node(v); 120 | const rank = node.rank; 121 | if (!_.isUndefined(rank)) { 122 | layering[rank][node.order] = v; 123 | } 124 | }); 125 | return layering; 126 | }; 127 | 128 | /* 129 | * Adjusts the ranks for all nodes in the graph such that all nodes v have 130 | * rank(v) >= 0 and at least one node w has rank(w) = 0. 131 | */ 132 | export const normalizeRanks = g => { 133 | const min = _.min(_.map(g.nodes(), v => g.node(v).rank)); 134 | _.forEach(g.nodes(), v => { 135 | const node = g.node(v); 136 | if (_.has(node, "rank")) { 137 | node.rank -= min; 138 | } 139 | }); 140 | }; 141 | 142 | export const removeEmptyRanks = g => { 143 | // Ranks may not start at 0, so we need to offset them 144 | const offset = _.min(_.map(g.nodes(), v => g.node(v).rank)); 145 | 146 | const layers = []; 147 | _.forEach(g.nodes(), v => { 148 | const rank = g.node(v).rank - offset; 149 | if (!layers[rank]) { 150 | layers[rank] = []; 151 | } 152 | layers[rank].push(v); 153 | }); 154 | 155 | let delta = 0; 156 | const nodeRankFactor = g.graph().nodeRankFactor; 157 | _.forEach(layers, (vs, i) => { 158 | if (_.isUndefined(vs) && i % nodeRankFactor !== 0) { 159 | --delta; 160 | } else if (delta) { 161 | _.forEach(vs, v => { 162 | g.node(v).rank += delta; 163 | }); 164 | } 165 | }); 166 | }; 167 | 168 | 169 | export const addBorderNode = (g, prefix, rank, order) => { 170 | const node = { 171 | width: 0, 172 | height: 0 173 | }; 174 | if (arguments.length >= 4) { 175 | node.rank = rank; 176 | node.order = order; 177 | } 178 | return addDummyNode(g, "border", node, prefix); 179 | }; 180 | 181 | /* 182 | * Partition a collection into two groups: `lhs` and `rhs`. If the supplied 183 | * function returns true for an entry it goes into `lhs`. Otherwise it goes 184 | * into `rhs. 185 | */ 186 | export const partition = (collection, fn) => { 187 | const result = { lhs: [], rhs: [] }; 188 | _.forEach(collection, value => { 189 | if (fn(value)) { 190 | result.lhs.push(value); 191 | } else { 192 | result.rhs.push(value); 193 | } 194 | }); 195 | return result; 196 | }; 197 | 198 | /* 199 | * Returns a new function that wraps `fn` with a timer. The wrapper logs the 200 | * time it takes to execute the function. 201 | */ 202 | export const time = (name, fn) => { 203 | const start = _.now(); 204 | try { 205 | return fn(); 206 | } finally { 207 | console.log(name + " time: " + (_.now() - start) + "ms"); 208 | } 209 | }; 210 | -------------------------------------------------------------------------------- /src/components/DragonflyNode.vue: -------------------------------------------------------------------------------- 1 | 153 | 154 | 200 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 75 | 76 | 165 | 166 | 169 | 223 | -------------------------------------------------------------------------------- /src/components/DragonflyEndpoint.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 210 | -------------------------------------------------------------------------------- /src/utils/dagre/rank/network-simplex.js: -------------------------------------------------------------------------------- 1 | import _ from "../lodash.js"; 2 | import feasibleTree from "./feasible-tree.js"; 3 | import { longestPath as initRank, slack } from "./util.js"; 4 | import { preorder, postorder } from "../../graphlib/alg/index.js"; 5 | import { simplify } from "../util.js"; 6 | 7 | const updateRanks = (t, g) => { 8 | const root = _.find(t.nodes(), v => !g.node(v).parent); 9 | let vs = preorder(t, root); 10 | vs = vs.slice(1); 11 | _.forEach(vs, v => { 12 | let parent = t.node(v).parent, 13 | edge = g.edge(v, parent), 14 | flipped = false; 15 | 16 | if (!edge) { 17 | edge = g.edge(parent, v); 18 | flipped = true; 19 | } 20 | 21 | g.node(v).rank = g.node(parent).rank + (flipped ? edge.minlen : -edge.minlen); 22 | }); 23 | }; 24 | 25 | /* 26 | * Returns true if the edge is in the tree. 27 | */ 28 | const isTreeEdge = (tree, u, v) => tree.hasEdge(u, v); 29 | 30 | /* 31 | * Returns true if the specified node is descendant of the root node per the 32 | * assigned low and lim attributes in the tree. 33 | */ 34 | const isDescendant = (tree, vLabel, rootLabel) => rootLabel.low <= vLabel.lim && vLabel.lim <= rootLabel.lim; 35 | 36 | const dfsAssignLowLim = (tree, visited, nextLim, v, parent) => { 37 | const low = nextLim; 38 | const label = tree.node(v); 39 | 40 | visited[v] = true; 41 | _.forEach(tree.neighbors(v), w => { 42 | if (!_.has(visited, w)) { 43 | nextLim = dfsAssignLowLim(tree, visited, nextLim, w, v); 44 | } 45 | }); 46 | 47 | label.low = low; 48 | label.lim = nextLim++; 49 | if (parent) { 50 | label.parent = parent; 51 | } else { 52 | // TODO should be able to remove this when we incrementally update low lim 53 | delete label.parent; 54 | } 55 | 56 | return nextLim; 57 | }; 58 | 59 | const leaveEdge = tree => _.find(tree.edges(), e => tree.edge(e).cutvalue < 0); 60 | 61 | const enterEdge = (t, g, edge) => { 62 | let v = edge.v; 63 | let w = edge.w; 64 | 65 | // For the rest of this function we assume that v is the tail and w is the 66 | // head, so if we don't have this edge in the graph we should flip it to 67 | // match the correct orientation. 68 | if (!g.hasEdge(v, w)) { 69 | v = edge.w; 70 | w = edge.v; 71 | } 72 | 73 | const vLabel = t.node(v); 74 | const wLabel = t.node(w); 75 | let tailLabel = vLabel; 76 | let flip = false; 77 | 78 | // If the root is in the tail of the edge then we need to flip the logic that 79 | // checks for the head and tail nodes in the candidates function below. 80 | if (vLabel.lim > wLabel.lim) { 81 | tailLabel = wLabel; 82 | flip = true; 83 | } 84 | 85 | const candidates = _.filter(g.edges(), edge => flip === isDescendant(t, t.node(edge.v), tailLabel) && 86 | flip !== isDescendant(t, t.node(edge.w), tailLabel)); 87 | 88 | return _.minBy(candidates, edge => slack(g, edge)); 89 | }; 90 | 91 | const exchangeEdges = (t, g, e, f) => { 92 | const v = e.v; 93 | const w = e.w; 94 | t.removeEdge(v, w); 95 | t.setEdge(f.v, f.w, {}); 96 | initLowLimValues(t); 97 | initCutValues(t, g); 98 | updateRanks(t, g); 99 | }; 100 | 101 | 102 | /* 103 | * Given the tight tree, its graph, and a child in the graph calculate and 104 | * return the cut value for the edge between the child and its parent. 105 | */ 106 | const calcCutValue = (t, g, child) => { 107 | const childLab = t.node(child); 108 | const parent = childLab.parent; 109 | // True if the child is on the tail end of the edge in the directed graph 110 | let childIsTail = true; 111 | // The graph's view of the tree edge we're inspecting 112 | let graphEdge = g.edge(child, parent); 113 | // The accumulated cut value for the edge between this node and its parent 114 | let cutValue = 0; 115 | 116 | if (!graphEdge) { 117 | childIsTail = false; 118 | graphEdge = g.edge(parent, child); 119 | } 120 | 121 | cutValue = graphEdge.weight; 122 | 123 | _.forEach(g.nodeEdges(child), e => { 124 | const isOutEdge = e.v === child, 125 | other = isOutEdge ? e.w : e.v; 126 | 127 | if (other !== parent) { 128 | const pointsToHead = isOutEdge === childIsTail, 129 | otherWeight = g.edge(e).weight; 130 | 131 | cutValue += pointsToHead ? otherWeight : -otherWeight; 132 | if (isTreeEdge(t, child, other)) { 133 | const otherCutValue = t.edge(child, other).cutvalue; 134 | cutValue += pointsToHead ? -otherCutValue : otherCutValue; 135 | } 136 | } 137 | }); 138 | 139 | return cutValue; 140 | }; 141 | 142 | function initLowLimValues(tree, root) { 143 | if (arguments.length < 2) { 144 | root = tree.nodes()[0]; 145 | } 146 | dfsAssignLowLim(tree, {}, 1, root); 147 | }; 148 | 149 | 150 | const assignCutValue = (t, g, child) => { 151 | const { parent } = t.node(child); 152 | t.edge(child, parent).cutvalue = calcCutValue(t, g, child); 153 | }; 154 | 155 | 156 | /* 157 | * Initializes cut values for all edges in the tree. 158 | */ 159 | const initCutValues = (t, g) => { 160 | let vs = postorder(t, t.nodes()); 161 | vs = vs.slice(0, vs.length - 1); 162 | _.forEach(vs, v => assignCutValue(t, g, v)); 163 | }; 164 | 165 | 166 | /* 167 | * The network simplex algorithm assigns ranks to each node in the input graph 168 | * and iteratively improves the ranking to reduce the length of edges. 169 | * 170 | * Preconditions: 171 | * 172 | * 1. The input graph must be a DAG. 173 | * 2. All nodes in the graph must have an object value. 174 | * 3. All edges in the graph must have "minlen" and "weight" attributes. 175 | * 176 | * Postconditions: 177 | * 178 | * 1. All nodes in the graph will have an assigned "rank" attribute that has 179 | * been optimized by the network simplex algorithm. Ranks start at 0. 180 | * 181 | * 182 | * A rough sketch of the algorithm is as follows: 183 | * 184 | * 1. Assign initial ranks to each node. We use the longest path algorithm, 185 | * which assigns ranks to the lowest position possible. In general this 186 | * leads to very wide bottom ranks and unnecessarily long edges. 187 | * 2. Construct a feasible tight tree. A tight tree is one such that all 188 | * edges in the tree have no slack (difference between length of edge 189 | * and minlen for the edge). This by itself greatly improves the assigned 190 | * rankings by shorting edges. 191 | * 3. Iteratively find edges that have negative cut values. Generally a 192 | * negative cut value indicates that the edge could be removed and a new 193 | * tree edge could be added to produce a more compact graph. 194 | * 195 | * Much of the algorithms here are derived from Gansner, et al., "A Technique 196 | * for Drawing Directed Graphs." The structure of the file roughly follows the 197 | * structure of the overall algorithm. 198 | */ 199 | const networkSimplex = g => { 200 | g = simplify(g); 201 | initRank(g); 202 | const t = feasibleTree(g); 203 | initLowLimValues(t); 204 | initCutValues(t, g); 205 | 206 | let e, f; 207 | while ((e = leaveEdge(t))) { 208 | f = enterEdge(t, g, e); 209 | exchangeEdges(t, g, e, f); 210 | } 211 | }; 212 | 213 | // Expose some internals for testing purposes 214 | networkSimplex.initLowLimValues = initLowLimValues; 215 | networkSimplex.initCutValues = initCutValues; 216 | networkSimplex.calcCutValue = calcCutValue; 217 | networkSimplex.leaveEdge = leaveEdge; 218 | networkSimplex.enterEdge = enterEdge; 219 | networkSimplex.exchangeEdges = exchangeEdges; 220 | 221 | 222 | export default networkSimplex 223 | -------------------------------------------------------------------------------- /interops/es-browser/index.css: -------------------------------------------------------------------------------- 1 | .dragonfly-viewport { 2 | overflow: hidden; 3 | width: 100%; 4 | height: 100%; 5 | position: relative; 6 | } 7 | .dragonfly-viewport .dragonfly-tools { 8 | background-color: #fff; 9 | box-shadow: rgba(0, 0, 0, 0.5) 0 0 20px 0; 10 | display: flex; 11 | flex-direction: row; 12 | position: absolute; 13 | right: 0; 14 | top: 0; 15 | z-index: 3; 16 | } 17 | .dragonfly-viewport .dragonfly-tools button { 18 | border: none; 19 | padding: 0 1em; 20 | margin: 0; 21 | background-color: #fff; 22 | outline: none; 23 | } 24 | .dragonfly-viewport .dragonfly-tools button:hover { 25 | background-color: #eeeeee; 26 | } 27 | .dragonfly-viewport .dragonfly-tools button.active { 28 | background-color: #9cdfff; 29 | } 30 | .dragonfly-viewport .dragonfly-edges-layer { 31 | position: absolute; 32 | top: 0; 33 | left: 0; 34 | width: 1px; 35 | height: 1px; 36 | z-index: 2; 37 | overflow: visible !important; 38 | color: #777; 39 | } 40 | .dragonfly-viewport .dragonfly-edges-layer .arrow { 41 | stroke: none; 42 | stroke-width: 0; 43 | fill: currentColor; 44 | z-index: 2; 45 | } 46 | .dragonfly-viewport .dragonfly-edges-layer .anchor { 47 | stroke: currentColor; 48 | stroke-width: 1; 49 | fill: #fff; 50 | z-index: 4; 51 | } 52 | .dragonfly-viewport .dragonfly-edges-layer .edge-arrow { 53 | z-index: 3; 54 | } 55 | .dragonfly-viewport .dragonfly-edges-layer path.edge { 56 | z-index: 5; 57 | stroke-width: 0.5; 58 | stroke: currentColor; 59 | fill: none; 60 | stroke-linecap: round; 61 | stroke-linejoin: round; 62 | pointer-events: none; 63 | cursor: pointer; 64 | } 65 | .dragonfly-viewport .dragonfly-edges-layer path.edge.linking { 66 | pointer-events: none; 67 | stroke-dasharray: 5 5; 68 | stroke-dashoffset: 0; 69 | } 70 | .dragonfly-viewport .dragonfly-edges-layer path.edge-area { 71 | stroke-width: 10; 72 | stroke: transparent; 73 | fill: none; 74 | stroke-linecap: butt; 75 | stroke-linejoin: round; 76 | stroke-opacity: 0.2; 77 | cursor: pointer; 78 | z-index: 1; 79 | } 80 | .dragonfly-viewport .dragonfly-edges-layer path.edge-area:hover { 81 | stroke: #00aaff; 82 | } 83 | .dragonfly-viewport .dragonfly-canvas { 84 | position: relative; 85 | overflow: visible; 86 | width: 100%; 87 | height: 100%; 88 | transform-origin: 0 0 0; 89 | z-index: 1; 90 | } 91 | .dragonfly-viewport .dragonfly-canvas:focus { 92 | outline: none; 93 | } 94 | .dragonfly-viewport .dragonfly-canvas .dragonfly-node { 95 | position: absolute; 96 | z-index: 2; 97 | border: solid 1px transparent; 98 | margin: -1px; 99 | box-sizing: border-box; 100 | width: fit-content; 101 | height: fit-content; 102 | } 103 | .dragonfly-viewport .dragonfly-canvas .dragonfly-node.selected { 104 | border: dashed 1px #777; 105 | z-index: 3; 106 | } 107 | .dragonfly-viewport .dragonfly-canvas .dragonfly-node:not(.selected).targeted { 108 | border: solid 1px #f00; 109 | } 110 | .dragonfly-viewport .dragonfly-canvas .dragonfly-node .dragonfly-node-inner { 111 | display: flex; 112 | align-items: center; 113 | justify-content: center; 114 | user-select: none; 115 | z-index: 1; 116 | } 117 | .dragonfly-viewport .dragonfly-canvas .dragonfly-node .dragonfly-endpoints { 118 | position: absolute; 119 | overflow: visible; 120 | display: flex; 121 | justify-content: space-evenly; 122 | align-items: center; 123 | z-index: 2; 124 | } 125 | .dragonfly-viewport .dragonfly-canvas .dragonfly-node .dragonfly-endpoints.left { 126 | left: 0; 127 | top: 0; 128 | height: 100%; 129 | width: 0; 130 | flex-direction: column; 131 | } 132 | .dragonfly-viewport .dragonfly-canvas .dragonfly-node .dragonfly-endpoints.right { 133 | right: 0; 134 | top: 0; 135 | height: 100%; 136 | width: 0; 137 | flex-direction: column; 138 | } 139 | .dragonfly-viewport .dragonfly-canvas .dragonfly-node .dragonfly-endpoints.top { 140 | top: 0; 141 | left: 0; 142 | width: 100%; 143 | height: 0; 144 | flex-direction: row; 145 | } 146 | .dragonfly-viewport .dragonfly-canvas .dragonfly-node .dragonfly-endpoints.bottom { 147 | bottom: 0; 148 | left: 0; 149 | width: 100%; 150 | height: 0; 151 | flex-direction: row; 152 | } 153 | .dragonfly-viewport .dragonfly-canvas .dragonfly-node .dragonfly-endpoints .dragonfly-endpoint { 154 | display: flex; 155 | align-items: center; 156 | justify-content: center; 157 | width: 10px; 158 | height: 10px; 159 | border-radius: 10px; 160 | border: solid 2px #777; 161 | background-color: #fff; 162 | position: relative; 163 | } 164 | .dragonfly-viewport .dragonfly-canvas .dragonfly-node .dragonfly-endpoints .dragonfly-endpoint.targeted { 165 | border: #f00 solid 2px; 166 | background-color: #fff; 167 | } 168 | .dragonfly-viewport .dragonfly-canvas .dragonfly-node .dragonfly-endpoints .dragonfly-endpoint > .label { 169 | position: absolute; 170 | width: max-content; 171 | font-size: 12px; 172 | transform: scale(0.8) translateZ(1); 173 | user-select: none; 174 | } 175 | .dragonfly-viewport .dragonfly-canvas .edge-label { 176 | position: absolute; 177 | z-index: 6; 178 | width: 0; 179 | height: 0; 180 | display: flex; 181 | align-items: center; 182 | justify-content: center; 183 | overflow: visible; 184 | } 185 | .dragonfly-viewport .dragonfly-canvas .dragonfly-zone { 186 | background-color: #eeeeee; 187 | position: absolute; 188 | display: flex; 189 | align-items: center; 190 | justify-content: center; 191 | z-index: 1; 192 | } 193 | .dragonfly-viewport .dragonfly-canvas .dragonfly-zone .dragonfly-zone-inner { 194 | width: 100%; 195 | height: 100%; 196 | z-index: 1; 197 | } 198 | .dragonfly-viewport .dragonfly-canvas .dragonfly-zone.selected { 199 | border: solid 0.5px #777; 200 | } 201 | .dragonfly-viewport .dragonfly-canvas .dragonfly-zone.selected .anchor { 202 | width: 6px; 203 | height: 6px; 204 | background-color: #fff; 205 | border: solid 1px #000; 206 | position: absolute; 207 | transform-origin: center; 208 | z-index: 2; 209 | } 210 | .dragonfly-viewport .dragonfly-canvas .dragonfly-zone.selected .anchor.e { 211 | transform: translateX(50%); 212 | right: 0; 213 | cursor: col-resize; 214 | } 215 | .dragonfly-viewport .dragonfly-canvas .dragonfly-zone.selected .anchor.w { 216 | transform: translateX(-50%); 217 | left: 0; 218 | cursor: col-resize; 219 | } 220 | .dragonfly-viewport .dragonfly-canvas .dragonfly-zone.selected .anchor.n { 221 | transform: translateY(-50%); 222 | top: 0; 223 | cursor: row-resize; 224 | } 225 | .dragonfly-viewport .dragonfly-canvas .dragonfly-zone.selected .anchor.s { 226 | transform: translateY(50%); 227 | bottom: 0; 228 | cursor: row-resize; 229 | } 230 | .dragonfly-viewport .dragonfly-canvas .dragonfly-zone.selected .anchor.ne { 231 | transform: translateY(-50%) translateX(50%); 232 | top: 0; 233 | right: 0; 234 | cursor: nesw-resize; 235 | } 236 | .dragonfly-viewport .dragonfly-canvas .dragonfly-zone.selected .anchor.se { 237 | transform: translateY(50%) translateX(50%); 238 | bottom: 0; 239 | right: 0; 240 | cursor: nwse-resize; 241 | } 242 | .dragonfly-viewport .dragonfly-canvas .dragonfly-zone.selected .anchor.nw { 243 | transform: translateY(-50%) translateX(-50%); 244 | top: 0; 245 | left: 0; 246 | cursor: nwse-resize; 247 | } 248 | .dragonfly-viewport .dragonfly-canvas .dragonfly-zone.selected .anchor.sw { 249 | transform: translateY(50%) translateX(-50%); 250 | bottom: 0; 251 | left: 0; 252 | cursor: nesw-resize; 253 | } 254 | .dragonfly-viewport .dragonfly-canvas > .area { 255 | position: absolute; 256 | z-index: 7; 257 | } 258 | .dragonfly-viewport .dragonfly-canvas > .area.select { 259 | border: none; 260 | background-color: rgba(128, 212, 255, 0.3); 261 | } 262 | .dragonfly-viewport .dragonfly-canvas > .area.zoom { 263 | background-color: transparent; 264 | border: dashed 1px #777777; 265 | } 266 | .dragonfly-viewport .dragonfly-grid { 267 | z-index: 0; 268 | position: absolute; 269 | top: 0; 270 | left: 0; 271 | width: 100%; 272 | height: 100%; 273 | } 274 | .dragonfly-viewport .dragonfly-grid svg { 275 | width: 100%; 276 | height: 100%; 277 | } 278 | .dragonfly-viewport .dragonfly-grid svg path { 279 | stroke: none; 280 | fill: #000; 281 | } 282 | .dragonfly-viewport .dragonfly-scale { 283 | position: absolute; 284 | bottom: 0; 285 | left: 0; 286 | color: #333; 287 | font-size: 12px; 288 | line-height: 1em; 289 | transform: scale(0.75); 290 | transform-origin: left; 291 | } 292 | .dragonfly-viewport .dragonfly-minimap { 293 | position: absolute; 294 | background-color: #fff; 295 | border: solid 1px #eee; 296 | border-right: none; 297 | border-bottom: none; 298 | right: 0; 299 | bottom: 0; 300 | z-index: 7; 301 | padding: 5px; 302 | width: 30%; 303 | height: 30%; 304 | display: flex; 305 | opacity: 0.6; 306 | } 307 | .dragonfly-viewport .dragonfly-minimap.minimized { 308 | width: 25px; 309 | height: 25px; 310 | overflow: hidden; 311 | } 312 | .dragonfly-viewport .dragonfly-minimap .switch { 313 | display: flex; 314 | width: 24px; 315 | height: 24px; 316 | justify-content: center; 317 | align-items: center; 318 | background-color: #fff; 319 | position: absolute; 320 | right: 0; 321 | bottom: 0; 322 | border-width: 1px; 323 | border-style: solid none none solid; 324 | border-color: #ccc; 325 | cursor: pointer; 326 | } 327 | .dragonfly-viewport .dragonfly-minimap .switch svg { 328 | pointer-events: none; 329 | } 330 | .dragonfly-viewport .dragonfly-minimap .dragonfly-minimap-inner { 331 | flex: 1; 332 | border: solid 1px #eee; 333 | display: flex; 334 | justify-content: center; 335 | align-items: center; 336 | } 337 | .dragonfly-viewport .dragonfly-minimap .dragonfly-minimap-inner .map { 338 | position: relative; 339 | background-color: #eee; 340 | } 341 | .dragonfly-viewport .dragonfly-minimap .dragonfly-minimap-inner .map .canvas { 342 | position: absolute; 343 | border: dashed #00aaff 1px; 344 | z-index: 1; 345 | pointer-events: none; 346 | overflow: visible; 347 | } 348 | .dragonfly-viewport .dragonfly-minimap .dragonfly-minimap-inner .map .thumbnail { 349 | position: absolute; 350 | width: 100%; 351 | height: 100%; 352 | top: 0; 353 | left: 0; 354 | } 355 | .dragonfly-viewport .dragonfly-minimap .dragonfly-minimap-inner .map .thumbnail svg { 356 | overflow: visible; 357 | width: 100%; 358 | height: 100%; 359 | } 360 | .dragonfly-viewport .dragonfly-minimap .dragonfly-minimap-inner .map .viewport { 361 | position: absolute; 362 | background-color: #000; 363 | opacity: 0.3; 364 | z-index: 2; 365 | cursor: move; 366 | } 367 | -------------------------------------------------------------------------------- /src/components/DragonflyMinimap.vue: -------------------------------------------------------------------------------- 1 | 80 | 81 | 261 | -------------------------------------------------------------------------------- /src/components/dragonfly-dag.less: -------------------------------------------------------------------------------- 1 | // variables 2 | @select-area-border: none; 3 | @select-area-color: rgba(128, 212, 255, 0.3); 4 | @zoom-area-border: dashed 1px #777777; 5 | @zoom-area-color: transparent; 6 | 7 | @edge-default-color: #777; 8 | @edge-default-width: 0.5; 9 | 10 | @linking-stroke-dasharray: 5 5; 11 | @linking-stroke-dashoffset: 0; 12 | 13 | @node-selected-border: dashed 1px #777; 14 | @node-targeted-border: solid 1px #f00; 15 | 16 | @endpoint-size: 10px; 17 | @endpoint-border: solid 2px #777; 18 | @endpoint-color: #fff; 19 | 20 | // z-indices 21 | @tools-z-index: 3; 22 | @edges-layer-z-index: 2; 23 | @canvas-z-index: 1; 24 | 25 | @zone-z-index: 1; 26 | @node-z-index: 2; 27 | @node-selected-z-index: 3; 28 | 29 | @node-endpoints-z-index: 2; 30 | @node-inner-z-index: 1; 31 | 32 | @endpoint-targeted-border-color: #f00; 33 | @endpoint-targeted-color: #fff; 34 | @endpoint-targeted-border-style: solid; 35 | @endpoint-targeted-border-width: 2px; 36 | @endpoint-targeted-border: @endpoint-targeted-border-color @endpoint-targeted-border-style @endpoint-targeted-border-width; 37 | 38 | 39 | .dragonfly-viewport { 40 | overflow: hidden; 41 | width: 100%; 42 | height: 100%; 43 | position: relative; 44 | 45 | .dragonfly-tools { 46 | background-color: #fff; 47 | box-shadow: rgba(0, 0, 0, 0.5) 0 0 20px 0; 48 | display: flex; 49 | flex-direction: row; 50 | position: absolute; 51 | right: 0; 52 | top: 0; 53 | z-index: @tools-z-index; 54 | 55 | button { 56 | border: none; 57 | padding: 0 1em; 58 | margin: 0; 59 | background-color: #fff; 60 | outline: none; 61 | 62 | &:hover { 63 | background-color: #eeeeee; 64 | } 65 | 66 | &.active { 67 | background-color: #9cdfff; 68 | } 69 | } 70 | } 71 | 72 | .dragonfly-edges-layer { 73 | position: absolute; 74 | top: 0; 75 | left: 0; 76 | z-index: @edges-layer-z-index; 77 | overflow: visible !important; 78 | color: @edge-default-color; 79 | pointer-events: none; 80 | 81 | .arrow { 82 | stroke: none; 83 | stroke-width: 0; 84 | fill: currentColor; 85 | z-index: 2; 86 | } 87 | 88 | .anchor { 89 | stroke: currentColor; 90 | stroke-width: 1; 91 | fill: #fff; 92 | z-index: 4; 93 | } 94 | 95 | .edge-arrow { 96 | z-index: 3; 97 | } 98 | 99 | path.edge { 100 | z-index: 5; 101 | stroke-width: @edge-default-width; 102 | stroke: currentColor; 103 | fill: none; 104 | stroke-linecap: round; 105 | stroke-linejoin: round; 106 | pointer-events: none; 107 | cursor: pointer; 108 | 109 | &.linking { 110 | pointer-events: none; 111 | stroke-dasharray: @linking-stroke-dasharray; 112 | stroke-dashoffset: @linking-stroke-dashoffset; 113 | } 114 | } 115 | 116 | path.edge-area { 117 | stroke-width: 10; 118 | stroke: transparent; 119 | fill: none; 120 | stroke-linecap: butt; 121 | stroke-linejoin: round; 122 | stroke-opacity: 0.2; 123 | cursor: pointer; 124 | z-index: 1; 125 | pointer-events: all; 126 | 127 | &:hover { 128 | stroke: #00aaff; 129 | } 130 | } 131 | 132 | 133 | } 134 | 135 | .dragonfly-canvas { 136 | position: relative; 137 | overflow: visible; 138 | width: 100%; 139 | height: 100%; 140 | transform-origin: 0 0 0; // 左上角缩放 141 | z-index: @canvas-z-index; 142 | 143 | &:focus { 144 | outline: none; 145 | } 146 | 147 | .dragonfly-node { 148 | position: absolute; 149 | z-index: @node-z-index; 150 | border: solid 1px transparent; 151 | margin: -1px; 152 | box-sizing: border-box; 153 | width: fit-content; 154 | height: fit-content; 155 | 156 | &:hover { 157 | z-index: @node-selected-z-index; 158 | } 159 | 160 | &.selected { 161 | border: @node-selected-border; 162 | z-index: @node-selected-z-index; 163 | } 164 | 165 | &:not(.selected).targeted { 166 | border: @node-targeted-border; 167 | } 168 | 169 | .dragonfly-node-inner { 170 | display: flex; 171 | align-items: center; 172 | justify-content: center; 173 | user-select: none; 174 | z-index: @node-inner-z-index; 175 | } 176 | 177 | .dragonfly-endpoints { 178 | position: absolute; 179 | overflow: visible; 180 | display: flex; 181 | justify-content: space-evenly; 182 | align-items: center; 183 | z-index: @node-endpoints-z-index; 184 | 185 | &.left { 186 | left: 0; 187 | top: 0; 188 | height: 100%; 189 | width: 0; 190 | flex-direction: column; 191 | 192 | .label { 193 | } 194 | } 195 | 196 | &.right { 197 | right: 0; 198 | top: 0; 199 | height: 100%; 200 | width: 0; 201 | flex-direction: column; 202 | } 203 | 204 | &.top { 205 | top: 0; 206 | left: 0; 207 | width: 100%; 208 | height: 0; 209 | flex-direction: row; 210 | } 211 | 212 | &.bottom { 213 | bottom: 0; 214 | left: 0; 215 | width: 100%; 216 | height: 0; 217 | flex-direction: row; 218 | } 219 | 220 | .dragonfly-endpoint { 221 | display: flex; 222 | align-items: center; 223 | justify-content: center; 224 | width: @endpoint-size; 225 | height: @endpoint-size; 226 | border-radius: @endpoint-size; 227 | border: @endpoint-border; 228 | background-color: @endpoint-color; 229 | position: relative; 230 | 231 | &.targeted { 232 | border: @endpoint-targeted-border; 233 | background-color: @endpoint-targeted-color; 234 | } 235 | 236 | & > .label { 237 | position: absolute; 238 | width: max-content; 239 | font-size: 12px; 240 | transform: scale(0.8) translateZ(1); 241 | user-select: none; 242 | } 243 | } 244 | } 245 | 246 | } 247 | 248 | .edge-label { 249 | position: absolute; 250 | z-index: 6; 251 | width: 0; 252 | height: 0; 253 | display: flex; 254 | align-items: center; 255 | justify-content: center; 256 | overflow: visible; 257 | } 258 | 259 | .dragonfly-zone { 260 | background-color: #eeeeee; 261 | position: absolute; 262 | display: flex; 263 | align-items: center; 264 | justify-content: center; 265 | z-index: @zone-z-index; 266 | 267 | .dragonfly-zone-inner { 268 | width: 100%; 269 | height: 100%; 270 | z-index: 1; 271 | } 272 | 273 | &.selected { 274 | border: solid 0.5px #777; 275 | 276 | .anchor { 277 | width: 6px; 278 | height: 6px; 279 | background-color: #fff; 280 | border: solid 1px #000; 281 | position: absolute; 282 | transform-origin: center; 283 | z-index: 2; 284 | 285 | &.e { 286 | transform: translateX(50%); 287 | right: 0; 288 | cursor: col-resize; 289 | } 290 | 291 | &.w { 292 | transform: translateX(-50%); 293 | left: 0; 294 | cursor: col-resize; 295 | } 296 | 297 | &.n { 298 | transform: translateY(-50%); 299 | top: 0; 300 | cursor: row-resize; 301 | } 302 | 303 | &.s { 304 | transform: translateY(50%); 305 | bottom: 0; 306 | cursor: row-resize; 307 | } 308 | 309 | &.ne { 310 | transform: translateY(-50%) translateX(50%); 311 | top: 0; 312 | right: 0; 313 | cursor: nesw-resize; 314 | } 315 | 316 | &.se { 317 | transform: translateY(50%) translateX(50%); 318 | bottom: 0; 319 | right: 0; 320 | cursor: nwse-resize; 321 | } 322 | 323 | &.nw { 324 | transform: translateY(-50%) translateX(-50%); 325 | top: 0; 326 | left: 0; 327 | cursor: nwse-resize; 328 | } 329 | 330 | &.sw { 331 | transform: translateY(50%) translateX(-50%); 332 | bottom: 0; 333 | left: 0; 334 | cursor: nesw-resize; 335 | } 336 | } 337 | } 338 | } 339 | 340 | & > .area { 341 | position: absolute; 342 | z-index: 7; 343 | 344 | &.select { 345 | border: @select-area-border; 346 | background-color: @select-area-color; 347 | } 348 | 349 | &.zoom { 350 | background-color: @zoom-area-color; 351 | border: @zoom-area-border; 352 | } 353 | } 354 | } 355 | 356 | .dragonfly-grid { 357 | z-index: 0; 358 | position: absolute; 359 | top: 0; 360 | left: 0; 361 | width: 100%; 362 | height: 100%; 363 | 364 | svg { 365 | width: 100%; 366 | height: 100%; 367 | 368 | path { 369 | stroke: none; 370 | fill: #000; 371 | } 372 | } 373 | } 374 | 375 | .dragonfly-scale { 376 | position: absolute; 377 | bottom: 0; 378 | left: 0; 379 | color: #333; 380 | font-size: 12px; 381 | line-height: 1em; 382 | transform: scale(0.75); 383 | transform-origin: left; 384 | } 385 | 386 | .dragonfly-minimap { 387 | position: absolute; 388 | border: solid 1px #eee; 389 | border-right: none; 390 | border-bottom: none; 391 | right: 0; 392 | bottom: 0; 393 | z-index: 7; 394 | padding: 5px; 395 | width: 320px; 396 | height: 240px; 397 | display: flex; 398 | box-shadow: 0 0 20px 0 #00000010; 399 | border-radius: 12px 0 0 0; 400 | 401 | &:before { 402 | content: ''; 403 | position: absolute; 404 | top: 0; 405 | bottom: 0; 406 | left: 0; 407 | right: 0; 408 | backdrop-filter: blur(16px); 409 | background-color: rgba(255, 255, 255, 0.3); 410 | border-radius: 12px 0 0 0; 411 | } 412 | 413 | & > * { 414 | opacity: 0.3; 415 | } 416 | 417 | &.minimized { 418 | width: 25px; 419 | height: 25px; 420 | overflow: hidden; 421 | } 422 | 423 | .switch { 424 | display: flex; 425 | width: 24px; 426 | height: 24px; 427 | justify-content: center; 428 | align-items: center; 429 | background-color: #fff; 430 | position: absolute; 431 | right: 0; 432 | bottom: 0; 433 | border-width: 1px; 434 | border-style: solid none none solid; 435 | border-color: #ccc; 436 | cursor: pointer; 437 | box-shadow: 0 0 4px 0 #000; 438 | 439 | svg { 440 | pointer-events: none; 441 | } 442 | } 443 | 444 | .dragonfly-minimap-inner { 445 | flex: 1; 446 | border: solid 1px #eee; 447 | display: flex; 448 | justify-content: center; 449 | align-items: center; 450 | 451 | .map { 452 | position: relative; 453 | background-color: rgba(0, 188, 212, 0.2); 454 | 455 | .canvas { 456 | position: absolute; 457 | border: dashed #00aaff 1px; 458 | z-index: 1; 459 | pointer-events: none; 460 | overflow: visible; 461 | } 462 | 463 | .thumbnail { 464 | position: absolute; 465 | width: 100%; 466 | height: 100%; 467 | top: 0; 468 | left: 0; 469 | 470 | svg { 471 | overflow: visible; 472 | width: 100%; 473 | height: 100%; 474 | } 475 | } 476 | 477 | .viewport { 478 | position: absolute; 479 | background-color: #000; 480 | opacity: 0.1; 481 | z-index: 2; 482 | cursor: move; 483 | } 484 | } 485 | } 486 | } 487 | } 488 | --------------------------------------------------------------------------------