├── .editorconfig ├── .eslintignore ├── .eslintrc.json ├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── dist ├── css │ ├── graph-editor-dark.css │ ├── graph-editor-dark.min.css │ ├── graph-editor-dark.scss │ ├── graph-editor.css │ ├── graph-editor.min.css │ └── graph-editor.scss └── js │ ├── graph-editor.js │ └── graph-editor.min.js ├── docs ├── events.jsdoc.html ├── fonts │ ├── OpenSans-Bold-webfont.eot │ ├── OpenSans-Bold-webfont.svg │ ├── OpenSans-Bold-webfont.woff │ ├── OpenSans-BoldItalic-webfont.eot │ ├── OpenSans-BoldItalic-webfont.svg │ ├── OpenSans-BoldItalic-webfont.woff │ ├── OpenSans-Italic-webfont.eot │ ├── OpenSans-Italic-webfont.svg │ ├── OpenSans-Italic-webfont.woff │ ├── OpenSans-Light-webfont.eot │ ├── OpenSans-Light-webfont.svg │ ├── OpenSans-Light-webfont.woff │ ├── OpenSans-LightItalic-webfont.eot │ ├── OpenSans-LightItalic-webfont.svg │ ├── OpenSans-LightItalic-webfont.woff │ ├── OpenSans-Regular-webfont.eot │ ├── OpenSans-Regular-webfont.svg │ └── OpenSans-Regular-webfont.woff ├── ge.Angle.html ├── ge.BBox.html ├── ge.ContainerSize.html ├── ge.GraphEditor.html ├── ge.Link.html ├── ge.Node.html ├── ge.Point.html ├── ge.SaveLoad.html ├── ge.TextSize.html ├── ge.html ├── ge.path.Line.html ├── ge.path.Path.html ├── ge.path.html ├── ge.shape.Circle.html ├── ge.shape.Rect.html ├── ge.shape.Shape.html ├── ge.shape.html ├── global.html ├── index.html ├── js_controls.js.html ├── js_events.js.html ├── js_init.js.html ├── js_link.js.html ├── js_node.js.html ├── js_options.js.html ├── js_path_base.js.html ├── js_path_line.js.html ├── js_shape_base.js.html ├── js_shape_circle.js.html ├── js_shape_rect.js.html ├── js_update.js.html ├── js_util_angle.js.html ├── js_util_bbox.js.html ├── js_util_common.js.html ├── js_util_container-size.js.html ├── js_util_point.js.html ├── js_util_save-load.js.html ├── js_util_text-size.js.html ├── main.js.html ├── scripts │ ├── linenumber.js │ └── prettify │ │ ├── Apache-License-2.0.txt │ │ ├── lang-css.js │ │ └── prettify.js ├── styles │ ├── jsdoc-default.css │ ├── prettify-jsdoc.css │ └── prettify-tomorrow.css └── types.jsdoc.html ├── examples ├── index.css └── index.html ├── jsdoc.conf.js ├── karma.conf.js ├── package.json ├── src ├── css │ ├── graph-editor-dark.scss │ └── graph-editor.scss ├── events.jsdoc ├── js │ ├── controls.js │ ├── events.js │ ├── init.js │ ├── link.js │ ├── node.js │ ├── options.js │ ├── path │ │ ├── base.js │ │ └── line.js │ ├── shape │ │ ├── base.js │ │ ├── circle.js │ │ └── rect.js │ ├── update.js │ └── util │ │ ├── angle.js │ │ ├── bbox.js │ │ ├── common.js │ │ ├── container-size.js │ │ ├── point.js │ │ ├── save-load.js │ │ └── text-size.js ├── main.js ├── types.jsdoc └── umd │ ├── umd-end.js │ └── umd-start.js └── tests ├── .eslintrc.json ├── common.test.js ├── module.test.js └── test-start.js /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | insert_final_newline = true 6 | charset = utf-8 7 | indent_style = tab 8 | indent_size = 4 9 | 10 | [*.html] 11 | indent_size = 2 12 | 13 | [package.json] 14 | indent_style = space 15 | indent_size = 2 16 | 17 | [*.md] 18 | indent_style = space 19 | indent_size = 2 20 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | src/umd/* 2 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "env": { 4 | "browser": true, 5 | "es6": true, 6 | "node": true, 7 | "jasmine": true 8 | }, 9 | "extends": "eslint:recommended", 10 | "plugins": [ 11 | "jasmine" 12 | ], 13 | "parserOptions": { 14 | "ecmaVersion": 6, 15 | "ecmaFeatures": { 16 | "jsx": true 17 | }, 18 | "sourceType": "module" 19 | }, 20 | "rules": { 21 | "indent": [ 22 | "warn", 23 | "tab", 24 | { 25 | "SwitchCase": 1 26 | } 27 | ], 28 | "linebreak-style": [ 29 | "error", 30 | "unix" 31 | ], 32 | "quotes": [ 33 | "error", 34 | "single" 35 | ], 36 | "semi": [ 37 | "error", 38 | "always" 39 | ], 40 | "no-unused-vars": "warn", 41 | "no-console": "off", 42 | "no-mixed-spaces-and-tabs": "off", 43 | "no-constant-condition": "off" 44 | }, 45 | "globals": { 46 | "$": true, 47 | "d3": true, 48 | "ge": true, 49 | "exports": true 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /build 3 | /tmp 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 dead-beef 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | 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, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | define rwildcard 2 | $(sort $(wildcard $1$2)) \ 3 | $(foreach d,$(sort $(wildcard $1*)),$(call rwildcard,$d/,$2)) 4 | endef 5 | 6 | BUILD_DIR := build 7 | DIST_DIR := dist 8 | DOC_DIR := docs 9 | DIRS := $(BUILD_DIR) $(DIST_DIR)/js $(DIST_DIR)/css 10 | 11 | NAME := graph-editor 12 | 13 | JS_FILES := src/umd/umd-start.js \ 14 | src/main.js \ 15 | $(sort $(wildcard src/js/util/*.js)) \ 16 | $(sort $(wildcard src/js/path/*.js)) \ 17 | $(sort $(wildcard src/js/shape/*.js)) \ 18 | $(sort $(filter-out %/common.js,$(wildcard src/js/*.js))) \ 19 | src/umd/umd-end.js 20 | 21 | CSS_MAIN := src/css/$(NAME).scss 22 | CSS_DARK_MAIN := src/css/$(NAME)-dark.scss 23 | CSS_DEPS := $(BUILD_DIR)/deps.mk 24 | 25 | DIST_FILES := $(DIST_DIR)/js/$(NAME).js \ 26 | $(DIST_DIR)/js/$(NAME).min.js \ 27 | $(DIST_DIR)/css/$(NAME).scss \ 28 | $(DIST_DIR)/css/$(NAME).css \ 29 | $(DIST_DIR)/css/$(NAME).min.css \ 30 | $(DIST_DIR)/css/$(NAME)-dark.scss \ 31 | $(DIST_DIR)/css/$(NAME)-dark.css \ 32 | $(DIST_DIR)/css/$(NAME)-dark.min.css 33 | 34 | .PHONY: all clean install rebuild lint docs watch test test-watch 35 | 36 | all: $(BUILD_DIR)/install.touch $(DIST_FILES) 37 | 38 | install: 39 | npm install 40 | 41 | watch: 42 | chokidar 'src/**/*' package.json Makefile -i '**/.*' -c 'env -u MAKEFILES make' 43 | 44 | clean: 45 | rm -rf $(BUILD_DIR) $(DIST_DIR) 46 | 47 | rebuild: 48 | make clean 49 | make 50 | 51 | test: all 52 | karma start karma.conf.js --single-run 53 | 54 | test-watch: 55 | karma start karma.conf.js 56 | 57 | lint: 58 | eslint src 59 | 60 | docs: 61 | rm -rf $(DOC_DIR)/* 62 | jsdoc -c jsdoc.conf.js 63 | mv $(DOC_DIR)/$(NAME)/*/* $(DOC_DIR)/ 64 | rm -rf $(DOC_DIR)/$(NAME) 65 | 66 | $(DIST_DIR)/js/$(NAME).js: $(JS_FILES) | $(DIST_DIR)/js 67 | eslint $? 68 | cat $(JS_FILES) >$@ 69 | 70 | $(DIST_DIR)/css/$(NAME).css: $(CSS_MAIN) | $(BUILD_DIR) $(DIST_DIR)/css 71 | node-sass $< >$(BUILD_DIR)/tmp.css 72 | postcss $(BUILD_DIR)/tmp.css --use autoprefixer --no-map -o $@ 73 | 74 | $(DIST_DIR)/css/$(NAME).scss: $(CSS_MAIN) | $(DIST_DIR)/css 75 | cp -f $< $@ 76 | 77 | $(DIST_DIR)/css/$(NAME)-dark.css: $(CSS_DARK_MAIN) | $(BUILD_DIR) $(DIST_DIR)/css 78 | node-sass $< >$(BUILD_DIR)/tmp.css 79 | postcss $(BUILD_DIR)/tmp.css --use autoprefixer --no-map -o $@ 80 | 81 | $(DIST_DIR)/css/$(NAME)-dark.scss: $(CSS_DARK_MAIN) | $(DIST_DIR)/css 82 | cp -f $< $@ 83 | 84 | %.min.js: %.js 85 | uglifyjs $< -c -m -o $@ 86 | 87 | %.min.css: %.css 88 | csso -i $< -o $@ 89 | 90 | $(DIRS): 91 | mkdir -p $@ 92 | 93 | $(CSS_DEPS): $(CSS_MAIN) $(CSS_DARK_MAIN) | $(BUILD_DIR) 94 | sass-makedepend -m -r -p $(DIST_DIR)/css/ $^ >$(BUILD_DIR)/tmp.mk 95 | mv -f $(BUILD_DIR)/tmp.mk $@ 96 | 97 | $(BUILD_DIR)/install.touch: package.json | $(BUILD_DIR) 98 | npm install 99 | touch $@ 100 | 101 | -include $(CSS_DEPS) 102 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # graph-editor - graph editor frontend component 2 | 3 | [![npm](https://img.shields.io/npm/v/graph-editor.svg)]( 4 | https://www.npmjs.com/package/graph-editor 5 | ) [![node](https://img.shields.io/node/v/graph-editor.svg)]( 6 | https://nodejs.org/ 7 | ) [![Libraries.io for GitHub](https://img.shields.io/librariesio/github/dead-beef/graph-editor.svg)]( 8 | https://libraries.io/npm/graph-editor/ 9 | ) [![license](https://img.shields.io/github/license/dead-beef/graph-editor.svg)]( 10 | https://github.com/dead-beef/graph-editor/blob/master/LICENSE 11 | ) 12 | 13 | ## Overview 14 | 15 | It exports a class which should be bound to an element and provided data for [Graph](https://en.wikipedia.org/wiki/Graph_%28abstract_data_type%29) visualisation and editing. 16 | 17 | ## Requirements 18 | 19 | - [`D3 v5`](https://d3js.org/) 20 | 21 | ## Installation 22 | 23 | ``` 24 | npm install graph-editor 25 | ``` 26 | 27 | ## Usage 28 | 29 | - [`Documentation`](https://dead-beef.github.io/graph-editor) 30 | - [`Usage example`](https://codepen.io/deadbeef/pen/wRVMej) 31 | 32 | ## Development 33 | 34 | ### Requirements 35 | 36 | - [`Node.js`](https://nodejs.org/) 37 | - [`NPM`](https://nodejs.org/) 38 | - [`Git`](https://git-scm.com/) 39 | - [`GNU Make`](https://www.gnu.org/software/make/) 40 | 41 | ### Installation 42 | 43 | ```bash 44 | git clone https://github.com/dead-beef/graph-editor.git 45 | cd graph-editor 46 | make install 47 | ``` 48 | 49 | ### Building 50 | 51 | ```bash 52 | # single run 53 | make 54 | # continuous 55 | make watch 56 | ``` 57 | 58 | ### Testing 59 | 60 | ```bash 61 | # unit, single run 62 | make test 63 | # unit, continuous 64 | make test-watch 65 | # test library bundle 66 | TEST_BUNDLE=1 make test 67 | # test minified library bundle 68 | TEST_MIN_BUNDLE=1 make test 69 | # select browsers (default: Chromium) 70 | TEST_BROWSERS="Firefox Chrome" make test 71 | ``` 72 | 73 | ### Code Linting 74 | 75 | ``` 76 | make lint 77 | ``` 78 | 79 | ### Documentation 80 | 81 | ``` 82 | make docs 83 | ``` 84 | 85 | ## Licenses 86 | 87 | * [`graph-editor`](https://github.com/dead-beef/graph-editor/blob/master/LICENSE) 88 | -------------------------------------------------------------------------------- /dist/css/graph-editor-dark.css: -------------------------------------------------------------------------------- 1 | /*$ge-graph-transition: stroke $ge-graph-transition-duration, 2 | fill $ge-graph-transition-duration, 3 | color $ge-graph-transition-duration !default;*/ 4 | .ge-graph * { 5 | /*transition: $ge-graph-transition;*/ 6 | -webkit-transition-property: stroke, fill, color; 7 | transition-property: stroke, fill, color; 8 | -webkit-transition-duration: 0.3s; 9 | transition-duration: 0.3s; } 10 | 11 | .ge-graph marker#ge-dragline-end path, .ge-graph marker#ge-dragline-end circle, .ge-graph marker#ge-dragline-start path, .ge-graph marker#ge-dragline-start circle { 12 | stroke: #a44; 13 | fill: #a44; } 14 | 15 | .ge-graph .ge-hidden { 16 | display: none; } 17 | 18 | .ge-graph .ge-dragline { 19 | pointer-events: none; 20 | stroke-width: 2px; 21 | stroke: #a44; } 22 | 23 | .ge-graph .ge-link { 24 | cursor: pointer; 25 | font-size: 14px; 26 | stroke-width: 2px; 27 | /* * { vector-effect: non-scaling-stroke } */ } 28 | .ge-graph .ge-link path { 29 | stroke: #888; 30 | fill: none; } 31 | .ge-graph .ge-link text { 32 | fill: #888; } 33 | .ge-graph .ge-link:hover path { 34 | stroke: #fff; } 35 | .ge-graph .ge-link:hover text { 36 | fill: #fff; } 37 | 38 | .ge-graph .ge-node { 39 | cursor: pointer; 40 | /* * { vector-effect: non-scaling-stroke } */ } 41 | .ge-graph .ge-node path { 42 | stroke-width: 2px; 43 | stroke: #888; 44 | fill: #111; } 45 | .ge-graph .ge-node div { 46 | display: -webkit-box; 47 | display: -ms-flexbox; 48 | display: flex; 49 | -webkit-box-orient: horizontal; 50 | -webkit-box-direction: normal; 51 | -ms-flex-direction: row; 52 | flex-direction: row; 53 | -webkit-box-align: center; 54 | -ms-flex-align: center; 55 | align-items: center; 56 | width: 100%; 57 | height: 100%; } 58 | .ge-graph .ge-node div span { 59 | display: -webkit-box; 60 | display: -ms-flexbox; 61 | display: flex; 62 | -webkit-box-orient: vertical; 63 | -webkit-box-direction: normal; 64 | -ms-flex-direction: column; 65 | flex-direction: column; 66 | -webkit-box-flex: 1; 67 | -ms-flex-positive: 1; 68 | flex-grow: 1; 69 | -webkit-box-align: center; 70 | -ms-flex-align: center; 71 | align-items: center; 72 | text-align: center; 73 | color: #888; 74 | font-size: 16px; 75 | word-wrap: break-word; } 76 | .ge-graph .ge-node:hover path { 77 | stroke: #fff; 78 | fill: #333; } 79 | .ge-graph .ge-node:hover div span { 80 | color: #fff; } 81 | 82 | .ge-graph.ge-selection .ge-link path { 83 | stroke: #888; } 84 | 85 | .ge-graph.ge-selection .ge-link text { 86 | fill: #888; } 87 | 88 | .ge-graph.ge-selection .ge-link:hover path { 89 | stroke: #c4c4c4; } 90 | 91 | .ge-graph.ge-selection .ge-link:hover text { 92 | fill: #c4c4c4; } 93 | 94 | .ge-graph.ge-selection .ge-link.ge-selection path { 95 | stroke: #aae; } 96 | 97 | .ge-graph.ge-selection .ge-link.ge-selection text { 98 | fill: #aae; } 99 | 100 | .ge-graph.ge-selection .ge-link.ge-selection:hover path { 101 | stroke: #d5d5f7; } 102 | 103 | .ge-graph.ge-selection .ge-link.ge-selection:hover text { 104 | fill: #d5d5f7; } 105 | 106 | .ge-graph.ge-selection .ge-node path { 107 | stroke: #888; 108 | fill: #111; } 109 | 110 | .ge-graph.ge-selection .ge-node span { 111 | color: #888; } 112 | 113 | .ge-graph.ge-selection .ge-node:hover path { 114 | stroke: #d5d5f7; 115 | fill: #222222; } 116 | 117 | .ge-graph.ge-selection .ge-node:hover span { 118 | color: #d5d5f7; } 119 | 120 | .ge-graph.ge-selection .ge-node.ge-selection path { 121 | stroke: #aae; 122 | fill: #111; } 123 | 124 | .ge-graph.ge-selection .ge-node.ge-selection span { 125 | color: #aae; } 126 | 127 | .ge-graph.ge-selection .ge-node.ge-selection:hover path { 128 | stroke: #d5d5f7; 129 | fill: #222222; } 130 | 131 | .ge-graph.ge-selection .ge-node.ge-selection:hover span { 132 | color: #d5d5f7; } 133 | 134 | .ge-graph.ge-selection .ge-link.ge-link-selection path, .ge-graph.ge-selection .ge-link.ge-selection.ge-link-selection path, .ge-graph.ge-link-selection .ge-link.ge-link-selection path, .ge-graph.ge-link-selection .ge-link.ge-selection.ge-link-selection path { 135 | stroke: #ada; } 136 | 137 | .ge-graph.ge-selection .ge-link.ge-link-selection text, .ge-graph.ge-selection .ge-link.ge-selection.ge-link-selection text, .ge-graph.ge-link-selection .ge-link.ge-link-selection text, .ge-graph.ge-link-selection .ge-link.ge-selection.ge-link-selection text { 138 | fill: #ada; } 139 | 140 | .ge-graph.ge-selection .ge-link.ge-link-selection:hover path, .ge-graph.ge-selection .ge-link.ge-selection.ge-link-selection:hover path, .ge-graph.ge-link-selection .ge-link.ge-link-selection:hover path, .ge-graph.ge-link-selection .ge-link.ge-selection.ge-link-selection:hover path { 141 | stroke: #d5d5f7; } 142 | 143 | .ge-graph.ge-selection .ge-link.ge-link-selection:hover text, .ge-graph.ge-selection .ge-link.ge-selection.ge-link-selection:hover text, .ge-graph.ge-link-selection .ge-link.ge-link-selection:hover text, .ge-graph.ge-link-selection .ge-link.ge-selection.ge-link-selection:hover text { 144 | fill: #d5d5f7; } 145 | -------------------------------------------------------------------------------- /dist/css/graph-editor-dark.min.css: -------------------------------------------------------------------------------- 1 | .ge-graph *{-webkit-transition-property:stroke,fill,color;transition-property:stroke,fill,color;-webkit-transition-duration:.3s;transition-duration:.3s}.ge-graph marker#ge-dragline-end circle,.ge-graph marker#ge-dragline-end path,.ge-graph marker#ge-dragline-start circle,.ge-graph marker#ge-dragline-start path{stroke:#a44;fill:#a44}.ge-graph .ge-hidden{display:none}.ge-graph .ge-dragline{pointer-events:none;stroke-width:2px;stroke:#a44}.ge-graph .ge-link{cursor:pointer;font-size:14px;stroke-width:2px}.ge-graph .ge-link path{stroke:#888;fill:none}.ge-graph .ge-link text{fill:#888}.ge-graph .ge-link:hover path{stroke:#fff}.ge-graph .ge-link:hover text{fill:#fff}.ge-graph .ge-node{cursor:pointer}.ge-graph .ge-node path{stroke-width:2px;stroke:#888;fill:#111}.ge-graph .ge-node div,.ge-graph .ge-node div span{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-direction:normal;-webkit-box-align:center;-ms-flex-align:center;align-items:center}.ge-graph .ge-node div{width:100%;height:100%;-webkit-box-orient:horizontal;-ms-flex-direction:row;flex-direction:row}.ge-graph .ge-node div span{-webkit-box-orient:vertical;-ms-flex-direction:column;flex-direction:column;-webkit-box-flex:1;-ms-flex-positive:1;flex-grow:1;text-align:center;color:#888;font-size:16px;word-wrap:break-word}.ge-graph .ge-node:hover path{stroke:#fff;fill:#333}.ge-graph .ge-node:hover div span{color:#fff}.ge-graph.ge-selection .ge-link path{stroke:#888}.ge-graph.ge-selection .ge-link text{fill:#888}.ge-graph.ge-selection .ge-link:hover path{stroke:#c4c4c4}.ge-graph.ge-selection .ge-link:hover text{fill:#c4c4c4}.ge-graph.ge-selection .ge-link.ge-selection path{stroke:#aae}.ge-graph.ge-selection .ge-link.ge-selection text{fill:#aae}.ge-graph.ge-selection .ge-link.ge-selection:hover path{stroke:#d5d5f7}.ge-graph.ge-selection .ge-link.ge-selection:hover text{fill:#d5d5f7}.ge-graph.ge-selection .ge-node path{stroke:#888;fill:#111}.ge-graph.ge-selection .ge-node span{color:#888}.ge-graph.ge-selection .ge-node.ge-selection:hover path,.ge-graph.ge-selection .ge-node:hover path{stroke:#d5d5f7;fill:#222}.ge-graph.ge-selection .ge-node.ge-selection:hover span,.ge-graph.ge-selection .ge-node:hover span{color:#d5d5f7}.ge-graph.ge-selection .ge-node.ge-selection path{stroke:#aae;fill:#111}.ge-graph.ge-selection .ge-node.ge-selection span{color:#aae}.ge-graph.ge-link-selection .ge-link.ge-link-selection path,.ge-graph.ge-link-selection .ge-link.ge-selection.ge-link-selection path,.ge-graph.ge-selection .ge-link.ge-link-selection path,.ge-graph.ge-selection .ge-link.ge-selection.ge-link-selection path{stroke:#ada}.ge-graph.ge-link-selection .ge-link.ge-link-selection text,.ge-graph.ge-link-selection .ge-link.ge-selection.ge-link-selection text,.ge-graph.ge-selection .ge-link.ge-link-selection text,.ge-graph.ge-selection .ge-link.ge-selection.ge-link-selection text{fill:#ada}.ge-graph.ge-link-selection .ge-link.ge-link-selection:hover path,.ge-graph.ge-link-selection .ge-link.ge-selection.ge-link-selection:hover path,.ge-graph.ge-selection .ge-link.ge-link-selection:hover path,.ge-graph.ge-selection .ge-link.ge-selection.ge-link-selection:hover path{stroke:#d5d5f7}.ge-graph.ge-link-selection .ge-link.ge-link-selection:hover text,.ge-graph.ge-link-selection .ge-link.ge-selection.ge-link-selection:hover text,.ge-graph.ge-selection .ge-link.ge-link-selection:hover text,.ge-graph.ge-selection .ge-link.ge-selection.ge-link-selection:hover text{fill:#d5d5f7} -------------------------------------------------------------------------------- /dist/css/graph-editor-dark.scss: -------------------------------------------------------------------------------- 1 | $ge-node-fg: #888 !default; 2 | $ge-node-bg: #111 !default; 3 | $ge-node-hover-fg: #fff !default; 4 | $ge-node-hover-bg: #333 !default; 5 | 6 | $ge-link-fg: #888 !default; 7 | $ge-link-hover-fg: #fff !default; 8 | 9 | $ge-dragline-fg: #a44 !default; 10 | 11 | $ge-selection-fg: #aae !default; 12 | $ge-selected-link-fg: #ada !default; 13 | $ge-selected-node-bg: #111 !default; 14 | 15 | $ge-unselected-fg: $ge-node-fg !default; 16 | $ge-unselected-bg: $ge-node-bg !default; 17 | 18 | @import "graph-editor"; 19 | -------------------------------------------------------------------------------- /dist/css/graph-editor.css: -------------------------------------------------------------------------------- 1 | /*$ge-graph-transition: stroke $ge-graph-transition-duration, 2 | fill $ge-graph-transition-duration, 3 | color $ge-graph-transition-duration !default;*/ 4 | .ge-graph * { 5 | /*transition: $ge-graph-transition;*/ 6 | -webkit-transition-property: stroke, fill, color; 7 | transition-property: stroke, fill, color; 8 | -webkit-transition-duration: 0.3s; 9 | transition-duration: 0.3s; } 10 | 11 | .ge-graph marker#ge-dragline-end path, .ge-graph marker#ge-dragline-end circle, .ge-graph marker#ge-dragline-start path, .ge-graph marker#ge-dragline-start circle { 12 | stroke: #a44; 13 | fill: #a44; } 14 | 15 | .ge-graph .ge-hidden { 16 | display: none; } 17 | 18 | .ge-graph .ge-dragline { 19 | pointer-events: none; 20 | stroke-width: 2px; 21 | stroke: #a44; } 22 | 23 | .ge-graph .ge-link { 24 | cursor: pointer; 25 | font-size: 14px; 26 | stroke-width: 2px; 27 | /* * { vector-effect: non-scaling-stroke } */ } 28 | .ge-graph .ge-link path { 29 | stroke: #000; 30 | fill: none; } 31 | .ge-graph .ge-link text { 32 | fill: #000; } 33 | .ge-graph .ge-link:hover path { 34 | stroke: #4aa; } 35 | .ge-graph .ge-link:hover text { 36 | fill: #4aa; } 37 | 38 | .ge-graph .ge-node { 39 | cursor: pointer; 40 | /* * { vector-effect: non-scaling-stroke } */ } 41 | .ge-graph .ge-node path { 42 | stroke-width: 2px; 43 | stroke: #000; 44 | fill: #ddd; } 45 | .ge-graph .ge-node div { 46 | display: -webkit-box; 47 | display: -ms-flexbox; 48 | display: flex; 49 | -webkit-box-orient: horizontal; 50 | -webkit-box-direction: normal; 51 | -ms-flex-direction: row; 52 | flex-direction: row; 53 | -webkit-box-align: center; 54 | -ms-flex-align: center; 55 | align-items: center; 56 | width: 100%; 57 | height: 100%; } 58 | .ge-graph .ge-node div span { 59 | display: -webkit-box; 60 | display: -ms-flexbox; 61 | display: flex; 62 | -webkit-box-orient: vertical; 63 | -webkit-box-direction: normal; 64 | -ms-flex-direction: column; 65 | flex-direction: column; 66 | -webkit-box-flex: 1; 67 | -ms-flex-positive: 1; 68 | flex-grow: 1; 69 | -webkit-box-align: center; 70 | -ms-flex-align: center; 71 | align-items: center; 72 | text-align: center; 73 | color: #000; 74 | font-size: 16px; 75 | word-wrap: break-word; } 76 | .ge-graph .ge-node:hover path { 77 | stroke: #000; 78 | fill: #aee; } 79 | .ge-graph .ge-node:hover div span { 80 | color: #000; } 81 | 82 | .ge-graph.ge-selection .ge-link path { 83 | stroke: #666; } 84 | 85 | .ge-graph.ge-selection .ge-link text { 86 | fill: #666; } 87 | 88 | .ge-graph.ge-selection .ge-link:hover path { 89 | stroke: #558888; } 90 | 91 | .ge-graph.ge-selection .ge-link:hover text { 92 | fill: #558888; } 93 | 94 | .ge-graph.ge-selection .ge-link.ge-selection path { 95 | stroke: #44a; } 96 | 97 | .ge-graph.ge-selection .ge-link.ge-selection text { 98 | fill: #44a; } 99 | 100 | .ge-graph.ge-selection .ge-link.ge-selection:hover path { 101 | stroke: #4477aa; } 102 | 103 | .ge-graph.ge-selection .ge-link.ge-selection:hover text { 104 | fill: #4477aa; } 105 | 106 | .ge-graph.ge-selection .ge-node path { 107 | stroke: #666; 108 | fill: #eee; } 109 | 110 | .ge-graph.ge-selection .ge-node span { 111 | color: #666; } 112 | 113 | .ge-graph.ge-selection .ge-node:hover path { 114 | stroke: #222255; 115 | fill: #aaccee; } 116 | 117 | .ge-graph.ge-selection .ge-node:hover span { 118 | color: #222255; } 119 | 120 | .ge-graph.ge-selection .ge-node.ge-selection path { 121 | stroke: #44a; 122 | fill: #aae; } 123 | 124 | .ge-graph.ge-selection .ge-node.ge-selection span { 125 | color: #44a; } 126 | 127 | .ge-graph.ge-selection .ge-node.ge-selection:hover path { 128 | stroke: #222255; 129 | fill: #aaccee; } 130 | 131 | .ge-graph.ge-selection .ge-node.ge-selection:hover span { 132 | color: #222255; } 133 | 134 | .ge-graph.ge-selection .ge-link.ge-link-selection path, .ge-graph.ge-selection .ge-link.ge-selection.ge-link-selection path, .ge-graph.ge-link-selection .ge-link.ge-link-selection path, .ge-graph.ge-link-selection .ge-link.ge-selection.ge-link-selection path { 135 | stroke: #488; } 136 | 137 | .ge-graph.ge-selection .ge-link.ge-link-selection text, .ge-graph.ge-selection .ge-link.ge-selection.ge-link-selection text, .ge-graph.ge-link-selection .ge-link.ge-link-selection text, .ge-graph.ge-link-selection .ge-link.ge-selection.ge-link-selection text { 138 | fill: #488; } 139 | 140 | .ge-graph.ge-selection .ge-link.ge-link-selection:hover path, .ge-graph.ge-selection .ge-link.ge-selection.ge-link-selection:hover path, .ge-graph.ge-link-selection .ge-link.ge-link-selection:hover path, .ge-graph.ge-link-selection .ge-link.ge-selection.ge-link-selection:hover path { 141 | stroke: #4477aa; } 142 | 143 | .ge-graph.ge-selection .ge-link.ge-link-selection:hover text, .ge-graph.ge-selection .ge-link.ge-selection.ge-link-selection:hover text, .ge-graph.ge-link-selection .ge-link.ge-link-selection:hover text, .ge-graph.ge-link-selection .ge-link.ge-selection.ge-link-selection:hover text { 144 | fill: #4477aa; } 145 | -------------------------------------------------------------------------------- /dist/css/graph-editor.min.css: -------------------------------------------------------------------------------- 1 | .ge-graph *{-webkit-transition-property:stroke,fill,color;transition-property:stroke,fill,color;-webkit-transition-duration:.3s;transition-duration:.3s}.ge-graph marker#ge-dragline-end circle,.ge-graph marker#ge-dragline-end path,.ge-graph marker#ge-dragline-start circle,.ge-graph marker#ge-dragline-start path{stroke:#a44;fill:#a44}.ge-graph .ge-hidden{display:none}.ge-graph .ge-dragline{pointer-events:none;stroke-width:2px;stroke:#a44}.ge-graph .ge-link{cursor:pointer;font-size:14px;stroke-width:2px}.ge-graph .ge-link path{stroke:#000;fill:none}.ge-graph .ge-link text{fill:#000}.ge-graph .ge-link:hover path{stroke:#4aa}.ge-graph .ge-link:hover text{fill:#4aa}.ge-graph .ge-node{cursor:pointer}.ge-graph .ge-node path{stroke-width:2px;stroke:#000;fill:#ddd}.ge-graph .ge-node div,.ge-graph .ge-node div span{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-direction:normal;-webkit-box-align:center;-ms-flex-align:center;align-items:center}.ge-graph .ge-node div{width:100%;height:100%;-webkit-box-orient:horizontal;-ms-flex-direction:row;flex-direction:row}.ge-graph .ge-node div span{-webkit-box-orient:vertical;-ms-flex-direction:column;flex-direction:column;-webkit-box-flex:1;-ms-flex-positive:1;flex-grow:1;text-align:center;color:#000;font-size:16px;word-wrap:break-word}.ge-graph .ge-node:hover path{stroke:#000;fill:#aee}.ge-graph .ge-node:hover div span{color:#000}.ge-graph.ge-selection .ge-link path{stroke:#666}.ge-graph.ge-selection .ge-link text{fill:#666}.ge-graph.ge-selection .ge-link:hover path{stroke:#588}.ge-graph.ge-selection .ge-link:hover text{fill:#588}.ge-graph.ge-selection .ge-link.ge-selection path{stroke:#44a}.ge-graph.ge-selection .ge-link.ge-selection text{fill:#44a}.ge-graph.ge-selection .ge-link.ge-selection:hover path{stroke:#47a}.ge-graph.ge-selection .ge-link.ge-selection:hover text{fill:#47a}.ge-graph.ge-selection .ge-node path{stroke:#666;fill:#eee}.ge-graph.ge-selection .ge-node span{color:#666}.ge-graph.ge-selection .ge-node.ge-selection:hover path,.ge-graph.ge-selection .ge-node:hover path{stroke:#225;fill:#ace}.ge-graph.ge-selection .ge-node.ge-selection:hover span,.ge-graph.ge-selection .ge-node:hover span{color:#225}.ge-graph.ge-selection .ge-node.ge-selection path{stroke:#44a;fill:#aae}.ge-graph.ge-selection .ge-node.ge-selection span{color:#44a}.ge-graph.ge-link-selection .ge-link.ge-link-selection path,.ge-graph.ge-link-selection .ge-link.ge-selection.ge-link-selection path,.ge-graph.ge-selection .ge-link.ge-link-selection path,.ge-graph.ge-selection .ge-link.ge-selection.ge-link-selection path{stroke:#488}.ge-graph.ge-link-selection .ge-link.ge-link-selection text,.ge-graph.ge-link-selection .ge-link.ge-selection.ge-link-selection text,.ge-graph.ge-selection .ge-link.ge-link-selection text,.ge-graph.ge-selection .ge-link.ge-selection.ge-link-selection text{fill:#488}.ge-graph.ge-link-selection .ge-link.ge-link-selection:hover path,.ge-graph.ge-link-selection .ge-link.ge-selection.ge-link-selection:hover path,.ge-graph.ge-selection .ge-link.ge-link-selection:hover path,.ge-graph.ge-selection .ge-link.ge-selection.ge-link-selection:hover path{stroke:#47a}.ge-graph.ge-link-selection .ge-link.ge-link-selection:hover text,.ge-graph.ge-link-selection .ge-link.ge-selection.ge-link-selection:hover text,.ge-graph.ge-selection .ge-link.ge-link-selection:hover text,.ge-graph.ge-selection .ge-link.ge-selection.ge-link-selection:hover text{fill:#47a} -------------------------------------------------------------------------------- /dist/css/graph-editor.scss: -------------------------------------------------------------------------------- 1 | $ge-graph-transition-duration: 0.3s !default; 2 | $ge-graph-transition-properties: stroke, fill, color; 3 | /*$ge-graph-transition: stroke $ge-graph-transition-duration, 4 | fill $ge-graph-transition-duration, 5 | color $ge-graph-transition-duration !default;*/ 6 | 7 | $ge-node-font-size: 16px !default; 8 | $ge-node-border: 2px !default; 9 | 10 | $ge-node-fg: #000 !default; 11 | $ge-node-bg: #ddd !default; 12 | $ge-node-hover-fg: #000 !default; 13 | $ge-node-hover-bg: #aee !default; 14 | 15 | $ge-link-font-size: 14px !default; 16 | $ge-link-size: 2px !default; 17 | 18 | $ge-link-fg: #000 !default; 19 | $ge-link-hover-fg: #4aa !default; 20 | 21 | $ge-dragline-fg: #a44 !default; 22 | $ge-dragline-size: 2px; 23 | 24 | $ge-selection-fg: #44a !default; 25 | $ge-selected-link-fg: #488 !default; 26 | $ge-selected-node-fg: $ge-selection-fg !default; 27 | $ge-selected-node-bg: #aae !default; 28 | 29 | $ge-unselected-fg: #666 !default; 30 | $ge-unselected-bg: #eee !default; 31 | 32 | $ge-unselected-link-hover-fg: mix($ge-unselected-fg, $ge-link-hover-fg) !default; 33 | $ge-unselected-node-hover-fg: mix($ge-selected-node-fg, $ge-node-hover-fg) !default; 34 | $ge-unselected-node-hover-bg: mix($ge-selected-node-bg, $ge-node-hover-bg) !default; 35 | $ge-selected-link-hover-fg: mix($ge-selection-fg, $ge-link-hover-fg) !default; 36 | $ge-selected-node-hover-fg: mix($ge-selected-node-fg, $ge-node-hover-fg) !default; 37 | $ge-selected-node-hover-bg: mix($ge-selected-node-bg, $ge-node-hover-bg) !default; 38 | 39 | 40 | .ge-graph { 41 | * { 42 | /*transition: $ge-graph-transition;*/ 43 | transition-property: $ge-graph-transition-properties; 44 | transition-duration: $ge-graph-transition-duration; 45 | } 46 | 47 | marker { 48 | &#ge-dragline-end, &#ge-dragline-start { 49 | path, circle { 50 | stroke: $ge-dragline-fg; 51 | fill: $ge-dragline-fg; 52 | } 53 | } 54 | } 55 | 56 | .ge-hidden { 57 | display: none; 58 | } 59 | 60 | .ge-dragline { 61 | pointer-events: none; 62 | stroke-width: $ge-dragline-size; 63 | stroke: $ge-dragline-fg; 64 | } 65 | 66 | .ge-link { 67 | cursor: pointer; 68 | 69 | font-size: $ge-link-font-size; 70 | stroke-width: $ge-link-size; 71 | 72 | /* * { vector-effect: non-scaling-stroke } */ 73 | 74 | path { 75 | stroke: $ge-link-fg; 76 | fill: none; 77 | } 78 | 79 | text { 80 | fill: $ge-link-fg; 81 | } 82 | 83 | &:hover { 84 | path { 85 | stroke: $ge-link-hover-fg; 86 | } 87 | text { 88 | fill: $ge-link-hover-fg; 89 | } 90 | } 91 | } 92 | 93 | .ge-node { 94 | cursor: pointer; 95 | 96 | /* * { vector-effect: non-scaling-stroke } */ 97 | 98 | path { 99 | stroke-width: $ge-node-border; 100 | stroke: $ge-node-fg; 101 | fill: $ge-node-bg; 102 | } 103 | 104 | div { 105 | display: flex; 106 | flex-direction: row; 107 | align-items: center; 108 | 109 | width: 100%; 110 | height: 100%; 111 | 112 | span { 113 | display: flex; 114 | flex-direction: column; 115 | flex-grow: 1; 116 | align-items: center; 117 | text-align: center; 118 | 119 | color: $ge-node-fg; 120 | font-size: $ge-node-font-size; 121 | word-wrap: break-word; 122 | } 123 | } 124 | 125 | &:hover { 126 | path { 127 | stroke: $ge-node-hover-fg; 128 | fill: $ge-node-hover-bg; 129 | } 130 | div { 131 | span { 132 | color: $ge-node-hover-fg; 133 | } 134 | } 135 | } 136 | } 137 | 138 | &.ge-selection { 139 | .ge-link { 140 | path { stroke: $ge-unselected-fg; } 141 | text { fill: $ge-unselected-fg; } 142 | 143 | &:hover { 144 | path { stroke: $ge-unselected-link-hover-fg; } 145 | text { fill: $ge-unselected-link-hover-fg; } 146 | } 147 | 148 | &.ge-selection { 149 | path { stroke: $ge-selection-fg; } 150 | text { fill: $ge-selection-fg; } 151 | 152 | &:hover { 153 | path { stroke: $ge-selected-link-hover-fg; } 154 | text { fill: $ge-selected-link-hover-fg; } 155 | } 156 | } 157 | } 158 | 159 | .ge-node { 160 | path { 161 | stroke: $ge-unselected-fg; 162 | fill: $ge-unselected-bg; 163 | } 164 | span { color: $ge-unselected-fg; } 165 | 166 | &:hover { 167 | path { 168 | stroke: $ge-unselected-node-hover-fg; 169 | fill: $ge-unselected-node-hover-bg; 170 | } 171 | span { color: $ge-unselected-node-hover-fg; } 172 | } 173 | 174 | &.ge-selection { 175 | path { 176 | stroke: $ge-selected-node-fg; 177 | fill: $ge-selected-node-bg; 178 | } 179 | span { color: $ge-selected-node-fg; } 180 | 181 | &:hover { 182 | path { 183 | stroke: $ge-selected-node-hover-fg; 184 | fill: $ge-selected-node-hover-bg; 185 | } 186 | span { color: $ge-selected-node-hover-fg; } 187 | } 188 | } 189 | } 190 | } 191 | 192 | &.ge-selection, &.ge-link-selection { 193 | .ge-link { 194 | &.ge-link-selection, &.ge-selection.ge-link-selection { 195 | path { stroke: $ge-selected-link-fg; } 196 | text { fill: $ge-selected-link-fg; } 197 | 198 | &:hover { 199 | path { stroke: $ge-selected-link-hover-fg; } 200 | text { fill: $ge-selected-link-hover-fg; } 201 | } 202 | } 203 | } 204 | } 205 | } 206 | -------------------------------------------------------------------------------- /docs/events.jsdoc.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | JSDoc: Source: events.jsdoc 6 | 7 | 8 | 9 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 |

Source: events.jsdoc

21 | 22 | 23 | 24 | 25 | 26 | 27 |
28 |
29 |
/**
 30 |  * Graph SVG click/touch event.
 31 |  * @event click
 32 |  * @this ge.GraphEditor
 33 |  * @param {Point} pos  Click/touch position.
 34 |  */
 35 | 
 36 | /**
 37 |  * Node click/touch event.
 38 |  * @event node-click
 39 |  * @this ge.GraphEditor
 40 |  * @param {Node} node  Clicked/touched node.
 41 |  */
 42 | 
 43 | /**
 44 |  * Link click/touch event.
 45 |  * @event link-click
 46 |  * @this ge.GraphEditor
 47 |  * @param {Link} link  Clicked/touched link.
 48 |  */
 49 | 
 50 | /**
 51 |  * New link start event.
 52 |  * @event new-link-start
 53 |  * @this ge.GraphEditor
 54 |  * @param {Node} source  New link source.
 55 |  */
 56 | 
 57 | /**
 58 |  * New link end event.
 59 |  * @event new-link-end
 60 |  * @this ge.GraphEditor
 61 |  * @param {Node} source  New link source.
 62 |  * @param {Node} target  New link target.
 63 |  */
 64 | 
 65 | /**
 66 |  * New link cancel event.
 67 |  * @event new-link-cancel
 68 |  * @this ge.GraphEditor
 69 |  * @param {Node} source  Cancelled link source.
 70 |  */
 71 | 
 72 | /**
 73 |  * Simulation start event.
 74 |  * @event simulation-start
 75 |  * @this ge.GraphEditor
 76 |  */
 77 | 
 78 | /**
 79 |  * Simulation stop event.
 80 |  * @event simulation-stop
 81 |  * @this ge.GraphEditor
 82 |  */
 83 | 
84 |
85 |
86 | 87 | 88 | 89 | 90 |
91 | 92 | 95 | 96 |
97 | 98 | 101 | 102 | 103 | 104 | 105 | 106 | -------------------------------------------------------------------------------- /docs/fonts/OpenSans-Bold-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dead-beef/graph-editor/0c1622e87e160a2fa006f2118603f5fe63d72221/docs/fonts/OpenSans-Bold-webfont.eot -------------------------------------------------------------------------------- /docs/fonts/OpenSans-Bold-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dead-beef/graph-editor/0c1622e87e160a2fa006f2118603f5fe63d72221/docs/fonts/OpenSans-Bold-webfont.woff -------------------------------------------------------------------------------- /docs/fonts/OpenSans-BoldItalic-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dead-beef/graph-editor/0c1622e87e160a2fa006f2118603f5fe63d72221/docs/fonts/OpenSans-BoldItalic-webfont.eot -------------------------------------------------------------------------------- /docs/fonts/OpenSans-BoldItalic-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dead-beef/graph-editor/0c1622e87e160a2fa006f2118603f5fe63d72221/docs/fonts/OpenSans-BoldItalic-webfont.woff -------------------------------------------------------------------------------- /docs/fonts/OpenSans-Italic-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dead-beef/graph-editor/0c1622e87e160a2fa006f2118603f5fe63d72221/docs/fonts/OpenSans-Italic-webfont.eot -------------------------------------------------------------------------------- /docs/fonts/OpenSans-Italic-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dead-beef/graph-editor/0c1622e87e160a2fa006f2118603f5fe63d72221/docs/fonts/OpenSans-Italic-webfont.woff -------------------------------------------------------------------------------- /docs/fonts/OpenSans-Light-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dead-beef/graph-editor/0c1622e87e160a2fa006f2118603f5fe63d72221/docs/fonts/OpenSans-Light-webfont.eot -------------------------------------------------------------------------------- /docs/fonts/OpenSans-Light-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dead-beef/graph-editor/0c1622e87e160a2fa006f2118603f5fe63d72221/docs/fonts/OpenSans-Light-webfont.woff -------------------------------------------------------------------------------- /docs/fonts/OpenSans-LightItalic-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dead-beef/graph-editor/0c1622e87e160a2fa006f2118603f5fe63d72221/docs/fonts/OpenSans-LightItalic-webfont.eot -------------------------------------------------------------------------------- /docs/fonts/OpenSans-LightItalic-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dead-beef/graph-editor/0c1622e87e160a2fa006f2118603f5fe63d72221/docs/fonts/OpenSans-LightItalic-webfont.woff -------------------------------------------------------------------------------- /docs/fonts/OpenSans-Regular-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dead-beef/graph-editor/0c1622e87e160a2fa006f2118603f5fe63d72221/docs/fonts/OpenSans-Regular-webfont.eot -------------------------------------------------------------------------------- /docs/fonts/OpenSans-Regular-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dead-beef/graph-editor/0c1622e87e160a2fa006f2118603f5fe63d72221/docs/fonts/OpenSans-Regular-webfont.woff -------------------------------------------------------------------------------- /docs/ge.path.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | JSDoc: Namespace: path 6 | 7 | 8 | 9 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 |

Namespace: path

21 | 22 | 23 | 24 | 25 | 26 | 27 |
28 | 29 |
30 | 31 |

32 | ge.path

33 | 34 | 35 |
36 | 37 |
38 |
39 | 40 | 41 |
Link shapes.
42 | 43 | 44 | 45 | 46 | 47 |
48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 |
Source:
75 |
78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 |
86 | 87 | 88 | 89 | 90 |
91 | 92 | 93 | 94 | 95 | 96 | 97 |

Classes

98 | 99 |
100 |
Line
101 |
102 | 103 |
Path
104 |
105 |
106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 |
122 | 123 |
124 | 125 | 126 | 127 | 128 |
129 | 130 | 133 | 134 |
135 | 136 | 139 | 140 | 141 | 142 | 143 | -------------------------------------------------------------------------------- /docs/ge.shape.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | JSDoc: Namespace: shape 6 | 7 | 8 | 9 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 |

Namespace: shape

21 | 22 | 23 | 24 | 25 | 26 | 27 |
28 | 29 |
30 | 31 |

32 | ge.shape

33 | 34 | 35 |
36 | 37 |
38 |
39 | 40 | 41 |
Node shapes.
42 | 43 | 44 | 45 | 46 | 47 |
48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 |
Source:
75 |
78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 |
86 | 87 | 88 | 89 | 90 |
91 | 92 | 93 | 94 | 95 | 96 | 97 |

Classes

98 | 99 |
100 |
Circle
101 |
102 | 103 |
Rect
104 |
105 | 106 |
Shape
107 |
108 |
109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 |
125 | 126 |
127 | 128 | 129 | 130 | 131 |
132 | 133 | 136 | 137 |
138 | 139 | 142 | 143 | 144 | 145 | 146 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | JSDoc: Home 6 | 7 | 8 | 9 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 |

Home

21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 |

graph-editor 0.1.1

30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 |
46 |

graph-editor - graph editor frontend component

npm node Libraries.io for GitHub license

47 |

Overview

It exports a class which should be bound to an element and provided data for Graph visualisation and editing.

48 |

Requirements

51 |

Installation

npm install graph-editor

Usage

55 |

Development

Requirements

61 |

Installation

git clone https://github.com/dead-beef/graph-editor.git
 62 | cd graph-editor
 63 | make install

Building

# single run
 64 | make
 65 | # continuous
 66 | make watch

Testing

# unit, single run
 67 | make test
 68 | # unit, continuous
 69 | make test-watch
 70 | # test library bundle
 71 | TEST_BUNDLE=1 make test
 72 | # test minified library bundle
 73 | TEST_MIN_BUNDLE=1 make test
 74 | # select browsers (default: Chromium)
 75 | TEST_BROWSERS="Firefox Chrome" make test

Code Linting

make lint

Documentation

make docs

Licenses

78 |
79 | 80 | 81 | 82 | 83 | 84 | 85 |
86 | 87 | 90 | 91 |
92 | 93 | 96 | 97 | 98 | 99 | 100 | -------------------------------------------------------------------------------- /docs/js_init.js.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | JSDoc: Source: js/init.js 6 | 7 | 8 | 9 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 |

Source: js/init.js

21 | 22 | 23 | 24 | 25 | 26 | 27 |
28 |
29 |
'use strict';
 30 | 
 31 | /**
 32 |  * Create SVG markers.
 33 |  * @private
 34 |  * @param   {D3Selection}    svg
 35 |  * @returns {ge.GraphEditor}
 36 |  */
 37 | ge.GraphEditor.prototype.initMarkers = function initMarkers(svg) {
 38 | 	/*var defs = d3.select('#' + this.options.css.markers).node();
 39 | 	if(defs !== null) {
 40 | 		return this;
 41 | 	}*/
 42 | 
 43 | 	/*defs = d3.select('head')
 44 | 		.append('svg')
 45 | 		.attr('id', this.options.css.markers)
 46 | 		.append('svg:defs');*/
 47 | 
 48 | 	var defs = svg.append('svg:defs');
 49 | 
 50 | 	defs.append('marker')
 51 | 		.attr('id', 'ge-dragline-end')
 52 | 		.attr('viewBox', '0 -7 12 14')
 53 | 		.attr('refX', '7')
 54 | 		.attr('refY', '0')
 55 | 		.attr('markerWidth', 3.5)
 56 | 		.attr('markerHeight', 3.5)
 57 | 		.attr('orient', 'auto')
 58 | 		.append('path')
 59 | 		.attr('d', 'M0,-5L10,0L0,5Z');
 60 | 
 61 | 	defs.append('marker')
 62 | 		.attr('id', 'ge-dragline-start')
 63 | 		.attr('viewBox', '-5 -5 5 5')
 64 | 		.attr('refX', -2)
 65 | 		.attr('refY', -2)
 66 | 		.attr('markerWidth', 4)
 67 | 		.attr('markerHeight', 4)
 68 | 		.append('circle')
 69 | 		.attr('r', 2)
 70 | 		.attr('cx', -2)
 71 | 		.attr('cy', -2);
 72 | 
 73 | 	return this;
 74 | };
 75 | 
 76 | /**
 77 |  * Initialize SVG element.
 78 |  * @private
 79 |  * @param   {D3Selection}    svg
 80 |  * @this    ge.GraphEditor
 81 |  * @returns {ge.GraphEditor}
 82 |  */
 83 | ge.GraphEditor.prototype.initSvg = function initSvg(svg) {
 84 | 	svg.attr('id', this.options.id)
 85 | 		.classed(this.options.css.graph, true)
 86 | 		.classed(this.options.css.digraph, this.options.directed);
 87 | 
 88 | 	this.initMarkers(svg);
 89 | 
 90 | 	var g = svg.append('g');
 91 | 	var defsContainer = svg.select('defs'); //g.append('defs');
 92 | 	var linkContainer = g.append('g');
 93 | 	var nodeContainer = g.append('g');
 94 | 
 95 | 	/**
 96 | 	 * Graph container element.
 97 | 	 * @readonly
 98 | 	 * @member {D3Selection}
 99 | 	 */
100 | 	this.container = g;
101 | 	/**
102 | 	 * Link text path elements.
103 | 	 * @readonly
104 | 	 * @member {D3Selection}
105 | 	 */
106 | 	this.defs = defsContainer.selectAll('.' + this.options.css.textpath);
107 | 	/**
108 | 	 * Link elements.
109 | 	 * @readonly
110 | 	 * @member {D3Selection}
111 | 	 */
112 | 	this.links = linkContainer.selectAll('g');
113 | 	/**
114 | 	 * Node elements.
115 | 	 * @readonly
116 | 	 * @member {D3Selection}
117 | 	 */
118 | 	this.nodes = nodeContainer.selectAll('g');
119 | 	/**
120 | 	 * 'Drag to link nodes' line.
121 | 	 * @readonly
122 | 	 * @member {D3Selection}
123 | 	 */
124 | 	this.dragLine = g.append('path')
125 | 		.classed(this.options.css.dragline, true)
126 | 		.classed(this.options.css.hide, true)
127 | 		.attr('marker-start', 'url(#ge-dragline-start)')
128 | 		.attr('marker-end', 'url(#ge-dragline-end)')
129 | 		.attr('d', 'M0,0L0,0');
130 | 	/**
131 | 	 * Text size calculator.
132 | 	 * @readonly
133 | 	 * @member {ge.TextSize}
134 | 	 */
135 | 	this.textSize = new ge.TextSize(
136 | 		svg.append('text')
137 | 			.classed(this.options.css.node, true)
138 | 			.node()
139 | 	);
140 | 
141 | 	/**
142 | 	 * Graph SVG element.
143 | 	 * @readonly
144 | 	 * @member {D3Selection}
145 | 	 */
146 | 	this.svg = svg;
147 | 
148 | 	return this;
149 | };
150 | 
151 | 
152 | /**
153 |  * Initialize graph data.
154 |  * @private
155 |  * @param   {ImportGraphData} data
156 |  * @this    ge.GraphEditor
157 |  * @returns {ge.GraphEditor}
158 |  */
159 | ge.GraphEditor.prototype.initData = function initData(data) {
160 | 	/**
161 | 	 * Nodes by ID.
162 | 	 * @readonly
163 | 	 * @member {Object<ID,Node>}
164 | 	 */
165 | 	this.nodeById = {};
166 | 	/**
167 | 	 * Links by ID.
168 | 	 * @readonly
169 | 	 * @member {Object<ID,Link>}
170 | 	 */
171 | 	this.linkById = {};
172 | 	/**
173 | 	 * Graph data.
174 | 	 * @readonly
175 | 	 * @member {GraphData}
176 | 	 */
177 | 	this.data = {
178 | 		nodes: [],
179 | 		links: []
180 | 	};
181 | 	this.addNodes(data.nodes, true);
182 | 	this.addLinks(data.links, true);
183 | 	return this;
184 | };
185 | 
186 | /**
187 |  * Initialize graph state.
188 |  * @private
189 |  * @this    ge.GraphEditor
190 |  * @returns {ge.GraphEditor}
191 |  */
192 | ge.GraphEditor.prototype.initState = function initState() {
193 | 	/**
194 | 	 * Graph editor state.
195 |      * @readonly
196 | 	 * @member {Object}
197 | 	 * @property {boolean}       zoomed             False if the graph is clicked.
198 | 	 * @property {boolean}       dragged            False if a node is clicked.
199 | 	 * @property {?D3Simulation} simulation         Current simulation object.
200 | 	 * @property {boolean}       simulationStarted  True if a simulation is started.
201 | 	 * @property {boolean}       dragToLink         True in 'drag to link nodes' mode.
202 | 	 * @property {?Node}         newLinkTarget      New link target in 'drag to link nodes' mode.
203 | 	 * @property {?Node}         selectedNode       Selected node.
204 | 	 * @property {?Link}         selectedLink       Selected link.
205 | 	 * @property {?Point}        dragPos            Last drag end position.
206 | 	 * @property {number}        zoom               Zoom level.
207 | 	 */
208 | 	this.state = {
209 | 		zoomed: false,
210 | 		dragged: false,
211 | 		simulation: null,
212 | 		simulationStarted: false,
213 | 		dragToLink: false,
214 | 		newLinkTarget: null,
215 | 		selectedNode: null,
216 | 		selectedLink: null,
217 | 		dragPos: null,
218 | 		zoom: 1
219 | 		//sizeScale: 1
220 | 	};
221 | 	return this;
222 | };
223 | 
224 |
225 |
226 | 227 | 228 | 229 | 230 |
231 | 232 | 235 | 236 |
237 | 238 | 241 | 242 | 243 | 244 | 245 | 246 | -------------------------------------------------------------------------------- /docs/js_options.js.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | JSDoc: Source: js/options.js 6 | 7 | 8 | 9 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 |

Source: js/options.js

21 | 22 | 23 | 24 | 25 | 26 | 27 |
28 |
29 |
'use strict';
 30 | 
 31 | /**
 32 |  * Default options for all graph types.
 33 |  * @type GraphOptions
 34 |  */
 35 | ge.GraphEditor.prototype.defaults = {
 36 | 	id: null,
 37 | 	directed: false,
 38 | 
 39 | 	node: {
 40 | 		shape: new ge.shape.Circle(),
 41 | 		border: 2,
 42 | 		size: {
 43 | 			def: 10,
 44 | 			min: 10
 45 | 		},
 46 | 		text: {
 47 | 			dx: 0,
 48 | 			dy: 0,
 49 | 			inside: true
 50 | 		}
 51 | 	},
 52 | 
 53 | 	link: {
 54 | 		size: {
 55 | 			def: 2
 56 | 		},
 57 | 		text: {
 58 | 			dx: 0,
 59 | 			dy: '1.1em',
 60 | 			offset: null,
 61 | 			anchor: null
 62 | 		}
 63 | 	},
 64 | 
 65 | 	simulation: {
 66 | 		create: ge.defaultSimulation,
 67 | 		start: false,
 68 | 		stop: true,
 69 | 		step: 1
 70 | 	},
 71 | 
 72 | 	transition: {
 73 | 		zoom: 250,
 74 | 		drag: 0,
 75 | 		simulation: 50
 76 | 	},
 77 | 
 78 | 	scale: {
 79 | 		min: 0.25,
 80 | 		max: 8.0,
 81 | 		/*size: {
 82 | 			min: 0.5,
 83 | 			max: 2.0
 84 | 		}*/
 85 | 	},
 86 | 
 87 | 	bbox: {
 88 | 		padding: 80
 89 | 	},
 90 | 
 91 | 	css: {
 92 | 		//markers: 'ge-markers',
 93 | 		node: 'ge-node',
 94 | 		graph: 'ge-graph',
 95 | 		digraph: 'ge-digraph',
 96 | 		hide: 'ge-hidden',
 97 | 		dragline: 'ge-dragline',
 98 | 		link: 'ge-link',
 99 | 		textpath: 'ge-text-path',
100 | 		//connect: 'ge-connect',
101 | 		selection: {
102 | 			node: 'ge-selection',
103 | 			link: 'ge-link-selection'
104 | 		}
105 | 	}
106 | };
107 | 
108 | /**
109 |  * Default options by graph type.
110 |  * @type Array<GraphOptions>
111 |  */
112 | ge.GraphEditor.prototype.typeDefaults = [
113 | 	{
114 | 		link: {
115 | 			shape: new ge.path.Line({
116 | 				loopStart: 0,
117 | 				loopEnd: 90
118 | 			}),
119 | 			text: {
120 | 				offset: '50%',
121 | 				anchor: 'middle'
122 | 			}
123 | 		}
124 | 	},
125 | 	{
126 | 		link: {
127 | 			shape: new ge.path.Line({
128 | 				arrow: true,
129 | 				loopStart: 0,
130 | 				loopEnd: 90
131 | 			}),
132 | 			text: {
133 | 				offset: '20%',
134 | 				anchor: 'start'
135 | 			}
136 | 		}
137 | 	}
138 | ];
139 | 
140 | /**
141 |  * Initialize graph options.
142 |  * @private
143 |  * @param	{GraphOptions}	  options
144 |  * @param	{D3Selection}	  svg
145 |  * @returns {ge.GraphEditor}
146 |  */
147 | ge.GraphEditor.prototype.initOptions = function initOptions(options, svg) {
148 | 	var directed;
149 | 	if(options && options.hasOwnProperty('directed')) {
150 | 		directed = options.directed;
151 | 	}
152 | 	else {
153 | 		directed = this.defaults.directed;
154 | 	}
155 | 
156 | 	var typeDefaults = this.typeDefaults[+directed];
157 | 
158 | 	var opt = ge.extend({}, this.defaults, typeDefaults, options);
159 | 
160 | 	opt.id = opt.id
161 | 		|| svg.attr('id')
162 | 		|| 'ge'.concat(Math.floor(Math.random() * 100));
163 | 
164 | 	var reversed = (100 - parseInt(opt.link.text.offset)) + '%';
165 | 	opt.link.text.offset = [ opt.link.text.offset, reversed ];
166 | 
167 | 	switch(opt.link.text.anchor) {
168 | 		case 'start':
169 | 			reversed = 'end';
170 | 			break;
171 | 		case 'end':
172 | 			reversed = 'start';
173 | 			break;
174 | 		default:
175 | 			reversed = 'middle';
176 | 	}
177 | 	opt.link.text.anchor = [ opt.link.text.anchor, reversed ];
178 | 
179 | 	this.options = opt;
180 | 	return this;
181 | };
182 | 
183 |
184 |
185 | 186 | 187 | 188 | 189 |
190 | 191 | 194 | 195 |
196 | 197 | 200 | 201 | 202 | 203 | 204 | 205 | -------------------------------------------------------------------------------- /docs/js_path_base.js.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | JSDoc: Source: js/path/base.js 6 | 7 | 8 | 9 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 |

Source: js/path/base.js

21 | 22 | 23 | 24 | 25 | 26 | 27 |
28 |
29 |
/**
30 |  * Link shapes.
31 |  * @namespace
32 |  */
33 | ge.path = new ge.SaveLoad();
34 | 
35 | /**
36 |  * Abstract base path class.
37 |  * @class
38 |  * @abstract
39 |  * @param {?object} [data]                JSON data.
40 |  * @param {?number} [data.loopStart=180]  Loop start angle in degrees.
41 |  * @param {?number} [data.loopEnd=270]    Loop end angle in degrees.
42 |  */
43 | ge.path.Path = function Path(data) { // eslint-disable-line no-unused-vars
44 | 	if(this.constructor === Path) {
45 | 		throw new Error('abstract class');
46 | 	}
47 | 	data = data || {};
48 | 	this.loopStart = new ge.Angle(+data.loopStart || 180);
49 | 	this.loopEnd = new ge.Angle(+data.loopEnd || 270);
50 | };
51 | 
52 | /* eslint-disable no-unused-vars */
53 | 
54 | /**
55 |  * Return text path and set link path.
56 |  * @abstract
57 |  * @param   {ge.Link}   link   Link data.
58 |  * @returns {string}           SVG path.
59 |  */
60 | ge.path.Path.prototype.path = function path(link) {
61 | 	throw new Error('abstract method');
62 | };
63 | 
64 | /* eslint-enable no-unused-vars */
65 | 
66 | /**
67 |  * Convert to JSON.
68 |  * @returns {object}  JSON data.
69 |  */
70 | ge.path.Path.prototype.toJson = function toJson() {
71 | 	return {
72 | 		loopStart: this.loopStart.deg,
73 | 		loopEnd: this.loopEnd.deg
74 | 	};
75 | };
76 | 
77 |
78 |
79 | 80 | 81 | 82 | 83 |
84 | 85 | 88 | 89 |
90 | 91 | 94 | 95 | 96 | 97 | 98 | 99 | -------------------------------------------------------------------------------- /docs/js_path_line.js.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | JSDoc: Source: js/path/line.js 6 | 7 | 8 | 9 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 |

Source: js/path/line.js

21 | 22 | 23 | 24 | 25 | 26 | 27 |
28 |
29 |
/**
 30 |  * Line.
 31 |  * @class
 32 |  * @param {?object}  [data]                JSON data.
 33 |  * @param {?number}  [data.loopStart=180]  Loop start angle in degrees.
 34 |  * @param {?number}  [data.loopEnd=270]    Loop end angle in degrees.
 35 |  * @param {?boolean} [data.arrow=false]
 36 |  * @param {?number}  [data.arrowLength=10]
 37 |  * @param {?number}  [data.arrowAngle=15]
 38 |  */
 39 | ge.path.Line = function Line(data) {
 40 | 	ge.path.Path.call(this, data);
 41 | 	this.arrow = !!data.arrow;
 42 | 	this.arrowLength = +data.arrowLength || 10;
 43 | 	this.arrowAngle = new ge.Angle(+data.arrowAngle || 15);
 44 | 	this.arrowAngle2 = new ge.Angle(-data.arrowAngle || -15);
 45 | };
 46 | 
 47 | ge.path.Line.prototype = Object.create(ge.path.Line);
 48 | Object.defineProperty(
 49 | 	ge.path.Line.prototype,
 50 | 	'constructor',
 51 | 	{ 
 52 | 		value: ge.path.Line,
 53 | 		enumerable: false,
 54 | 		writable: true
 55 | 	}
 56 | );
 57 | ge.path.addClass(ge.path.Line);
 58 | 
 59 | /**
 60 |  * Return arrow path.
 61 |  * @param   {ge.Point}   src   Link source.
 62 |  * @param   {ge.Point}   dst   Link target.
 63 |  * @returns {string}           SVG path.
 64 |  */
 65 | ge.path.Line.prototype.arrowPath = function arrow(src, dst) {
 66 | 	var b = src.clone().sub(dst).normalize();
 67 | 	var c = b.clone().rotate(this.arrowAngle2).mul(this.arrowLength).add(dst);
 68 | 	b.rotate(this.arrowAngle).mul(this.arrowLength).add(dst);
 69 | 	return 'M'.concat(
 70 | 		dst.x, ',', dst.y,
 71 | 		'L', b.x, ',', b.y,
 72 | 		'L', c.x, ',', c.y,
 73 | 		'Z'
 74 | 	);
 75 | };
 76 | 
 77 | /**
 78 |  * Return text path and set link path.
 79 |  * @param   {ge.Link}   link   Link data.
 80 |  * @returns {string}           SVG path.
 81 |  */
 82 | ge.path.Line.prototype.path = function path(link) {
 83 | 	var src, dst;
 84 | 
 85 | 	if(link.source === link.target) {
 86 | 		src = link.source.shape.getPoint(link.source, this.loopStart);
 87 | 		dst = link.source.shape.getPoint(link.source, this.loopEnd);
 88 | 		var rx = link.source.width / 2;
 89 | 		var ry = link.source.height / 2;
 90 | 
 91 | 		link.reversed = false;
 92 | 		link.path = 'M'.concat(
 93 | 			src.x, ',', src.y,
 94 | 			'A', rx, ',', ry, ',0,1,0,', dst.x, ',', dst.y
 95 | 		);
 96 | 
 97 | 		/*if(this.arrow) {
 98 | 		}*/
 99 | 
100 | 		return link.path;
101 | 	}
102 | 
103 | 	src = new ge.Point(link.source.x, link.source.y);
104 | 	dst = new ge.Point(link.target.x, link.target.y);
105 | 	var src2 = link.source.shape.intersect(link.source, link.target);
106 | 	var dst2 = link.target.shape.intersect(link.target, link.source);
107 | 	src = src2 || src;
108 | 	dst = dst2 || dst;
109 | 
110 | 	var dst3 = dst;
111 | 
112 | 	if(this.arrow) {
113 | 		dst3 = dst.clone().sub(src);
114 | 		var l = dst3.length();
115 | 		var l2 = this.arrowLength * this.arrowAngle.cos;
116 | 		dst3.mul(1 - l2 / l).add(src);
117 | 	}
118 | 	
119 | 	link.path = 'M'.concat(src.x, ',', src.y, 'L', dst3.x, ',', dst3.y);
120 | 
121 | 	if(this.arrow) {
122 | 		link.path += this.arrowPath(src, dst);
123 | 	}
124 | 
125 | 	if((link.reversed = src.x > dst.x)) {
126 | 		src2 = src;
127 | 		src = dst;
128 | 		dst = src2;
129 | 	}
130 | 
131 | 	return 'M'.concat(src.x, ',', src.y, 'L', dst.x, ',', dst.y);
132 | };
133 | 
134 |
135 |
136 | 137 | 138 | 139 | 140 |
141 | 142 | 145 | 146 |
147 | 148 | 151 | 152 | 153 | 154 | 155 | 156 | -------------------------------------------------------------------------------- /docs/js_shape_base.js.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | JSDoc: Source: js/shape/base.js 6 | 7 | 8 | 9 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 |

Source: js/shape/base.js

21 | 22 | 23 | 24 | 25 | 26 | 27 |
28 |
29 |
/**
 30 |  * Node shapes.
 31 |  * @namespace
 32 |  */
 33 | 
 34 | ge.shape = new ge.SaveLoad();
 35 | 
 36 | /**
 37 |  * Abstract base shape class.
 38 |  * @class
 39 |  * @abstract
 40 |  * @param {?object} [data]           JSON data.
 41 |  */
 42 | ge.shape.Shape = function Shape(data) { // eslint-disable-line no-unused-vars
 43 | 	if(this.constructor === Shape) {
 44 | 		throw new Error('abstract class');
 45 | 	}
 46 | };
 47 | 
 48 | /* eslint-disable no-unused-vars */
 49 | 
 50 | /**
 51 |  * Return SVG path.
 52 |  * @abstract
 53 |  * @param   {ge.Node}   node   Node data.
 54 |  * @returns {string}           SVG path.
 55 |  */
 56 | ge.shape.Shape.prototype.path = function path(node) {
 57 | 	throw new Error('abstract method');
 58 | };
 59 | 
 60 | /**
 61 |  * Return point at angle.
 62 |  * @abstract
 63 |  * @param   {ge.Node}   node   Node data.
 64 |  * @param   {ge.Angle}  angle  Angle.
 65 |  * @returns {ge.Point}
 66 |  */
 67 | ge.shape.Shape.prototype.getPoint = function getPoint(node, angle) {
 68 | 	throw new Error('abstract method');
 69 | };
 70 | 
 71 | /**
 72 |  * Resize a node to fit its title inside.
 73 |  * @abstract
 74 |  * @param {ge.Node}            node       Node data.
 75 |  * @param {ge.ContainerSize}   size       Text container size.
 76 |  * @param {ge.TextSize}        textSize   Text size calculator.
 77 |  */
 78 | ge.shape.Shape.prototype.doFitTitleInside = function doFitTitleInside(
 79 | 	node,
 80 | 	size,
 81 | 	textSize
 82 | ) {
 83 | 	throw new Error('abstract method');
 84 | };
 85 | 
 86 | 
 87 | /* eslint-enable no-unused-vars */
 88 | 
 89 | /**
 90 |  * Convert to JSON.
 91 |  * @returns {object}  JSON data.
 92 |  */
 93 | ge.shape.Shape.prototype.toJson = function toJson() {
 94 | 	return {};
 95 | };
 96 | 
 97 | /**
 98 |  * Intersect with a link.
 99 |  * @param   {ge.Node}   node   Node data.
100 |  * @param   {ge.Node}   node2  Node data.
101 |  * @returns {?ge.Point}        Intersection point.
102 |  */
103 | ge.shape.Shape.prototype.intersect = function intersect(node, node2) {
104 | 	var a = Math.atan2(node2.y - node.y, node2.x - node.x);
105 | 	return this.getPoint(node, new ge.Angle(a, true));
106 | };
107 | 
108 | /**
109 |  * Resize a node to fit its title inside if necessary.
110 |  * @param {ge.Node}     node       Node data.
111 |  * @param {ge.TextSize} textSize   Text size calculator.
112 |  */
113 | ge.shape.Shape.prototype.fitTitleInside = function fitTitleInside(
114 | 	node,
115 | 	textSize
116 | ) {
117 | 	if(node.title === node.prevTitle) {
118 | 		return;
119 | 	}
120 | 	var size = textSize.containerSize(node.title);
121 | 	this.doFitTitleInside(node, size, textSize);
122 | 	node.prevTitle = node.title;
123 | };
124 | 
125 |
126 |
127 | 128 | 129 | 130 | 131 |
132 | 133 | 136 | 137 |
138 | 139 | 142 | 143 | 144 | 145 | 146 | 147 | -------------------------------------------------------------------------------- /docs/js_shape_circle.js.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | JSDoc: Source: js/shape/circle.js 6 | 7 | 8 | 9 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 |

Source: js/shape/circle.js

21 | 22 | 23 | 24 | 25 | 26 | 27 |
28 |
29 |
/**
 30 |  * Circle.
 31 |  * @class
 32 |  * @param {?object} [data]           JSON data.
 33 |  */
 34 | ge.shape.Circle = function Circle(data) {
 35 | 	ge.shape.Shape.call(this, data);
 36 | };
 37 | 
 38 | ge.shape.Circle.prototype = Object.create(ge.shape.Shape.prototype);
 39 | Object.defineProperty(
 40 | 	ge.shape.Circle.prototype,
 41 | 	'constructor',
 42 | 	{ 
 43 | 		value: ge.shape.Circle,
 44 | 		enumerable: false,
 45 | 		writable: true
 46 | 	}
 47 | );
 48 | ge.shape.addClass(ge.shape.Circle);
 49 | 
 50 | /**
 51 |  * Return SVG path.
 52 |  * @param   {ge.Node}   node   Node data.
 53 |  * @returns {string}           SVG path.
 54 |  */
 55 | ge.shape.Circle.prototype.path = function path(node) {
 56 | 	var dx = node.width, rx = dx / 2, ry = node.height / 2;
 57 | 	var arc = 'a'.concat(rx, ',', ry, ',0,1,0,');
 58 | 	return 'M'.concat(-rx, ',0', arc, dx, ',0', arc, -dx, ',0');
 59 | };
 60 | 
 61 | /**
 62 |  * Return point at angle.
 63 |  * @abstract
 64 |  * @param   {ge.Node}   node   Node data.
 65 |  * @param   {ge.Angle}  angle  Angle.
 66 |  * @returns {ge.Point}
 67 |  */
 68 | ge.shape.Circle.prototype.getPoint = function getPoint(node, angle) {
 69 | 	return new ge.Point(
 70 | 		node.x + node.width * angle.cos * 0.5,
 71 | 		node.y + node.height * angle.sin * 0.5
 72 | 	);
 73 | };
 74 | 
 75 | /**
 76 |  * Resize a node to fit its title inside.
 77 |  * @abstract
 78 |  * @param {ge.Node}            node       Node data.
 79 |  * @param {ge.ContainerSize}   size       Text container size.
 80 |  * @param {ge.TextSize}        textSize   Text size calculator.
 81 |  */
 82 | ge.shape.Circle.prototype.doFitTitleInside = function doFitTitleInside(
 83 | 	node,
 84 | 	size/*,
 85 | 	textSize*/
 86 | ) {
 87 | 	node.width = node.height = Math.max(
 88 | 		Math.ceil(Math.sqrt(size.minArea * 2)),
 89 | 		size.minWidth,
 90 | 		size.minHeight
 91 | 	);
 92 | 	node.textWidth = node.textHeight = Math.ceil(node.width / 1.414213);
 93 | };
 94 | 
95 |
96 |
97 | 98 | 99 | 100 | 101 |
102 | 103 | 106 | 107 |
108 | 109 | 112 | 113 | 114 | 115 | 116 | 117 | -------------------------------------------------------------------------------- /docs/js_shape_rect.js.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | JSDoc: Source: js/shape/rect.js 6 | 7 | 8 | 9 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 |

Source: js/shape/rect.js

21 | 22 | 23 | 24 | 25 | 26 | 27 |
28 |
29 |
/**
 30 |  * Rectangle.
 31 |  * @class
 32 |  * @param {?object} [data]           JSON data.
 33 |  * @param {?number} [data.aspect=1]  Aspect ratio.
 34 |  */
 35 | ge.shape.Rect = function Rect(data) {
 36 | 	data = data || {};
 37 | 	ge.shape.Shape.call(this, data);
 38 | 	/**
 39 | 	 * Aspect ratio.
 40 | 	 * @member {number}
 41 | 	 */
 42 | 	this.aspect = data.aspect || 1;
 43 | };
 44 | 
 45 | ge.shape.Rect.prototype = Object.create(ge.shape.Shape.prototype);
 46 | Object.defineProperty(
 47 | 	ge.shape.Rect.prototype,
 48 | 	'constructor',
 49 | 	{ 
 50 | 		value: ge.shape.Rect,
 51 | 		enumerable: false,
 52 | 		writable: true
 53 | 	}
 54 | );
 55 | ge.shape.addClass(ge.shape.Rect);
 56 | 
 57 | /**
 58 |  * Convert to JSON.
 59 |  * @abstract
 60 |  * @returns {object}  JSON data.
 61 |  */
 62 | ge.shape.Rect.prototype.toJson = function toJson() {
 63 | 	return {
 64 | 		aspect: this.aspect
 65 | 	};
 66 | };
 67 | 
 68 | /**
 69 |  * Return SVG path.
 70 |  * @param   {ge.Node}   node   Node data.
 71 |  * @returns {string}           SVG path.
 72 |  */
 73 | ge.shape.Rect.prototype.path = function path(node) {
 74 | 	var w = node.width, h = node.height;
 75 | 	return 'M'.concat(
 76 | 		(-w / 2), ',', (-h / 2),
 77 | 		'l', w, ',0l0,', h, 'l', -w, ',0z'
 78 | 	);
 79 | };
 80 | 
 81 | /**
 82 |  * Return point at angle.
 83 |  * @abstract
 84 |  * @param   {ge.Node}   node   Node data.
 85 |  * @param   {ge.Angle}  angle  Angle.
 86 |  * @returns {ge.Point}
 87 |  */
 88 | ge.shape.Rect.prototype.getPoint = function getPoint(node, angle) {
 89 | 	var w = node.width / 2;
 90 | 	var h = node.height / 2;
 91 | 
 92 | 	var x, y, t;
 93 | 	var ret = new ge.Point(node.x, node.y);
 94 | 
 95 | 	x = null;
 96 | 	if(angle.cos > 1e-8) {
 97 | 		x = w;
 98 | 	}
 99 | 	else if(angle.cos < -1e-8) {
100 | 		x = -w;
101 | 	}
102 | 	if(x !== null) {
103 | 		t = x / angle.cos;
104 | 		y = t * angle.sin;
105 | 		if(y < h + 1e-8 && y > -h - 1e-8) {
106 | 			ret.x += x;
107 | 			ret.y += y;
108 | 			return ret;
109 | 		}
110 | 	}
111 | 
112 | 	y = null;
113 | 	if(angle.sin > 1e-8) {
114 | 		y = h;
115 | 	}
116 | 	else if(angle.sin < -1e-8) {
117 | 		y = -h;
118 | 	}
119 | 	if(y !== null) {
120 | 		t = y / angle.sin;
121 | 		x = t * angle.cos;
122 | 		if(x < w + 1e-8 && x > -w - 1e-8) {
123 | 			ret.x += x;
124 | 			ret.y += y;
125 | 			return ret;
126 | 		}
127 | 	}
128 | 
129 | 	// throw new Error()
130 | 	return ret;
131 | };
132 | 
133 | /**
134 |  * Resize a node to fit its title inside.
135 |  * @abstract
136 |  * @param {ge.Node}            node       Node data.
137 |  * @param {ge.ContainerSize}   size       Text container size.
138 |  * @param {ge.TextSize}        textSize   Text size calculator.
139 |  */
140 | ge.shape.Rect.prototype.doFitTitleInside = function doFitTitleInside(
141 | 	node,
142 | 	size/*,
143 | 	textSize*/
144 | ) {
145 | 	node.width = Math.max(
146 | 		Math.ceil(this.aspect * Math.sqrt(size.minArea)),
147 | 		size.minWidth
148 | 	);
149 | 	node.height = Math.max(size.minHeight, node.width / this.aspect);
150 | 	node.textWidth = node.width;
151 | 	node.textHeight = node.height;
152 | };
153 | 
154 |
155 |
156 | 157 | 158 | 159 | 160 |
161 | 162 | 165 | 166 |
167 | 168 | 171 | 172 | 173 | 174 | 175 | 176 | -------------------------------------------------------------------------------- /docs/js_util_angle.js.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | JSDoc: Source: js/util/angle.js 6 | 7 | 8 | 9 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 |

Source: js/util/angle.js

21 | 22 | 23 | 24 | 25 | 26 | 27 |
28 |
29 |
/**
30 |  * Angle constructor.
31 |  * @class
32 |  * @classdesc Angle class.
33 |  * @param {?number}   value   Angle value.
34 |  * @param {?boolean}  rad     true if value is in radians, false if in degrees.
35 |  */
36 | ge.Angle = function Angle(value, rad) {
37 | 	/**
38 | 	 * Angle in degrees.
39 | 	 * @member {number}
40 | 	 */
41 | 	this.deg = 0;
42 | 	/**
43 | 	 * Angle in radians.
44 | 	 * @member {number}
45 | 	 */
46 | 	this.rad = 0;
47 | 
48 | 	if(rad) {
49 | 		this.rad = +value || 0;
50 | 		this.deg = this.rad * 180.0 / Math.PI;
51 | 	}
52 | 	else {
53 | 		this.deg = +value || 0;
54 | 		this.rad = this.deg * Math.PI / 180.0;
55 | 	}
56 | 
57 | 	/**
58 | 	 * Sine.
59 | 	 * @member {number}
60 | 	 */
61 | 	this.sin = Math.sin(this.rad);
62 | 	/**
63 | 	 * Cosine.
64 | 	 * @member {number}
65 | 	 */
66 | 	this.cos = Math.cos(this.rad);
67 | };
68 | 
69 |
70 |
71 | 72 | 73 | 74 | 75 |
76 | 77 | 80 | 81 |
82 | 83 | 86 | 87 | 88 | 89 | 90 | 91 | -------------------------------------------------------------------------------- /docs/js_util_bbox.js.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | JSDoc: Source: js/util/bbox.js 6 | 7 | 8 | 9 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 |

Source: js/util/bbox.js

21 | 22 | 23 | 24 | 25 | 26 | 27 |
28 |
29 |
/**
30 |  * Bounding box constructor.
31 |  * @class
32 |  * @classdesc Bounding box class.
33 |  * @param {?number}  left   Left X coordinate.
34 |  * @param {?number}  top    Top Y coordinate.
35 |  * @param {?number}  right  Right X coordinate.
36 |  * @param {?number}  bottom Bottom Y coordinate.
37 |  */
38 | ge.BBox = function BBox(left, top, right, bottom) {
39 | 	/**
40 | 	 * Left X coordinate.
41 | 	 * @member {number}
42 | 	 */
43 | 	this.left = +left || 0;
44 | 	/**
45 | 	 * Top Y coordinate.
46 | 	 * @member {number}
47 | 	 */
48 | 	this.top = +top || 0;
49 | 	/**
50 | 	 * Right X coordinate.
51 | 	 * @member {number}
52 | 	 */
53 | 	this.right = +right || 0;
54 | 	/**
55 | 	 * Bottom Y coordinate.
56 | 	 * @member {number}
57 | 	 */
58 | 	this.bottom = +bottom || 0;
59 | };
60 | 
61 |
62 |
63 | 64 | 65 | 66 | 67 |
68 | 69 | 72 | 73 |
74 | 75 | 78 | 79 | 80 | 81 | 82 | 83 | -------------------------------------------------------------------------------- /docs/js_util_common.js.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | JSDoc: Source: js/util/common.js 6 | 7 | 8 | 9 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 |

Source: js/util/common.js

21 | 22 | 23 | 24 | 25 | 26 | 27 |
28 |
29 |
/**
 30 |  * Get object ID.
 31 |  * @param {?(Object|undefined)} obj
 32 |  * @returns {?ID}
 33 |  */
 34 | ge.id = function id(obj) {
 35 | 	return obj && obj.id || null;
 36 | };
 37 | 
 38 | /**
 39 |  * @param {Object} _this
 40 |  * @param {function} func
 41 |  * @returns {function}
 42 |  */
 43 | ge.bind = function bind(_this, func) {
 44 | 	return function bound() {
 45 | 		return func.apply(_this, arguments);
 46 | 	};
 47 | };
 48 | 
 49 | /**
 50 |  * Compare numbers or arrays of numbers.
 51 |  * @param {(number|Array<number>)} u
 52 |  * @param {(number|Array<number>)} v
 53 |  * @param {number}                 [eps=1e-5] Precision.
 54 |  * @returns {boolean}
 55 |  */
 56 | ge.equal = function equal(u, v, eps) {
 57 | 	eps = eps || 1e-5;
 58 | 	var eq = function(x, y) { return Math.abs(x - y) < eps; };
 59 | 
 60 | 	if(u === null || v === null
 61 | 	   || u === undefined || v === undefined
 62 | 	   || typeof u === 'number' && Array.isArray(v)
 63 | 	   || typeof v === 'number' && Array.isArray(u)) {
 64 | 		return false;
 65 | 	}
 66 | 
 67 | 	if(typeof u === 'number' && typeof v === 'number') {
 68 | 		return eq(u, v);
 69 | 	}
 70 | 
 71 | 	if(!Array.isArray(u) || !Array.isArray(v)) {
 72 | 		throw new Error(
 73 | 			'ge.equal: invalid argument type: '
 74 | 				.concat(u.toString(), ' ', v.toString())
 75 | 		);
 76 | 	}
 77 | 
 78 | 	if(u.length !== v.length) {
 79 | 		return false;
 80 | 	}
 81 | 
 82 | 	for(var i = 0; i < u.length; ++i) {
 83 | 		if(!eq(u[i], v[i])) {
 84 | 			return false;
 85 | 		}
 86 | 	}
 87 | 
 88 | 	return true;
 89 | };
 90 | 
 91 | /**
 92 |  * Default simulation update function.
 93 |  * @param   {?D3Simulation} simulation  Old simulation object.
 94 |  * @param   {Array<ge.Node>}   nodes    Graph nodes.
 95 |  * @param   {Array<ge.Link>}   links    Graph links.
 96 |  * @this    ge.GraphEditor
 97 |  * @returns {D3Simulation}              New/updated simulation object.
 98 |  */
 99 | ge.defaultSimulation = function defaultSimulation(simulation, nodes, links) {
100 | 	if(!simulation) {
101 | 		simulation = d3.forceSimulation()
102 | 			.force('charge', d3.forceManyBody())
103 | 			.force('link', d3.forceLink())
104 | 			.force('center', d3.forceCenter());
105 | 	}
106 | 
107 | 	var dist = 2 * d3.max(nodes, function(d) {
108 | 		return Math.max(d.width, d.height);
109 | 	});
110 | 
111 | 	var cx = d3.mean(nodes, function(d) { return d.x; });
112 | 	var cy = d3.mean(nodes, function(d) { return d.y; });
113 | 
114 | 	simulation.nodes(nodes);
115 | 	simulation.force('center').x(cx).y(cy);
116 | 	simulation.force('link').links(links).distance(dist);
117 | 
118 | 	return simulation;
119 | };
120 | 
121 | /**
122 |  * Extend an object.
123 |  * @param   {object}  dst  Object to extend.
124 |  * @param   {object}  src  Source object.
125 |  * @returns {object}       Extended object.
126 |  */
127 | ge._extend = function _extend(dst, src) {
128 | 	if(!src) {
129 | 		return dst;
130 | 	}
131 | 	if(typeof src !== 'object') {
132 | 		throw new Error('src is not an object: ' + src.toString());
133 | 	}
134 | 
135 | 	if(!dst) {
136 | 		dst = {};
137 | 	}
138 | 	else if(typeof dst !== 'object') {
139 | 		throw new Error('dst is not an object: ' + dst.toString());
140 | 	}
141 | 
142 | 	for(var key in src) {
143 | 		var value = src[key];
144 | 		if(typeof value === 'object' && value !== null) {
145 | 			if(Array.isArray(value)) {
146 | 				dst[key] = value.slice();
147 | 			}
148 | 			else if(value.constructor.name !== 'Object') {
149 | 				dst[key] = value;
150 | 			}
151 | 			else {
152 | 				var dstValue = dst[key];
153 | 				if(!dstValue
154 | 				   || typeof dstValue !== 'object'
155 | 				   || Array.isArray(dstValue)) {
156 | 					dst[key] = dstValue = {};
157 | 				}
158 | 				ge._extend(dstValue, value);
159 | 			}
160 | 		}
161 | 		else {
162 | 			dst[key] = value;
163 | 		}
164 | 	}
165 | 	return dst;
166 | };
167 | 
168 | /**
169 |  * Extend an object.
170 |  * @param   {object}     dst  Object to extend.
171 |  * @param   {...object}  src  Source objects.
172 |  * @returns {object}          Extended object.
173 |  */
174 | ge.extend = function extend(dst, src) { // eslint-disable-line no-unused-vars
175 | 	for(var i = 1; i < arguments.length; ++i) {
176 | 		dst = ge._extend(dst, arguments[i]);
177 | 	}
178 | 	return dst;
179 | };
180 | 
181 | /*ge.debounceD3 = function debounceD3(func, delay) {
182 | 	var timeout = null;
183 | 
184 | 	return function() {
185 | 		var context = this;
186 | 		var args = arguments;
187 | 		var d3ev = d3.event;
188 | 
189 | 		if(timeout !== null) {
190 | 			clearTimeout(timeout);
191 | 		}
192 | 
193 | 		timeout = setTimeout(
194 | 			function() {
195 | 				var tmp = d3.event;
196 | 				timeout = null;
197 | 				d3.event = d3ev;
198 | 				try {
199 | 					func.apply(context, args);
200 | 				}
201 | 				finally {
202 | 					d3.event = tmp;
203 | 				}
204 | 			},
205 | 			delay
206 | 		);
207 | 	};
208 | };*/
209 | 
210 |
211 |
212 | 213 | 214 | 215 | 216 |
217 | 218 | 221 | 222 |
223 | 224 | 227 | 228 | 229 | 230 | 231 | 232 | -------------------------------------------------------------------------------- /docs/js_util_container-size.js.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | JSDoc: Source: js/util/container-size.js 6 | 7 | 8 | 9 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 |

Source: js/util/container-size.js

21 | 22 | 23 | 24 | 25 | 26 | 27 |
28 |
29 |
/**
30 |  * Container size constructor.
31 |  * @class
32 |  * @classdesc Point class.
33 |  * @param {?number}  [minWidth=1]                   Minimum width.
34 |  * @param {?number}  [minHeight=1]                  Minimum height.
35 |  * @param {?number}  [minArea=minWidth*minHeight]   Minimum area.
36 |  */
37 | ge.ContainerSize = function ContainerSize(minWidth, minHeight, minArea) {
38 | 	/**
39 | 	 * Minimum width.
40 | 	 * @member {number}
41 | 	 */
42 | 	this.minWidth = +minWidth || 1;
43 | 	/**
44 | 	 * Minimum height.
45 | 	 * @member {number}
46 | 	 */
47 | 	this.minHeight = +minHeight || 1;
48 | 	/**
49 | 	 * Minimum area.
50 | 	 * @member {number}
51 | 	 */
52 | 	this.minArea = +minArea || this.minWidth * this.minHeight;
53 | };
54 | 
55 |
56 |
57 | 58 | 59 | 60 | 61 |
62 | 63 | 66 | 67 |
68 | 69 | 72 | 73 | 74 | 75 | 76 | 77 | -------------------------------------------------------------------------------- /docs/js_util_point.js.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | JSDoc: Source: js/util/point.js 6 | 7 | 8 | 9 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 |

Source: js/util/point.js

21 | 22 | 23 | 24 | 25 | 26 | 27 |
28 |
29 |
/**
 30 |  * Point constructor.
 31 |  * @class
 32 |  * @classdesc Point class.
 33 |  * @param {?number}  x  X coordinate.
 34 |  * @param {?number}  y  Y coordinate.
 35 |  */
 36 | ge.Point = function Point(x, y) {
 37 | 	/**
 38 | 	 * X coordinate.
 39 | 	 * @member {number}
 40 | 	 */
 41 | 	this.x = +x || 0;
 42 | 	/**
 43 | 	 * Y coordinate.
 44 | 	 * @member {number}
 45 | 	 */
 46 | 	this.y = +y || 0;
 47 | };
 48 | 
 49 | /**
 50 |  * Add.
 51 |  * @param {ge.Point} p
 52 |  * @returns {ge.Point}
 53 |  */
 54 | ge.Point.prototype.add = function add(p) {
 55 | 	this.x += p.x;
 56 | 	this.y += p.y;
 57 | 	return this;
 58 | };
 59 | 
 60 | /**
 61 |  * Subtract.
 62 |  * @param {ge.Point} p
 63 |  * @returns {ge.Point}
 64 |  */
 65 | ge.Point.prototype.sub = function sub(p) {
 66 | 	this.x -= p.x;
 67 | 	this.y -= p.y;
 68 | 	return this;
 69 | };
 70 | 
 71 | /**
 72 |  * Multiply.
 73 |  * @param {number} k
 74 |  * @returns {ge.Point}
 75 |  */
 76 | ge.Point.prototype.mul = function mul(k) {
 77 | 	this.x *= k;
 78 | 	this.y *= k;
 79 | 	return this;
 80 | };
 81 | 
 82 | /**
 83 |  * Return vector length.
 84 |  * @returns {number}
 85 |  */
 86 | ge.Point.prototype.length = function length() {
 87 | 	return Math.sqrt(this.x * this.x + this.y * this.y);
 88 | };
 89 | 
 90 | /**
 91 |  * Normalize.
 92 |  * @returns {ge.Point}
 93 |  */
 94 | ge.Point.prototype.normalize = function normalize() {
 95 | 	var l = this.length();
 96 | 	if(l > 1e-8) {
 97 | 		this.x /= l;
 98 | 		this.y /= l;
 99 | 	}
100 | 	return this;
101 | };
102 | 
103 | /**
104 |  * Rotate.
105 |  * @param {ge.Angle} a
106 |  * @returns {ge.Point}
107 |  */
108 | ge.Point.prototype.rotate = function rotate(a) {
109 | 	var x = this.x, y = this.y;
110 | 	this.x = x * a.cos - y * a.sin;
111 | 	this.y = x * a.sin + y * a.cos;
112 | 	return this;
113 | };
114 | 
115 | /**
116 |  * Clone.
117 |  * @returns {ge.Point}
118 |  */
119 | ge.Point.prototype.clone = function clone() {
120 | 	return new ge.Point(this.x, this.y);
121 | };
122 | 
123 |
124 |
125 | 126 | 127 | 128 | 129 |
130 | 131 | 134 | 135 |
136 | 137 | 140 | 141 | 142 | 143 | 144 | 145 | -------------------------------------------------------------------------------- /docs/js_util_save-load.js.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | JSDoc: Source: js/util/save-load.js 6 | 7 | 8 | 9 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 |

Source: js/util/save-load.js

21 | 22 | 23 | 24 | 25 | 26 | 27 |
28 |
29 |
/**
 30 |  * Class for saving/loading JSON.
 31 |  * @class
 32 |  * @param {?number}  x  X coordinate.
 33 |  * @param {?number}  y  Y coordinate.
 34 |  */
 35 | ge.SaveLoad = function SaveLoad() {
 36 | 	/**
 37 | 	 * Class constructors by name.
 38 | 	 * @member {object}
 39 | 	 */
 40 | 	this.classes = {};
 41 | };
 42 | 
 43 | /**
 44 |  * Add a class.
 45 |  * @param {function} constructor              Class constructor.
 46 |  * @param {string}   [name=constructor.name]  Class name.
 47 |  * @param {boolean}  [overwrite=false]        Overwrite if class exists.
 48 |  */
 49 | ge.SaveLoad.prototype.addClass = function addClass(
 50 | 	constructor,
 51 | 	name,
 52 | 	overwrite
 53 | ) {
 54 | 	name = name || constructor.name;
 55 | 	if(this.classes[name] && !overwrite) {
 56 | 		throw new Error('class "' + name +'" exists');
 57 | 	}
 58 | 	this.classes[name] = constructor;
 59 | };
 60 | 
 61 | /**
 62 |  * Get class by name.
 63 |  * @param   {string}   name  Class name.
 64 |  * @returns {function}       Class constructor.
 65 |  */
 66 | ge.SaveLoad.prototype.getClass = function getClass(name) {
 67 | 	var ret = this.classes[name];
 68 | 	if(!ret) {
 69 | 		throw new Error('class "' + name +'" not found');
 70 | 	}
 71 | 	return ret;
 72 | };
 73 | 
 74 | /**
 75 |  * Load from JSON.
 76 |  * @param   {object}  data     JSON data.
 77 |  * @returns {object}
 78 |  */
 79 | ge.SaveLoad.prototype.fromJson = function fromJson(data) {
 80 | 	var cls = this.getClass(data['class']);
 81 | 	return new cls(data);
 82 | };
 83 | 
 84 | /**
 85 |  * Save to JSON.
 86 |  * @param   {object}  obj                          Object to save.
 87 |  * @param   {string}  [name=obj.constructor.name]  Class name.
 88 |  * @returns {object}
 89 |  */
 90 | ge.SaveLoad.prototype.toJson = function toJson(obj, name) {
 91 | 	var ret = obj.toJson();
 92 | 	ret['class'] = name || obj.constructor.name;
 93 | 	return ret;
 94 | };
 95 | 
96 |
97 |
98 | 99 | 100 | 101 | 102 |
103 | 104 | 107 | 108 |
109 | 110 | 113 | 114 | 115 | 116 | 117 | 118 | -------------------------------------------------------------------------------- /docs/js_util_text-size.js.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | JSDoc: Source: js/util/text-size.js 6 | 7 | 8 | 9 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 |

Source: js/util/text-size.js

21 | 22 | 23 | 24 | 25 | 26 | 27 |
28 |
29 |
/**
 30 |  * Text size calculator constructor.
 31 |  * @class
 32 |  * @classdesc Text size calculator class.
 33 |  * @param {SVGElement} el  SVG <text> element.
 34 |  */
 35 | ge.TextSize = function TextSize(el) {
 36 | 	el.setAttribute('style', 'stroke:none;fill:none');
 37 | 	this.el = el;
 38 | 	return this;
 39 | };
 40 | 
 41 | /**
 42 |  * Compute text width.
 43 |  * @param {string} text
 44 |  * @returns {number}
 45 |  */
 46 | ge.TextSize.prototype.width = function length(text) {
 47 | 	this.el.textContent = text;
 48 | 	return this.el.getComputedTextLength();
 49 | };
 50 | 
 51 | /**
 52 |  * Compute text width and height.
 53 |  * @param {string} text
 54 |  * @returns {ge.Point}
 55 |  */
 56 | ge.TextSize.prototype.size = function size(text) {
 57 | 	this.el.textContent = text;
 58 | 	var bbox = this.el.getBBox();
 59 | 	return new ge.Point(bbox.width, bbox.height);
 60 | };
 61 | 
 62 | /**
 63 |  * Compute text container size.
 64 |  * @param {string} text
 65 |  * @returns {ge.ContainerSize}
 66 |  */
 67 | ge.TextSize.prototype.containerSize = function containerSize(text) {
 68 | 	var size = this.size(text);
 69 | 	var words = text.split(/\s+/);
 70 | 	var width;
 71 | 	if(words.length == 1) {
 72 | 		width = size.x;
 73 | 	}
 74 | 	else {
 75 | 		width = 0;
 76 | 		var self = this;
 77 | 		words.forEach(function(word) {
 78 | 			width = Math.max(width, self.width(word));
 79 | 		});
 80 | 	}
 81 | 	return new ge.ContainerSize(
 82 | 		width,
 83 | 		size.y,
 84 | 		size.x * size.y
 85 | 	);
 86 | };
 87 | 
88 |
89 |
90 | 91 | 92 | 93 | 94 |
95 | 96 | 99 | 100 |
101 | 102 | 105 | 106 | 107 | 108 | 109 | 110 | -------------------------------------------------------------------------------- /docs/main.js.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | JSDoc: Source: main.js 6 | 7 | 8 | 9 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 |

Source: main.js

21 | 22 | 23 | 24 | 25 | 26 | 27 |
28 |
29 |
'use strict';
 30 | 
 31 | /* eslint-disable no-unused-vars */
 32 | 
 33 | /**
 34 |  * Graph editor module.
 35 |  * @namespace
 36 |  */
 37 | var ge = exports;
 38 | 
 39 | /**
 40 |  * Graph editor constructor.
 41 |  * @class
 42 |  * @classdesc Graph editor class.
 43 |  * @param {(SVGElement|Selector|D3Selection)} svg     SVG element.
 44 |  * @param {ImportGraphData}                   data    Graph data.
 45 |  * @param {GraphOptions}                      options Graph options.
 46 |  */
 47 | ge.GraphEditor = function GraphEditor(svg, data, options) {
 48 | 	if(!svg.select) {
 49 | 		svg = d3.select(svg);
 50 | 	}
 51 | 
 52 | 	this.initOptions(options, svg)
 53 | 		.initSvg(svg)
 54 | 		.initData(data)
 55 | 		.initState();
 56 | 
 57 | 	/**
 58 | 	 * @readonly
 59 | 	 * @member {D3Dispatch}
 60 | 	 */
 61 | 	this.dispatch = d3.dispatch(
 62 | 		'node-click', 'link-click',
 63 | 		'new-link-start', 'new-link-end', 'new-link-cancel',
 64 | 		'click',
 65 | 		'simulation-start', 'simulation-stop'
 66 | 	);
 67 | 
 68 | 	/**
 69 | 	 * Graph bounding box.
 70 | 	 * @readonly
 71 | 	 * @member {BBox}
 72 | 	 */
 73 | 	this.bbox = [[0, 0], [0, 0]];
 74 | 	/**
 75 | 	 * Graph zoom behavior.
 76 | 	 * @readonly
 77 | 	 * @member {D3Zoom}
 78 | 	 */
 79 | 	this.zoom = this.zoomEvents(d3.zoom())
 80 | 		.scaleExtent([this.options.scale.min, this.options.scale.max]);
 81 | 	/**
 82 | 	 * Node drag behavior.
 83 | 	 * @readonly
 84 | 	 * @member {D3Drag}
 85 | 	 */
 86 | 	this.drag = this.dragEvents(d3.drag());
 87 | 	this.svg.call(this.zoom);
 88 | 
 89 | 	/**
 90 | 	 * Window resize event handler.
 91 | 	 * @readonly
 92 | 	 * @member {function}
 93 | 	 */
 94 | 	this.onresize = ge.bind(this, this.resized);
 95 | 	window.addEventListener('resize', this.onresize);
 96 | 
 97 | 	this.updateBBox();
 98 | 
 99 | 	setTimeout(ge.bind(this, function() {
100 | 		this.update()
101 | 			.resized()
102 | 			.simulation(this.options.simulation.start);
103 | 	}), 10);
104 | 
105 | 	return this;
106 | };
107 | 
108 | /* eslint-enable no-unused-vars */
109 | 
110 |
111 |
112 | 113 | 114 | 115 | 116 |
117 | 118 | 121 | 122 |
123 | 124 | 127 | 128 | 129 | 130 | 131 | 132 | -------------------------------------------------------------------------------- /docs/scripts/linenumber.js: -------------------------------------------------------------------------------- 1 | /*global document */ 2 | (function() { 3 | var source = document.getElementsByClassName('prettyprint source linenums'); 4 | var i = 0; 5 | var lineNumber = 0; 6 | var lineId; 7 | var lines; 8 | var totalLines; 9 | var anchorHash; 10 | 11 | if (source && source[0]) { 12 | anchorHash = document.location.hash.substring(1); 13 | lines = source[0].getElementsByTagName('li'); 14 | totalLines = lines.length; 15 | 16 | for (; i < totalLines; i++) { 17 | lineNumber++; 18 | lineId = 'line' + lineNumber; 19 | lines[i].id = lineId; 20 | if (lineId === anchorHash) { 21 | lines[i].className += ' selected'; 22 | } 23 | } 24 | } 25 | })(); 26 | -------------------------------------------------------------------------------- /docs/scripts/prettify/lang-css.js: -------------------------------------------------------------------------------- 1 | PR.registerLangHandler(PR.createSimpleLexer([["pln",/^[\t\n\f\r ]+/,null," \t\r\n "]],[["str",/^"(?:[^\n\f\r"\\]|\\(?:\r\n?|\n|\f)|\\[\S\s])*"/,null],["str",/^'(?:[^\n\f\r'\\]|\\(?:\r\n?|\n|\f)|\\[\S\s])*'/,null],["lang-css-str",/^url\(([^"')]*)\)/i],["kwd",/^(?:url|rgb|!important|@import|@page|@media|@charset|inherit)(?=[^\w-]|$)/i,null],["lang-css-kw",/^(-?(?:[_a-z]|\\[\da-f]+ ?)(?:[\w-]|\\\\[\da-f]+ ?)*)\s*:/i],["com",/^\/\*[^*]*\*+(?:[^*/][^*]*\*+)*\//],["com", 2 | /^(?:<\!--|--\>)/],["lit",/^(?:\d+|\d*\.\d+)(?:%|[a-z]+)?/i],["lit",/^#[\da-f]{3,6}/i],["pln",/^-?(?:[_a-z]|\\[\da-f]+ ?)(?:[\w-]|\\\\[\da-f]+ ?)*/i],["pun",/^[^\s\w"']+/]]),["css"]);PR.registerLangHandler(PR.createSimpleLexer([],[["kwd",/^-?(?:[_a-z]|\\[\da-f]+ ?)(?:[\w-]|\\\\[\da-f]+ ?)*/i]]),["css-kw"]);PR.registerLangHandler(PR.createSimpleLexer([],[["str",/^[^"')]+/]]),["css-str"]); 3 | -------------------------------------------------------------------------------- /docs/styles/jsdoc-default.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'Open Sans'; 3 | font-weight: normal; 4 | font-style: normal; 5 | src: url('../fonts/OpenSans-Regular-webfont.eot'); 6 | src: 7 | local('Open Sans'), 8 | local('OpenSans'), 9 | url('../fonts/OpenSans-Regular-webfont.eot?#iefix') format('embedded-opentype'), 10 | url('../fonts/OpenSans-Regular-webfont.woff') format('woff'), 11 | url('../fonts/OpenSans-Regular-webfont.svg#open_sansregular') format('svg'); 12 | } 13 | 14 | @font-face { 15 | font-family: 'Open Sans Light'; 16 | font-weight: normal; 17 | font-style: normal; 18 | src: url('../fonts/OpenSans-Light-webfont.eot'); 19 | src: 20 | local('Open Sans Light'), 21 | local('OpenSans Light'), 22 | url('../fonts/OpenSans-Light-webfont.eot?#iefix') format('embedded-opentype'), 23 | url('../fonts/OpenSans-Light-webfont.woff') format('woff'), 24 | url('../fonts/OpenSans-Light-webfont.svg#open_sanslight') format('svg'); 25 | } 26 | 27 | html 28 | { 29 | overflow: auto; 30 | background-color: #fff; 31 | font-size: 14px; 32 | } 33 | 34 | body 35 | { 36 | font-family: 'Open Sans', sans-serif; 37 | line-height: 1.5; 38 | color: #4d4e53; 39 | background-color: white; 40 | } 41 | 42 | a, a:visited, a:active { 43 | color: #0095dd; 44 | text-decoration: none; 45 | } 46 | 47 | a:hover { 48 | text-decoration: underline; 49 | } 50 | 51 | header 52 | { 53 | display: block; 54 | padding: 0px 4px; 55 | } 56 | 57 | tt, code, kbd, samp { 58 | font-family: Consolas, Monaco, 'Andale Mono', monospace; 59 | } 60 | 61 | .class-description { 62 | font-size: 130%; 63 | line-height: 140%; 64 | margin-bottom: 1em; 65 | margin-top: 1em; 66 | } 67 | 68 | .class-description:empty { 69 | margin: 0; 70 | } 71 | 72 | #main { 73 | float: left; 74 | width: 70%; 75 | } 76 | 77 | article dl { 78 | margin-bottom: 40px; 79 | } 80 | 81 | article img { 82 | max-width: 100%; 83 | } 84 | 85 | section 86 | { 87 | display: block; 88 | background-color: #fff; 89 | padding: 12px 24px; 90 | border-bottom: 1px solid #ccc; 91 | margin-right: 30px; 92 | } 93 | 94 | .variation { 95 | display: none; 96 | } 97 | 98 | .signature-attributes { 99 | font-size: 60%; 100 | color: #aaa; 101 | font-style: italic; 102 | font-weight: lighter; 103 | } 104 | 105 | nav 106 | { 107 | display: block; 108 | float: right; 109 | margin-top: 28px; 110 | width: 30%; 111 | box-sizing: border-box; 112 | border-left: 1px solid #ccc; 113 | padding-left: 16px; 114 | } 115 | 116 | nav ul { 117 | font-family: 'Lucida Grande', 'Lucida Sans Unicode', arial, sans-serif; 118 | font-size: 100%; 119 | line-height: 17px; 120 | padding: 0; 121 | margin: 0; 122 | list-style-type: none; 123 | } 124 | 125 | nav ul a, nav ul a:visited, nav ul a:active { 126 | font-family: Consolas, Monaco, 'Andale Mono', monospace; 127 | line-height: 18px; 128 | color: #4D4E53; 129 | } 130 | 131 | nav h3 { 132 | margin-top: 12px; 133 | } 134 | 135 | nav li { 136 | margin-top: 6px; 137 | } 138 | 139 | footer { 140 | display: block; 141 | padding: 6px; 142 | margin-top: 12px; 143 | font-style: italic; 144 | font-size: 90%; 145 | } 146 | 147 | h1, h2, h3, h4 { 148 | font-weight: 200; 149 | margin: 0; 150 | } 151 | 152 | h1 153 | { 154 | font-family: 'Open Sans Light', sans-serif; 155 | font-size: 48px; 156 | letter-spacing: -2px; 157 | margin: 12px 24px 20px; 158 | } 159 | 160 | h2, h3.subsection-title 161 | { 162 | font-size: 30px; 163 | font-weight: 700; 164 | letter-spacing: -1px; 165 | margin-bottom: 12px; 166 | } 167 | 168 | h3 169 | { 170 | font-size: 24px; 171 | letter-spacing: -0.5px; 172 | margin-bottom: 12px; 173 | } 174 | 175 | h4 176 | { 177 | font-size: 18px; 178 | letter-spacing: -0.33px; 179 | margin-bottom: 12px; 180 | color: #4d4e53; 181 | } 182 | 183 | h5, .container-overview .subsection-title 184 | { 185 | font-size: 120%; 186 | font-weight: bold; 187 | letter-spacing: -0.01em; 188 | margin: 8px 0 3px 0; 189 | } 190 | 191 | h6 192 | { 193 | font-size: 100%; 194 | letter-spacing: -0.01em; 195 | margin: 6px 0 3px 0; 196 | font-style: italic; 197 | } 198 | 199 | table 200 | { 201 | border-spacing: 0; 202 | border: 0; 203 | border-collapse: collapse; 204 | } 205 | 206 | td, th 207 | { 208 | border: 1px solid #ddd; 209 | margin: 0px; 210 | text-align: left; 211 | vertical-align: top; 212 | padding: 4px 6px; 213 | display: table-cell; 214 | } 215 | 216 | thead tr 217 | { 218 | background-color: #ddd; 219 | font-weight: bold; 220 | } 221 | 222 | th { border-right: 1px solid #aaa; } 223 | tr > th:last-child { border-right: 1px solid #ddd; } 224 | 225 | .ancestors, .attribs { color: #999; } 226 | .ancestors a, .attribs a 227 | { 228 | color: #999 !important; 229 | text-decoration: none; 230 | } 231 | 232 | .clear 233 | { 234 | clear: both; 235 | } 236 | 237 | .important 238 | { 239 | font-weight: bold; 240 | color: #950B02; 241 | } 242 | 243 | .yes-def { 244 | text-indent: -1000px; 245 | } 246 | 247 | .type-signature { 248 | color: #aaa; 249 | } 250 | 251 | .name, .signature { 252 | font-family: Consolas, Monaco, 'Andale Mono', monospace; 253 | } 254 | 255 | .details { margin-top: 14px; border-left: 2px solid #DDD; } 256 | .details dt { width: 120px; float: left; padding-left: 10px; padding-top: 6px; } 257 | .details dd { margin-left: 70px; } 258 | .details ul { margin: 0; } 259 | .details ul { list-style-type: none; } 260 | .details li { margin-left: 30px; padding-top: 6px; } 261 | .details pre.prettyprint { margin: 0 } 262 | .details .object-value { padding-top: 0; } 263 | 264 | .description { 265 | margin-bottom: 1em; 266 | margin-top: 1em; 267 | } 268 | 269 | .code-caption 270 | { 271 | font-style: italic; 272 | font-size: 107%; 273 | margin: 0; 274 | } 275 | 276 | .prettyprint 277 | { 278 | border: 1px solid #ddd; 279 | width: 80%; 280 | overflow: auto; 281 | } 282 | 283 | .prettyprint.source { 284 | width: inherit; 285 | } 286 | 287 | .prettyprint code 288 | { 289 | font-size: 100%; 290 | line-height: 18px; 291 | display: block; 292 | padding: 4px 12px; 293 | margin: 0; 294 | background-color: #fff; 295 | color: #4D4E53; 296 | } 297 | 298 | .prettyprint code span.line 299 | { 300 | display: inline-block; 301 | } 302 | 303 | .prettyprint.linenums 304 | { 305 | padding-left: 70px; 306 | -webkit-user-select: none; 307 | -moz-user-select: none; 308 | -ms-user-select: none; 309 | user-select: none; 310 | } 311 | 312 | .prettyprint.linenums ol 313 | { 314 | padding-left: 0; 315 | } 316 | 317 | .prettyprint.linenums li 318 | { 319 | border-left: 3px #ddd solid; 320 | } 321 | 322 | .prettyprint.linenums li.selected, 323 | .prettyprint.linenums li.selected * 324 | { 325 | background-color: lightyellow; 326 | } 327 | 328 | .prettyprint.linenums li * 329 | { 330 | -webkit-user-select: text; 331 | -moz-user-select: text; 332 | -ms-user-select: text; 333 | user-select: text; 334 | } 335 | 336 | .params .name, .props .name, .name code { 337 | color: #4D4E53; 338 | font-family: Consolas, Monaco, 'Andale Mono', monospace; 339 | font-size: 100%; 340 | } 341 | 342 | .params td.description > p:first-child, 343 | .props td.description > p:first-child 344 | { 345 | margin-top: 0; 346 | padding-top: 0; 347 | } 348 | 349 | .params td.description > p:last-child, 350 | .props td.description > p:last-child 351 | { 352 | margin-bottom: 0; 353 | padding-bottom: 0; 354 | } 355 | 356 | .disabled { 357 | color: #454545; 358 | } 359 | -------------------------------------------------------------------------------- /docs/styles/prettify-jsdoc.css: -------------------------------------------------------------------------------- 1 | /* JSDoc prettify.js theme */ 2 | 3 | /* plain text */ 4 | .pln { 5 | color: #000000; 6 | font-weight: normal; 7 | font-style: normal; 8 | } 9 | 10 | /* string content */ 11 | .str { 12 | color: #006400; 13 | font-weight: normal; 14 | font-style: normal; 15 | } 16 | 17 | /* a keyword */ 18 | .kwd { 19 | color: #000000; 20 | font-weight: bold; 21 | font-style: normal; 22 | } 23 | 24 | /* a comment */ 25 | .com { 26 | font-weight: normal; 27 | font-style: italic; 28 | } 29 | 30 | /* a type name */ 31 | .typ { 32 | color: #000000; 33 | font-weight: normal; 34 | font-style: normal; 35 | } 36 | 37 | /* a literal value */ 38 | .lit { 39 | color: #006400; 40 | font-weight: normal; 41 | font-style: normal; 42 | } 43 | 44 | /* punctuation */ 45 | .pun { 46 | color: #000000; 47 | font-weight: bold; 48 | font-style: normal; 49 | } 50 | 51 | /* lisp open bracket */ 52 | .opn { 53 | color: #000000; 54 | font-weight: bold; 55 | font-style: normal; 56 | } 57 | 58 | /* lisp close bracket */ 59 | .clo { 60 | color: #000000; 61 | font-weight: bold; 62 | font-style: normal; 63 | } 64 | 65 | /* a markup tag name */ 66 | .tag { 67 | color: #006400; 68 | font-weight: normal; 69 | font-style: normal; 70 | } 71 | 72 | /* a markup attribute name */ 73 | .atn { 74 | color: #006400; 75 | font-weight: normal; 76 | font-style: normal; 77 | } 78 | 79 | /* a markup attribute value */ 80 | .atv { 81 | color: #006400; 82 | font-weight: normal; 83 | font-style: normal; 84 | } 85 | 86 | /* a declaration */ 87 | .dec { 88 | color: #000000; 89 | font-weight: bold; 90 | font-style: normal; 91 | } 92 | 93 | /* a variable name */ 94 | .var { 95 | color: #000000; 96 | font-weight: normal; 97 | font-style: normal; 98 | } 99 | 100 | /* a function name */ 101 | .fun { 102 | color: #000000; 103 | font-weight: bold; 104 | font-style: normal; 105 | } 106 | 107 | /* Specify class=linenums on a pre to get line numbering */ 108 | ol.linenums { 109 | margin-top: 0; 110 | margin-bottom: 0; 111 | } 112 | -------------------------------------------------------------------------------- /docs/styles/prettify-tomorrow.css: -------------------------------------------------------------------------------- 1 | /* Tomorrow Theme */ 2 | /* Original theme - https://github.com/chriskempson/tomorrow-theme */ 3 | /* Pretty printing styles. Used with prettify.js. */ 4 | /* SPAN elements with the classes below are added by prettyprint. */ 5 | /* plain text */ 6 | .pln { 7 | color: #4d4d4c; } 8 | 9 | @media screen { 10 | /* string content */ 11 | .str { 12 | color: #718c00; } 13 | 14 | /* a keyword */ 15 | .kwd { 16 | color: #8959a8; } 17 | 18 | /* a comment */ 19 | .com { 20 | color: #8e908c; } 21 | 22 | /* a type name */ 23 | .typ { 24 | color: #4271ae; } 25 | 26 | /* a literal value */ 27 | .lit { 28 | color: #f5871f; } 29 | 30 | /* punctuation */ 31 | .pun { 32 | color: #4d4d4c; } 33 | 34 | /* lisp open bracket */ 35 | .opn { 36 | color: #4d4d4c; } 37 | 38 | /* lisp close bracket */ 39 | .clo { 40 | color: #4d4d4c; } 41 | 42 | /* a markup tag name */ 43 | .tag { 44 | color: #c82829; } 45 | 46 | /* a markup attribute name */ 47 | .atn { 48 | color: #f5871f; } 49 | 50 | /* a markup attribute value */ 51 | .atv { 52 | color: #3e999f; } 53 | 54 | /* a declaration */ 55 | .dec { 56 | color: #f5871f; } 57 | 58 | /* a variable name */ 59 | .var { 60 | color: #c82829; } 61 | 62 | /* a function name */ 63 | .fun { 64 | color: #4271ae; } } 65 | /* Use higher contrast and text-weight for printable form. */ 66 | @media print, projection { 67 | .str { 68 | color: #060; } 69 | 70 | .kwd { 71 | color: #006; 72 | font-weight: bold; } 73 | 74 | .com { 75 | color: #600; 76 | font-style: italic; } 77 | 78 | .typ { 79 | color: #404; 80 | font-weight: bold; } 81 | 82 | .lit { 83 | color: #044; } 84 | 85 | .pun, .opn, .clo { 86 | color: #440; } 87 | 88 | .tag { 89 | color: #006; 90 | font-weight: bold; } 91 | 92 | .atn { 93 | color: #404; } 94 | 95 | .atv { 96 | color: #060; } } 97 | /* Style */ 98 | /* 99 | pre.prettyprint { 100 | background: white; 101 | font-family: Consolas, Monaco, 'Andale Mono', monospace; 102 | font-size: 12px; 103 | line-height: 1.5; 104 | border: 1px solid #ccc; 105 | padding: 10px; } 106 | */ 107 | 108 | /* Specify class=linenums on a pre to get line numbering */ 109 | ol.linenums { 110 | margin-top: 0; 111 | margin-bottom: 0; } 112 | 113 | /* IE indents via margin-left */ 114 | li.L0, 115 | li.L1, 116 | li.L2, 117 | li.L3, 118 | li.L4, 119 | li.L5, 120 | li.L6, 121 | li.L7, 122 | li.L8, 123 | li.L9 { 124 | /* */ } 125 | 126 | /* Alternate shading for lines */ 127 | li.L1, 128 | li.L3, 129 | li.L5, 130 | li.L7, 131 | li.L9 { 132 | /* */ } 133 | -------------------------------------------------------------------------------- /examples/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | padding: 0; 4 | width: 100%; 5 | height: 100%; 6 | display: flex; 7 | flex-direction: row; 8 | color: #ffffff; 9 | background-color: #222222; 10 | } 11 | 12 | h4, h5 { 13 | text-align: center; 14 | margin: 0; 15 | width: 100%; 16 | } 17 | 18 | h5 { 19 | margin-top: 10px; 20 | } 21 | 22 | #graph-container { 23 | margin: 1em; 24 | padding: 0; 25 | border: 1px solid #ffffff; 26 | display: flex; 27 | flex-grow: 1; 28 | } 29 | 30 | #sidebar { 31 | margin: 1em; 32 | padding: 1em; 33 | border: 1px solid #ffffff; 34 | display: flex; 35 | flex-direction: column; 36 | width: 300px; 37 | overflow-y: auto; 38 | overflow-x: hidden; 39 | } 40 | 41 | #graph { 42 | width: 100%; 43 | height: 100%; 44 | } 45 | 46 | @media(max-width: 600px) { 47 | body { 48 | flex-direction: column; 49 | } 50 | #graph-container { 51 | order: 2; 52 | height: 100%; 53 | } 54 | #sidebar { 55 | order: 1; 56 | width: auto; 57 | height: auto; 58 | } 59 | } 60 | 61 | .hide { 62 | display: none !important; 63 | } 64 | 65 | .form { 66 | display: flex; 67 | flex-direction: column; 68 | min-height: 100px; 69 | } 70 | 71 | .form > div { 72 | display: flex; 73 | flex-direction: row; 74 | align-items: center; 75 | } 76 | 77 | .form > div > * { 78 | display: flex; 79 | margin: 4px; 80 | } 81 | 82 | .form > div > label { 83 | min-width: 50px; 84 | } 85 | 86 | .form > div > input, .form > div > select { 87 | flex-grow: 1; 88 | flex-shrink: 1; 89 | } 90 | 91 | input, select { 92 | background: #000000; 93 | border: 1px solid #ffffff; 94 | color: #ffffff; 95 | padding: 4px; 96 | } 97 | -------------------------------------------------------------------------------- /examples/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 |
12 | 48 | 49 | 50 | 169 | 170 | 171 | -------------------------------------------------------------------------------- /jsdoc.conf.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | plugins: [], 5 | recurseDepth: 10, 6 | source: { 7 | include: './src/', 8 | exclude: ['./src/umd/umd-start.js', './src/umd/umd-end.js'], 9 | includePattern: '.+\\.js(doc|x)?$', 10 | excludePattern: '(^|\\/|\\\\)_' 11 | }, 12 | sourceType: 'module', 13 | tags: { 14 | allowUnknownTags: true, 15 | dictionaries: ['jsdoc', 'closure'] 16 | }, 17 | templates: { 18 | cleverLinks: true, 19 | //monospaceLinks: false, 20 | }, 21 | opts: { 22 | template: 'templates/default', 23 | encoding: 'utf8', 24 | destination: './docs/', 25 | recurse: true, 26 | 'private': true, 27 | 'package': './package.json', 28 | verbose: true, 29 | readme: './README.md', 30 | //tutorials: 'path/to/tutorials', 31 | } 32 | }; 33 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | module.exports = (config) => { 2 | const packageJson = require('./package.json'); 3 | const path = require('path'); 4 | 5 | let files = [ 6 | require.resolve('jquery'), 7 | path.join(path.dirname(require.resolve('d3')), 'd3.js') 8 | ]; 9 | 10 | if(packageJson.dependencies.jquery !== undefined) { 11 | files.push({ 12 | pattern: require.resolve('jasmine-jquery'), 13 | watched: false, 14 | served: true 15 | }); 16 | } 17 | 18 | if(process.env.TEST_MIN_BUNDLE) { 19 | files.push(path.join(__dirname, 'dist/js/graph-editor.min.js')); 20 | } 21 | else if(process.env.TEST_BUNDLE) { 22 | files.push(path.join(__dirname, './dist/js/graph-editor.js')); 23 | } 24 | else { 25 | files.push.apply(files, [ 26 | path.join(__dirname, 'tests/test-start.js'), 27 | path.join(__dirname, 'src/main.js'), 28 | path.join(__dirname, 'src/js/**/*.js') 29 | ]); 30 | } 31 | 32 | files.push(path.join(__dirname, 'tests/**/*.test.js')); 33 | 34 | let browsers = process.env.TEST_BROWSERS; 35 | if(browsers) { 36 | browsers = browsers.split(/\s+/); 37 | } 38 | if(!(browsers && browsers[0])) { 39 | browsers = ['Chromium']; 40 | } 41 | 42 | config.set({ 43 | basePath: '../', 44 | 45 | files: files, 46 | 47 | frameworks: ['jasmine'], 48 | 49 | reporters: ['spec'], 50 | specReporter: { 51 | maxLogLines: 5, 52 | suppressErrorSummary: true, 53 | suppressFailed: false, 54 | suppressPassed: false, 55 | suppressSkipped: true, 56 | showSpecTiming: false, 57 | failFast: false 58 | }, 59 | 60 | port: 9876, 61 | 62 | colors: true, 63 | logLevel: config.LOG_INFO, 64 | 65 | autoWatch: true, 66 | singleRun: false, 67 | 68 | browsers: browsers 69 | }); 70 | }; 71 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "graph-editor", 3 | "version": "0.1.1", 4 | "author": "dead-beef", 5 | "contributors": [], 6 | "license": "MIT", 7 | "private": false, 8 | "description": "Graph editor frontend component", 9 | "keywords": [ 10 | "dom", 11 | "svg", 12 | "graph", 13 | "editor" 14 | ], 15 | "main": "dist/js/graph-editor.js", 16 | "files": [ 17 | "dist/" 18 | ], 19 | "homepage": "https://dead-beef.github.io/graph-editor", 20 | "repository": { 21 | "type": "git", 22 | "url": "git+https://github.com/dead-beef/graph-editor" 23 | }, 24 | "bugs": { 25 | "url": "https://github.com/dead-beef/graph-editor/issues" 26 | }, 27 | "engines" : { 28 | "node" : ">=6.11.4" 29 | }, 30 | "dependencies": { 31 | "d3": "^5.7.0" 32 | }, 33 | "devDependencies": { 34 | "autoprefixer": "^7.2.5", 35 | "chokidar-cli": "^1.2.0", 36 | "csso-cli": "^1.0.0", 37 | "eslint": "^4.15.0", 38 | "eslint-plugin-jasmine": "^2.10.1", 39 | "http-server": "^0.11.1", 40 | "jasmine-core": "^2.9.0", 41 | "jasmine-jquery": "^2.1.1", 42 | "jasmine-spec-reporter": "^4.1.1", 43 | "jsdoc": "^3.5.5", 44 | "karma": "^2.0.0", 45 | "karma-chrome-launcher": "^2.2.0", 46 | "karma-firefox-launcher": "^1.1.0", 47 | "karma-jasmine": "^1.1.1", 48 | "karma-sourcemap-loader": "^0.3.7", 49 | "karma-spec-reporter": "0.0.32", 50 | "node-sass": "^4.7.2", 51 | "postcss-cli": "^4.1.1", 52 | "sass-makedepend": "^0.2.1", 53 | "source-map-concat-cli": "^1.0.0", 54 | "uglify-js": "^3.3.8" 55 | }, 56 | "scripts": { 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/css/graph-editor-dark.scss: -------------------------------------------------------------------------------- 1 | $ge-node-fg: #888 !default; 2 | $ge-node-bg: #111 !default; 3 | $ge-node-hover-fg: #fff !default; 4 | $ge-node-hover-bg: #333 !default; 5 | 6 | $ge-link-fg: #888 !default; 7 | $ge-link-hover-fg: #fff !default; 8 | 9 | $ge-dragline-fg: #a44 !default; 10 | 11 | $ge-selection-fg: #aae !default; 12 | $ge-selected-link-fg: #ada !default; 13 | $ge-selected-node-bg: #111 !default; 14 | 15 | $ge-unselected-fg: $ge-node-fg !default; 16 | $ge-unselected-bg: $ge-node-bg !default; 17 | 18 | @import "graph-editor"; 19 | -------------------------------------------------------------------------------- /src/css/graph-editor.scss: -------------------------------------------------------------------------------- 1 | $ge-graph-transition-duration: 0.3s !default; 2 | $ge-graph-transition-properties: stroke, fill, color; 3 | /*$ge-graph-transition: stroke $ge-graph-transition-duration, 4 | fill $ge-graph-transition-duration, 5 | color $ge-graph-transition-duration !default;*/ 6 | 7 | $ge-node-font-size: 16px !default; 8 | $ge-node-border: 2px !default; 9 | 10 | $ge-node-fg: #000 !default; 11 | $ge-node-bg: #ddd !default; 12 | $ge-node-hover-fg: #000 !default; 13 | $ge-node-hover-bg: #aee !default; 14 | 15 | $ge-link-font-size: 14px !default; 16 | $ge-link-size: 2px !default; 17 | 18 | $ge-link-fg: #000 !default; 19 | $ge-link-hover-fg: #4aa !default; 20 | 21 | $ge-dragline-fg: #a44 !default; 22 | $ge-dragline-size: 2px; 23 | 24 | $ge-selection-fg: #44a !default; 25 | $ge-selected-link-fg: #488 !default; 26 | $ge-selected-node-fg: $ge-selection-fg !default; 27 | $ge-selected-node-bg: #aae !default; 28 | 29 | $ge-unselected-fg: #666 !default; 30 | $ge-unselected-bg: #eee !default; 31 | 32 | $ge-unselected-link-hover-fg: mix($ge-unselected-fg, $ge-link-hover-fg) !default; 33 | $ge-unselected-node-hover-fg: mix($ge-selected-node-fg, $ge-node-hover-fg) !default; 34 | $ge-unselected-node-hover-bg: mix($ge-selected-node-bg, $ge-node-hover-bg) !default; 35 | $ge-selected-link-hover-fg: mix($ge-selection-fg, $ge-link-hover-fg) !default; 36 | $ge-selected-node-hover-fg: mix($ge-selected-node-fg, $ge-node-hover-fg) !default; 37 | $ge-selected-node-hover-bg: mix($ge-selected-node-bg, $ge-node-hover-bg) !default; 38 | 39 | 40 | .ge-graph { 41 | * { 42 | /*transition: $ge-graph-transition;*/ 43 | transition-property: $ge-graph-transition-properties; 44 | transition-duration: $ge-graph-transition-duration; 45 | } 46 | 47 | marker { 48 | &#ge-dragline-end, &#ge-dragline-start { 49 | path, circle { 50 | stroke: $ge-dragline-fg; 51 | fill: $ge-dragline-fg; 52 | } 53 | } 54 | } 55 | 56 | .ge-hidden { 57 | display: none; 58 | } 59 | 60 | .ge-dragline { 61 | pointer-events: none; 62 | stroke-width: $ge-dragline-size; 63 | stroke: $ge-dragline-fg; 64 | } 65 | 66 | .ge-link { 67 | cursor: pointer; 68 | 69 | font-size: $ge-link-font-size; 70 | stroke-width: $ge-link-size; 71 | 72 | /* * { vector-effect: non-scaling-stroke } */ 73 | 74 | path { 75 | stroke: $ge-link-fg; 76 | fill: none; 77 | } 78 | 79 | text { 80 | fill: $ge-link-fg; 81 | } 82 | 83 | &:hover { 84 | path { 85 | stroke: $ge-link-hover-fg; 86 | } 87 | text { 88 | fill: $ge-link-hover-fg; 89 | } 90 | } 91 | } 92 | 93 | .ge-node { 94 | cursor: pointer; 95 | 96 | /* * { vector-effect: non-scaling-stroke } */ 97 | 98 | path { 99 | stroke-width: $ge-node-border; 100 | stroke: $ge-node-fg; 101 | fill: $ge-node-bg; 102 | } 103 | 104 | div { 105 | display: flex; 106 | flex-direction: row; 107 | align-items: center; 108 | 109 | width: 100%; 110 | height: 100%; 111 | 112 | span { 113 | display: flex; 114 | flex-direction: column; 115 | flex-grow: 1; 116 | align-items: center; 117 | text-align: center; 118 | 119 | color: $ge-node-fg; 120 | font-size: $ge-node-font-size; 121 | word-wrap: break-word; 122 | } 123 | } 124 | 125 | &:hover { 126 | path { 127 | stroke: $ge-node-hover-fg; 128 | fill: $ge-node-hover-bg; 129 | } 130 | div { 131 | span { 132 | color: $ge-node-hover-fg; 133 | } 134 | } 135 | } 136 | } 137 | 138 | &.ge-selection { 139 | .ge-link { 140 | path { stroke: $ge-unselected-fg; } 141 | text { fill: $ge-unselected-fg; } 142 | 143 | &:hover { 144 | path { stroke: $ge-unselected-link-hover-fg; } 145 | text { fill: $ge-unselected-link-hover-fg; } 146 | } 147 | 148 | &.ge-selection { 149 | path { stroke: $ge-selection-fg; } 150 | text { fill: $ge-selection-fg; } 151 | 152 | &:hover { 153 | path { stroke: $ge-selected-link-hover-fg; } 154 | text { fill: $ge-selected-link-hover-fg; } 155 | } 156 | } 157 | } 158 | 159 | .ge-node { 160 | path { 161 | stroke: $ge-unselected-fg; 162 | fill: $ge-unselected-bg; 163 | } 164 | span { color: $ge-unselected-fg; } 165 | 166 | &:hover { 167 | path { 168 | stroke: $ge-unselected-node-hover-fg; 169 | fill: $ge-unselected-node-hover-bg; 170 | } 171 | span { color: $ge-unselected-node-hover-fg; } 172 | } 173 | 174 | &.ge-selection { 175 | path { 176 | stroke: $ge-selected-node-fg; 177 | fill: $ge-selected-node-bg; 178 | } 179 | span { color: $ge-selected-node-fg; } 180 | 181 | &:hover { 182 | path { 183 | stroke: $ge-selected-node-hover-fg; 184 | fill: $ge-selected-node-hover-bg; 185 | } 186 | span { color: $ge-selected-node-hover-fg; } 187 | } 188 | } 189 | } 190 | } 191 | 192 | &.ge-selection, &.ge-link-selection { 193 | .ge-link { 194 | &.ge-link-selection, &.ge-selection.ge-link-selection { 195 | path { stroke: $ge-selected-link-fg; } 196 | text { fill: $ge-selected-link-fg; } 197 | 198 | &:hover { 199 | path { stroke: $ge-selected-link-hover-fg; } 200 | text { fill: $ge-selected-link-hover-fg; } 201 | } 202 | } 203 | } 204 | } 205 | } 206 | -------------------------------------------------------------------------------- /src/events.jsdoc: -------------------------------------------------------------------------------- 1 | /** 2 | * Graph SVG click/touch event. 3 | * @event click 4 | * @this ge.GraphEditor 5 | * @param {Point} pos Click/touch position. 6 | */ 7 | 8 | /** 9 | * Node click/touch event. 10 | * @event node-click 11 | * @this ge.GraphEditor 12 | * @param {Node} node Clicked/touched node. 13 | */ 14 | 15 | /** 16 | * Link click/touch event. 17 | * @event link-click 18 | * @this ge.GraphEditor 19 | * @param {Link} link Clicked/touched link. 20 | */ 21 | 22 | /** 23 | * New link start event. 24 | * @event new-link-start 25 | * @this ge.GraphEditor 26 | * @param {Node} source New link source. 27 | */ 28 | 29 | /** 30 | * New link end event. 31 | * @event new-link-end 32 | * @this ge.GraphEditor 33 | * @param {Node} source New link source. 34 | * @param {Node} target New link target. 35 | */ 36 | 37 | /** 38 | * New link cancel event. 39 | * @event new-link-cancel 40 | * @this ge.GraphEditor 41 | * @param {Node} source Cancelled link source. 42 | */ 43 | 44 | /** 45 | * Simulation start event. 46 | * @event simulation-start 47 | * @this ge.GraphEditor 48 | */ 49 | 50 | /** 51 | * Simulation stop event. 52 | * @event simulation-stop 53 | * @this ge.GraphEditor 54 | */ 55 | -------------------------------------------------------------------------------- /src/js/events.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Add an event handler. 5 | * @param {string} event 6 | * @param {?function} handler 7 | * @returns {ge.GraphEditor} 8 | * @see [d3.dispatch.on]{@link https://github.com/d3/d3-dispatch#dispatch_on} 9 | */ 10 | ge.GraphEditor.prototype.on = function on(event, handler) { 11 | this.dispatch.on(event, handler); 12 | return this; 13 | }; 14 | 15 | /** 16 | * Get mouse or touch coordinates relative to container. 17 | * @private 18 | * @param {(HTMLElement|SVGElement)} [container=this.container.node()] 19 | * @returns {Point} 20 | * @see [d3.mouse]{@link https://github.com/d3/d3-selection/blob/master/README.md#mouse} 21 | * @see [d3.touch]{@link https://github.com/d3/d3-selection/blob/master/README.md#touch} 22 | */ 23 | ge.GraphEditor.prototype.clickPosition = function clickPosition(container) { 24 | container = container || this.container.node(); 25 | return d3.touch(container) || d3.mouse(container); 26 | }; 27 | 28 | /** 29 | * Get touched node. 30 | * @private 31 | * @returns {?Node} 32 | * @see [d3.event]{@link https://github.com/d3/d3-selection/blob/master/README.md#event} 33 | * @see [document.elementFromPoint]{@link https://developer.mozilla.org/en-US/docs/Web/API/Document/elementFromPoint} 34 | */ 35 | ge.GraphEditor.prototype.touchedNode = function touchedNode() { 36 | var x = d3.event.touches[0].pageX; 37 | var y = d3.event.touches[0].pageY; 38 | var el = document.elementFromPoint(x, y); 39 | var nodeClass = this.options.css.node; 40 | while(true) { 41 | if(!el) { 42 | return null; 43 | } 44 | switch(el.tagName.toLowerCase()) { 45 | case 'svg': 46 | case 'body': 47 | return null; 48 | case 'g': 49 | if(el.classList.contains(nodeClass)) { 50 | return d3.select(el).datum(); 51 | } 52 | } 53 | el = el.parentNode; 54 | } 55 | }; 56 | 57 | /** 58 | * Set node drag event handlers. 59 | * @private 60 | * @param {D3Drag} drag Node drag behavior. 61 | * @fires node-click 62 | * @fires new-link-start 63 | * @fires new-link-end 64 | * @fires new-link-cancel 65 | * @returns {D3Drag} 66 | */ 67 | ge.GraphEditor.prototype.dragEvents = function dragEvents(drag) { 68 | var self = this; 69 | 70 | return drag 71 | .on('start', function dragStart(d) { 72 | self.state.dragged = false; 73 | self.state.dragPos = null; 74 | d3.select(this).raise(); 75 | 76 | if(self.state.simulationStarted && !self.state.dragToLink) { 77 | d.fx = d.x; 78 | d.fy = d.y; 79 | self.simulation('restart'); 80 | } 81 | }) 82 | .on('drag', function drag(d) { 83 | if(self.state.dragToLink) { 84 | var mouse = self.clickPosition(); 85 | var path = ''.concat( 86 | 'M', d.x, ',', d.y, 87 | 'L', mouse[0], ',', mouse[1] 88 | ); 89 | 90 | if(!self.state.dragged) { 91 | self.state.dragged = true; 92 | self.dragLine 93 | .classed(self.options.css.hide, false) 94 | .attr('d', path); 95 | self.dispatch.call('new-link-start', self, d); 96 | } 97 | else { 98 | self.transition(self.dragLine, 'drag') 99 | .attr('d', path); 100 | } 101 | } 102 | else { 103 | self.state.dragged = true; 104 | d.x += d3.event.dx; 105 | d.y += d3.event.dy; 106 | if(self.state.simulationStarted) { 107 | d.fx = d.x; 108 | d.fy = d.y; 109 | self.simulation('restart'); 110 | } 111 | self.updateNode(this); 112 | } 113 | }) 114 | .on('end', function dragEnd(d) { 115 | d.fx = null; 116 | d.fy = null; 117 | 118 | self.state.dragPos = self.clickPosition(); 119 | 120 | if(self.state.dragged) { 121 | self.state.dragged = false; 122 | if(self.state.dragToLink) { 123 | self.dragLine.classed(self.options.css.hide, true); 124 | if(self.state.newLinkTarget) { 125 | var target = self.state.newLinkTarget; 126 | self.state.newLinkTarget = null; 127 | self.dispatch.call( 128 | 'new-link-end', 129 | self, d, target 130 | ); 131 | } 132 | else { 133 | self.dispatch.call('new-link-cancel', self, d); 134 | } 135 | } 136 | else if(self.state.simulationStarted) { 137 | self.simulation('restart'); 138 | } 139 | } 140 | else { 141 | self.dispatch.call('node-click', self, d); 142 | } 143 | }); 144 | }; 145 | 146 | /** 147 | * Set zoom event handlers. 148 | * @private 149 | * @param {D3Zoom} zoom Graph zoom behavior. 150 | * @fires click 151 | * @returns {D3Zoom} 152 | */ 153 | ge.GraphEditor.prototype.zoomEvents = function zoomEvents(zoom) { 154 | var self = this; 155 | //var prevScale = 1; 156 | 157 | return zoom 158 | .duration(self.options.transition.zoom) 159 | .on('end', function zoomEnd() { 160 | if(!self.state.zoomed) { 161 | var pos = self.clickPosition(); 162 | if(!ge.equal(self.state.dragPos, pos)) { 163 | self.dispatch.call('click', self, pos); 164 | } 165 | else { 166 | self.state.dragPos = null; 167 | } 168 | } 169 | self.state.zoomed = false; 170 | }) 171 | .on('zoom', function zoom() { 172 | self.state.zoomed = true; 173 | 174 | /*var scale = d3.event.transform.k; 175 | 176 | if(Math.abs(scale - prevScale) > 0.001) 177 | { 178 | prevScale = scale; 179 | 180 | if(scale > graph.options.sizeScaleMax) 181 | { 182 | self.state.sizeScale = graph.options.sizeScaleMax / scale; 183 | } 184 | else if(scale < graph.options.sizeScaleMin) 185 | { 186 | self.state.sizeScale = graph.options.sizeScaleMin / scale; 187 | } 188 | else 189 | { 190 | self.state.sizeScale = 1; 191 | } 192 | 193 | self.updateSize(); 194 | }*/ 195 | 196 | var type; 197 | 198 | if(ge.equal(d3.event.transform.k, self.state.zoom)) { 199 | type = 'drag'; 200 | } 201 | else { 202 | type = 'zoom'; 203 | self.state.zoom = d3.event.transform.k; 204 | } 205 | 206 | self.transition(self.container, type, 'zoom') 207 | .attr( 208 | 'transform', 209 | d3.event.transform.toString() 210 | ); 211 | }); 212 | }; 213 | 214 | /** 215 | * Set link event handlers. 216 | * @private 217 | * @param {D3Selection} links 218 | * @fires link-click 219 | * @returns {D3Selection} 220 | */ 221 | ge.GraphEditor.prototype.linkEvents = function linkEvents(links) { 222 | var self = this; 223 | 224 | return links 225 | .on('mousedown', function(/*d*/) { 226 | d3.event.stopPropagation(); 227 | d3.event.preventDefault(); 228 | }) 229 | .on('touchstart', function(/*d*/) { 230 | self.state.dragPos = self.clickPosition(); 231 | }) 232 | .on('mouseup', function(d) { 233 | d3.event.stopPropagation(); 234 | d3.event.preventDefault(); 235 | self.dispatch.call('link-click', self, d); 236 | }); 237 | }; 238 | 239 | 240 | /** 241 | * Set node event handlers. 242 | * @private 243 | * @param {D3Selection} nodes 244 | * @returns {D3Selection} 245 | */ 246 | ge.GraphEditor.prototype.nodeEvents = function nodeEvents(nodes) { 247 | var self = this; 248 | 249 | return nodes 250 | /*.attr( 251 | 'transform', 252 | function(d) { 253 | return 'translate('.concat(d.x, ',', d.y, ')'); 254 | } 255 | )*/ 256 | .on('mouseover', function mouseOver(d) { 257 | if(self.state.dragToLink) { 258 | self.state.newLinkTarget = d; 259 | /*d3.select(this).classed( 260 | self.options.css.connect, 261 | true 262 | );*/ 263 | } 264 | }) 265 | .on('mouseout', function mouseOut(/*d*/) { 266 | if(self.state.dragToLink) { 267 | self.state.newLinkTarget = null; 268 | } 269 | /*d3.select(this).classed( 270 | self.options.css.connect, 271 | false 272 | );*/ 273 | }) 274 | .on('touchmove', function touchMove() { 275 | if(self.state.dragToLink) { 276 | self.state.newLinkTarget = self.touchedNode(); 277 | } 278 | }) 279 | .call(self.drag); 280 | }; 281 | -------------------------------------------------------------------------------- /src/js/init.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Create SVG markers. 5 | * @private 6 | * @param {D3Selection} svg 7 | * @returns {ge.GraphEditor} 8 | */ 9 | ge.GraphEditor.prototype.initMarkers = function initMarkers(svg) { 10 | /*var defs = d3.select('#' + this.options.css.markers).node(); 11 | if(defs !== null) { 12 | return this; 13 | }*/ 14 | 15 | /*defs = d3.select('head') 16 | .append('svg') 17 | .attr('id', this.options.css.markers) 18 | .append('svg:defs');*/ 19 | 20 | var defs = svg.append('svg:defs'); 21 | 22 | defs.append('marker') 23 | .attr('id', 'ge-dragline-end') 24 | .attr('viewBox', '0 -7 12 14') 25 | .attr('refX', '7') 26 | .attr('refY', '0') 27 | .attr('markerWidth', 3.5) 28 | .attr('markerHeight', 3.5) 29 | .attr('orient', 'auto') 30 | .append('path') 31 | .attr('d', 'M0,-5L10,0L0,5Z'); 32 | 33 | defs.append('marker') 34 | .attr('id', 'ge-dragline-start') 35 | .attr('viewBox', '-5 -5 5 5') 36 | .attr('refX', -2) 37 | .attr('refY', -2) 38 | .attr('markerWidth', 4) 39 | .attr('markerHeight', 4) 40 | .append('circle') 41 | .attr('r', 2) 42 | .attr('cx', -2) 43 | .attr('cy', -2); 44 | 45 | return this; 46 | }; 47 | 48 | /** 49 | * Initialize SVG element. 50 | * @private 51 | * @param {D3Selection} svg 52 | * @this ge.GraphEditor 53 | * @returns {ge.GraphEditor} 54 | */ 55 | ge.GraphEditor.prototype.initSvg = function initSvg(svg) { 56 | svg.attr('id', this.options.id) 57 | .classed(this.options.css.graph, true) 58 | .classed(this.options.css.digraph, this.options.directed); 59 | 60 | this.initMarkers(svg); 61 | 62 | var g = svg.append('g'); 63 | var defsContainer = svg.select('defs'); //g.append('defs'); 64 | var linkContainer = g.append('g'); 65 | var nodeContainer = g.append('g'); 66 | 67 | /** 68 | * Graph container element. 69 | * @readonly 70 | * @member {D3Selection} 71 | */ 72 | this.container = g; 73 | /** 74 | * Link text path elements. 75 | * @readonly 76 | * @member {D3Selection} 77 | */ 78 | this.defs = defsContainer.selectAll('.' + this.options.css.textpath); 79 | /** 80 | * Link elements. 81 | * @readonly 82 | * @member {D3Selection} 83 | */ 84 | this.links = linkContainer.selectAll('g'); 85 | /** 86 | * Node elements. 87 | * @readonly 88 | * @member {D3Selection} 89 | */ 90 | this.nodes = nodeContainer.selectAll('g'); 91 | /** 92 | * 'Drag to link nodes' line. 93 | * @readonly 94 | * @member {D3Selection} 95 | */ 96 | this.dragLine = g.append('path') 97 | .classed(this.options.css.dragline, true) 98 | .classed(this.options.css.hide, true) 99 | .attr('marker-start', 'url(#ge-dragline-start)') 100 | .attr('marker-end', 'url(#ge-dragline-end)') 101 | .attr('d', 'M0,0L0,0'); 102 | /** 103 | * Text size calculator. 104 | * @readonly 105 | * @member {ge.TextSize} 106 | */ 107 | this.textSize = new ge.TextSize( 108 | svg.append('text') 109 | .classed(this.options.css.node, true) 110 | .node() 111 | ); 112 | 113 | /** 114 | * Graph SVG element. 115 | * @readonly 116 | * @member {D3Selection} 117 | */ 118 | this.svg = svg; 119 | 120 | return this; 121 | }; 122 | 123 | 124 | /** 125 | * Initialize graph data. 126 | * @private 127 | * @param {ImportGraphData} data 128 | * @this ge.GraphEditor 129 | * @returns {ge.GraphEditor} 130 | */ 131 | ge.GraphEditor.prototype.initData = function initData(data) { 132 | /** 133 | * Nodes by ID. 134 | * @readonly 135 | * @member {Object} 136 | */ 137 | this.nodeById = {}; 138 | /** 139 | * Links by ID. 140 | * @readonly 141 | * @member {Object} 142 | */ 143 | this.linkById = {}; 144 | /** 145 | * Graph data. 146 | * @readonly 147 | * @member {GraphData} 148 | */ 149 | this.data = { 150 | nodes: [], 151 | links: [] 152 | }; 153 | this.addNodes(data.nodes, true); 154 | this.addLinks(data.links, true); 155 | return this; 156 | }; 157 | 158 | /** 159 | * Initialize graph state. 160 | * @private 161 | * @this ge.GraphEditor 162 | * @returns {ge.GraphEditor} 163 | */ 164 | ge.GraphEditor.prototype.initState = function initState() { 165 | /** 166 | * Graph editor state. 167 | * @readonly 168 | * @member {Object} 169 | * @property {boolean} zoomed False if the graph is clicked. 170 | * @property {boolean} dragged False if a node is clicked. 171 | * @property {?D3Simulation} simulation Current simulation object. 172 | * @property {boolean} simulationStarted True if a simulation is started. 173 | * @property {boolean} dragToLink True in 'drag to link nodes' mode. 174 | * @property {?Node} newLinkTarget New link target in 'drag to link nodes' mode. 175 | * @property {?Node} selectedNode Selected node. 176 | * @property {?Link} selectedLink Selected link. 177 | * @property {?Point} dragPos Last drag end position. 178 | * @property {number} zoom Zoom level. 179 | */ 180 | this.state = { 181 | zoomed: false, 182 | dragged: false, 183 | simulation: null, 184 | simulationStarted: false, 185 | dragToLink: false, 186 | newLinkTarget: null, 187 | selectedNode: null, 188 | selectedLink: null, 189 | dragPos: null, 190 | zoom: 1 191 | //sizeScale: 1 192 | }; 193 | return this; 194 | }; 195 | -------------------------------------------------------------------------------- /src/js/link.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Graph link constructor. 3 | * @class 4 | * @classdesc Graph link class. 5 | * @param {ge.GraphEditor} graph Link container. 6 | * @param {ImportLinkData} data Link data. 7 | */ 8 | ge.Link = function Link(graph, data) { 9 | /** 10 | * Source node. 11 | * @member {ge.Node} 12 | */ 13 | this.source = graph.getData(data.source); 14 | if(!this.source) { 15 | throw new Error('invalid link source'); 16 | } 17 | /** 18 | * Target node. 19 | * @member {ge.Node} 20 | */ 21 | this.target = graph.getData(data.target); 22 | if(!this.target) { 23 | throw new Error('invalid link target'); 24 | } 25 | /** 26 | * ID. 27 | * @readonly 28 | * @member {ID} 29 | */ 30 | this.id = /*data.id || */graph.linkId(this.source, this.target); 31 | /** 32 | * Path generator. 33 | * @member {ge.path.Path} 34 | */ 35 | this.shape = data.shape 36 | ? ge.path.fromJson(data.shape) 37 | : graph.options.link.shape; 38 | /** 39 | * Title. 40 | * @member {string} 41 | */ 42 | this.title = data.title === undefined ? this.id : data.title; 43 | /** 44 | * User data. 45 | * @member {*} 46 | */ 47 | this.data = data.data; 48 | /** 49 | * SVG path. 50 | * @readonly 51 | * @member {string} 52 | */ 53 | this.path = ''; 54 | /** 55 | * True if text path is reversed. 56 | * @readonly 57 | * @member {boolean} 58 | */ 59 | this.reversed = false; 60 | /** 61 | * SVG element ID. 62 | * @readonly 63 | * @member {string} 64 | */ 65 | this.elementId = graph.options.id.concat('l', this.id); 66 | /** 67 | * SVG element selector. 68 | * @readonly 69 | * @member {string} 70 | */ 71 | this.selector = '#' + this.elementId; 72 | /** 73 | * SVG element ID. 74 | * @readonly 75 | * @member {string} 76 | */ 77 | this.pathId = graph.options.id.concat('p', this.id); 78 | /** 79 | * SVG element selector. 80 | * @readonly 81 | * @member {Selector} 82 | */ 83 | this.pathSelector = '#' + this.pathId; 84 | /** 85 | * SVG text path element ID. 86 | * @readonly 87 | * @member {string} 88 | */ 89 | this.defId = graph.options.id.concat('d', this.id); 90 | /** 91 | * SVG text path element selector. 92 | * @readonly 93 | * @member {Selector} 94 | */ 95 | this.defSelector = '#' + this.defId; 96 | 97 | return this; 98 | }; 99 | 100 | /** 101 | * Initialize SVG elements. 102 | * @static 103 | * @param {D3Selection} links SVG element enter selection. 104 | * @param {D3Selection} defs SVG element enter selection. 105 | * @param {object} opts Link options. 106 | * @param {number} opts.text.dx Link title X offset. 107 | * @param {number} opts.text.dy Link title Y offset. 108 | * @param {string} cls CSS class. 109 | */ 110 | ge.Link.initSelection = function initSelection(links, defs, opts) { 111 | defs.attr('id', function(d) { return d.defId; }); 112 | 113 | links.attr('id', function(d) { return d.elementId; }); 114 | links.append('path') 115 | .attr('id', function(d) { return d.pathId; }); 116 | links.append('text') 117 | .attr('dx', opts.text.dx) 118 | .attr('dy', opts.text.dy) 119 | .append('textPath') 120 | .attr('xlink:href', function(d) { return d.defSelector; }); 121 | }; 122 | 123 | /** 124 | * Update SVG elements. 125 | * @static 126 | * @param {D3Selection} links SVG element selection. 127 | * @param {D3Selection} defs SVG element selection. 128 | * @param {object} opts Link options. 129 | * @param {function} transition Transition generator. 130 | * @see ge.GraphEditor.updateLink 131 | */ 132 | ge.Link.updateSelection = function updateSelection( 133 | links, defs, 134 | opts, transition 135 | ) { 136 | transition(defs) 137 | .attr('d', function(d) { return d.shape.path(d); }); 138 | 139 | transition(links.select('path')) 140 | .style('stroke-width', function(d) { return d.size + 'px'; }) 141 | .attr('d', function(d) { return d.path; }); 142 | 143 | var offset = opts.text.offset; 144 | var anchor = opts.text.anchor; 145 | 146 | transition(links.select('textPath')) 147 | .text(function(d) { return d.title; }) 148 | .attr('startOffset', function(d) { return offset[+d.reversed]; }) 149 | .attr('text-anchor', function(d) { return anchor[+d.reversed]; }); 150 | }; 151 | 152 | /** 153 | * Initialize SVG elements. 154 | * @param {D3Selection} links SVG element enter selection. 155 | * @see ge.GraphEditor.update 156 | * @see ge.GraphEditor.linkEvents 157 | */ 158 | ge.Link.prototype.initSelection = ge.Link.initSelection; 159 | 160 | /** 161 | * Update SVG elements. 162 | * @param {D3Selection} links SVG element selection. 163 | * @see ge.GraphEditor.updateLink 164 | */ 165 | ge.Link.prototype.updateSelection = ge.Link.updateSelection; 166 | 167 | /** 168 | * Update link data. 169 | */ 170 | ge.Link.prototype.update = function update() { 171 | this.shape(this); 172 | }; 173 | 174 | /** 175 | * Convert to JSON. 176 | * @param {ge.GraphEditor} graph Graph. 177 | * @returns {ExportLinkData} JSON data. 178 | */ 179 | ge.Link.prototype.toJson = function toJson(graph) { 180 | return { 181 | id: this.id, 182 | source: this.source.id, 183 | target: this.target.id, 184 | size: this.size, 185 | title: this.title, 186 | data: this.data, 187 | shape: this.shape === graph.options.link.shape 188 | ? null 189 | : ge.path.toJson(this.shape) 190 | }; 191 | }; 192 | -------------------------------------------------------------------------------- /src/js/node.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Graph node constructor. 3 | * @class 4 | * @classdesc Graph node class. 5 | * @param {ge.GraphEditor} graph Graph 6 | * @param {ImportNodeData} data Node JSON data 7 | * @see ge.GraphEditor.initNode 8 | */ 9 | ge.Node = function Node(graph, data) { 10 | var opts = graph.options.node; 11 | /** 12 | * ID. 13 | * @readonly 14 | * @member {ID} 15 | */ 16 | this.id = data.id || graph.nodeId(); 17 | /** 18 | * X coordinate. 19 | * @member {number} 20 | */ 21 | this.x = +data.x || 0; 22 | /** 23 | * Y coordinate. 24 | * @member {number} 25 | */ 26 | this.y = +data.y || 0; 27 | /** 28 | * Width. 29 | * @member {number} 30 | */ 31 | this.width = Math.max(+data.width || opts.size.def, opts.size.min); 32 | /** 33 | * Height. 34 | * @member {number} 35 | */ 36 | this.height = Math.max(+data.height || opts.size.def, opts.size.min); 37 | /** 38 | * Shape. 39 | * @member {Shape} 40 | */ 41 | this.shape = data.shape 42 | ? ge.shape.fromJson(data.shape) 43 | : opts.shape; 44 | /** 45 | * Title. 46 | * @member {string} 47 | */ 48 | this.title = data.title == null ? this.id.toString() : data.title; 49 | /** 50 | * Text X offset. 51 | * @readonly 52 | * @member {?number} 53 | */ 54 | this.textX = null; 55 | /** 56 | * Text Y offset. 57 | * @readonly 58 | * @member {?number} 59 | */ 60 | this.textY = null; 61 | /** 62 | * Text container width. 63 | * @readonly 64 | * @member {?number} 65 | */ 66 | this.textWidth = null; 67 | /** 68 | * Text container height. 69 | * @readonly 70 | * @member {?number} 71 | */ 72 | this.textHeight = null; 73 | /** 74 | * True if node title is inside its shape. 75 | * @readonly 76 | * @member {ge.GraphEditor} 77 | */ 78 | this.textInside = opts.text.inside; 79 | /** 80 | * Text size calculator. 81 | * @readonly 82 | * @member {ge.TextSize} 83 | */ 84 | this.textSize = graph.textSize; 85 | /** 86 | * User data. 87 | * @member {*} 88 | */ 89 | this.data = data.data; 90 | /** 91 | * SVG element ID. 92 | * @readonly 93 | * @member {string} 94 | */ 95 | this.elementId = graph.options.id.concat('n', this.id); 96 | /** 97 | * SVG element selector. 98 | * @readonly 99 | * @member {Selector} 100 | */ 101 | this.selector = '#' + this.elementId; 102 | /** 103 | * SVG group transform. 104 | * @readonly 105 | * @member {?string} 106 | */ 107 | this.transform = null; 108 | /** 109 | * Previous title. 110 | * @private 111 | * @member {?string} 112 | */ 113 | this.prevTitle = null; 114 | 115 | return this; 116 | }; 117 | 118 | /** 119 | * Initialize SVG elements. 120 | * @static 121 | * @param {D3Selection} nodes SVG element enter selection. 122 | * @param {string} cls CSS class. 123 | */ 124 | ge.Node.initSelection = function initSelection(nodes) { 125 | nodes.attr('id', function(d) { return d.elementId; }); 126 | nodes.append('path'); 127 | nodes.append('foreignObject') 128 | .append('xhtml:div') 129 | .append('xhtml:span'); 130 | }; 131 | 132 | /** 133 | * Update SVG elements. 134 | * @static 135 | * @param {D3Selection} nodes SVG element selection. 136 | * @param {function} transition Transition generator. 137 | * @param {ge.TextSize} textSize Text size calculator. 138 | */ 139 | ge.Node.updateSelection = function updateSelection( 140 | nodes, 141 | transition, 142 | textSize 143 | ) { 144 | console.log(nodes); 145 | transition(nodes) 146 | .attr( 147 | 'transform', 148 | function(d) { 149 | return 'translate('.concat(d.x, ',', d.y, ')'); 150 | } 151 | ); 152 | 153 | nodes.each(function(d) { 154 | //if(d.textInside) { 155 | d.shape.fitTitleInside(d, textSize); 156 | //} 157 | /*else { 158 | d.shape.fitTitleOutside(d, textSize); 159 | }*/ 160 | }); 161 | 162 | transition(nodes.select('foreignObject')) 163 | .attr('x', function(d) { return -d.textWidth / 2; }) 164 | .attr('y', function(d) { return -d.textHeight / 2; }) 165 | .attr('width', function(d) { return d.textWidth; }) 166 | .attr('height', function(d) { return d.textHeight; }) 167 | .select('span') 168 | .text(function(d) { return d.title; }); 169 | 170 | transition(nodes.select('path')) 171 | .attr('d', function(d) { return d.shape.path(d); }); 172 | }; 173 | 174 | /** 175 | * Initialize SVG elements. 176 | * @param {D3Selection} nodes SVG element enter selection. 177 | */ 178 | ge.Node.prototype.initSelection = ge.Node.initSelection; 179 | 180 | /** 181 | * Update SVG elements. 182 | * @param {D3Selection} nodes SVG element selection. 183 | * @param {function} transition Transition generator. 184 | */ 185 | ge.Node.prototype.updateSelection = ge.Node.updateSelection; 186 | 187 | /** 188 | * Update node data. 189 | * @returns {boolean} True if node position changed. 190 | */ 191 | ge.Node.prototype.update = function update() { 192 | var moved = false; 193 | 194 | if(this.textInside) { 195 | this.shape.fitTextInside(this); 196 | } 197 | 198 | var transform = 'translate('.concat(this.x, ',', this.y, ')'); 199 | if(transform !== this.transform) { 200 | this.transform = transform; 201 | moved = true; 202 | } 203 | 204 | return moved; 205 | }; 206 | 207 | /** 208 | * Convert to JSON. 209 | * @param {ge.GraphEditor} graph Graph. 210 | * @param {number} [ox=0] Origin X coordinate. 211 | * @param {number} [oy=0] Origin Y coordinate. 212 | * @returns {ExportNodeData} JSON data. 213 | */ 214 | ge.Node.prototype.toJson = function toJson(graph, ox, oy) { 215 | return { 216 | id: this.id, 217 | x: this.x - ox, 218 | y: this.y - oy, 219 | width: this.width, 220 | height: this.height, 221 | shape: this.shape === graph.options.node.shape 222 | ? null 223 | : ge.shape.toJson(this.shape), 224 | title: this.title, 225 | data: this.data 226 | }; 227 | }; 228 | -------------------------------------------------------------------------------- /src/js/options.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Default options for all graph types. 5 | * @type GraphOptions 6 | */ 7 | ge.GraphEditor.prototype.defaults = { 8 | id: null, 9 | directed: false, 10 | 11 | node: { 12 | shape: new ge.shape.Circle(), 13 | border: 2, 14 | size: { 15 | def: 10, 16 | min: 10 17 | }, 18 | text: { 19 | dx: 0, 20 | dy: 0, 21 | inside: true 22 | } 23 | }, 24 | 25 | link: { 26 | size: { 27 | def: 2 28 | }, 29 | text: { 30 | dx: 0, 31 | dy: '1.1em', 32 | offset: null, 33 | anchor: null 34 | } 35 | }, 36 | 37 | simulation: { 38 | create: ge.defaultSimulation, 39 | start: false, 40 | stop: true, 41 | step: 1 42 | }, 43 | 44 | transition: { 45 | zoom: 250, 46 | drag: 0, 47 | simulation: 50 48 | }, 49 | 50 | scale: { 51 | min: 0.25, 52 | max: 8.0, 53 | /*size: { 54 | min: 0.5, 55 | max: 2.0 56 | }*/ 57 | }, 58 | 59 | bbox: { 60 | padding: 80 61 | }, 62 | 63 | css: { 64 | //markers: 'ge-markers', 65 | node: 'ge-node', 66 | graph: 'ge-graph', 67 | digraph: 'ge-digraph', 68 | hide: 'ge-hidden', 69 | dragline: 'ge-dragline', 70 | link: 'ge-link', 71 | textpath: 'ge-text-path', 72 | //connect: 'ge-connect', 73 | selection: { 74 | node: 'ge-selection', 75 | link: 'ge-link-selection' 76 | } 77 | } 78 | }; 79 | 80 | /** 81 | * Default options by graph type. 82 | * @type Array 83 | */ 84 | ge.GraphEditor.prototype.typeDefaults = [ 85 | { 86 | link: { 87 | shape: new ge.path.Line({ 88 | loopStart: 0, 89 | loopEnd: 90 90 | }), 91 | text: { 92 | offset: '50%', 93 | anchor: 'middle' 94 | } 95 | } 96 | }, 97 | { 98 | link: { 99 | shape: new ge.path.Line({ 100 | arrow: true, 101 | loopStart: 0, 102 | loopEnd: 90 103 | }), 104 | text: { 105 | offset: '20%', 106 | anchor: 'start' 107 | } 108 | } 109 | } 110 | ]; 111 | 112 | /** 113 | * Initialize graph options. 114 | * @private 115 | * @param {GraphOptions} options 116 | * @param {D3Selection} svg 117 | * @returns {ge.GraphEditor} 118 | */ 119 | ge.GraphEditor.prototype.initOptions = function initOptions(options, svg) { 120 | var directed; 121 | if(options && options.hasOwnProperty('directed')) { 122 | directed = options.directed; 123 | } 124 | else { 125 | directed = this.defaults.directed; 126 | } 127 | 128 | var typeDefaults = this.typeDefaults[+directed]; 129 | 130 | var opt = ge.extend({}, this.defaults, typeDefaults, options); 131 | 132 | opt.id = opt.id 133 | || svg.attr('id') 134 | || 'ge'.concat(Math.floor(Math.random() * 100)); 135 | 136 | var reversed = (100 - parseInt(opt.link.text.offset)) + '%'; 137 | opt.link.text.offset = [ opt.link.text.offset, reversed ]; 138 | 139 | switch(opt.link.text.anchor) { 140 | case 'start': 141 | reversed = 'end'; 142 | break; 143 | case 'end': 144 | reversed = 'start'; 145 | break; 146 | default: 147 | reversed = 'middle'; 148 | } 149 | opt.link.text.anchor = [ opt.link.text.anchor, reversed ]; 150 | 151 | this.options = opt; 152 | return this; 153 | }; 154 | -------------------------------------------------------------------------------- /src/js/path/base.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Link shapes. 3 | * @namespace 4 | */ 5 | ge.path = new ge.SaveLoad(); 6 | 7 | /** 8 | * Abstract base path class. 9 | * @class 10 | * @abstract 11 | * @param {?object} [data] JSON data. 12 | * @param {?number} [data.loopStart=180] Loop start angle in degrees. 13 | * @param {?number} [data.loopEnd=270] Loop end angle in degrees. 14 | */ 15 | ge.path.Path = function Path(data) { // eslint-disable-line no-unused-vars 16 | if(this.constructor === Path) { 17 | throw new Error('abstract class'); 18 | } 19 | data = data || {}; 20 | this.loopStart = new ge.Angle(+data.loopStart || 180); 21 | this.loopEnd = new ge.Angle(+data.loopEnd || 270); 22 | }; 23 | 24 | /* eslint-disable no-unused-vars */ 25 | 26 | /** 27 | * Return text path and set link path. 28 | * @abstract 29 | * @param {ge.Link} link Link data. 30 | * @returns {string} SVG path. 31 | */ 32 | ge.path.Path.prototype.path = function path(link) { 33 | throw new Error('abstract method'); 34 | }; 35 | 36 | /* eslint-enable no-unused-vars */ 37 | 38 | /** 39 | * Convert to JSON. 40 | * @returns {object} JSON data. 41 | */ 42 | ge.path.Path.prototype.toJson = function toJson() { 43 | return { 44 | loopStart: this.loopStart.deg, 45 | loopEnd: this.loopEnd.deg 46 | }; 47 | }; 48 | -------------------------------------------------------------------------------- /src/js/path/line.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Line. 3 | * @class 4 | * @param {?object} [data] JSON data. 5 | * @param {?number} [data.loopStart=180] Loop start angle in degrees. 6 | * @param {?number} [data.loopEnd=270] Loop end angle in degrees. 7 | * @param {?boolean} [data.arrow=false] 8 | * @param {?number} [data.arrowLength=10] 9 | * @param {?number} [data.arrowAngle=15] 10 | */ 11 | ge.path.Line = function Line(data) { 12 | ge.path.Path.call(this, data); 13 | this.arrow = !!data.arrow; 14 | this.arrowLength = +data.arrowLength || 10; 15 | this.arrowAngle = new ge.Angle(+data.arrowAngle || 15); 16 | this.arrowAngle2 = new ge.Angle(-data.arrowAngle || -15); 17 | }; 18 | 19 | ge.path.Line.prototype = Object.create(ge.path.Line); 20 | Object.defineProperty( 21 | ge.path.Line.prototype, 22 | 'constructor', 23 | { 24 | value: ge.path.Line, 25 | enumerable: false, 26 | writable: true 27 | } 28 | ); 29 | ge.path.addClass(ge.path.Line); 30 | 31 | /** 32 | * Return arrow path. 33 | * @param {ge.Point} src Link source. 34 | * @param {ge.Point} dst Link target. 35 | * @returns {string} SVG path. 36 | */ 37 | ge.path.Line.prototype.arrowPath = function arrow(src, dst) { 38 | var b = src.clone().sub(dst).normalize(); 39 | var c = b.clone().rotate(this.arrowAngle2).mul(this.arrowLength).add(dst); 40 | b.rotate(this.arrowAngle).mul(this.arrowLength).add(dst); 41 | return 'M'.concat( 42 | dst.x, ',', dst.y, 43 | 'L', b.x, ',', b.y, 44 | 'L', c.x, ',', c.y, 45 | 'Z' 46 | ); 47 | }; 48 | 49 | /** 50 | * Return text path and set link path. 51 | * @param {ge.Link} link Link data. 52 | * @returns {string} SVG path. 53 | */ 54 | ge.path.Line.prototype.path = function path(link) { 55 | var src, dst; 56 | 57 | if(link.source === link.target) { 58 | src = link.source.shape.getPoint(link.source, this.loopStart); 59 | dst = link.source.shape.getPoint(link.source, this.loopEnd); 60 | var rx = link.source.width / 2; 61 | var ry = link.source.height / 2; 62 | 63 | link.reversed = false; 64 | link.path = 'M'.concat( 65 | src.x, ',', src.y, 66 | 'A', rx, ',', ry, ',0,1,0,', dst.x, ',', dst.y 67 | ); 68 | 69 | /*if(this.arrow) { 70 | }*/ 71 | 72 | return link.path; 73 | } 74 | 75 | src = new ge.Point(link.source.x, link.source.y); 76 | dst = new ge.Point(link.target.x, link.target.y); 77 | var src2 = link.source.shape.intersect(link.source, link.target); 78 | var dst2 = link.target.shape.intersect(link.target, link.source); 79 | src = src2 || src; 80 | dst = dst2 || dst; 81 | 82 | var dst3 = dst; 83 | 84 | if(this.arrow) { 85 | dst3 = dst.clone().sub(src); 86 | var l = dst3.length(); 87 | var l2 = this.arrowLength * this.arrowAngle.cos; 88 | dst3.mul(1 - l2 / l).add(src); 89 | } 90 | 91 | link.path = 'M'.concat(src.x, ',', src.y, 'L', dst3.x, ',', dst3.y); 92 | 93 | if(this.arrow) { 94 | link.path += this.arrowPath(src, dst); 95 | } 96 | 97 | if((link.reversed = src.x > dst.x)) { 98 | src2 = src; 99 | src = dst; 100 | dst = src2; 101 | } 102 | 103 | return 'M'.concat(src.x, ',', src.y, 'L', dst.x, ',', dst.y); 104 | }; 105 | -------------------------------------------------------------------------------- /src/js/shape/base.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Node shapes. 3 | * @namespace 4 | */ 5 | 6 | ge.shape = new ge.SaveLoad(); 7 | 8 | /** 9 | * Abstract base shape class. 10 | * @class 11 | * @abstract 12 | * @param {?object} [data] JSON data. 13 | */ 14 | ge.shape.Shape = function Shape(data) { // eslint-disable-line no-unused-vars 15 | if(this.constructor === Shape) { 16 | throw new Error('abstract class'); 17 | } 18 | }; 19 | 20 | /* eslint-disable no-unused-vars */ 21 | 22 | /** 23 | * Return SVG path. 24 | * @abstract 25 | * @param {ge.Node} node Node data. 26 | * @returns {string} SVG path. 27 | */ 28 | ge.shape.Shape.prototype.path = function path(node) { 29 | throw new Error('abstract method'); 30 | }; 31 | 32 | /** 33 | * Return point at angle. 34 | * @abstract 35 | * @param {ge.Node} node Node data. 36 | * @param {ge.Angle} angle Angle. 37 | * @returns {ge.Point} 38 | */ 39 | ge.shape.Shape.prototype.getPoint = function getPoint(node, angle) { 40 | throw new Error('abstract method'); 41 | }; 42 | 43 | /** 44 | * Resize a node to fit its title inside. 45 | * @abstract 46 | * @param {ge.Node} node Node data. 47 | * @param {ge.ContainerSize} size Text container size. 48 | * @param {ge.TextSize} textSize Text size calculator. 49 | */ 50 | ge.shape.Shape.prototype.doFitTitleInside = function doFitTitleInside( 51 | node, 52 | size, 53 | textSize 54 | ) { 55 | throw new Error('abstract method'); 56 | }; 57 | 58 | 59 | /* eslint-enable no-unused-vars */ 60 | 61 | /** 62 | * Convert to JSON. 63 | * @returns {object} JSON data. 64 | */ 65 | ge.shape.Shape.prototype.toJson = function toJson() { 66 | return {}; 67 | }; 68 | 69 | /** 70 | * Intersect with a link. 71 | * @param {ge.Node} node Node data. 72 | * @param {ge.Node} node2 Node data. 73 | * @returns {?ge.Point} Intersection point. 74 | */ 75 | ge.shape.Shape.prototype.intersect = function intersect(node, node2) { 76 | var a = Math.atan2(node2.y - node.y, node2.x - node.x); 77 | return this.getPoint(node, new ge.Angle(a, true)); 78 | }; 79 | 80 | /** 81 | * Resize a node to fit its title inside if necessary. 82 | * @param {ge.Node} node Node data. 83 | * @param {ge.TextSize} textSize Text size calculator. 84 | */ 85 | ge.shape.Shape.prototype.fitTitleInside = function fitTitleInside( 86 | node, 87 | textSize 88 | ) { 89 | if(node.title === node.prevTitle) { 90 | return; 91 | } 92 | var size = textSize.containerSize(node.title); 93 | this.doFitTitleInside(node, size, textSize); 94 | node.prevTitle = node.title; 95 | }; 96 | -------------------------------------------------------------------------------- /src/js/shape/circle.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Circle. 3 | * @class 4 | * @param {?object} [data] JSON data. 5 | */ 6 | ge.shape.Circle = function Circle(data) { 7 | ge.shape.Shape.call(this, data); 8 | }; 9 | 10 | ge.shape.Circle.prototype = Object.create(ge.shape.Shape.prototype); 11 | Object.defineProperty( 12 | ge.shape.Circle.prototype, 13 | 'constructor', 14 | { 15 | value: ge.shape.Circle, 16 | enumerable: false, 17 | writable: true 18 | } 19 | ); 20 | ge.shape.addClass(ge.shape.Circle); 21 | 22 | /** 23 | * Return SVG path. 24 | * @param {ge.Node} node Node data. 25 | * @returns {string} SVG path. 26 | */ 27 | ge.shape.Circle.prototype.path = function path(node) { 28 | var dx = node.width, rx = dx / 2, ry = node.height / 2; 29 | var arc = 'a'.concat(rx, ',', ry, ',0,1,0,'); 30 | return 'M'.concat(-rx, ',0', arc, dx, ',0', arc, -dx, ',0'); 31 | }; 32 | 33 | /** 34 | * Return point at angle. 35 | * @abstract 36 | * @param {ge.Node} node Node data. 37 | * @param {ge.Angle} angle Angle. 38 | * @returns {ge.Point} 39 | */ 40 | ge.shape.Circle.prototype.getPoint = function getPoint(node, angle) { 41 | return new ge.Point( 42 | node.x + node.width * angle.cos * 0.5, 43 | node.y + node.height * angle.sin * 0.5 44 | ); 45 | }; 46 | 47 | /** 48 | * Resize a node to fit its title inside. 49 | * @abstract 50 | * @param {ge.Node} node Node data. 51 | * @param {ge.ContainerSize} size Text container size. 52 | * @param {ge.TextSize} textSize Text size calculator. 53 | */ 54 | ge.shape.Circle.prototype.doFitTitleInside = function doFitTitleInside( 55 | node, 56 | size/*, 57 | textSize*/ 58 | ) { 59 | node.width = node.height = Math.max( 60 | Math.ceil(Math.sqrt(size.minArea * 2)), 61 | size.minWidth, 62 | size.minHeight 63 | ); 64 | node.textWidth = node.textHeight = Math.ceil(node.width / 1.414213); 65 | }; 66 | -------------------------------------------------------------------------------- /src/js/shape/rect.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Rectangle. 3 | * @class 4 | * @param {?object} [data] JSON data. 5 | * @param {?number} [data.aspect=1] Aspect ratio. 6 | */ 7 | ge.shape.Rect = function Rect(data) { 8 | data = data || {}; 9 | ge.shape.Shape.call(this, data); 10 | /** 11 | * Aspect ratio. 12 | * @member {number} 13 | */ 14 | this.aspect = data.aspect || 1; 15 | }; 16 | 17 | ge.shape.Rect.prototype = Object.create(ge.shape.Shape.prototype); 18 | Object.defineProperty( 19 | ge.shape.Rect.prototype, 20 | 'constructor', 21 | { 22 | value: ge.shape.Rect, 23 | enumerable: false, 24 | writable: true 25 | } 26 | ); 27 | ge.shape.addClass(ge.shape.Rect); 28 | 29 | /** 30 | * Convert to JSON. 31 | * @abstract 32 | * @returns {object} JSON data. 33 | */ 34 | ge.shape.Rect.prototype.toJson = function toJson() { 35 | return { 36 | aspect: this.aspect 37 | }; 38 | }; 39 | 40 | /** 41 | * Return SVG path. 42 | * @param {ge.Node} node Node data. 43 | * @returns {string} SVG path. 44 | */ 45 | ge.shape.Rect.prototype.path = function path(node) { 46 | var w = node.width, h = node.height; 47 | return 'M'.concat( 48 | (-w / 2), ',', (-h / 2), 49 | 'l', w, ',0l0,', h, 'l', -w, ',0z' 50 | ); 51 | }; 52 | 53 | /** 54 | * Return point at angle. 55 | * @abstract 56 | * @param {ge.Node} node Node data. 57 | * @param {ge.Angle} angle Angle. 58 | * @returns {ge.Point} 59 | */ 60 | ge.shape.Rect.prototype.getPoint = function getPoint(node, angle) { 61 | var w = node.width / 2; 62 | var h = node.height / 2; 63 | 64 | var x, y, t; 65 | var ret = new ge.Point(node.x, node.y); 66 | 67 | x = null; 68 | if(angle.cos > 1e-8) { 69 | x = w; 70 | } 71 | else if(angle.cos < -1e-8) { 72 | x = -w; 73 | } 74 | if(x !== null) { 75 | t = x / angle.cos; 76 | y = t * angle.sin; 77 | if(y < h + 1e-8 && y > -h - 1e-8) { 78 | ret.x += x; 79 | ret.y += y; 80 | return ret; 81 | } 82 | } 83 | 84 | y = null; 85 | if(angle.sin > 1e-8) { 86 | y = h; 87 | } 88 | else if(angle.sin < -1e-8) { 89 | y = -h; 90 | } 91 | if(y !== null) { 92 | t = y / angle.sin; 93 | x = t * angle.cos; 94 | if(x < w + 1e-8 && x > -w - 1e-8) { 95 | ret.x += x; 96 | ret.y += y; 97 | return ret; 98 | } 99 | } 100 | 101 | // throw new Error() 102 | return ret; 103 | }; 104 | 105 | /** 106 | * Resize a node to fit its title inside. 107 | * @abstract 108 | * @param {ge.Node} node Node data. 109 | * @param {ge.ContainerSize} size Text container size. 110 | * @param {ge.TextSize} textSize Text size calculator. 111 | */ 112 | ge.shape.Rect.prototype.doFitTitleInside = function doFitTitleInside( 113 | node, 114 | size/*, 115 | textSize*/ 116 | ) { 117 | node.width = Math.max( 118 | Math.ceil(this.aspect * Math.sqrt(size.minArea)), 119 | size.minWidth 120 | ); 121 | node.height = Math.max(size.minHeight, node.width / this.aspect); 122 | node.textWidth = node.width; 123 | node.textHeight = node.height; 124 | }; 125 | -------------------------------------------------------------------------------- /src/js/util/angle.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Angle constructor. 3 | * @class 4 | * @classdesc Angle class. 5 | * @param {?number} value Angle value. 6 | * @param {?boolean} rad true if value is in radians, false if in degrees. 7 | */ 8 | ge.Angle = function Angle(value, rad) { 9 | /** 10 | * Angle in degrees. 11 | * @member {number} 12 | */ 13 | this.deg = 0; 14 | /** 15 | * Angle in radians. 16 | * @member {number} 17 | */ 18 | this.rad = 0; 19 | 20 | if(rad) { 21 | this.rad = +value || 0; 22 | this.deg = this.rad * 180.0 / Math.PI; 23 | } 24 | else { 25 | this.deg = +value || 0; 26 | this.rad = this.deg * Math.PI / 180.0; 27 | } 28 | 29 | /** 30 | * Sine. 31 | * @member {number} 32 | */ 33 | this.sin = Math.sin(this.rad); 34 | /** 35 | * Cosine. 36 | * @member {number} 37 | */ 38 | this.cos = Math.cos(this.rad); 39 | }; 40 | -------------------------------------------------------------------------------- /src/js/util/bbox.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Bounding box constructor. 3 | * @class 4 | * @classdesc Bounding box class. 5 | * @param {?number} left Left X coordinate. 6 | * @param {?number} top Top Y coordinate. 7 | * @param {?number} right Right X coordinate. 8 | * @param {?number} bottom Bottom Y coordinate. 9 | */ 10 | ge.BBox = function BBox(left, top, right, bottom) { 11 | /** 12 | * Left X coordinate. 13 | * @member {number} 14 | */ 15 | this.left = +left || 0; 16 | /** 17 | * Top Y coordinate. 18 | * @member {number} 19 | */ 20 | this.top = +top || 0; 21 | /** 22 | * Right X coordinate. 23 | * @member {number} 24 | */ 25 | this.right = +right || 0; 26 | /** 27 | * Bottom Y coordinate. 28 | * @member {number} 29 | */ 30 | this.bottom = +bottom || 0; 31 | }; 32 | -------------------------------------------------------------------------------- /src/js/util/common.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Get object ID. 3 | * @param {?(Object|undefined)} obj 4 | * @returns {?ID} 5 | */ 6 | ge.id = function id(obj) { 7 | return obj && obj.id || null; 8 | }; 9 | 10 | /** 11 | * @param {Object} _this 12 | * @param {function} func 13 | * @returns {function} 14 | */ 15 | ge.bind = function bind(_this, func) { 16 | return function bound() { 17 | return func.apply(_this, arguments); 18 | }; 19 | }; 20 | 21 | /** 22 | * Compare numbers or arrays of numbers. 23 | * @param {(number|Array)} u 24 | * @param {(number|Array)} v 25 | * @param {number} [eps=1e-5] Precision. 26 | * @returns {boolean} 27 | */ 28 | ge.equal = function equal(u, v, eps) { 29 | eps = eps || 1e-5; 30 | var eq = function(x, y) { return Math.abs(x - y) < eps; }; 31 | 32 | if(u === null || v === null 33 | || u === undefined || v === undefined 34 | || typeof u === 'number' && Array.isArray(v) 35 | || typeof v === 'number' && Array.isArray(u)) { 36 | return false; 37 | } 38 | 39 | if(typeof u === 'number' && typeof v === 'number') { 40 | return eq(u, v); 41 | } 42 | 43 | if(!Array.isArray(u) || !Array.isArray(v)) { 44 | throw new Error( 45 | 'ge.equal: invalid argument type: ' 46 | .concat(u.toString(), ' ', v.toString()) 47 | ); 48 | } 49 | 50 | if(u.length !== v.length) { 51 | return false; 52 | } 53 | 54 | for(var i = 0; i < u.length; ++i) { 55 | if(!eq(u[i], v[i])) { 56 | return false; 57 | } 58 | } 59 | 60 | return true; 61 | }; 62 | 63 | /** 64 | * Default simulation update function. 65 | * @param {?D3Simulation} simulation Old simulation object. 66 | * @param {Array} nodes Graph nodes. 67 | * @param {Array} links Graph links. 68 | * @this ge.GraphEditor 69 | * @returns {D3Simulation} New/updated simulation object. 70 | */ 71 | ge.defaultSimulation = function defaultSimulation(simulation, nodes, links) { 72 | if(!simulation) { 73 | simulation = d3.forceSimulation() 74 | .force('charge', d3.forceManyBody()) 75 | .force('link', d3.forceLink()) 76 | .force('center', d3.forceCenter()); 77 | } 78 | 79 | var dist = 2 * d3.max(nodes, function(d) { 80 | return Math.max(d.width, d.height); 81 | }); 82 | 83 | var cx = d3.mean(nodes, function(d) { return d.x; }); 84 | var cy = d3.mean(nodes, function(d) { return d.y; }); 85 | 86 | simulation.nodes(nodes); 87 | simulation.force('center').x(cx).y(cy); 88 | simulation.force('link').links(links).distance(dist); 89 | 90 | return simulation; 91 | }; 92 | 93 | /** 94 | * Extend an object. 95 | * @param {object} dst Object to extend. 96 | * @param {object} src Source object. 97 | * @returns {object} Extended object. 98 | */ 99 | ge._extend = function _extend(dst, src) { 100 | if(!src) { 101 | return dst; 102 | } 103 | if(typeof src !== 'object') { 104 | throw new Error('src is not an object: ' + src.toString()); 105 | } 106 | 107 | if(!dst) { 108 | dst = {}; 109 | } 110 | else if(typeof dst !== 'object') { 111 | throw new Error('dst is not an object: ' + dst.toString()); 112 | } 113 | 114 | for(var key in src) { 115 | var value = src[key]; 116 | if(typeof value === 'object' && value !== null) { 117 | if(Array.isArray(value)) { 118 | dst[key] = value.slice(); 119 | } 120 | else if(value.constructor.name !== 'Object') { 121 | dst[key] = value; 122 | } 123 | else { 124 | var dstValue = dst[key]; 125 | if(!dstValue 126 | || typeof dstValue !== 'object' 127 | || Array.isArray(dstValue)) { 128 | dst[key] = dstValue = {}; 129 | } 130 | ge._extend(dstValue, value); 131 | } 132 | } 133 | else { 134 | dst[key] = value; 135 | } 136 | } 137 | return dst; 138 | }; 139 | 140 | /** 141 | * Extend an object. 142 | * @param {object} dst Object to extend. 143 | * @param {...object} src Source objects. 144 | * @returns {object} Extended object. 145 | */ 146 | ge.extend = function extend(dst, src) { // eslint-disable-line no-unused-vars 147 | for(var i = 1; i < arguments.length; ++i) { 148 | dst = ge._extend(dst, arguments[i]); 149 | } 150 | return dst; 151 | }; 152 | 153 | /*ge.debounceD3 = function debounceD3(func, delay) { 154 | var timeout = null; 155 | 156 | return function() { 157 | var context = this; 158 | var args = arguments; 159 | var d3ev = d3.event; 160 | 161 | if(timeout !== null) { 162 | clearTimeout(timeout); 163 | } 164 | 165 | timeout = setTimeout( 166 | function() { 167 | var tmp = d3.event; 168 | timeout = null; 169 | d3.event = d3ev; 170 | try { 171 | func.apply(context, args); 172 | } 173 | finally { 174 | d3.event = tmp; 175 | } 176 | }, 177 | delay 178 | ); 179 | }; 180 | };*/ 181 | -------------------------------------------------------------------------------- /src/js/util/container-size.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Container size constructor. 3 | * @class 4 | * @classdesc Point class. 5 | * @param {?number} [minWidth=1] Minimum width. 6 | * @param {?number} [minHeight=1] Minimum height. 7 | * @param {?number} [minArea=minWidth*minHeight] Minimum area. 8 | */ 9 | ge.ContainerSize = function ContainerSize(minWidth, minHeight, minArea) { 10 | /** 11 | * Minimum width. 12 | * @member {number} 13 | */ 14 | this.minWidth = +minWidth || 1; 15 | /** 16 | * Minimum height. 17 | * @member {number} 18 | */ 19 | this.minHeight = +minHeight || 1; 20 | /** 21 | * Minimum area. 22 | * @member {number} 23 | */ 24 | this.minArea = +minArea || this.minWidth * this.minHeight; 25 | }; 26 | -------------------------------------------------------------------------------- /src/js/util/point.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Point constructor. 3 | * @class 4 | * @classdesc Point class. 5 | * @param {?number} x X coordinate. 6 | * @param {?number} y Y coordinate. 7 | */ 8 | ge.Point = function Point(x, y) { 9 | /** 10 | * X coordinate. 11 | * @member {number} 12 | */ 13 | this.x = +x || 0; 14 | /** 15 | * Y coordinate. 16 | * @member {number} 17 | */ 18 | this.y = +y || 0; 19 | }; 20 | 21 | /** 22 | * Add. 23 | * @param {ge.Point} p 24 | * @returns {ge.Point} 25 | */ 26 | ge.Point.prototype.add = function add(p) { 27 | this.x += p.x; 28 | this.y += p.y; 29 | return this; 30 | }; 31 | 32 | /** 33 | * Subtract. 34 | * @param {ge.Point} p 35 | * @returns {ge.Point} 36 | */ 37 | ge.Point.prototype.sub = function sub(p) { 38 | this.x -= p.x; 39 | this.y -= p.y; 40 | return this; 41 | }; 42 | 43 | /** 44 | * Multiply. 45 | * @param {number} k 46 | * @returns {ge.Point} 47 | */ 48 | ge.Point.prototype.mul = function mul(k) { 49 | this.x *= k; 50 | this.y *= k; 51 | return this; 52 | }; 53 | 54 | /** 55 | * Return vector length. 56 | * @returns {number} 57 | */ 58 | ge.Point.prototype.length = function length() { 59 | return Math.sqrt(this.x * this.x + this.y * this.y); 60 | }; 61 | 62 | /** 63 | * Normalize. 64 | * @returns {ge.Point} 65 | */ 66 | ge.Point.prototype.normalize = function normalize() { 67 | var l = this.length(); 68 | if(l > 1e-8) { 69 | this.x /= l; 70 | this.y /= l; 71 | } 72 | return this; 73 | }; 74 | 75 | /** 76 | * Rotate. 77 | * @param {ge.Angle} a 78 | * @returns {ge.Point} 79 | */ 80 | ge.Point.prototype.rotate = function rotate(a) { 81 | var x = this.x, y = this.y; 82 | this.x = x * a.cos - y * a.sin; 83 | this.y = x * a.sin + y * a.cos; 84 | return this; 85 | }; 86 | 87 | /** 88 | * Clone. 89 | * @returns {ge.Point} 90 | */ 91 | ge.Point.prototype.clone = function clone() { 92 | return new ge.Point(this.x, this.y); 93 | }; 94 | -------------------------------------------------------------------------------- /src/js/util/save-load.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Class for saving/loading JSON. 3 | * @class 4 | * @param {?number} x X coordinate. 5 | * @param {?number} y Y coordinate. 6 | */ 7 | ge.SaveLoad = function SaveLoad() { 8 | /** 9 | * Class constructors by name. 10 | * @member {object} 11 | */ 12 | this.classes = {}; 13 | }; 14 | 15 | /** 16 | * Add a class. 17 | * @param {function} constructor Class constructor. 18 | * @param {string} [name=constructor.name] Class name. 19 | * @param {boolean} [overwrite=false] Overwrite if class exists. 20 | */ 21 | ge.SaveLoad.prototype.addClass = function addClass( 22 | constructor, 23 | name, 24 | overwrite 25 | ) { 26 | name = name || constructor.name; 27 | if(this.classes[name] && !overwrite) { 28 | throw new Error('class "' + name +'" exists'); 29 | } 30 | this.classes[name] = constructor; 31 | }; 32 | 33 | /** 34 | * Get class by name. 35 | * @param {string} name Class name. 36 | * @returns {function} Class constructor. 37 | */ 38 | ge.SaveLoad.prototype.getClass = function getClass(name) { 39 | var ret = this.classes[name]; 40 | if(!ret) { 41 | throw new Error('class "' + name +'" not found'); 42 | } 43 | return ret; 44 | }; 45 | 46 | /** 47 | * Load from JSON. 48 | * @param {object} data JSON data. 49 | * @returns {object} 50 | */ 51 | ge.SaveLoad.prototype.fromJson = function fromJson(data) { 52 | var cls = this.getClass(data['class']); 53 | return new cls(data); 54 | }; 55 | 56 | /** 57 | * Save to JSON. 58 | * @param {object} obj Object to save. 59 | * @param {string} [name=obj.constructor.name] Class name. 60 | * @returns {object} 61 | */ 62 | ge.SaveLoad.prototype.toJson = function toJson(obj, name) { 63 | var ret = obj.toJson(); 64 | ret['class'] = name || obj.constructor.name; 65 | return ret; 66 | }; 67 | -------------------------------------------------------------------------------- /src/js/util/text-size.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Text size calculator constructor. 3 | * @class 4 | * @classdesc Text size calculator class. 5 | * @param {SVGElement} el SVG element. 6 | */ 7 | ge.TextSize = function TextSize(el) { 8 | el.setAttribute('style', 'stroke:none;fill:none'); 9 | this.el = el; 10 | return this; 11 | }; 12 | 13 | /** 14 | * Compute text width. 15 | * @param {string} text 16 | * @returns {number} 17 | */ 18 | ge.TextSize.prototype.width = function length(text) { 19 | this.el.textContent = text; 20 | return this.el.getComputedTextLength(); 21 | }; 22 | 23 | /** 24 | * Compute text width and height. 25 | * @param {string} text 26 | * @returns {ge.Point} 27 | */ 28 | ge.TextSize.prototype.size = function size(text) { 29 | this.el.textContent = text; 30 | var bbox = this.el.getBBox(); 31 | return new ge.Point(bbox.width, bbox.height); 32 | }; 33 | 34 | /** 35 | * Compute text container size. 36 | * @param {string} text 37 | * @returns {ge.ContainerSize} 38 | */ 39 | ge.TextSize.prototype.containerSize = function containerSize(text) { 40 | var size = this.size(text); 41 | var words = text.split(/\s+/); 42 | var width; 43 | if(words.length == 1) { 44 | width = size.x; 45 | } 46 | else { 47 | width = 0; 48 | var self = this; 49 | words.forEach(function(word) { 50 | width = Math.max(width, self.width(word)); 51 | }); 52 | } 53 | return new ge.ContainerSize( 54 | width, 55 | size.y, 56 | size.x * size.y 57 | ); 58 | }; 59 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* eslint-disable no-unused-vars */ 4 | 5 | /** 6 | * Graph editor module. 7 | * @namespace 8 | */ 9 | var ge = exports; 10 | 11 | /** 12 | * Graph editor constructor. 13 | * @class 14 | * @classdesc Graph editor class. 15 | * @param {(SVGElement|Selector|D3Selection)} svg SVG element. 16 | * @param {ImportGraphData} data Graph data. 17 | * @param {GraphOptions} options Graph options. 18 | */ 19 | ge.GraphEditor = function GraphEditor(svg, data, options) { 20 | if(!svg.select) { 21 | svg = d3.select(svg); 22 | } 23 | 24 | this.initOptions(options, svg) 25 | .initSvg(svg) 26 | .initData(data) 27 | .initState(); 28 | 29 | /** 30 | * @readonly 31 | * @member {D3Dispatch} 32 | */ 33 | this.dispatch = d3.dispatch( 34 | 'node-click', 'link-click', 35 | 'new-link-start', 'new-link-end', 'new-link-cancel', 36 | 'click', 37 | 'simulation-start', 'simulation-stop' 38 | ); 39 | 40 | /** 41 | * Graph bounding box. 42 | * @readonly 43 | * @member {BBox} 44 | */ 45 | this.bbox = [[0, 0], [0, 0]]; 46 | /** 47 | * Graph zoom behavior. 48 | * @readonly 49 | * @member {D3Zoom} 50 | */ 51 | this.zoom = this.zoomEvents(d3.zoom()) 52 | .scaleExtent([this.options.scale.min, this.options.scale.max]); 53 | /** 54 | * Node drag behavior. 55 | * @readonly 56 | * @member {D3Drag} 57 | */ 58 | this.drag = this.dragEvents(d3.drag()); 59 | this.svg.call(this.zoom); 60 | 61 | /** 62 | * Window resize event handler. 63 | * @readonly 64 | * @member {function} 65 | */ 66 | this.onresize = ge.bind(this, this.resized); 67 | window.addEventListener('resize', this.onresize); 68 | 69 | this.updateBBox(); 70 | 71 | setTimeout(ge.bind(this, function() { 72 | this.update() 73 | .resized() 74 | .simulation(this.options.simulation.start); 75 | }), 10); 76 | 77 | return this; 78 | }; 79 | 80 | /* eslint-enable no-unused-vars */ 81 | -------------------------------------------------------------------------------- /src/types.jsdoc: -------------------------------------------------------------------------------- 1 | /** 2 | * D3 selection. 3 | * @typedef {Object} D3Selection 4 | * @see [d3-selection]{@link https://github.com/d3/d3-selection#api-reference} 5 | */ 6 | 7 | /** 8 | * D3 dispatch. 9 | * @typedef {Object} D3Dispatch 10 | * @see [d3.dispatch]{@link https://github.com/d3/d3-dispatch#api-reference} 11 | */ 12 | 13 | /** 14 | * D3 force simulation. 15 | * @typedef {Object} D3Simulation 16 | * @see [d3-force]{@link https://github.com/d3/d3-force#api-reference} 17 | */ 18 | 19 | /** 20 | * D3 drag behavior. 21 | * @typedef {Object} D3Drag 22 | * @see [d3.drag]{@link https://github.com/d3/d3-drag/blob/master/README.md#api-reference} 23 | */ 24 | 25 | /** 26 | * D3 zoom behavior. 27 | * @typedef {Object} D3Zoom 28 | * @see [d3.zoom]{@link https://github.com/d3/d3-zoom/blob/master/README.md#api-reference} 29 | */ 30 | 31 | /** 32 | * CSS selector. 33 | * @typedef {string} Selector 34 | */ 35 | 36 | /** 37 | * Node or link ID. 38 | * @typedef {(number|string)} ID 39 | */ 40 | 41 | /** 42 | * Imported node data. 43 | * @typedef {Object} ImportNodeData 44 | * @property {ID} [id] Node ID. 45 | * @property {number} [x] Center X coordinate. 46 | * @property {number} [y] Center Y coordinate. 47 | * @property {number} [size] Size. 48 | * @property {string} [title] Title. 49 | * @property {*} [data] User data. 50 | */ 51 | 52 | /** 53 | * Imported link data. 54 | * @typedef {Object} ImportLinkData 55 | * @property {(ID|ImportNodeData)} source Source node. 56 | * @property {(ID|ImportNodeData)} target Target node. 57 | * @property {number} [size] Size. 58 | * @property {string} [title] Title. 59 | * @property {*} [data] User data. 60 | */ 61 | 62 | /** 63 | * Imported graph data. 64 | * @typedef {Object} ImportGraphData 65 | * @property {Array} nodes Node data. 66 | * @property {Array} links Link data. 67 | */ 68 | 69 | /** 70 | * Exported node data. 71 | * @typedef {Object} ExportNodeData 72 | * @property {ID} id Node ID. 73 | * @property {number} x Center X coordinate. 74 | * @property {number} y Center Y coordinate. 75 | * @property {number} size Size. 76 | * @property {string} title Title. 77 | * @property {*} data User data. 78 | */ 79 | 80 | /** 81 | * Exported link data. 82 | * @typedef {Object} ExportLinkData 83 | * @property {ID} source Source node ID. 84 | * @property {ID} target Target node ID. 85 | * @property {number} [size] Size. 86 | * @property {string} [title] Title. 87 | * @property {*} [data] User data. 88 | */ 89 | 90 | /** 91 | * Exported graph data. 92 | * @typedef {Object} ExportGraphData 93 | * @property {Array} nodes Node data. 94 | * @property {Array} links Link data. 95 | */ 96 | 97 | /** 98 | * Graph data. 99 | * @typedef {Object} GraphData 100 | * @property {Array} nodes Node data. 101 | * @property {Array} links Link data. 102 | */ 103 | 104 | /** 105 | * Node or link reference. 106 | * @typedef {(D3Selection|SVGElement|Node|Link|ID|Selector)} Reference 107 | */ 108 | 109 | /** 110 | * Added nodes and links. 111 | * @typedef {Array} AddedObjects 112 | * @property {Array} 0 Nodes. 113 | * @property {Array} 1 Links. 114 | */ 115 | 116 | /** 117 | * Selection. 118 | * @typedef {Object} Selection 119 | * @property {?Node} node Selected node. 120 | * @property {?Link} link Selected link. 121 | */ 122 | 123 | 124 | /** 125 | * Graph options. 126 | * @typedef {Object} GraphOptions 127 | * @property {?string} 128 | * [id='ge' + ] 129 | * Graph SVG ID. 130 | * @property {boolean} 131 | * [directed=false] 132 | * Graph type. 133 | * @property {number} 134 | * [node.border=2] 135 | * Node border size. 136 | * @property {number} 137 | * [node.size.def=10] 138 | * Default node size. 139 | * @property {number} 140 | * [node.size.min=10] 141 | * Minimal node size. 142 | * @property {number|string} 143 | * [node.text.dx=0] 144 | * Node title X offset. 145 | * @property {number|string} 146 | * [node.text.dy=0] 147 | * Node title Y offset. 148 | * @property {boolean} 149 | * [node.text.inside=true] 150 | * Title inside/outside nodes. 151 | * @property {function} 152 | * [link.path={@link ge.defaultLinkPath}] 153 | * Link SVG path generator. 154 | * @property {number} 155 | * [link.size.def=2] 156 | * Default link size. 157 | * @property {number|string} 158 | * [link.text.dx=0] 159 | * Link title X offset. 160 | * @property {number|string} 161 | * [link.text.dy='1.1em'] 162 | * Link title Y offset. 163 | * @property {string} 164 | * [link.text.offset='50%' for undirected graphs, '20%' for directed] 165 | * Link title start-offset in %. 166 | * @property {string} 167 | * [link.text.anchor='middle' for undirected graphs, 'start' for directed] 168 | * Link title text-anchor ('start' | 'middle' | 'end'). 169 | * @property {number} 170 | * [link.arc.start=180] 171 | * Reflexive link start angle in degrees. 172 | * @property {number} 173 | * [link.arc.end=270] 174 | * Reflexive link end angle in degrees. 175 | * @property {function} 176 | * [simulation.create={@link ge.defaultSimulation}] 177 | * Simulation start function. 178 | * @property {boolean} 179 | * [simulation.start=false] 180 | * Start simulation after creating graph. 181 | * @property {boolean} 182 | * [simulation.stop=true] 183 | * Stop simulation when it converges. 184 | * @property {number} 185 | * [simulation.step=1] 186 | * DOM update interval in simulation ticks. 187 | * @property {number} 188 | * [transition.zoom=250] 189 | * Zoom transition duration in milliseconds. 190 | * @property {number} 191 | * [transition.drag=50] 192 | * Drag transition duration in milliseconds. 193 | * @property {number} 194 | * [transition.simulation=50] 195 | * Simulation transition duration in milliseconds. 196 | * @property {number} 197 | * [scale.min=0.25] 198 | * Minimal zoom level. 199 | * @property {number} 200 | * [scale.max=8.0] 201 | * Maximal zoom level. 202 | * @property {number} 203 | * [bbox.padding=80] 204 | * Graph bounding box padding. 205 | * @property {string} 206 | * [css.graph='ge-graph'] 207 | * Graph CSS classes. 208 | * @property {string} 209 | * [css.digraph='ge-digraph'] 210 | * Directed graph CSS classes. 211 | * @property {string} 212 | * [css.node='ge-node'] 213 | * Node CSS classes. 214 | * @property {string} 215 | * [css.link='ge-link'] 216 | * Link CSS classes. 217 | * @property {string} 218 | * [css.hide='ge-hide'] 219 | * Hidden element CSS classes. 220 | * @property {string} 221 | * [css.dragline='ge-dragline'] 222 | * Drag line CSS classes. 223 | * @property {string} 224 | * [css.selection.node='ge-selection'] 225 | * Selected node CSS classes. 226 | * @property {string} 227 | * [css.selection.link='ge-link-selection'] 228 | * Selected link CSS classes. 229 | */ 230 | -------------------------------------------------------------------------------- /src/umd/umd-end.js: -------------------------------------------------------------------------------- 1 | return exports;}); 2 | -------------------------------------------------------------------------------- /src/umd/umd-start.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | (function(root, factory) { 4 | if(typeof define === 'function' && define.amd) { 5 | define(['d3'], factory); 6 | } 7 | else if(typeof module === 'object' && module.exports) { 8 | module.exports = factory(require('d3')); 9 | } 10 | else { 11 | root.ge = factory(root.d3); 12 | } 13 | })(this, function(d3) { 14 | var exports = {}; 15 | -------------------------------------------------------------------------------- /tests/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": false, 3 | "globals": { 4 | "ge": true, 5 | "jasmine": true, 6 | "beforeEach": true, 7 | "beforeAll": true, 8 | "afterEach": true, 9 | "afterAll": true, 10 | "describe": true, 11 | "inject": true, 12 | "expect": true, 13 | "spyOn": true, 14 | "it": true, 15 | "element": true, 16 | "by": true, 17 | "browser": true 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /tests/module.test.js: -------------------------------------------------------------------------------- 1 | describe('module', function() { 2 | it('should be defined', function() { 3 | expect(ge).toBeDefined(); 4 | }); 5 | }); 6 | -------------------------------------------------------------------------------- /tests/test-start.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-vars */ 2 | var exports = {}; 3 | var ge = exports; 4 | /* eslint-enable no-unused-vars */ 5 | --------------------------------------------------------------------------------