├── 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 |
2 | 1 : {{ parseFloat(scale.toPrecision(3)) }}
3 |
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 |
2 |
3 |
4 |
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 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
16 |
--------------------------------------------------------------------------------
/src/components/grid/DotGrid.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
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 |
2 |
3 |
12 |
13 |
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 |
2 |
3 |
Nodes
4 |
{{ node }}
5 |
Edges
6 |
{{ edge }}
7 |
Zones
8 |
{{ zone }}
9 |
Positions
10 |
{{ positions }}
11 |
Selection
12 |
{{ selection }}
13 |
14 |
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 |
2 |
3 |
4 |
9 |
10 |
11 |
12 |
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 |
2 |
3 |
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 |
2 |
3 |
4 |
5 |
10 |
12 |
13 |
14 |
15 |
16 |
17 |
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 |
2 |
10 |
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 |
2 |
3 | 锁定
5 |
6 |
8 | 框选
9 |
10 |
12 | 滚动
13 |
14 | 缩放
16 |
17 | |
18 | 锁定
20 |
21 | 移动
23 |
24 | 连接
26 |
27 | |
28 | 锁定
30 |
31 | 缩放
33 |
34 | 滚动
36 |
37 |
38 |
39 |
40 |
80 |
--------------------------------------------------------------------------------
/src/components/DragonflyCanvasEdgesLayer.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
16 |
17 |
18 |
28 |
29 |
30 |
31 |
32 |
35 |
44 |
45 |
46 |
47 |
52 |
53 |
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 |
2 |
3 |
缩放比例
4 |
6 |
7 |
缩放范围
8 | {$emit('update:minZoomScale', value[0]);$emit('update:maxZoomScale', value[1])}"/>
14 |
15 |
16 | 显示连线标签
17 |
21 |
22 |
23 | 显示箭头
24 |
28 |
29 |
30 | 箭头比例
31 |
33 |
34 |
35 | 箭头位置
36 |
38 |
39 |
画布滚屏行为
40 |
42 |
43 | 锁定
44 |
45 |
46 | 缩放
47 |
48 |
49 | 滚动
50 |
51 |
52 |
53 |
画布拖拽行为
54 |
56 |
57 | 锁定
58 |
59 |
60 | 框选
61 |
62 |
63 | 滚动
64 |
65 |
66 | 缩放
67 |
68 |
69 |
70 |
节点拖拽行为
71 |
73 |
74 | 锁定
75 |
76 |
77 | 移动
78 |
79 |
80 | 连接
81 |
82 |
83 |
84 |
85 |
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 |
2 |
6 |
14 |
15 |
16 |
17 |
27 |
28 |
29 |
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 |
2 |
7 |
15 |
16 |
21 |
22 |
30 |
31 |
32 |
35 |
36 |
37 |
42 |
43 |
44 |
45 |
46 |
47 |
51 | {{ edge.label }}
52 |
53 |
54 |
55 |
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 |
155 |
161 |
172 |
173 |
174 |
178 |
179 |
180 |
184 |
185 |
186 |
190 |
191 |
192 |
196 |
197 |
198 |
199 |
200 |
--------------------------------------------------------------------------------
/src/App.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | 添加节点
5 | 添加区域
6 | 自动布局
7 |
8 |
9 |
10 |
11 |
12 |
45 |
46 | Hi, {{ node.id }}
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 | {{ edge.label }}
57 |
58 |
59 |
60 |
61 |
73 |
74 |
75 |
76 |
165 |
166 |
169 |
223 |
--------------------------------------------------------------------------------
/src/components/DragonflyEndpoint.vue:
--------------------------------------------------------------------------------
1 |
2 |
14 |
17 | {{ endpoint.label ?? label }}
18 |
19 |
20 |
21 |
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 |
2 |
3 |
4 |
9 |
10 |
16 |
27 |
36 |
51 |
52 |
53 |
54 |
55 |
78 |
79 |
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 |
--------------------------------------------------------------------------------