├── .eslintrc.json ├── .github ├── dependabot.yml └── workflows │ ├── demo.yml │ ├── publish.yml │ └── test.yml ├── .gitignore ├── .npmignore ├── CHANGES.md ├── LICENSE-MIT.txt ├── README.md ├── __tests__ ├── basics.js ├── editing.js ├── graphchanges.js ├── nav.js └── thumbnail.js ├── bin └── the-graph-render ├── examples ├── assets │ ├── loading.gif │ └── photobooth.json.js ├── demo-full.html ├── demo-full.js ├── demo-simple.html ├── demo-simple.js ├── demo-thumbnail.html └── demo-thumbnail.js ├── index.js ├── jest-setup.js ├── package.json ├── render.jsjob.js ├── scripts └── build-font-awesome-javascript.js ├── spec ├── fixtures │ └── photobooth.json └── render-cli.js ├── the-graph-editor ├── clipboard.js └── index.html ├── the-graph-nav └── the-graph-nav.js ├── the-graph-thumb └── the-graph-thumb.js ├── the-graph ├── SVGImage.js ├── TextBG.js ├── arcs.js ├── factories.js ├── font-awesome-unicode-map.js ├── geometryutils.js ├── hammer.js ├── merge.js ├── mixins.js ├── render.js ├── the-graph-app.js ├── the-graph-autolayout.js ├── the-graph-edge.js ├── the-graph-graph.js ├── the-graph-group.js ├── the-graph-iip.js ├── the-graph-library.js ├── the-graph-menu.js ├── the-graph-modalbg.js ├── the-graph-node-menu-port.js ├── the-graph-node-menu-ports.js ├── the-graph-node-menu.js ├── the-graph-node.js ├── the-graph-port.js └── the-graph-tooltip.js ├── themes ├── dark │ ├── the-graph-spectrum.styl │ └── the-graph.styl ├── default │ └── the-graph.styl ├── light │ ├── the-graph-spectrum.styl │ └── the-graph.styl ├── the-graph-dark.styl └── the-graph-light.styl └── webpack.config.js /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "airbnb", 3 | "env": { 4 | "browser": true 5 | }, 6 | "rules": { 7 | "no-param-reassign": 1, 8 | "no-underscore-dangle": 0, 9 | "react/destructuring-assignment": 1, 10 | "react/prop-types": 1, 11 | "react/prefer-es6-class": 1, 12 | "react/prefer-stateless-function": 1, 13 | "react/no-children-prop": 1, 14 | "react/no-find-dom-node": 1 15 | }, 16 | "parserOptions": { 17 | "ecmaFeatures": { 18 | "jsx": true 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: npm 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | versioning-strategy: increase-if-necessary 8 | - package-ecosystem: github-actions 9 | directory: "/" 10 | schedule: 11 | interval: weekly 12 | -------------------------------------------------------------------------------- /.github/workflows/demo.yml: -------------------------------------------------------------------------------- 1 | name: Publish on GitHub Pages 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v2.3.4 13 | - uses: actions/setup-node@v2.1.5 14 | with: 15 | node-version: 12 16 | - run: npm install 17 | - run: npm test 18 | deploy: 19 | needs: build 20 | runs-on: ubuntu-latest 21 | steps: 22 | - uses: actions/checkout@v2.3.4 23 | - uses: actions/setup-node@v2.1.5 24 | with: 25 | node-version: 12 26 | - run: npm install 27 | - run: npm run build 28 | - uses: peaceiris/actions-gh-pages@v3 29 | with: 30 | github_token: ${{ secrets.GITHUB_TOKEN }} 31 | publish_dir: ./dist 32 | keep_files: false 33 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish Node.js Package 2 | 3 | on: 4 | push: 5 | tags: 6 | - "*" 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v2.3.4 13 | - uses: actions/setup-node@v2.1.5 14 | with: 15 | node-version: 12 16 | - run: npm install 17 | - run: npm test 18 | 19 | publish-npm: 20 | needs: build 21 | runs-on: ubuntu-latest 22 | steps: 23 | - uses: actions/checkout@v2.3.4 24 | - uses: actions/setup-node@v2.1.5 25 | with: 26 | node-version: 12 27 | registry-url: https://registry.npmjs.org/ 28 | - run: npm install 29 | - run: npm run build 30 | - run: npm publish 31 | env: 32 | NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}} 33 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Node CI 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | test: 7 | name: Run test suite 8 | runs-on: ubuntu-latest 9 | strategy: 10 | matrix: 11 | node-version: [12.x] 12 | steps: 13 | - uses: actions/checkout@v2.3.4 14 | - name: Use Node.js ${{ matrix.node-version }} 15 | uses: actions/setup-node@v2.1.5 16 | with: 17 | node-version: ${{ matrix.node-version }} 18 | - run: npm install 19 | - run: npm test 20 | env: 21 | CI: true 22 | merge-me: 23 | name: Auto-merge dependency updates 24 | needs: test 25 | runs-on: ubuntu-latest 26 | steps: 27 | - uses: ridedott/merge-me-action@v2.2.21 28 | with: 29 | GITHUB_LOGIN: 'dependabot[bot]' 30 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | bower_components/ 2 | node_modules/ 3 | /.idea 4 | /build 5 | /dist 6 | npm-debug.log 7 | /spec/temp/ 8 | /coverage 9 | package-lock.json 10 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | /build 2 | /bower_components 3 | *.tgz 4 | /dist/fonts 5 | /dist/demo-* 6 | -------------------------------------------------------------------------------- /CHANGES.md: -------------------------------------------------------------------------------- 1 | ## 0.14.0 (git master) 2 | 3 | Changes: 4 | 5 | * the-graph CSS themes are now in the `dist/` folder instead of `theme/` folder 6 | * `editor.getDefaultMenus` method has now been removed 7 | 8 | Internal changes: 9 | 10 | * Now using WebPack for the builds 11 | 12 | ## 0.12.0 (2018 February 19) 13 | 14 | New features 15 | 16 | * `the-graph-render`: CLI tool for a graph to PNG/JPEG/SVG output. 17 | This is especially useful for generating images for documentation purposes. 18 | Implemented by executing the-graph codebase in a browser using JsJob. 19 | Some of the rendering API is available experimentally under `TheGraph.render`, or one can execute the .jsjob programatically in Node.js. 20 | 21 | ## 0.11.1 (2017 December 27) 22 | 23 | Bugfixes 24 | 25 | * Fix node menu not showing due missing require(). Regression in 0.11.0 26 | 27 | ## 0.11.0 (2017 December 27) 28 | 29 | Breaking changes 30 | 31 | * Upgraded from React 14 to React 15. 32 | It is no longer a dependency, so applications using the-graph can (and must) pull in an appropriate version. 33 | 34 | Internal changes 35 | 36 | * Tests are now using Jest instead of Mocha/Chai. 37 | They are ran locally against JsDom, which makes them also in Pull Requests which was not possible with Saucelabs. 38 | * More migration of code into proper CommonJS modules 39 | 40 | ## 0.10.2 (2017 August 23) 41 | 42 | Bugfixes 43 | 44 | * Fixed unable to create edge from node context menu 45 | 46 | ## 0.10.1 (2017 August 17) 47 | 48 | Bugfixes 49 | 50 | * Fixed compatibility with browsers only supporting `TouchEvent`, including Safari on iOS. 51 | * Fixed exception on long-press if no menu was defined 52 | 53 | UI changes 54 | 55 | * Movement threshold for starting panning reduced, making it a bit easier 56 | 57 | ## 0.10.0 (2017 June 28) 58 | 59 | UI changes 60 | 61 | * Edges cannot be dropped on target port. Have to tap to complete edge connection. 62 | * Menu item cannot be opened by swiping and releasing. Have to tap to perform menu action. 63 | 64 | Breaking changes 65 | 66 | * Polymer element `the-graph-thumb` has been removed. 67 | Should instead use the JavaScript API `TheGraph.thumb.render()`, 68 | as shown in `examples/demo-thumbnail.html`. 69 | * Polymer element `the-graph-nav` has been removed. 70 | Should instead use the new React component `TheGraph.nav.Component`, 71 | as shown in `examples/demo-full.html` 72 | * Polymer element `the-graph` has been removed. 73 | Use React component `TheGraph.Graph` instead. 74 | * Polymer element `the-graph-editor` has been removed. 75 | Use React component `TheGraph.App` instead, as shown in `examples/demo-simple.html` 76 | 77 | Deprecated APIs, to be removed 78 | 79 | * `TheGraph.editor.getDefaultMenus()`, should be explicitly set by app. 80 | * `TheGraph.autolayout.applyAutolayout()`, should be included in app if wanted. 81 | * `TheGraph.App::updateIcon()`, should instead pass `nodeIcons` prop. 82 | * `TheGraph.App::getComponent()`, should instead use info from the passed in `library` prop. 83 | * Property `getMenuDef` of `TheGraph.App` is deprecated, should pass the data in `menus` prop instead. 84 | * All methods on React elements are planned to be deprecated in favor of passing props. 85 | 86 | Added APIs 87 | 88 | * `TheGraph.library.libraryFromGraph()`, returns component library from a `fbp-graph.Graph` instance 89 | in format compatible with the `library` prop. 90 | 91 | Bugfixes 92 | 93 | * Changing `graph` prop of React element should now correctly reset and follow new graph instance. 94 | 95 | Internal changes 96 | 97 | * Usage of PolymerGestures has been replaced by hammer.js 98 | * No longer depends on Polymer or webcomponents 99 | * All dependencies are installed via NPM, bower is no longer used 100 | * Some more modules have been converted to proper CommonJS 101 | 102 | ## 0.9.0 (2017 May 6) 103 | 104 | New features 105 | 106 | * `the-graph-editor` Polymer element and `Graph` React component now support a `readonly` property. 107 | When set to true, destructive actions 108 | 109 | Internal changes 110 | 111 | * `menuCallback`: An empty object is considered falsy and will not show a menu. 112 | 113 | ## 0.8.0 (2017 May 6) 114 | 115 | Additions 116 | 117 | * `fbp-graph` dependency is now exposed as `fbpGraph` on the top-level module. 118 | Ex: `TheGraph.fbpGraph` when including `dist/the-graph.js`. 119 | 120 | Breaking changes 121 | 122 | * Polymer element `the-graph-nav` no longer takes and directly manipulates `editor`. 123 | Instead it fires events like `panto`. And it expects `graph` and `view` attributes to be set. 124 | Tapping the element does not manipulate anything, only fires the `tap` event. 125 | See `examples/demo-full.html` for usage. 126 | * Polymer element `the-graph-editor` no longer accepts a JSON string as input for `graph` property. 127 | Instead the property must always be a `fbpGraph.Graph` instance. 128 | The event `graphInitialised`, which was used for this old async behavior has also been removed. 129 | 130 | ## 0.7.0 (2017 March 2) 131 | 132 | Breaking changes 133 | 134 | * Polymer elements no longer automatically include the neccesary JS files. 135 | Instead users must include `dist/the-graph.js`, which bundles the needed JavaScript and provides API under `window.TheGraph`. 136 | The file is included in `the-graph` NPM packages. 137 | This is preparation for removing the Polymer dependency, instead providing JS APIs and React components. 138 | 139 | ## 0.6.0 (2017 January 5) 140 | 141 | * Add all dependencies besides Polymer to NPM. 142 | In the fututure NPM will be the recommended way to install, and Bower is considered deprecated. 143 | 144 | ## 0.5.0 (2017 January 5) 145 | 146 | * Depend on [fbp-graph](https://github.com/flowbased/fbp-graph) instead of NoFlo. 147 | Build size significantly reduced. 148 | * Examples were cleaned up and can now be found under examples/ 149 | 150 | ## 0.4.4 (2016 July 27) 151 | 152 | * Arrows on edges (#277) thanks @ifitzpatrick 153 | * Font Awesome 4.6.3 154 | 155 | ## 0.4.2 (2016 June 24) 156 | 157 | * Fix pinch on touch screens (#286) 158 | 159 | ## 0.4.1 (2016 May 23) 160 | 161 | * Hotkeys: delete, f for fit, s to zoom selection (#272) thanks @ifitzpatrick 162 | 163 | ## 0.4.0 (2015 December 5) 164 | 165 | * React 0.14.3 (#231) thanks @u39kun 166 | 167 | ## 0.3.12 (2015 September 30) 168 | 169 | * Build dependencies 170 | * JSHint with inline scripts; remove grunt-lint-inline 171 | * Polymer 0.5.6 172 | * Fix tooltip bug (#226) 173 | * Fix pinch-to-zoom crash (introduced with #218) 174 | 175 | ## 0.3.11 (2015 August 6) 176 | 177 | * Fire graphInitialised event (#204) 178 | * Allow max/min zoom parameterised (#218) 179 | * Ports of type 'any' highlight for incoming edges (#220) 180 | * Better thumbnail drawing (#221) 181 | * (previous 4 thanks @townxelliot) 182 | * Icon/library fix (#223) 183 | * Font Awesome 4.4.0 184 | * React 0.13.3 185 | 186 | ## 0.3.10 (2015 January 23) 187 | 188 | * Font Awesome 4.3.0 189 | * React 0.12.2 190 | * ~~Polymer 0.5.3~~ (doesn't work in Safari 7-8) 191 | 192 | ## 0.3.9 (2015 January 20) 193 | 194 | * Define offset of graph editor (#190) thanks @fabiancook 195 | * Improve heartbeats for animated edges (#194) thanks @lhausermann 196 | * Fire "nodes" event when selected nodes change 197 | * Fire "edges" event when selected edges change 198 | * Remove selection on delete (#195) 199 | 200 | ## 0.3.8 (2014 December 19) 201 | 202 | * Update to Polymer 0.5.2 203 | 204 | ## 0.3.7 (2014 December 11) 205 | 206 | * `React.createFactory` and `displayName` for all React elements 207 | * Can't access element `key` in React >=0.12.0 ([reduced case](http://jsbin.com/wuseho/1/edit?js,output)) 208 | 209 | ## 0.3.6 (2014 December 11) 210 | 211 | * Update to [React 0.12.1](https://github.com/facebook/react/releases/tag/v0.12.1) 212 | * Move to [klayjs-noflo](https://github.com/noflo/klayjs-noflo) 213 | * Fix Windows build (#192) 214 | 215 | ## 0.3.5 (Durham, 2014 November 14) 216 | 217 | * Update to [Polymer 0.5.1](https://github.com/Polymer/polymer/releases/tag/0.5.1) 218 | 219 | ## 0.3.4 (London, 2014 October 23) 220 | 221 | * Enable SVG icon to be loaded (#178) thanks @lhausermann 222 | * font-awesome to 4.2.0 223 | 224 | ## 0.3.3 (2014 September 18) 225 | 226 | * Copy and paste (#167) thanks @mpricope 227 | 228 | ## 0.3.2 (2014 September 11) 229 | 230 | * Deployed with noflo-ui 0.2.0 231 | * map-style zooming to node (#165) thanks @djdeath 232 | 233 | ## 0.3.1 (2014 September 4) 234 | 235 | * PolymerGestures fixed with Polymer 0.4.0 236 | 237 | ## 0.3.0 (2014 August 21) 238 | 239 | * Factories for component customization (#157) thanks @hayesmg 240 | * Node height expands when there are many ports (#158) 241 | 242 | ## 0.2.6 (Helsinki, 2014 August 4) 243 | 244 | * lib updates 245 | * including noflo via npm; build with `grunt browserify` 246 | 247 | ## 0.2.5 (Helsinki, 2014 June 3) 248 | 249 | * `grunt build` now builds the Font Awesome unicode map, so we can use aliases #149 250 | 251 | ## 0.2.3 (Frisco, 2014 May 25) 252 | 253 | * [loopback-tweak](https://cloud.githubusercontent.com/assets/395307/3077862/ee7f0c1a-e447-11e3-920d-6aebe75cfd76.gif) 254 | 255 | ## 0.2.0 (Frisco, 2014 May 17) 256 | 257 | * Working with Polymer 0.2.3 and native custom elements. 258 | 259 | ## 0.1.0 (2014 April 2) 260 | 261 | * First all-SVG version 262 | 263 | ## 0.0.2 (2014 February 24) 264 | 265 | * Goodbye slow DOM 266 | 267 | ## 0.0.1 (2013 Nov 28) 268 | 269 | * All custom elements 270 | -------------------------------------------------------------------------------- /LICENSE-MIT.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013-2016 TheGrid (Rituwall Inc.) 2 | 3 | Permission is hereby granted, free of charge, to any person 4 | obtaining a copy of this software and associated documentation 5 | files (the "Software"), to deal in the Software without 6 | restriction, including without limitation the rights to use, 7 | copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the 9 | Software is furnished to do so, subject to the following 10 | conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 19 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | The Graph Editor [![MIT license](http://img.shields.io/badge/License-MIT-brightgreen.svg)](#license) 2 | ================ 3 | 4 | This project provides a set [React](https://facebook.github.io/react) components for viewing and editing node-based graphs. 5 | The focus is on graphs used for dataflow and [Flow-based programming](https://en.wikipedia.org/wiki/Flow-based_programming). 6 | 7 | The graph structure is stored by [fbp-graph](https://github.com/flowbased/fbp-graph), which supports extendable metadata and undo/redo. 8 | 9 | You can optionally use [klayjs-noflo](https://github.com/noflo/klayjs-noflo) for automatic layout of graphs. 10 | 11 | `the-graph` is used as the editor in the [Flowhub IDE](https://app.flowhub.io). 12 | 13 | ## Examples 14 | 15 | * Basic demo. [code](./examples/demo-simple.js) | 16 | [Run](https://flowhub.github.io/the-graph/demo-simple.html) 17 | * Stresstest. [code](./examples/demo-full.js) | 18 | [Run](https://flowhub.github.io/the-graph/demo-full.html) 19 | * Thumbnail. [code](./examples/demo-thumbnail.js) | 20 | [Run](https://flowhub.github.io/the-graph/demo-thumbnail.html) 21 | 22 | ## Using 23 | 24 | Install via NPM 25 | 26 | npm install the-graph 27 | 28 | See the examples for how to include the `.js` and `.css` files, and API usage. 29 | 30 | ## License 31 | 32 | [The MIT License](./LICENSE-MIT.txt) 33 | 34 | ## Support 35 | Please refer to . 36 | 37 | ## Developing 38 | 39 | Clone the repo 40 | 41 | git clone https://github.com/flowhub/the-graph.git # or your own fork on Github 42 | cd the-graph 43 | 44 | Install dependencies and build 45 | 46 | npm install 47 | npm run build 48 | 49 | Run the demo server 50 | 51 | npm start 52 | 53 | or for interactive demo. 54 | 55 | Send pull requests on Github! 56 | -------------------------------------------------------------------------------- /__tests__/basics.js: -------------------------------------------------------------------------------- 1 | const { render } = require("enzyme"); 2 | const fbpGraph = require("fbp-graph"); 3 | const TheGraph = require("../index.js"); 4 | 5 | const parseFBP = fbpString => 6 | new Promise((resolve, reject) => 7 | fbpGraph.graph.loadFBP( 8 | fbpString, 9 | (err, graph) => 10 | err instanceof fbpGraph.Graph 11 | ? resolve(err) 12 | : err ? reject(err) : resolve(graph) 13 | ) 14 | ); 15 | 16 | const dummyComponent = { 17 | inports: [ 18 | { 19 | name: "in", 20 | type: "all" 21 | } 22 | ], 23 | outports: [ 24 | { 25 | name: "out", 26 | type: "all" 27 | } 28 | ] 29 | }; 30 | 31 | const name = "'42' -> CONFIG foo(Foo) OUT -> IN bar(Bar)"; 32 | const library = { Foo: dummyComponent, Bar: dummyComponent }; 33 | 34 | describe("Basics", function() { 35 | describe("loading a simple graph", function() { 36 | let rendered; 37 | 38 | beforeAll(async () => { 39 | const graph = await parseFBP(name); 40 | rendered = render(TheGraph.App({ graph, library })); 41 | }); 42 | 43 | it("should render 2 nodes", () => { 44 | expect(rendered.find(".node")).toHaveLength(2); 45 | }); 46 | 47 | it("should render 1 edge", () => { 48 | expect(rendered.find(".edge")).toHaveLength(1); 49 | }); 50 | 51 | it("should render 1 IIP", () => { 52 | expect(rendered.find(".iip")).toHaveLength(1); 53 | }); 54 | }); 55 | }); 56 | -------------------------------------------------------------------------------- /__tests__/editing.js: -------------------------------------------------------------------------------- 1 | const { mount } = require("enzyme"); 2 | const fbpGraph = require("fbp-graph"); 3 | const TheGraph = require("../index.js"); 4 | 5 | const parseFBP = fbpString => 6 | new Promise((resolve, reject) => 7 | fbpGraph.graph.loadFBP( 8 | fbpString, 9 | (err, graph) => 10 | err instanceof fbpGraph.Graph 11 | ? resolve(err) 12 | : err ? reject(err) : resolve(graph) 13 | ) 14 | ); 15 | 16 | const dummyComponent = { 17 | inports: [ 18 | { 19 | name: "in", 20 | type: "all" 21 | } 22 | ], 23 | outports: [ 24 | { 25 | name: "out", 26 | type: "all" 27 | } 28 | ] 29 | }; 30 | 31 | const name = "'42' -> CONFIG foo(Foo) OUT -> IN bar(Bar)"; 32 | const library = { Foo: dummyComponent, Bar: dummyComponent }; 33 | 34 | const simulate = ( 35 | node, 36 | type, 37 | data = {}, 38 | opts = { bubbles: true, cancelable: true } 39 | ) => node.dispatchEvent(Object.assign(new Event(type, opts), data)); 40 | 41 | describe("Editor navigation", () => { 42 | let mounted, svg, raf; 43 | 44 | beforeEach(async () => { 45 | const graph = await parseFBP(name); 46 | raf = window.requestAnimationFrame = jest.fn(); 47 | mounted = mount(TheGraph.App({ graph, library })); 48 | svg = mounted.getDOMNode().getElementsByClassName("app-svg")[0]; 49 | }); 50 | 51 | afterEach(() => mounted.unmount()); 52 | 53 | describe("dragging on background", () => { 54 | it("should pan graph view", () => { 55 | const deltaX = 100; 56 | const deltaY = 200; 57 | expect(mounted.state("x")).toBe(0); 58 | expect(mounted.state("y")).toBe(0); 59 | simulate(svg, "panstart"); 60 | simulate(svg, "panmove", { gesture: { deltaX, deltaY } }); 61 | simulate(svg, "panend"); 62 | expect(mounted.state("x")).toBe(deltaX); 63 | expect(mounted.state("y")).toBe(deltaY); 64 | }); 65 | }); 66 | 67 | describe("mouse scrolling up", () => { 68 | it("should zoom in", () => { 69 | const deltaY = -100; 70 | expect(mounted.state("scale")).toBe(1); 71 | svg.onwheel = null; 72 | simulate(svg, "wheel", { deltaY }); 73 | expect(raf).toHaveBeenCalledTimes(1); 74 | raf.mock.calls[0][0](); 75 | expect(mounted.state("scale")).toBe(1.2); 76 | }); 77 | }); 78 | 79 | describe("mouse scrolling down", () => { 80 | it("should zoom out", () => { 81 | const deltaY = 100; 82 | expect(mounted.state("scale")).toBe(1); 83 | svg.onwheel = null; 84 | simulate(svg, "wheel", { deltaY }); 85 | expect(raf).toHaveBeenCalledTimes(1); 86 | raf.mock.calls[0][0](); 87 | expect(mounted.state("scale")).toBe(0.8); 88 | }); 89 | }); 90 | 91 | describe("multitouch pinch", () => { 92 | it("should zoom in/out", () => { 93 | expect(mounted.state("scale")).toBe(1); 94 | const touches = [ 95 | { target: svg, identifier: "0", clientX: 0, clientY: 0 }, 96 | { target: svg, identifier: "1", clientX: 100, clientY: 100 } 97 | ]; 98 | simulate(svg, "touchstart", { touches, changedTouches: touches }); 99 | touches[1].clientX = 50; 100 | simulate(svg, "touchmove", { touches, changedTouches: [touches[1]] }); 101 | simulate(svg, "touchend", { touches, changedTouches: touches }); 102 | expect(mounted.state("scale")).toBe(0.7905694150420948); 103 | simulate(svg, "touchstart", { touches, changedTouches: touches }); 104 | touches[1].clientX = 100; 105 | simulate(svg, "touchmove", { touches, changedTouches: [touches[1]] }); 106 | simulate(svg, "touchend", { touches, changedTouches: touches }); 107 | expect(mounted.state("scale")).toBe(1); 108 | }); 109 | }); 110 | 111 | describe("hovering an node", () => { 112 | it("should highlight node"); 113 | }); 114 | describe("hovering an edge", () => { 115 | it("should highlight edge"); 116 | }); 117 | describe("hovering exported port", () => { 118 | it("should highlight exported port"); 119 | }); 120 | describe("hovering node group", () => { 121 | it("should highlight the group"); 122 | }); 123 | }); 124 | 125 | describe("Editor", () => { 126 | let mounted, svg, raf, selectedNodes, selectedEdges; 127 | 128 | beforeEach(async () => { 129 | selectedNodes = {}; 130 | selectedEdges = {}; 131 | const graph = await parseFBP(name); 132 | raf = window.requestAnimationFrame = jest.fn(); 133 | mounted = mount( 134 | TheGraph.App({ 135 | graph, 136 | library, 137 | onNodeSelection: (id, node, toggle) => { 138 | if (toggle) return (selectedNodes[id] = !selectedNodes[id]); 139 | selectedNodes = selectedNodes[id] ? {} : { [id]: true }; 140 | } 141 | }) 142 | ); 143 | svg = mounted.getDOMNode().getElementsByClassName("app-svg")[0]; 144 | }); 145 | 146 | afterEach(() => mounted.unmount()); 147 | 148 | describe("dragging on node", () => { 149 | it("should move the node", () => { 150 | const deltaX = 100; 151 | const deltaY = 200; 152 | expect(mounted.props().graph.nodes[0].metadata.x).toBe(0); 153 | expect(mounted.props().graph.nodes[0].metadata.y).toBe(0); 154 | const [node] = mounted.getDOMNode().getElementsByClassName("node"); 155 | simulate(node, "panstart"); 156 | simulate(node, "panmove", { gesture: { deltaX, deltaY } }); 157 | simulate(node, "panend"); 158 | raf.mock.calls.forEach(([c]) => c()); 159 | expect(mounted.props().graph.nodes[0].metadata.x).toBe(108); 160 | expect(mounted.props().graph.nodes[0].metadata.y).toBe(216); 161 | }); 162 | }); 163 | 164 | describe("dragging on exported port", () => { 165 | it("should move the port"); 166 | }); 167 | 168 | describe("dragging from node port", () => { 169 | it("should start making edge", () => { 170 | const deltaX = 100; 171 | const deltaY = 200; 172 | expect(svg.getElementsByClassName("edge")).toHaveLength(1); 173 | const [port] = svg.getElementsByClassName("port"); 174 | simulate(port, "panstart"); 175 | simulate(port, "panmove", { gesture: { deltaX, deltaY } }); 176 | raf.mock.calls.forEach(([c]) => c()); 177 | expect(svg.getElementsByClassName("edge")).toHaveLength(2); 178 | }); 179 | }); 180 | 181 | describe("dropping started edge on port", () => { 182 | it("should connect the edge", () => { 183 | const deltaX = 100; 184 | const deltaY = 200; 185 | const nodes = [...svg.getElementsByClassName("node")]; 186 | const [p1, p2] = nodes.map( 187 | (n, i) => 188 | n 189 | .getElementsByClassName(i ? "outports" : "inports")[0] 190 | .getElementsByClassName("port")[0] 191 | ); 192 | simulate(p1, "panstart"); 193 | simulate(p1, "panmove", { gesture: { deltaX, deltaY } }); 194 | simulate(p1, "panend"); 195 | simulate(p2, "tap"); 196 | raf.mock.calls.forEach(([c]) => c()); 197 | expect(mounted.props().graph.edges).toHaveLength(2); 198 | }); 199 | }); 200 | 201 | describe("dropping started edge outside", () => { 202 | it("should not connect the edge", () => { 203 | const deltaX = 100; 204 | const deltaY = 200; 205 | const [p1] = svg 206 | .getElementsByClassName("node")[0] 207 | .getElementsByClassName("inports")[0] 208 | .getElementsByClassName("port"); 209 | simulate(p1, "panstart"); 210 | simulate(p1, "panmove", { gesture: { deltaX, deltaY } }); 211 | simulate(p1, "panend"); 212 | simulate(svg, "click"); 213 | raf.mock.calls.forEach(([c]) => c()); 214 | expect(mounted.props().graph.edges).toHaveLength(1); 215 | }); 216 | }); 217 | 218 | describe("clicking exported port", () => { 219 | it("does nothing"); 220 | }); 221 | 222 | describe("clicking unselected node", () => { 223 | it("should add node to selection", () => { 224 | expect(selectedNodes).toEqual({}); 225 | simulate(svg.getElementsByClassName("node")[0], "tap"); 226 | raf.mock.calls.forEach(([c]) => c()); 227 | expect(selectedNodes).toEqual({ foo: true }); 228 | }); 229 | }); 230 | 231 | describe("clicking selected node", () => { 232 | it("should remove node from selection", () => { 233 | selectedNodes = { foo: true }; 234 | simulate(svg.getElementsByClassName("node")[0], "tap"); 235 | raf.mock.calls.forEach(([c]) => c()); 236 | expect(selectedNodes).toEqual({}); 237 | }); 238 | }); 239 | 240 | describe("clicking unselected edge", () => { 241 | it("should add edge to selection"); 242 | }); 243 | describe("clicking selected edge", () => { 244 | it("should remove edge from selection"); 245 | }); 246 | describe("selected nodes", () => { 247 | it("are visualized with a bounding box"); 248 | describe("when dragging the box", () => { 249 | it("moves all nodes in selection"); 250 | }); 251 | }); 252 | describe("node groups", () => { 253 | it("are visualized with a bounding box"); 254 | it("shows group name"); 255 | it("shows group description"); 256 | describe("when dragging on label", () => { 257 | it("moves all nodes in group"); 258 | }); 259 | describe("when dragging on bounding box", () => { 260 | it("does nothing"); 261 | }); 262 | }); 263 | describe("right-click node", () => { 264 | it("should open menu for node"); 265 | }); 266 | describe("right-click node port", () => { 267 | it("should open menu for port"); 268 | }); 269 | describe("right-click edge", () => { 270 | it("should open menu for edge"); 271 | }); 272 | describe("right-click exported port", () => { 273 | it("should open menu for exported port"); 274 | }); 275 | describe("right-click node group", () => { 276 | it("should open menu for group"); 277 | }); 278 | describe("right-click background", () => { 279 | it("should open menu for editor"); 280 | }); 281 | describe("long-press", () => { 282 | it("should work same as right-click"); 283 | }); 284 | }); 285 | 286 | describe("Editor menus", () => { 287 | describe("node menu", () => { 288 | it("shows node name"); 289 | it("shows component icon"); 290 | it("should have delete action"); 291 | it("should have copy action"); 292 | it("should show in and outports"); 293 | describe("clicking port", () => { 294 | it("should start edge"); 295 | }); 296 | }); 297 | describe("node port menu", () => { 298 | it("should have export action"); 299 | }); 300 | describe("edge menu", () => { 301 | it("shows edge name"); 302 | it("should have delete action"); 303 | }); 304 | describe("exported port menu", () => { 305 | it("should have delete action"); 306 | }); 307 | describe("node selection menu", () => { 308 | it("should have group action"); 309 | it("should have copy action"); 310 | }); 311 | describe("editor menu", () => { 312 | it("should have paste action"); 313 | }); 314 | }); 315 | -------------------------------------------------------------------------------- /__tests__/graphchanges.js: -------------------------------------------------------------------------------- 1 | describe("Graph changes", () => { 2 | describe("adding node", () => { 3 | it("should update editor"); 4 | }); 5 | describe("removing node", () => { 6 | it("should update editor"); 7 | }); 8 | describe("adding edge", () => { 9 | it("should update editor"); 10 | }); 11 | describe("removing edge", () => { 12 | it("should update editor"); 13 | }); 14 | describe("adding inport", () => { 15 | it("should update editor"); 16 | }); 17 | describe("removing inport", () => { 18 | it("should update editor"); 19 | }); 20 | describe("adding outport", () => { 21 | it("should update editor"); 22 | }); 23 | describe("removing outport", () => { 24 | it("should update editor"); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /__tests__/nav.js: -------------------------------------------------------------------------------- 1 | describe("Navigation", () => { 2 | it("renders a simplified version of graph"); 3 | it("supports dark & light theme"); 4 | it("supports specifying nodeSize"); 5 | it("supports fill,stroke,edge styles"); 6 | }); 7 | -------------------------------------------------------------------------------- /__tests__/thumbnail.js: -------------------------------------------------------------------------------- 1 | describe("Thumbnail", () => { 2 | it("renders a thumbnail of graph"); 3 | it("visualizes the viewport area"); 4 | it("allows to pan viewport"); 5 | it("hides when whole graph is in view"); 6 | }); 7 | -------------------------------------------------------------------------------- /bin/the-graph-render: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | // Node.js CLI tool for rendering an image of the graph 4 | 5 | const buffer = require('buffer'); 6 | const fs = require('fs'); 7 | const path = require('path'); 8 | const http = require('http'); 9 | 10 | const argv = require('yargs').argv 11 | const jsjob = require('jsjob'); 12 | 13 | function unpackUrl(dataurl) { 14 | var mimetype = dataurl.substring(dataurl.indexOf(':'), dataurl.indexOf(';')); 15 | var encoding = dataurl.substring(dataurl.indexOf(';')+1, dataurl.indexOf(',')); 16 | if (encoding != 'base64') { 17 | throw new Error('Dataurl must have base64 encoding, got ' + encoding); 18 | } 19 | 20 | var encoded = dataurl.substring(dataurl.indexOf(','), dataurl.length); 21 | var raw = buffer.Buffer.from(encoded, 'base64'); 22 | return raw; 23 | } 24 | 25 | function setupJobServer(jobData, options, callback) { 26 | if (!options.port) { options.port = 9999; } 27 | if (!options.path) { options.path = '/the-graph-render.js'; } 28 | 29 | function onRequest(req, res) { 30 | if (req.url == options.path) { 31 | res.end(jobData); 32 | } else { 33 | res.writeHead(404); 34 | res.end(); 35 | } 36 | } 37 | 38 | server = http.createServer(onRequest); 39 | server.listen(options.port, function(err) { 40 | return callback(err, server, options); 41 | }); 42 | } 43 | 44 | 45 | function runRender(graphData, options, callback) { 46 | if (!options.job) { options.job = 'http://localhost:9999/the-graph-render.js'; } 47 | 48 | var runnerConfig = { 49 | verbose: options.verbose, 50 | }; 51 | var runner = new jsjob.Runner(runnerConfig); 52 | runner.start(function(err) { 53 | if (err) return callback(err); 54 | 55 | runner.runJob(options.job, graphData, options, function(err, result, details) { 56 | if (err) { return callback(err); } 57 | 58 | runner.stop(function(err) { 59 | return callback(err, result); 60 | }); 61 | }); 62 | }); 63 | 64 | } 65 | 66 | function render(graphPath, options, callback) { 67 | if (!options.format) { options.format = 'png' } 68 | if (!options.output) { 69 | options.output = graphPath.replace(path.extname(graphPath), '.'+options.format) 70 | } 71 | 72 | const p = path.join(__dirname, '../dist/the-graph-render.js'); 73 | const defaultJobData = fs.readFileSync(p); 74 | 75 | fs.readFile(graphPath, 'utf-8', function(err, d) { 76 | if (err) { return callback(err) } 77 | try { 78 | graphData = JSON.parse(d); 79 | } catch (err) { 80 | return callback(err); 81 | } 82 | 83 | setupJobServer(defaultJobData, {}, function(err, server) { 84 | if (err) return callback(err); 85 | 86 | runRender(graphData, options, function (err, output, details) { 87 | if (err) { return callback(err); } 88 | 89 | if (output.indexOf('data:') == 0) { 90 | output = unpackUrl(output); 91 | } 92 | 93 | fs.writeFile(options.output, output, function(err) { 94 | return callback(err, options.output); 95 | }); 96 | }); 97 | }); 98 | }); 99 | 100 | } 101 | 102 | function main() { 103 | var callback = function(err, out) { 104 | if (err) { 105 | console.error(err); 106 | console.error(err.stack); 107 | process.exit(1); 108 | } 109 | console.log('Written to', out); 110 | process.exit(0); 111 | }; 112 | render(argv._[0], argv, callback); 113 | } 114 | 115 | if (!module.parent) { 116 | main(); 117 | } 118 | -------------------------------------------------------------------------------- /examples/assets/loading.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flowhub/the-graph/6f6823fcd1a0956532ec68357a84aee2740e2640/examples/assets/loading.gif -------------------------------------------------------------------------------- /examples/demo-full.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Graph Editor 5 | 6 | 7 | 8 | 9 | 10 | 11 | 38 | 39 | 40 | 41 | 42 |
43 | 44 | 45 | 46 |
47 | 48 | 49 | 50 |
51 | 52 |
53 |
loading custom elements...
54 |
55 | 56 | 57 | -------------------------------------------------------------------------------- /examples/demo-full.js: -------------------------------------------------------------------------------- 1 | const fbpGraph = require('fbp-graph'); 2 | const React = require('react'); 3 | const ReactDOM = require('react-dom'); 4 | const TheGraph = require('../index.js'); 5 | 6 | require('font-awesome/css/font-awesome.css'); 7 | require('../themes/the-graph-dark.styl'); 8 | require('../themes/the-graph-light.styl'); 9 | 10 | // Context menu specification 11 | function deleteNode(graph, itemKey, item) { 12 | graph.removeNode(itemKey); 13 | } 14 | function deleteEdge(graph, itemKey, item) { 15 | graph.removeEdge(item.from.node, item.from.port, item.to.node, item.to.port); 16 | } 17 | const contextMenus = { 18 | main: null, 19 | selection: null, 20 | nodeInport: null, 21 | nodeOutport: null, 22 | graphInport: null, 23 | graphOutport: null, 24 | edge: { 25 | icon: 'long-arrow-right', 26 | s4: { 27 | icon: 'trash', 28 | iconLabel: 'delete', 29 | action: deleteEdge, 30 | }, 31 | }, 32 | node: { 33 | s4: { 34 | icon: 'trash', 35 | iconLabel: 'delete', 36 | action: deleteNode, 37 | }, 38 | }, 39 | group: { 40 | icon: 'th', 41 | s4: { 42 | icon: 'trash', 43 | iconLabel: 'ungroup', 44 | action(graph, itemKey, item) { 45 | graph.removeGroup(itemKey); 46 | }, 47 | }, 48 | }, 49 | }; 50 | 51 | const appState = { 52 | graph: new fbpGraph.Graph(), 53 | library: {}, 54 | iconOverrides: {}, 55 | theme: 'dark', 56 | editorViewX: 0, 57 | editorViewY: 0, 58 | editorScale: 1, 59 | }; 60 | 61 | // Attach nav 62 | function fitGraphInView() { 63 | editor.triggerFit(); 64 | } 65 | 66 | function panEditorTo() { 67 | } 68 | 69 | function renderNav() { 70 | const view = [ 71 | appState.editorViewX, appState.editorViewY, 72 | window.innerWidth, window.innerHeight, 73 | ]; 74 | const props = { 75 | height: 162, 76 | width: 216, 77 | graph: appState.graph, 78 | onTap: fitGraphInView, 79 | onPanTo: panEditorTo, 80 | viewrectangle: view, 81 | viewscale: appState.editorScale, 82 | }; 83 | 84 | const element = React.createElement(TheGraph.nav.Component, props); 85 | ReactDOM.render(element, document.getElementById('nav')); 86 | } 87 | function editorPanChanged(x, y, scale) { 88 | appState.editorViewX = -x; 89 | appState.editorViewY = -y; 90 | appState.editorScale = scale; 91 | renderNav(); 92 | } 93 | 94 | function renderApp() { 95 | const editor = document.getElementById('editor'); 96 | editor.className = `the-graph-${appState.theme}`; 97 | 98 | const props = { 99 | width: window.innerWidth, 100 | height: window.innerWidth, 101 | graph: appState.graph, 102 | library: appState.library, 103 | menus: contextMenus, 104 | nodeIcons: appState.iconOverrides, 105 | onPanScale: editorPanChanged, 106 | }; 107 | 108 | editor.width = props.width; 109 | editor.height = props.height; 110 | const element = React.createElement(TheGraph.App, props); 111 | ReactDOM.render(element, editor); 112 | 113 | renderNav(); 114 | } 115 | renderApp(); // initial 116 | 117 | // Follow changes in window size 118 | window.addEventListener('resize', renderApp); 119 | 120 | // Toggle theme 121 | let theme = 'dark'; 122 | document.getElementById('theme').addEventListener('click', () => { 123 | theme = (theme === 'dark' ? 'light' : 'dark'); 124 | appState.theme = theme; 125 | renderApp(); 126 | }); 127 | 128 | // Autolayout button 129 | document.getElementById('autolayout').addEventListener('click', () => { 130 | // TODO: support via React props 131 | editor.triggerAutolayout(); 132 | }); 133 | 134 | // Focus a node 135 | document.getElementById('focus').addEventListener('click', () => { 136 | // TODO: support via React props 137 | const { nodes } = appState.graph; 138 | const randomNode = nodes[Math.floor(Math.random() * nodes.length)]; 139 | editor.focusNode(randomNode); 140 | }); 141 | 142 | // Simulate node icon updates 143 | const iconKeys = Object.keys(TheGraph.FONT_AWESOME); 144 | window.setInterval(() => { 145 | const { nodes } = appState.graph; 146 | if (nodes.length > 0) { 147 | const randomNodeId = nodes[Math.floor(Math.random() * nodes.length)].id; 148 | const randomIcon = iconKeys[Math.floor(Math.random() * iconKeys.length)]; 149 | appState.iconOverrides[randomNodeId] = randomIcon; 150 | renderApp(); 151 | } 152 | }, 1000); 153 | 154 | // Simulate un/triggering errors 155 | let errorNodeId = null; 156 | const makeRandomError = function () { 157 | if (errorNodeId) { 158 | editor.removeErrorNode(errorNodeId); 159 | } 160 | const { nodes } = appState.graph; 161 | if (nodes.length > 0) { 162 | errorNodeId = nodes[Math.floor(Math.random() * nodes.length)].id; 163 | editor.addErrorNode(errorNodeId); 164 | editor.updateErrorNodes(); 165 | } 166 | }; 167 | // window.setInterval(makeRandomError, 3551); // TODO: support error nodes via React props 168 | // makeRandomError(); 169 | 170 | // Load initial graph 171 | const loadingMessage = document.getElementById('loading-message'); 172 | window.loadGraph = function (json) { 173 | // Load graph 174 | loadingMessage.innerHTML = 'loading graph data...'; 175 | 176 | const graphData = json.data ? JSON.parse(json.data.files['noflo.json'].content) : json; 177 | 178 | fbpGraph.graph.loadJSON(JSON.stringify(graphData), (err, graph) => { 179 | if (err) { 180 | loadingMessage.innerHTML = `error loading graph: ${err.toString()}`; 181 | return; 182 | } 183 | // Remove loading message 184 | const loading = document.getElementById('loading'); 185 | loading.parentNode.removeChild(loading); 186 | // Synthesize component library from graph 187 | appState.library = TheGraph.library.libraryFromGraph(graph); 188 | // Set loaded graph 189 | appState.graph = graph; 190 | appState.graph.on('endTransaction', renderApp); // graph changed 191 | renderApp(); 192 | 193 | console.log('loaded', graph); 194 | }); 195 | }; 196 | require('./assets/photobooth.json.js'); 197 | -------------------------------------------------------------------------------- /examples/demo-simple.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Graph Editor Demo 5 | 6 | 7 | 8 | 9 | 10 | 32 | 33 | 34 | 35 | 36 |
37 | 38 |
39 | 40 | 41 | 42 | 43 | 44 |
45 |
46 |
loading custom elements...
47 |
48 | 49 | 50 | -------------------------------------------------------------------------------- /examples/demo-simple.js: -------------------------------------------------------------------------------- 1 | const fbpGraph = require('fbp-graph'); 2 | const React = require('react'); 3 | const ReactDOM = require('react-dom'); 4 | const TheGraph = require('../index.js'); 5 | 6 | require('font-awesome/css/font-awesome.css'); 7 | require('../themes/the-graph-dark.styl'); 8 | require('../themes/the-graph-light.styl'); 9 | 10 | // Remove loading message 11 | document.body.removeChild(document.getElementById('loading')); 12 | 13 | // The graph editor 14 | const editor = document.getElementById('editor'); 15 | 16 | // Component library 17 | const library = { 18 | basic: { 19 | name: 'basic', 20 | description: 'basic demo component', 21 | icon: 'eye', 22 | inports: [ 23 | { name: 'in0', type: 'all' }, 24 | { name: 'in1', type: 'all' }, 25 | { name: 'in2', type: 'all' }, 26 | ], 27 | outports: [ 28 | { name: 'out', type: 'all' }, 29 | ], 30 | }, 31 | tall: { 32 | name: 'tall', 33 | description: 'tall demo component', 34 | icon: 'cog', 35 | inports: [ 36 | { name: 'in0', type: 'all' }, 37 | { name: 'in1', type: 'all' }, 38 | { name: 'in2', type: 'all' }, 39 | { name: 'in3', type: 'all' }, 40 | { name: 'in4', type: 'all' }, 41 | { name: 'in5', type: 'all' }, 42 | { name: 'in6', type: 'all' }, 43 | { name: 'in7', type: 'all' }, 44 | { name: 'in8', type: 'all' }, 45 | { name: 'in9', type: 'all' }, 46 | { name: 'in10', type: 'all' }, 47 | { name: 'in11', type: 'all' }, 48 | { name: 'in12', type: 'all' }, 49 | ], 50 | outports: [ 51 | { name: 'out0', type: 'all' }, 52 | ], 53 | }, 54 | }; 55 | 56 | // Load empty graph 57 | let graph = new fbpGraph.Graph(); 58 | 59 | function renderEditor() { 60 | const props = { 61 | readonly: false, 62 | height: window.innerHeight, 63 | width: window.innerWidth, 64 | graph, 65 | library, 66 | }; 67 | // console.log('render', props); 68 | const editor = document.getElementById('editor'); 69 | editor.width = props.width; 70 | editor.height = props.height; 71 | const element = React.createElement(TheGraph.App, props); 72 | ReactDOM.render(element, editor); 73 | } 74 | graph.on('endTransaction', renderEditor); // graph changed 75 | window.addEventListener('resize', renderEditor); 76 | 77 | // Add node button 78 | const addnode = function () { 79 | const id = Math.round(Math.random() * 100000).toString(36); 80 | const component = Math.random() > 0.5 ? 'basic' : 'tall'; 81 | const metadata = { 82 | label: component, 83 | x: Math.round(Math.random() * 800), 84 | y: Math.round(Math.random() * 600), 85 | }; 86 | const newNode = graph.addNode(id, component, metadata); 87 | return newNode; 88 | }; 89 | document.getElementById('addnode').addEventListener('click', addnode); 90 | 91 | // Add edge button 92 | const addedge = function (outNodeID) { 93 | const { nodes } = graph; 94 | const len = nodes.length; 95 | if (len < 1) { return; } 96 | const node1 = outNodeID || nodes[Math.floor(Math.random() * len)].id; 97 | const node2 = nodes[Math.floor(Math.random() * len)].id; 98 | const port1 = `out${Math.floor(Math.random() * 3)}`; 99 | const port2 = `in${Math.floor(Math.random() * 12)}`; 100 | const meta = { route: Math.floor(Math.random() * 10) }; 101 | const newEdge = graph.addEdge(node1, port1, node2, port2, meta); 102 | return newEdge; 103 | }; 104 | document.getElementById('addedge').addEventListener('click', (event) => { addedge(); }); 105 | 106 | // Random graph button 107 | document.getElementById('random').addEventListener('click', () => { 108 | graph.startTransaction('randomgraph'); 109 | for (let i = 0; i < 20; i++) { 110 | const node = addnode(); 111 | addedge(node.id); 112 | addedge(node.id); 113 | } 114 | graph.endTransaction('randomgraph'); 115 | }); 116 | 117 | // Get graph button 118 | document.getElementById('get').addEventListener('click', () => { 119 | const graphJSON = JSON.stringify(graph.toJSON(), null, 2); 120 | alert(graphJSON); 121 | // you can use the var graphJSON to save the graph definition in a file/database 122 | }); 123 | 124 | // Clear button 125 | document.getElementById('clear').addEventListener('click', () => { 126 | graph = new fbpGraph.Graph(); 127 | renderEditor(); 128 | }); 129 | -------------------------------------------------------------------------------- /examples/demo-thumbnail.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | the-graph-thumb example 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /examples/demo-thumbnail.js: -------------------------------------------------------------------------------- 1 | const fbpGraph = require('fbp-graph'); 2 | const TheGraph = require('../index.js'); 3 | 4 | window.loadGraph = function (json) { 5 | // Load graph 6 | const graphData = json.data.files['noflo.json'].content; 7 | fbpGraph.graph.loadJSON(graphData, (err, graph) => { 8 | if (err) { 9 | throw err; 10 | } 11 | 12 | // Render the numbnail 13 | const thumb = document.getElementById('thumb'); 14 | const properties = TheGraph.thumb.styleFromTheme('dark'); 15 | properties.width = thumb.width; 16 | properties.height = thumb.height; 17 | properties.nodeSize = 60; 18 | properties.lineWidth = 1; 19 | const context = thumb.getContext('2d'); 20 | const info = TheGraph.thumb.render(context, graph, properties); 21 | }); 22 | }; 23 | const body = document.querySelector('body'); 24 | const script = document.createElement('script'); 25 | script.type = 'application/javascript'; 26 | // Clock 27 | script.src = 'https://api.github.com/gists/7135158?callback=loadGraph'; 28 | // Gesture object building (lots of ports!) 29 | // script.src = 'https://api.github.com/gists/7022120?callback=loadGraph'; 30 | // Gesture data gathering (big graph) 31 | // script.src = 'https://api.github.com/gists/7022262?callback=loadGraph'; 32 | // Edge algo test 33 | // script.src = 'https://api.github.com/gists/6890344?callback=loadGraph'; 34 | body.appendChild(script); 35 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | // Module object 2 | const TheGraph = {}; 3 | 4 | // Bundle and expose fbp-graph as public API 5 | TheGraph.fbpGraph = require('fbp-graph'); 6 | 7 | // Pull in Ease from NPM, react.animate needs it as a global 8 | TheGraph.Ease = require('ease-component'); 9 | 10 | if (typeof window !== 'undefined' && typeof window.Ease === 'undefined') { 11 | window.Ease = TheGraph.Ease; 12 | } 13 | 14 | const defaultNodeSize = 72; 15 | const defaultNodeRadius = 8; 16 | 17 | const moduleVars = { 18 | // Context menus 19 | contextPortSize: 36, 20 | // Zoom breakpoints 21 | zbpBig: 1.2, 22 | zbpNormal: 0.4, 23 | zbpSmall: 0.01, 24 | config: { 25 | nodeSize: defaultNodeSize, 26 | nodeWidth: defaultNodeSize, 27 | nodeRadius: defaultNodeRadius, 28 | nodeHeight: defaultNodeSize, 29 | autoSizeNode: true, 30 | maxPortCount: 9, 31 | nodeHeightIncrement: 12, 32 | focusAnimationDuration: 1500, 33 | }, 34 | }; 35 | Object.keys(moduleVars).forEach((key) => { 36 | TheGraph[key] = moduleVars[key]; 37 | }); 38 | 39 | if (typeof window !== 'undefined') { 40 | // rAF shim 41 | window.requestAnimationFrame = window.requestAnimationFrame 42 | || window.webkitRequestAnimationFrame 43 | || window.mozRequestAnimationFrame 44 | || window.msRequestAnimationFrame; 45 | } 46 | 47 | // HACK, goes away when everything is CommonJS compatible 48 | const g = { TheGraph }; 49 | 50 | TheGraph.factories = require('./the-graph/factories.js'); 51 | TheGraph.merge = require('./the-graph/merge.js'); 52 | 53 | require('./the-graph/the-graph-app.js').register(g); 54 | require('./the-graph/the-graph-graph.js').register(g); 55 | require('./the-graph/the-graph-node.js').register(g); 56 | require('./the-graph/the-graph-node-menu.js').register(g); 57 | require('./the-graph/the-graph-node-menu-port.js').register(g); 58 | require('./the-graph/the-graph-node-menu-ports.js').register(g); 59 | require('./the-graph/the-graph-port.js').register(g); 60 | require('./the-graph/the-graph-edge.js').register(g); 61 | require('./the-graph/the-graph-iip.js').register(g); 62 | require('./the-graph/the-graph-group.js').register(g); 63 | 64 | TheGraph.menu = require('./the-graph/the-graph-menu.js'); 65 | // compat 66 | TheGraph.Menu = TheGraph.menu.Menu; 67 | TheGraph.factories.menu = TheGraph.menu.factories; 68 | TheGraph.config.menu = TheGraph.menu.config; 69 | TheGraph.config.menu.iconRect.rx = TheGraph.config.nodeRadius; 70 | TheGraph.config.menu.iconRect.ry = TheGraph.config.nodeRadius; 71 | 72 | TheGraph.modalbg = require('./the-graph/the-graph-modalbg.js'); 73 | // compat 74 | TheGraph.ModalBG = TheGraph.modalbg.ModalBG; 75 | TheGraph.config.ModalBG = TheGraph.config.factories; 76 | TheGraph.factories.ModalBG = TheGraph.modalbg.factories; 77 | 78 | TheGraph.FONT_AWESOME = require('./the-graph/font-awesome-unicode-map.js'); 79 | 80 | const geometryutils = require('./the-graph/geometryutils'); 81 | // compat 82 | TheGraph.findMinMax = geometryutils.findMinMax; 83 | TheGraph.findNodeFit = geometryutils.findNodeFit; 84 | TheGraph.findFit = geometryutils.findFit; 85 | 86 | TheGraph.tooltip = require('./the-graph/the-graph-tooltip.js'); 87 | // compat 88 | TheGraph.Tooltip = TheGraph.tooltip.Tooltip; 89 | TheGraph.config.tooltip = TheGraph.tooltip.config; 90 | TheGraph.factories.tooltip = TheGraph.tooltip.factories; 91 | 92 | TheGraph.mixins = require('./the-graph/mixins.js'); 93 | TheGraph.arcs = require('./the-graph/arcs.js'); 94 | 95 | TheGraph.TextBG = require('./the-graph/TextBG.js'); 96 | TheGraph.SVGImage = require('./the-graph/SVGImage.js'); 97 | 98 | TheGraph.thumb = require('./the-graph-thumb/the-graph-thumb.js'); 99 | 100 | TheGraph.nav = require('./the-graph-nav/the-graph-nav.js'); 101 | 102 | TheGraph.autolayout = require('./the-graph/the-graph-autolayout.js'); 103 | TheGraph.library = require('./the-graph/the-graph-library.js'); 104 | 105 | TheGraph.clipboard = require('./the-graph-editor/clipboard.js'); 106 | 107 | TheGraph.render = require('./the-graph/render.js'); 108 | 109 | TheGraph.render.register(g); 110 | 111 | module.exports = TheGraph; 112 | -------------------------------------------------------------------------------- /jest-setup.js: -------------------------------------------------------------------------------- 1 | const Enzyme = require('enzyme'); 2 | const Adapter = require('enzyme-adapter-react-15'); 3 | 4 | Enzyme.configure({ adapter: new Adapter() }); 5 | document.documentElement.ontouchstart = () => {}; 6 | window.ontouchstart = () => {}; 7 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "the-graph", 3 | "version": "0.13.1", 4 | "description": "flow-based programming graph editing", 5 | "author": "Forrest Oliphant, the Grid", 6 | "license": "MIT", 7 | "main": "index.js", 8 | "bin": { 9 | "the-graph-render": "./bin/the-graph-render" 10 | }, 11 | "dependencies": { 12 | "@pleasetrythisathome/react.animate": "0.0.4", 13 | "create-react-class": "^15.6.2", 14 | "ease-component": "^1.0.0", 15 | "fbp-graph": "^0.7.0", 16 | "font-awesome": "^4.7.0", 17 | "hammerjs": "^2.0.8", 18 | "klayjs-noflo": "^0.3.1", 19 | "tv4": "^1.3.0", 20 | "yargs": "^16.1.1" 21 | }, 22 | "devDependencies": { 23 | "bluebird": "^3.5.1", 24 | "chai": "^4.1.2", 25 | "css-loader": "^5.0.1", 26 | "enzyme": "^3.2.0", 27 | "enzyme-adapter-react-15": "^1.0.5", 28 | "eslint": "^7.13.0", 29 | "eslint-config-airbnb": "^18.2.1", 30 | "eslint-plugin-import": "^2.22.1", 31 | "eslint-plugin-jsx-a11y": "^6.4.1", 32 | "eslint-plugin-react": "^7.21.5", 33 | "events": "^3.2.0", 34 | "file-loader": "^6.2.0", 35 | "html-webpack-plugin": "^5.0.0", 36 | "http-server": "^0.12.3", 37 | "jest": "^21.2.1", 38 | "jest-enzyme": "^4.0.1", 39 | "jsjob": "^0.10.13", 40 | "mocha": "^8.2.1", 41 | "noflo-canvas": "0.4.2", 42 | "react": "^15.6.2", 43 | "react-dom": "^15.6.2", 44 | "react-test-renderer": "^15.6.2", 45 | "style-loader": "^2.0.0", 46 | "stylus": "~0.54.5", 47 | "stylus-loader": "^5.0.0", 48 | "webpack": "^5.5.1", 49 | "webpack-cli": "^4.2.0" 50 | }, 51 | "scripts": { 52 | "lint": "eslint --fix index.js render.jsjob.js the-graph-thumb the-graph-nav the-graph-editor", 53 | "fontawesome": "node scripts/build-font-awesome-javascript.js", 54 | "stylus": "stylus -c -o dist -m themes/*.styl", 55 | "prebuild": "npm run fontawesome", 56 | "build": "webpack", 57 | "postbuild": "npm run stylus", 58 | "pretest": "npm run lint && npm run build", 59 | "test": "jest", 60 | "start": "http-server dist/ -p 3000 -s -c-1" 61 | }, 62 | "repository": { 63 | "type": "git", 64 | "url": "git://github.com/flowhub/the-graph.git" 65 | }, 66 | "keywords": [ 67 | "graph" 68 | ], 69 | "peerDependencies": { 70 | "react": "<16.0.0", 71 | "react-dom": "<16.0.0" 72 | }, 73 | "jest": { 74 | "coveragePathIgnorePatterns": [ 75 | "/node_modules/", 76 | "/dist/", 77 | "/jest-setup.js" 78 | ], 79 | "setupFiles": [ 80 | "/jest-setup.js" 81 | ], 82 | "setupTestFrameworkScriptFile": "jest-enzyme" 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /render.jsjob.js: -------------------------------------------------------------------------------- 1 | // JsJob entrypoint for rendering a FBP graph to an SVG/JPEG/PNG 2 | 3 | const TheGraph = require('./index'); 4 | 5 | require('./themes/the-graph-dark.styl'); 6 | require('./themes/the-graph-light.styl'); 7 | 8 | function waitForStyleLoad(callback) { 9 | // FIXME: check properly, https://gist.github.com/cvan/8a188df72a95a35888b70e5fda80450d 10 | setTimeout(callback, 500); 11 | } 12 | 13 | window.jsJobRun = function jsJobRun(inputdata, options, callback) { 14 | let loader = TheGraph.fbpGraph.graph.loadJSON; 15 | let graphData = inputdata; 16 | if (inputdata.fbp) { 17 | graphData = inputdata.fbp; 18 | loader = TheGraph.fbpGraph.graph.loadFBP; 19 | } 20 | 21 | loader(graphData, (err, graph) => { 22 | if (err) { 23 | callback(err); 24 | return; 25 | } 26 | console.log('loaded graph'); 27 | 28 | waitForStyleLoad(() => { 29 | let node; 30 | try { 31 | node = TheGraph.render.graphToDOM(graph, options); 32 | } catch (e) { 33 | callback(e); 34 | return; 35 | } 36 | TheGraph.render.exportImage(node, options, callback); 37 | }); 38 | }); 39 | }; 40 | -------------------------------------------------------------------------------- /scripts/build-font-awesome-javascript.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This generates ../the-graph/font-awesome-unicode-map.js for use in our SVG 3 | */ 4 | 5 | const fs = require('fs'); 6 | 7 | function generateFile(err, data) { 8 | if (err) { 9 | throw err; 10 | } 11 | 12 | const linePattern = /@fa-var-[^;]*/g; 13 | const lines = data.match(linePattern); 14 | const icons = {}; 15 | lines.forEach((line) => { 16 | const namePattern = /@fa-var-(.*): "\\(.*)"/; 17 | const match = namePattern.exec(line); 18 | if (match) { 19 | const key = match[1]; 20 | let u = `%u${match[2]}`; 21 | u = unescape(u); 22 | icons[key] = u; 23 | } 24 | }); 25 | 26 | const output = `// This file is generated via \`npm run fontawesome\`\nmodule.exports = ${JSON.stringify(icons, null, 2).replace(/"/g, '\'')};`; 27 | 28 | fs.writeFile(`${__dirname}/../the-graph/font-awesome-unicode-map.js`, output, (writeErr) => { 29 | if (writeErr) { 30 | throw writeErr; 31 | } 32 | console.log(`Font Awesome icons map saved with ${Object.keys(icons).length} icons and aliases.`); 33 | }); 34 | } 35 | 36 | fs.readFile(`${__dirname}/../node_modules/font-awesome/less/variables.less`, 'utf8', generateFile); 37 | -------------------------------------------------------------------------------- /spec/render-cli.js: -------------------------------------------------------------------------------- 1 | const chai = require('chai'); 2 | const bluebird = require('bluebird'); 3 | 4 | const child_process = require('child_process'); 5 | const fs = require('fs'); 6 | const path = require('path'); 7 | 8 | // cannot be bluebird.promisified, because returns data 9 | function execFile(prog, args, options) { 10 | return new Promise((resolve, reject) => 11 | child_process.execFile(prog, args, options, (err, stdout, stderr) => { 12 | if (err) reject(err) 13 | else resolve({ stdout: stdout, stderr: stderr }) 14 | }) 15 | ) 16 | } 17 | 18 | function readFile(fp) { 19 | return new Promise((resolve, reject) => 20 | fs.readFile(fp, (err, data) => { 21 | if (err) reject(err) 22 | else resolve(data) 23 | }) 24 | ) 25 | } 26 | 27 | //bluebird.promisifyAll(fs.readFile)(options.output) 28 | 29 | function runRenderCli(inputGraph, options) { 30 | const prog = path.join(__dirname, '../bin/the-graph-render'); 31 | 32 | //const graphPath = path.join(__dirname, 'temp/graph.json'); 33 | options.output = path.join(__dirname, 'temp/rendered.tmp'); 34 | 35 | var args = [ inputGraph ]; 36 | Object.keys(options).forEach((key) => { 37 | args.push('--'+key); 38 | args.push(options[key]); 39 | }) 40 | return bluebird.resolve(null).then(() => { 41 | //console.log('running', [prog].concat(args).join(' ')); 42 | return execFile(prog, args) 43 | }).then((out) => { 44 | chai.expect(out.stdout).to.include('Written to'); 45 | chai.expect(out.stdout).to.include(options.output); 46 | return readFile(options.output) 47 | }).then((data) => { 48 | return data 49 | }) 50 | } 51 | 52 | function fixture(name) { 53 | return path.join(__dirname, 'fixtures/'+name); 54 | } 55 | 56 | const pb = fixture('photobooth.json'); 57 | const jpegMagic = Buffer.from([0xff, 0xd8]); 58 | const pngMagic = Buffer.from([0x89, 0x50]); 59 | const renderTimeout = 10*1000; 60 | 61 | describe('the-graph-render', () => { 62 | 63 | before(() => { 64 | const tempDir = path.join(__dirname,'temp'); 65 | bluebird.promisify(fs.access)(tempDir).catch((stats) => { 66 | return bluebird.promisify(fs.mkdir)(tempDir) 67 | }); 68 | }) 69 | 70 | describe('with no options', () => { 71 | it('should output PNG file', () => { 72 | return runRenderCli(pb, {}).then((out) => { 73 | const magic = out.slice(0, 2).toString('hex'); 74 | chai.expect(magic).to.equal(pngMagic.toString('hex')) 75 | }) 76 | }).timeout(renderTimeout) 77 | }) 78 | 79 | describe('requesting JPEG', () => { 80 | it('should output JPEG file', () => { 81 | return runRenderCli(pb, { format: 'jpeg', quality: 1.0 }).then((out) => { 82 | const magic = out.slice(0, 2).toString('hex'); 83 | chai.expect(magic).to.equal(jpegMagic.toString('hex')) 84 | }) 85 | }).timeout(renderTimeout) 86 | }) 87 | 88 | describe('requesting SVG', () => { 89 | it.skip('should output SVG file', () => { 90 | return runRenderCli(pb, { format: 'svg' }).then((out) => { 91 | const contents = out.toString('utf8'); 92 | chai.expect(contents).to.include(''); 93 | }) 94 | }).timeout(renderTimeout) 95 | }) 96 | 97 | }) 98 | -------------------------------------------------------------------------------- /the-graph-editor/clipboard.js: -------------------------------------------------------------------------------- 1 | let clipboardContent = {}; // XXX: hidden state 2 | 3 | function cloneObject(obj) { 4 | return JSON.parse(JSON.stringify(obj)); 5 | } 6 | 7 | function makeNewId(label) { 8 | let num = 60466176; // 36^5 9 | num = Math.floor(Math.random() * num); 10 | const id = `${label}_${num.toString(36)}`; 11 | return id; 12 | } 13 | 14 | function copy(graph, keys) { 15 | // Duplicate all the nodes before putting them in clipboard 16 | // this will make this work also with cut/Paste and once we 17 | // decide if/how we will implement cross-document copy&paste will work there too 18 | clipboardContent = { nodes: [], edges: [] }; 19 | const map = {}; 20 | let i; let len; 21 | for (i = 0, len = keys.length; i < len; i += 1) { 22 | const node = graph.getNode(keys[i]); 23 | const newNode = cloneObject(node); 24 | newNode.id = makeNewId(node.component); 25 | clipboardContent.nodes.push(newNode); 26 | map[node.id] = newNode.id; 27 | } 28 | for (i = 0, len = graph.edges.length; i < len; i += 1) { 29 | const edge = graph.edges[i]; 30 | const fromNode = edge.from.node; 31 | const toNode = edge.to.node; 32 | if (map[fromNode] && map[toNode]) { 33 | const newEdge = cloneObject(edge); 34 | newEdge.from.node = map[fromNode]; 35 | newEdge.to.node = map[toNode]; 36 | clipboardContent.edges.push(newEdge); 37 | } 38 | } 39 | } 40 | 41 | function paste(graph) { 42 | const map = {}; 43 | const pasted = { nodes: [], edges: [] }; 44 | let i; let 45 | len; 46 | for (i = 0, len = clipboardContent.nodes.length; i < len; i += 1) { 47 | const node = clipboardContent.nodes[i]; 48 | const meta = cloneObject(node.metadata); 49 | meta.x += 36; 50 | meta.y += 36; 51 | const newNode = graph.addNode(makeNewId(node.component), node.component, meta); 52 | map[node.id] = newNode.id; 53 | pasted.nodes.push(newNode); 54 | } 55 | for (i = 0, len = clipboardContent.edges.length; i < len; i += 1) { 56 | const edge = clipboardContent.edges[i]; 57 | const newEdgeMeta = cloneObject(edge.metadata); 58 | let newEdge; 59 | if (typeof edge.from.index === 'number' || typeof edge.to.index === 'number') { 60 | // One or both ports are addressable 61 | const fromIndex = edge.from.index || null; 62 | const toIndex = edge.to.index || null; 63 | newEdge = graph.addEdgeIndex( 64 | map[edge.from.node], 65 | edge.from.port, 66 | fromIndex, 67 | map[edge.to.node], 68 | edge.to.port, 69 | toIndex, 70 | newEdgeMeta, 71 | ); 72 | } else { 73 | newEdge = graph.addEdge( 74 | map[edge.from.node], 75 | edge.from.port, 76 | map[edge.to.node], 77 | edge.to.port, 78 | newEdgeMeta, 79 | ); 80 | } 81 | pasted.edges.push(newEdge); 82 | } 83 | return pasted; 84 | } 85 | 86 | module.exports = { 87 | copy, 88 | paste, 89 | }; 90 | -------------------------------------------------------------------------------- /the-graph-editor/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /the-graph-nav/the-graph-nav.js: -------------------------------------------------------------------------------- 1 | const React = require('react'); 2 | const createReactClass = require('create-react-class'); 3 | const Hammer = require('hammerjs'); 4 | const thumb = require('../the-graph-thumb/the-graph-thumb.js'); 5 | 6 | function calculateStyleFromTheme(theme) { 7 | const style = {}; 8 | if (theme === 'dark') { 9 | style.viewBoxBorder = 'hsla(190, 100%, 80%, 0.4)'; 10 | style.viewBoxBorder2 = 'hsla( 10, 60%, 32%, 0.3)'; 11 | style.outsideFill = 'hsla(0, 0%, 0%, 0.4)'; 12 | style.backgroundColor = 'hsla(0, 0%, 0%, 0.9)'; 13 | } else { 14 | style.viewBoxBorder = 'hsla(190, 100%, 20%, 0.8)'; 15 | style.viewBoxBorder2 = 'hsla( 10, 60%, 80%, 0.8)'; 16 | style.outsideFill = 'hsla(0, 0%, 100%, 0.4)'; 17 | style.backgroundColor = 'hsla(0, 0%, 100%, 0.9)'; 18 | } 19 | return style; 20 | } 21 | 22 | function renderViewRectangle(context, viewrect, props) { 23 | context.clearRect(0, 0, props.width, props.height); 24 | context.fillStyle = props.outsideFill; 25 | 26 | // Scaled view rectangle 27 | let x = Math.round((props.viewrectangle[0] / props.scale - props.thumbrectangle[0]) 28 | * props.thumbscale); 29 | let y = Math.round((props.viewrectangle[1] / props.scale - props.thumbrectangle[1]) 30 | * props.thumbscale); 31 | let w = Math.round((props.viewrectangle[2] * props.thumbscale) / props.scale); 32 | let h = Math.round((props.viewrectangle[3] * props.thumbscale) / props.scale); 33 | 34 | let hide = false; 35 | if (x < 0 && y < 0 && w > props.width - x && h > props.height - y) { 36 | // Hide map 37 | hide = true; 38 | return { 39 | hide, 40 | }; 41 | } 42 | // Show map 43 | hide = false; 44 | 45 | // Clip to bounds 46 | // Left 47 | if (x < 0) { 48 | w += x; 49 | x = 0; 50 | viewrect.style.borderLeftColor = props.viewBoxBorder2; 51 | } else { 52 | viewrect.style.borderLeftColor = props.viewBoxBorder; 53 | context.fillRect(0, 0, x, props.height); 54 | } 55 | // Top 56 | if (y < 0) { 57 | h += y; 58 | y = 0; 59 | viewrect.style.borderTopColor = props.viewBoxBorder2; 60 | } else { 61 | viewrect.style.borderTopColor = props.viewBoxBorder; 62 | context.fillRect(x, 0, w, y); 63 | } 64 | // Right 65 | if (w > props.width - x) { 66 | w = props.width - x; 67 | viewrect.style.borderRightColor = props.viewBoxBorder2; 68 | } else { 69 | viewrect.style.borderRightColor = props.viewBoxBorder; 70 | context.fillRect(x + w, 0, props.width - (x + w), props.height); 71 | } 72 | // Bottom 73 | if (h > props.height - y) { 74 | h = props.height - y; 75 | viewrect.style.borderBottomColor = props.viewBoxBorder2; 76 | } else { 77 | viewrect.style.borderBottomColor = props.viewBoxBorder; 78 | context.fillRect(x, y + h, w, props.height - (y + h)); 79 | } 80 | 81 | // Size and translate rect 82 | viewrect.style.left = `${x}px`; 83 | viewrect.style.top = `${y}px`; 84 | viewrect.style.width = `${w}px`; 85 | viewrect.style.height = `${h}px`; 86 | 87 | return { 88 | hide, 89 | }; 90 | } 91 | 92 | function renderThumbnailFromProps(context, props) { 93 | const style = { 94 | ...props, 95 | }; 96 | style.graph = null; 97 | style.lineWidth = props.nodeLineWidth; 98 | const info = thumb.render(context, props.graph, style); 99 | return info; 100 | } 101 | function renderViewboxFromProps(context, viewbox, thumbInfo, props) { 102 | const style = { 103 | ...props, 104 | }; 105 | style.graph = null; 106 | style.scale = props.viewscale; 107 | const thumbW = thumbInfo.rectangle[2]; 108 | const thumbH = thumbInfo.rectangle[3]; 109 | style.thumbscale = (thumbW > thumbH) ? props.width / thumbW : props.height / thumbH; 110 | style.thumbrectangle = thumbInfo.rectangle; 111 | const info = renderViewRectangle(context, viewbox, style); 112 | return info; 113 | } 114 | 115 | // https://toddmotto.com/react-create-class-versus-component/ 116 | const Component = createReactClass({ 117 | propTypes: { 118 | }, 119 | getDefaultProps() { 120 | return { 121 | width: 200, 122 | height: 150, 123 | hidden: false, // FIXME: drop?? 124 | backgroundColor: 'hsla(184, 8%, 75%, 0.9)', 125 | outsideFill: 'hsla(0, 0%, 0%, 0.4)', 126 | nodeSize: 60, 127 | nodeLineWidth: 1, 128 | viewrectangle: [0, 0, 0, 0], 129 | viewscale: 1.0, 130 | viewBoxBorder: 'hsla(190, 100%, 80%, 0.4)', 131 | viewBoxBorder2: 'hsla( 10, 60%, 32%, 0.3)', 132 | viewBoxBorderStyle: 'dotted', 133 | graph: null, // NOTE: should not attach to events, that is responsibility of outer code 134 | }; 135 | }, 136 | getInitialState() { 137 | return { 138 | thumbscale: 1.0, 139 | currentPan: [0.0, 0.0], 140 | }; 141 | }, 142 | componentDidMount() { 143 | this._updatePan(); 144 | this._renderElements(); 145 | this._setupEvents(); 146 | }, 147 | componentDidUpdate() { 148 | this._updatePan(); 149 | this._renderElements(); 150 | }, 151 | _refThumbCanvas(canvas) { 152 | this._thumbContext = canvas.getContext('2d'); 153 | }, 154 | _refViewboxCanvas(canvas) { 155 | this._viewboxContext = canvas.getContext('2d'); 156 | }, 157 | _refViewboxElement(el) { 158 | this._viewboxElement = el; 159 | }, 160 | _refTopElement(el) { 161 | this._topElement = el; 162 | }, 163 | _renderElements() { 164 | const t = renderThumbnailFromProps(this._thumbContext, this.props); 165 | // this.state.thumbscale = t.scale; 166 | renderViewboxFromProps(this._viewboxContext, this._viewboxElement, t, this.props); 167 | }, 168 | _updatePan() { 169 | this.state.currentPan = [ 170 | -(this.props.viewrectangle[0]), 171 | -(this.props.viewrectangle[1]), 172 | ]; 173 | }, 174 | _setupEvents() { 175 | this.hammer = new Hammer.Manager(this._topElement, { 176 | recognizers: [ 177 | [Hammer.Tap], 178 | [Hammer.Pan, { direction: Hammer.DIRECTION_ALL }], 179 | ], 180 | }); 181 | this.hammer.on('tap', ((event) => { 182 | if (this.props.onTap) { 183 | this.props.onTap(null, event); 184 | } 185 | })); 186 | this.hammer.on('panmove', ((event) => { 187 | if (this.props.onPanTo) { 188 | // Calculate where event pans to, in editor coordinates 189 | let x = this.state.currentPan[0]; 190 | let y = this.state.currentPan[1]; 191 | const panscale = this.state.thumbscale / this.props.viewscale; 192 | x -= event.deltaX / panscale; 193 | y -= event.deltaY / panscale; 194 | const panTo = { x: Math.round(x), y: Math.round(y) }; 195 | // keep track of the current pan, because prop/component update 196 | // may be delayed, or never arrive. 197 | this.state.currentPan[0] = panTo.x; 198 | this.state.currentPan[1] = panTo.y; 199 | this.props.onPanTo(panTo, event); 200 | } 201 | })); 202 | }, 203 | render() { 204 | const p = this.props; 205 | const thumbStyle = { 206 | position: 'absolute', 207 | top: 0, 208 | left: 0, 209 | }; 210 | const wrapperStyle = { 211 | height: p.height, 212 | width: p.width, 213 | overflow: 'hidden', 214 | cursor: 'move', 215 | backgroundColor: p.backgroundColor, 216 | }; 217 | const thumbProps = { 218 | key: 'thumb', 219 | ref: this._refThumbCanvas, 220 | width: p.width, 221 | height: p.height, 222 | style: thumbStyle, 223 | }; 224 | const viewboxCanvas = { 225 | key: 'viewbox', 226 | ref: this._refViewboxCanvas, 227 | width: p.width, 228 | height: p.height, 229 | style: thumbStyle, 230 | }; 231 | // FIXME: find better way to populate the props from render function 232 | const viewboxDiv = { 233 | key: 'viewboxdiv', 234 | ref: this._refViewboxElement, 235 | style: { 236 | position: 'absolute', 237 | top: 0, 238 | left: 0, 239 | width: p.width, 240 | height: p.height, 241 | borderStyle: 'dotted', 242 | borderWidth: 1, 243 | }, 244 | }; 245 | // Elements 246 | return React.createElement('div', { key: 'nav', style: wrapperStyle, ref: this._refTopElement }, [ 247 | React.createElement('div', viewboxDiv), 248 | React.createElement('canvas', viewboxCanvas), 249 | React.createElement('canvas', thumbProps), 250 | ]); 251 | }, 252 | }); 253 | 254 | module.exports = { 255 | render: renderViewRectangle, 256 | calculateStyleFromTheme, 257 | Component, 258 | }; 259 | -------------------------------------------------------------------------------- /the-graph-thumb/the-graph-thumb.js: -------------------------------------------------------------------------------- 1 | function drawEdge(context, scale, source, target, route, properties) { 2 | // Draw path 3 | try { 4 | [context.strokeStyle] = properties.edgeColors; 5 | if (route) { 6 | // Color if route defined 7 | context.strokeStyle = properties.edgeColors[route]; 8 | } 9 | const fromX = Math.round(source.metadata.x * scale) - 0.5; 10 | const fromY = Math.round(source.metadata.y * scale) - 0.5; 11 | const toX = Math.round(target.metadata.x * scale) - 0.5; 12 | const toY = Math.round(target.metadata.y * scale) - 0.5; 13 | context.beginPath(); 14 | context.moveTo(fromX, fromY); 15 | context.lineTo(toX, toY); 16 | context.stroke(); 17 | } catch (error) { 18 | // FIXME: handle? 19 | } 20 | } 21 | 22 | function styleFromTheme(theme) { 23 | const style = {}; 24 | if (theme === 'dark') { 25 | style.fill = 'hsl(184, 8%, 10%)'; 26 | style.stroke = 'hsl(180, 11%, 70%)'; 27 | style.edgeColors = [ 28 | 'white', 29 | 'hsl( 0, 100%, 46%)', 30 | 'hsl( 35, 100%, 46%)', 31 | 'hsl( 60, 100%, 46%)', 32 | 'hsl(135, 100%, 46%)', 33 | 'hsl(160, 100%, 46%)', 34 | 'hsl(185, 100%, 46%)', 35 | 'hsl(210, 100%, 46%)', 36 | 'hsl(285, 100%, 46%)', 37 | 'hsl(310, 100%, 46%)', 38 | 'hsl(335, 100%, 46%)', 39 | ]; 40 | } else { 41 | // Light 42 | style.fill = 'hsl(184, 8%, 75%)'; 43 | style.stroke = 'hsl(180, 11%, 20%)'; 44 | // Tweaked to make thin lines more visible 45 | style.edgeColors = [ 46 | 'hsl( 0, 0%, 50%)', 47 | 'hsl( 0, 100%, 40%)', 48 | 'hsl( 29, 100%, 40%)', 49 | 'hsl( 47, 100%, 40%)', 50 | 'hsl(138, 100%, 40%)', 51 | 'hsl(160, 73%, 50%)', 52 | 'hsl(181, 100%, 40%)', 53 | 'hsl(216, 100%, 40%)', 54 | 'hsl(260, 100%, 40%)', 55 | 'hsl(348, 100%, 50%)', 56 | 'hsl(328, 100%, 40%)', 57 | ]; 58 | } 59 | return style; 60 | } 61 | 62 | function renderThumbnail(context, graph, properties) { 63 | // Reset origin 64 | context.setTransform(1, 0, 0, 1, 0, 0); 65 | // Clear 66 | context.clearRect(0, 0, properties.width, properties.height); 67 | context.lineWidth = properties.lineWidth; 68 | 69 | // Find dimensions 70 | const toDraw = []; 71 | let minX = Infinity; 72 | let minY = Infinity; 73 | let maxX = -Infinity; 74 | let maxY = -Infinity; 75 | const nodes = {}; 76 | 77 | // Process nodes 78 | graph.nodes.forEach((process) => { 79 | if (process.metadata 80 | && !Number.isNaN(process.metadata.x) 81 | && !Number.isNaN(process.metadata.y)) { 82 | toDraw.push(process); 83 | nodes[process.id] = process; 84 | minX = Math.min(minX, process.metadata.x); 85 | minY = Math.min(minY, process.metadata.y); 86 | maxX = Math.max(maxX, process.metadata.x); 87 | maxY = Math.max(maxY, process.metadata.y); 88 | } 89 | }); 90 | 91 | // Process exported ports 92 | if (graph.inports) { 93 | Object.keys(graph.inports).forEach((key) => { 94 | const exp = graph.inports[key]; 95 | if (exp.metadata && !Number.isNaN(exp.metadata.x) && !Number.isNaN(exp.metadata.y)) { 96 | toDraw.push(exp); 97 | minX = Math.min(minX, exp.metadata.x); 98 | minY = Math.min(minY, exp.metadata.y); 99 | maxX = Math.max(maxX, exp.metadata.x); 100 | maxY = Math.max(maxY, exp.metadata.y); 101 | } 102 | }); 103 | } 104 | if (graph.outports) { 105 | Object.keys(graph.outports).forEach((key) => { 106 | const exp = graph.outports[key]; 107 | if (exp.metadata && !Number.isNaN(exp.metadata.x) && !Number.isNaN(exp.metadata.y)) { 108 | toDraw.push(exp); 109 | minX = Math.min(minX, exp.metadata.x); 110 | minY = Math.min(minY, exp.metadata.y); 111 | maxX = Math.max(maxX, exp.metadata.x); 112 | maxY = Math.max(maxY, exp.metadata.y); 113 | } 114 | }); 115 | } 116 | 117 | // Nothing to draw 118 | if (toDraw.length === 0) { 119 | return { scale: 1.0, rectangle: [0, 0, 0, 0] }; 120 | } 121 | 122 | // Sanity check graph size 123 | if (!Number.isFinite(minX) 124 | || !Number.isFinite(minY) 125 | || !Number.isFinite(maxX) 126 | || !Number.isFinite(maxY)) { 127 | throw new Error('the-graph-thumb: Invalid space spanned'); 128 | } 129 | 130 | minX -= properties.nodeSize; 131 | minY -= properties.nodeSize; 132 | maxX += properties.nodeSize * 2; 133 | maxY += properties.nodeSize * 2; 134 | const w = maxX - minX; 135 | const h = maxY - minY; 136 | // For the-graph-nav to bind 137 | const thumbrectangle = []; 138 | thumbrectangle[0] = minX; 139 | thumbrectangle[1] = minY; 140 | thumbrectangle[2] = w; 141 | thumbrectangle[3] = h; 142 | // Scale dimensions 143 | const scale = (w > h) ? properties.width / w : properties.height / h; 144 | const thumbscale = scale; 145 | const size = Math.round(properties.nodeSize * scale); 146 | const sizeHalf = size / 2; 147 | // Translate origin to match 148 | context.setTransform(1, 0, 0, 1, 0 - minX * scale, 0 - minY * scale); 149 | 150 | // Draw connection from inports to nodes 151 | if (graph.inports) { 152 | Object.keys(graph.inports).forEach((key) => { 153 | const exp = graph.inports[key]; 154 | if (exp.metadata && !Number.isNaN(exp.metadata.x) && !Number.isNaN(exp.metadata.y)) { 155 | const target = nodes[exp.process]; 156 | if (!target) { 157 | return; 158 | } 159 | drawEdge(context, scale, exp, target, 2, properties); 160 | } 161 | }); 162 | } 163 | // Draw connection from nodes to outports 164 | if (graph.outports) { 165 | Object.keys(graph.outports).forEach((key) => { 166 | const exp = graph.outports[key]; 167 | if (exp.metadata && !Number.isNaN(exp.metadata.x) && !Number.isNaN(exp.metadata.y)) { 168 | const source = nodes[exp.process]; 169 | if (!source) { 170 | return; 171 | } 172 | drawEdge(context, scale, source, exp, 5, properties); 173 | } 174 | }); 175 | } 176 | 177 | // Draw edges 178 | graph.edges.forEach((connection) => { 179 | const source = nodes[connection.from.node]; 180 | const target = nodes[connection.to.node]; 181 | if (!source || !target) { 182 | return; 183 | } 184 | drawEdge(context, scale, source, target, connection.metadata.route, properties); 185 | }); 186 | 187 | // Draw nodes 188 | toDraw.forEach((node) => { 189 | const x = Math.round(node.metadata.x * scale); 190 | const y = Math.round(node.metadata.y * scale); 191 | 192 | // Outer circle 193 | context.strokeStyle = properties.strokeStyle; 194 | context.fillStyle = properties.fillStyle; 195 | context.beginPath(); 196 | if (node.process && !node.component) { 197 | context.arc(x, y, sizeHalf / 2, 0, 2 * Math.PI, false); 198 | } else { 199 | context.arc(x, y, sizeHalf, 0, 2 * Math.PI, false); 200 | } 201 | context.fill(); 202 | context.stroke(); 203 | 204 | // Inner circle 205 | context.beginPath(); 206 | const smallRadius = Math.max(sizeHalf - 1.5, 1); 207 | if (node.process && !node.component) { 208 | // Exported port 209 | context.arc(x, y, smallRadius / 2, 0, 2 * Math.PI, false); 210 | } else { 211 | // Regular node 212 | context.arc(x, y, smallRadius, 0, 2 * Math.PI, false); 213 | } 214 | context.fill(); 215 | }); 216 | 217 | return { 218 | rectangle: thumbrectangle, 219 | scale: thumbscale, 220 | }; 221 | } 222 | 223 | module.exports = { 224 | render: renderThumbnail, 225 | styleFromTheme, 226 | }; 227 | -------------------------------------------------------------------------------- /the-graph/SVGImage.js: -------------------------------------------------------------------------------- 1 | const React = require('react'); 2 | const createReactClass = require('create-react-class'); 3 | 4 | const SVGImage = React.createFactory(createReactClass({ 5 | displayName: 'TheGraphSVGImage', 6 | render() { 7 | let html = '`; 14 | 15 | return React.createElement('g', { 16 | className: this.props.className, 17 | dangerouslySetInnerHTML: { __html: html }, 18 | }); 19 | }, 20 | })); 21 | 22 | module.exports = SVGImage; 23 | -------------------------------------------------------------------------------- /the-graph/TextBG.js: -------------------------------------------------------------------------------- 1 | const React = require('react'); 2 | const createReactClass = require('create-react-class'); 3 | 4 | const TextBG = React.createFactory(createReactClass({ 5 | displayName: 'TheGraphTextBG', 6 | render() { 7 | let { text } = this.props; 8 | if (!text) { 9 | text = ''; 10 | } 11 | const { height } = this.props; 12 | const width = (text.length * this.props.height * 2) / 3; 13 | const radius = this.props.height / 2; 14 | 15 | let { x } = this.props; 16 | const y = this.props.y - height / 2; 17 | 18 | if (this.props.halign === 'center') { 19 | x -= width / 2; 20 | } 21 | if (this.props.halign === 'right') { 22 | x -= width; 23 | } 24 | 25 | return React.createElement( 26 | 'g', 27 | { 28 | className: (this.props.className ? this.props.className : 'text-bg'), 29 | }, 30 | React.createElement('rect', { 31 | className: 'text-bg-rect', 32 | x, 33 | y, 34 | rx: radius, 35 | ry: radius, 36 | height: height * 1.1, 37 | width, 38 | }), 39 | React.createElement('text', { 40 | className: (this.props.textClassName ? this.props.textClassName : 'text-bg-text'), 41 | x: this.props.x, 42 | y: this.props.y, 43 | children: text, 44 | }), 45 | ); 46 | }, 47 | })); 48 | 49 | module.exports = TextBG; 50 | -------------------------------------------------------------------------------- /the-graph/arcs.js: -------------------------------------------------------------------------------- 1 | // SVG arc math 2 | const angleToX = function (percent, radius) { 3 | return radius * Math.cos(2 * Math.PI * percent); 4 | }; 5 | const angleToY = function (percent, radius) { 6 | return radius * Math.sin(2 * Math.PI * percent); 7 | }; 8 | const makeArcPath = function (startPercent, endPercent, radius) { 9 | return [ 10 | 'M', angleToX(startPercent, radius), angleToY(startPercent, radius), 11 | 'A', radius, radius, 0, 0, 0, angleToX(endPercent, radius), angleToY(endPercent, radius), 12 | ].join(' '); 13 | }; 14 | const arcs = { 15 | n4: makeArcPath(7 / 8, 5 / 8, 36), 16 | s4: makeArcPath(3 / 8, 1 / 8, 36), 17 | e4: makeArcPath(1 / 8, -1 / 8, 36), 18 | w4: makeArcPath(5 / 8, 3 / 8, 36), 19 | inport: makeArcPath(-1 / 4, 1 / 4, 4), 20 | outport: makeArcPath(1 / 4, -1 / 4, 4), 21 | inportBig: makeArcPath(-1 / 4, 1 / 4, 6), 22 | outportBig: makeArcPath(1 / 4, -1 / 4, 6), 23 | }; 24 | module.exports = arcs; 25 | -------------------------------------------------------------------------------- /the-graph/factories.js: -------------------------------------------------------------------------------- 1 | const React = require('react'); 2 | 3 | const SVGImage = require('./SVGImage'); 4 | 5 | // Standard functions for creating SVG/HTML elements 6 | exports.createGroup = function (options, content) { 7 | let args = ['g', options]; 8 | 9 | if (Array.isArray(content)) { 10 | args = args.concat(content); 11 | } 12 | 13 | return React.createElement(...args); 14 | }; 15 | 16 | exports.createRect = function (options) { 17 | return React.createElement('rect', options); 18 | }; 19 | 20 | exports.createText = function (options) { 21 | return React.createElement('text', options); 22 | }; 23 | 24 | exports.createCircle = function (options) { 25 | return React.createElement('circle', options); 26 | }; 27 | 28 | exports.createPath = function (options) { 29 | return React.createElement('path', options); 30 | }; 31 | 32 | exports.createPolygon = function (options) { 33 | return React.createElement('polygon', options); 34 | }; 35 | 36 | exports.createImg = function (options) { 37 | return SVGImage(options); 38 | }; 39 | 40 | exports.createCanvas = function (options) { 41 | return React.createElement('canvas', options); 42 | }; 43 | 44 | exports.createSvg = function (options, content) { 45 | let args = ['svg', options]; 46 | 47 | if (Array.isArray(content)) { 48 | args = args.concat(content); 49 | } 50 | 51 | return React.createElement(...args); 52 | }; 53 | -------------------------------------------------------------------------------- /the-graph/font-awesome-unicode-map.js: -------------------------------------------------------------------------------- 1 | // This file is generated via `npm run fontawesome` 2 | module.exports = { 3 | '500px': '', 4 | 'address-book': '', 5 | 'address-book-o': '', 6 | 'address-card': '', 7 | 'address-card-o': '', 8 | 'adjust': '', 9 | 'adn': '', 10 | 'align-center': '', 11 | 'align-justify': '', 12 | 'align-left': '', 13 | 'align-right': '', 14 | 'amazon': '', 15 | 'ambulance': '', 16 | 'american-sign-language-interpreting': '', 17 | 'anchor': '', 18 | 'android': '', 19 | 'angellist': '', 20 | 'angle-double-down': '', 21 | 'angle-double-left': '', 22 | 'angle-double-right': '', 23 | 'angle-double-up': '', 24 | 'angle-down': '', 25 | 'angle-left': '', 26 | 'angle-right': '', 27 | 'angle-up': '', 28 | 'apple': '', 29 | 'archive': '', 30 | 'area-chart': '', 31 | 'arrow-circle-down': '', 32 | 'arrow-circle-left': '', 33 | 'arrow-circle-o-down': '', 34 | 'arrow-circle-o-left': '', 35 | 'arrow-circle-o-right': '', 36 | 'arrow-circle-o-up': '', 37 | 'arrow-circle-right': '', 38 | 'arrow-circle-up': '', 39 | 'arrow-down': '', 40 | 'arrow-left': '', 41 | 'arrow-right': '', 42 | 'arrow-up': '', 43 | 'arrows': '', 44 | 'arrows-alt': '', 45 | 'arrows-h': '', 46 | 'arrows-v': '', 47 | 'asl-interpreting': '', 48 | 'assistive-listening-systems': '', 49 | 'asterisk': '', 50 | 'at': '', 51 | 'audio-description': '', 52 | 'automobile': '', 53 | 'backward': '', 54 | 'balance-scale': '', 55 | 'ban': '', 56 | 'bandcamp': '', 57 | 'bank': '', 58 | 'bar-chart': '', 59 | 'bar-chart-o': '', 60 | 'barcode': '', 61 | 'bars': '', 62 | 'bath': '', 63 | 'bathtub': '', 64 | 'battery': '', 65 | 'battery-0': '', 66 | 'battery-1': '', 67 | 'battery-2': '', 68 | 'battery-3': '', 69 | 'battery-4': '', 70 | 'battery-empty': '', 71 | 'battery-full': '', 72 | 'battery-half': '', 73 | 'battery-quarter': '', 74 | 'battery-three-quarters': '', 75 | 'bed': '', 76 | 'beer': '', 77 | 'behance': '', 78 | 'behance-square': '', 79 | 'bell': '', 80 | 'bell-o': '', 81 | 'bell-slash': '', 82 | 'bell-slash-o': '', 83 | 'bicycle': '', 84 | 'binoculars': '', 85 | 'birthday-cake': '', 86 | 'bitbucket': '', 87 | 'bitbucket-square': '', 88 | 'bitcoin': '', 89 | 'black-tie': '', 90 | 'blind': '', 91 | 'bluetooth': '', 92 | 'bluetooth-b': '', 93 | 'bold': '', 94 | 'bolt': '', 95 | 'bomb': '', 96 | 'book': '', 97 | 'bookmark': '', 98 | 'bookmark-o': '', 99 | 'braille': '', 100 | 'briefcase': '', 101 | 'btc': '', 102 | 'bug': '', 103 | 'building': '', 104 | 'building-o': '', 105 | 'bullhorn': '', 106 | 'bullseye': '', 107 | 'bus': '', 108 | 'buysellads': '', 109 | 'cab': '', 110 | 'calculator': '', 111 | 'calendar': '', 112 | 'calendar-check-o': '', 113 | 'calendar-minus-o': '', 114 | 'calendar-o': '', 115 | 'calendar-plus-o': '', 116 | 'calendar-times-o': '', 117 | 'camera': '', 118 | 'camera-retro': '', 119 | 'car': '', 120 | 'caret-down': '', 121 | 'caret-left': '', 122 | 'caret-right': '', 123 | 'caret-square-o-down': '', 124 | 'caret-square-o-left': '', 125 | 'caret-square-o-right': '', 126 | 'caret-square-o-up': '', 127 | 'caret-up': '', 128 | 'cart-arrow-down': '', 129 | 'cart-plus': '', 130 | 'cc': '', 131 | 'cc-amex': '', 132 | 'cc-diners-club': '', 133 | 'cc-discover': '', 134 | 'cc-jcb': '', 135 | 'cc-mastercard': '', 136 | 'cc-paypal': '', 137 | 'cc-stripe': '', 138 | 'cc-visa': '', 139 | 'certificate': '', 140 | 'chain': '', 141 | 'chain-broken': '', 142 | 'check': '', 143 | 'check-circle': '', 144 | 'check-circle-o': '', 145 | 'check-square': '', 146 | 'check-square-o': '', 147 | 'chevron-circle-down': '', 148 | 'chevron-circle-left': '', 149 | 'chevron-circle-right': '', 150 | 'chevron-circle-up': '', 151 | 'chevron-down': '', 152 | 'chevron-left': '', 153 | 'chevron-right': '', 154 | 'chevron-up': '', 155 | 'child': '', 156 | 'chrome': '', 157 | 'circle': '', 158 | 'circle-o': '', 159 | 'circle-o-notch': '', 160 | 'circle-thin': '', 161 | 'clipboard': '', 162 | 'clock-o': '', 163 | 'clone': '', 164 | 'close': '', 165 | 'cloud': '', 166 | 'cloud-download': '', 167 | 'cloud-upload': '', 168 | 'cny': '', 169 | 'code': '', 170 | 'code-fork': '', 171 | 'codepen': '', 172 | 'codiepie': '', 173 | 'coffee': '', 174 | 'cog': '', 175 | 'cogs': '', 176 | 'columns': '', 177 | 'comment': '', 178 | 'comment-o': '', 179 | 'commenting': '', 180 | 'commenting-o': '', 181 | 'comments': '', 182 | 'comments-o': '', 183 | 'compass': '', 184 | 'compress': '', 185 | 'connectdevelop': '', 186 | 'contao': '', 187 | 'copy': '', 188 | 'copyright': '', 189 | 'creative-commons': '', 190 | 'credit-card': '', 191 | 'credit-card-alt': '', 192 | 'crop': '', 193 | 'crosshairs': '', 194 | 'css3': '', 195 | 'cube': '', 196 | 'cubes': '', 197 | 'cut': '', 198 | 'cutlery': '', 199 | 'dashboard': '', 200 | 'dashcube': '', 201 | 'database': '', 202 | 'deaf': '', 203 | 'deafness': '', 204 | 'dedent': '', 205 | 'delicious': '', 206 | 'desktop': '', 207 | 'deviantart': '', 208 | 'diamond': '', 209 | 'digg': '', 210 | 'dollar': '', 211 | 'dot-circle-o': '', 212 | 'download': '', 213 | 'dribbble': '', 214 | 'drivers-license': '', 215 | 'drivers-license-o': '', 216 | 'dropbox': '', 217 | 'drupal': '', 218 | 'edge': '', 219 | 'edit': '', 220 | 'eercast': '', 221 | 'eject': '', 222 | 'ellipsis-h': '', 223 | 'ellipsis-v': '', 224 | 'empire': '', 225 | 'envelope': '', 226 | 'envelope-o': '', 227 | 'envelope-open': '', 228 | 'envelope-open-o': '', 229 | 'envelope-square': '', 230 | 'envira': '', 231 | 'eraser': '', 232 | 'etsy': '', 233 | 'eur': '', 234 | 'euro': '', 235 | 'exchange': '', 236 | 'exclamation': '', 237 | 'exclamation-circle': '', 238 | 'exclamation-triangle': '', 239 | 'expand': '', 240 | 'expeditedssl': '', 241 | 'external-link': '', 242 | 'external-link-square': '', 243 | 'eye': '', 244 | 'eye-slash': '', 245 | 'eyedropper': '', 246 | 'fa': '', 247 | 'facebook': '', 248 | 'facebook-f': '', 249 | 'facebook-official': '', 250 | 'facebook-square': '', 251 | 'fast-backward': '', 252 | 'fast-forward': '', 253 | 'fax': '', 254 | 'feed': '', 255 | 'female': '', 256 | 'fighter-jet': '', 257 | 'file': '', 258 | 'file-archive-o': '', 259 | 'file-audio-o': '', 260 | 'file-code-o': '', 261 | 'file-excel-o': '', 262 | 'file-image-o': '', 263 | 'file-movie-o': '', 264 | 'file-o': '', 265 | 'file-pdf-o': '', 266 | 'file-photo-o': '', 267 | 'file-picture-o': '', 268 | 'file-powerpoint-o': '', 269 | 'file-sound-o': '', 270 | 'file-text': '', 271 | 'file-text-o': '', 272 | 'file-video-o': '', 273 | 'file-word-o': '', 274 | 'file-zip-o': '', 275 | 'files-o': '', 276 | 'film': '', 277 | 'filter': '', 278 | 'fire': '', 279 | 'fire-extinguisher': '', 280 | 'firefox': '', 281 | 'first-order': '', 282 | 'flag': '', 283 | 'flag-checkered': '', 284 | 'flag-o': '', 285 | 'flash': '', 286 | 'flask': '', 287 | 'flickr': '', 288 | 'floppy-o': '', 289 | 'folder': '', 290 | 'folder-o': '', 291 | 'folder-open': '', 292 | 'folder-open-o': '', 293 | 'font': '', 294 | 'font-awesome': '', 295 | 'fonticons': '', 296 | 'fort-awesome': '', 297 | 'forumbee': '', 298 | 'forward': '', 299 | 'foursquare': '', 300 | 'free-code-camp': '', 301 | 'frown-o': '', 302 | 'futbol-o': '', 303 | 'gamepad': '', 304 | 'gavel': '', 305 | 'gbp': '', 306 | 'ge': '', 307 | 'gear': '', 308 | 'gears': '', 309 | 'genderless': '', 310 | 'get-pocket': '', 311 | 'gg': '', 312 | 'gg-circle': '', 313 | 'gift': '', 314 | 'git': '', 315 | 'git-square': '', 316 | 'github': '', 317 | 'github-alt': '', 318 | 'github-square': '', 319 | 'gitlab': '', 320 | 'gittip': '', 321 | 'glass': '', 322 | 'glide': '', 323 | 'glide-g': '', 324 | 'globe': '', 325 | 'google': '', 326 | 'google-plus': '', 327 | 'google-plus-circle': '', 328 | 'google-plus-official': '', 329 | 'google-plus-square': '', 330 | 'google-wallet': '', 331 | 'graduation-cap': '', 332 | 'gratipay': '', 333 | 'grav': '', 334 | 'group': '', 335 | 'h-square': '', 336 | 'hacker-news': '', 337 | 'hand-grab-o': '', 338 | 'hand-lizard-o': '', 339 | 'hand-o-down': '', 340 | 'hand-o-left': '', 341 | 'hand-o-right': '', 342 | 'hand-o-up': '', 343 | 'hand-paper-o': '', 344 | 'hand-peace-o': '', 345 | 'hand-pointer-o': '', 346 | 'hand-rock-o': '', 347 | 'hand-scissors-o': '', 348 | 'hand-spock-o': '', 349 | 'hand-stop-o': '', 350 | 'handshake-o': '', 351 | 'hard-of-hearing': '', 352 | 'hashtag': '', 353 | 'hdd-o': '', 354 | 'header': '', 355 | 'headphones': '', 356 | 'heart': '', 357 | 'heart-o': '', 358 | 'heartbeat': '', 359 | 'history': '', 360 | 'home': '', 361 | 'hospital-o': '', 362 | 'hotel': '', 363 | 'hourglass': '', 364 | 'hourglass-1': '', 365 | 'hourglass-2': '', 366 | 'hourglass-3': '', 367 | 'hourglass-end': '', 368 | 'hourglass-half': '', 369 | 'hourglass-o': '', 370 | 'hourglass-start': '', 371 | 'houzz': '', 372 | 'html5': '', 373 | 'i-cursor': '', 374 | 'id-badge': '', 375 | 'id-card': '', 376 | 'id-card-o': '', 377 | 'ils': '', 378 | 'image': '', 379 | 'imdb': '', 380 | 'inbox': '', 381 | 'indent': '', 382 | 'industry': '', 383 | 'info': '', 384 | 'info-circle': '', 385 | 'inr': '', 386 | 'instagram': '', 387 | 'institution': '', 388 | 'internet-explorer': '', 389 | 'intersex': '', 390 | 'ioxhost': '', 391 | 'italic': '', 392 | 'joomla': '', 393 | 'jpy': '', 394 | 'jsfiddle': '', 395 | 'key': '', 396 | 'keyboard-o': '', 397 | 'krw': '', 398 | 'language': '', 399 | 'laptop': '', 400 | 'lastfm': '', 401 | 'lastfm-square': '', 402 | 'leaf': '', 403 | 'leanpub': '', 404 | 'legal': '', 405 | 'lemon-o': '', 406 | 'level-down': '', 407 | 'level-up': '', 408 | 'life-bouy': '', 409 | 'life-buoy': '', 410 | 'life-ring': '', 411 | 'life-saver': '', 412 | 'lightbulb-o': '', 413 | 'line-chart': '', 414 | 'link': '', 415 | 'linkedin': '', 416 | 'linkedin-square': '', 417 | 'linode': '', 418 | 'linux': '', 419 | 'list': '', 420 | 'list-alt': '', 421 | 'list-ol': '', 422 | 'list-ul': '', 423 | 'location-arrow': '', 424 | 'lock': '', 425 | 'long-arrow-down': '', 426 | 'long-arrow-left': '', 427 | 'long-arrow-right': '', 428 | 'long-arrow-up': '', 429 | 'low-vision': '', 430 | 'magic': '', 431 | 'magnet': '', 432 | 'mail-forward': '', 433 | 'mail-reply': '', 434 | 'mail-reply-all': '', 435 | 'male': '', 436 | 'map': '', 437 | 'map-marker': '', 438 | 'map-o': '', 439 | 'map-pin': '', 440 | 'map-signs': '', 441 | 'mars': '', 442 | 'mars-double': '', 443 | 'mars-stroke': '', 444 | 'mars-stroke-h': '', 445 | 'mars-stroke-v': '', 446 | 'maxcdn': '', 447 | 'meanpath': '', 448 | 'medium': '', 449 | 'medkit': '', 450 | 'meetup': '', 451 | 'meh-o': '', 452 | 'mercury': '', 453 | 'microchip': '', 454 | 'microphone': '', 455 | 'microphone-slash': '', 456 | 'minus': '', 457 | 'minus-circle': '', 458 | 'minus-square': '', 459 | 'minus-square-o': '', 460 | 'mixcloud': '', 461 | 'mobile': '', 462 | 'mobile-phone': '', 463 | 'modx': '', 464 | 'money': '', 465 | 'moon-o': '', 466 | 'mortar-board': '', 467 | 'motorcycle': '', 468 | 'mouse-pointer': '', 469 | 'music': '', 470 | 'navicon': '', 471 | 'neuter': '', 472 | 'newspaper-o': '', 473 | 'object-group': '', 474 | 'object-ungroup': '', 475 | 'odnoklassniki': '', 476 | 'odnoklassniki-square': '', 477 | 'opencart': '', 478 | 'openid': '', 479 | 'opera': '', 480 | 'optin-monster': '', 481 | 'outdent': '', 482 | 'pagelines': '', 483 | 'paint-brush': '', 484 | 'paper-plane': '', 485 | 'paper-plane-o': '', 486 | 'paperclip': '', 487 | 'paragraph': '', 488 | 'paste': '', 489 | 'pause': '', 490 | 'pause-circle': '', 491 | 'pause-circle-o': '', 492 | 'paw': '', 493 | 'paypal': '', 494 | 'pencil': '', 495 | 'pencil-square': '', 496 | 'pencil-square-o': '', 497 | 'percent': '', 498 | 'phone': '', 499 | 'phone-square': '', 500 | 'photo': '', 501 | 'picture-o': '', 502 | 'pie-chart': '', 503 | 'pied-piper': '', 504 | 'pied-piper-alt': '', 505 | 'pied-piper-pp': '', 506 | 'pinterest': '', 507 | 'pinterest-p': '', 508 | 'pinterest-square': '', 509 | 'plane': '', 510 | 'play': '', 511 | 'play-circle': '', 512 | 'play-circle-o': '', 513 | 'plug': '', 514 | 'plus': '', 515 | 'plus-circle': '', 516 | 'plus-square': '', 517 | 'plus-square-o': '', 518 | 'podcast': '', 519 | 'power-off': '', 520 | 'print': '', 521 | 'product-hunt': '', 522 | 'puzzle-piece': '', 523 | 'qq': '', 524 | 'qrcode': '', 525 | 'question': '', 526 | 'question-circle': '', 527 | 'question-circle-o': '', 528 | 'quora': '', 529 | 'quote-left': '', 530 | 'quote-right': '', 531 | 'ra': '', 532 | 'random': '', 533 | 'ravelry': '', 534 | 'rebel': '', 535 | 'recycle': '', 536 | 'reddit': '', 537 | 'reddit-alien': '', 538 | 'reddit-square': '', 539 | 'refresh': '', 540 | 'registered': '', 541 | 'remove': '', 542 | 'renren': '', 543 | 'reorder': '', 544 | 'repeat': '', 545 | 'reply': '', 546 | 'reply-all': '', 547 | 'resistance': '', 548 | 'retweet': '', 549 | 'rmb': '', 550 | 'road': '', 551 | 'rocket': '', 552 | 'rotate-left': '', 553 | 'rotate-right': '', 554 | 'rouble': '', 555 | 'rss': '', 556 | 'rss-square': '', 557 | 'rub': '', 558 | 'ruble': '', 559 | 'rupee': '', 560 | 's15': '', 561 | 'safari': '', 562 | 'save': '', 563 | 'scissors': '', 564 | 'scribd': '', 565 | 'search': '', 566 | 'search-minus': '', 567 | 'search-plus': '', 568 | 'sellsy': '', 569 | 'send': '', 570 | 'send-o': '', 571 | 'server': '', 572 | 'share': '', 573 | 'share-alt': '', 574 | 'share-alt-square': '', 575 | 'share-square': '', 576 | 'share-square-o': '', 577 | 'shekel': '', 578 | 'sheqel': '', 579 | 'shield': '', 580 | 'ship': '', 581 | 'shirtsinbulk': '', 582 | 'shopping-bag': '', 583 | 'shopping-basket': '', 584 | 'shopping-cart': '', 585 | 'shower': '', 586 | 'sign-in': '', 587 | 'sign-language': '', 588 | 'sign-out': '', 589 | 'signal': '', 590 | 'signing': '', 591 | 'simplybuilt': '', 592 | 'sitemap': '', 593 | 'skyatlas': '', 594 | 'skype': '', 595 | 'slack': '', 596 | 'sliders': '', 597 | 'slideshare': '', 598 | 'smile-o': '', 599 | 'snapchat': '', 600 | 'snapchat-ghost': '', 601 | 'snapchat-square': '', 602 | 'snowflake-o': '', 603 | 'soccer-ball-o': '', 604 | 'sort': '', 605 | 'sort-alpha-asc': '', 606 | 'sort-alpha-desc': '', 607 | 'sort-amount-asc': '', 608 | 'sort-amount-desc': '', 609 | 'sort-asc': '', 610 | 'sort-desc': '', 611 | 'sort-down': '', 612 | 'sort-numeric-asc': '', 613 | 'sort-numeric-desc': '', 614 | 'sort-up': '', 615 | 'soundcloud': '', 616 | 'space-shuttle': '', 617 | 'spinner': '', 618 | 'spoon': '', 619 | 'spotify': '', 620 | 'square': '', 621 | 'square-o': '', 622 | 'stack-exchange': '', 623 | 'stack-overflow': '', 624 | 'star': '', 625 | 'star-half': '', 626 | 'star-half-empty': '', 627 | 'star-half-full': '', 628 | 'star-half-o': '', 629 | 'star-o': '', 630 | 'steam': '', 631 | 'steam-square': '', 632 | 'step-backward': '', 633 | 'step-forward': '', 634 | 'stethoscope': '', 635 | 'sticky-note': '', 636 | 'sticky-note-o': '', 637 | 'stop': '', 638 | 'stop-circle': '', 639 | 'stop-circle-o': '', 640 | 'street-view': '', 641 | 'strikethrough': '', 642 | 'stumbleupon': '', 643 | 'stumbleupon-circle': '', 644 | 'subscript': '', 645 | 'subway': '', 646 | 'suitcase': '', 647 | 'sun-o': '', 648 | 'superpowers': '', 649 | 'superscript': '', 650 | 'support': '', 651 | 'table': '', 652 | 'tablet': '', 653 | 'tachometer': '', 654 | 'tag': '', 655 | 'tags': '', 656 | 'tasks': '', 657 | 'taxi': '', 658 | 'telegram': '', 659 | 'television': '', 660 | 'tencent-weibo': '', 661 | 'terminal': '', 662 | 'text-height': '', 663 | 'text-width': '', 664 | 'th': '', 665 | 'th-large': '', 666 | 'th-list': '', 667 | 'themeisle': '', 668 | 'thermometer': '', 669 | 'thermometer-0': '', 670 | 'thermometer-1': '', 671 | 'thermometer-2': '', 672 | 'thermometer-3': '', 673 | 'thermometer-4': '', 674 | 'thermometer-empty': '', 675 | 'thermometer-full': '', 676 | 'thermometer-half': '', 677 | 'thermometer-quarter': '', 678 | 'thermometer-three-quarters': '', 679 | 'thumb-tack': '', 680 | 'thumbs-down': '', 681 | 'thumbs-o-down': '', 682 | 'thumbs-o-up': '', 683 | 'thumbs-up': '', 684 | 'ticket': '', 685 | 'times': '', 686 | 'times-circle': '', 687 | 'times-circle-o': '', 688 | 'times-rectangle': '', 689 | 'times-rectangle-o': '', 690 | 'tint': '', 691 | 'toggle-down': '', 692 | 'toggle-left': '', 693 | 'toggle-off': '', 694 | 'toggle-on': '', 695 | 'toggle-right': '', 696 | 'toggle-up': '', 697 | 'trademark': '', 698 | 'train': '', 699 | 'transgender': '', 700 | 'transgender-alt': '', 701 | 'trash': '', 702 | 'trash-o': '', 703 | 'tree': '', 704 | 'trello': '', 705 | 'tripadvisor': '', 706 | 'trophy': '', 707 | 'truck': '', 708 | 'try': '', 709 | 'tty': '', 710 | 'tumblr': '', 711 | 'tumblr-square': '', 712 | 'turkish-lira': '', 713 | 'tv': '', 714 | 'twitch': '', 715 | 'twitter': '', 716 | 'twitter-square': '', 717 | 'umbrella': '', 718 | 'underline': '', 719 | 'undo': '', 720 | 'universal-access': '', 721 | 'university': '', 722 | 'unlink': '', 723 | 'unlock': '', 724 | 'unlock-alt': '', 725 | 'unsorted': '', 726 | 'upload': '', 727 | 'usb': '', 728 | 'usd': '', 729 | 'user': '', 730 | 'user-circle': '', 731 | 'user-circle-o': '', 732 | 'user-md': '', 733 | 'user-o': '', 734 | 'user-plus': '', 735 | 'user-secret': '', 736 | 'user-times': '', 737 | 'users': '', 738 | 'vcard': '', 739 | 'vcard-o': '', 740 | 'venus': '', 741 | 'venus-double': '', 742 | 'venus-mars': '', 743 | 'viacoin': '', 744 | 'viadeo': '', 745 | 'viadeo-square': '', 746 | 'video-camera': '', 747 | 'vimeo': '', 748 | 'vimeo-square': '', 749 | 'vine': '', 750 | 'vk': '', 751 | 'volume-control-phone': '', 752 | 'volume-down': '', 753 | 'volume-off': '', 754 | 'volume-up': '', 755 | 'warning': '', 756 | 'wechat': '', 757 | 'weibo': '', 758 | 'weixin': '', 759 | 'whatsapp': '', 760 | 'wheelchair': '', 761 | 'wheelchair-alt': '', 762 | 'wifi': '', 763 | 'wikipedia-w': '', 764 | 'window-close': '', 765 | 'window-close-o': '', 766 | 'window-maximize': '', 767 | 'window-minimize': '', 768 | 'window-restore': '', 769 | 'windows': '', 770 | 'won': '', 771 | 'wordpress': '', 772 | 'wpbeginner': '', 773 | 'wpexplorer': '', 774 | 'wpforms': '', 775 | 'wrench': '', 776 | 'xing': '', 777 | 'xing-square': '', 778 | 'y-combinator': '', 779 | 'y-combinator-square': '', 780 | 'yahoo': '', 781 | 'yc': '', 782 | 'yc-square': '', 783 | 'yelp': '', 784 | 'yen': '', 785 | 'yoast': '', 786 | 'youtube': '', 787 | 'youtube-play': '', 788 | 'youtube-square': '' 789 | }; -------------------------------------------------------------------------------- /the-graph/geometryutils.js: -------------------------------------------------------------------------------- 1 | function findMinMax(graph, nodes) { 2 | let inports; let 3 | outports; 4 | if (nodes === undefined) { 5 | nodes = graph.nodes.map((node) => node.id); 6 | // Only look at exports when calculating the whole graph 7 | inports = graph.inports; 8 | outports = graph.outports; 9 | } 10 | if (nodes.length < 1) { 11 | return undefined; 12 | } 13 | let minX = Infinity; 14 | let minY = Infinity; 15 | let maxX = -Infinity; 16 | let maxY = -Infinity; 17 | 18 | // Loop through nodes 19 | let len = nodes.length; 20 | for (let i = 0; i < len; i += 1) { 21 | const key = nodes[i]; 22 | const node = graph.getNode(key); 23 | if (node && node.metadata) { 24 | if (node.metadata.x < minX) { minX = node.metadata.x; } 25 | if (node.metadata.y < minY) { minY = node.metadata.y; } 26 | const x = node.metadata.x + node.metadata.width; 27 | const y = node.metadata.y + node.metadata.height; 28 | if (x > maxX) { maxX = x; } 29 | if (y > maxY) { maxY = y; } 30 | } 31 | } 32 | // Loop through exports 33 | let keys; let 34 | exp; 35 | if (inports) { 36 | keys = Object.keys(inports); 37 | len = keys.length; 38 | for (let i = 0; i < len; i += 1) { 39 | exp = inports[keys[i]]; 40 | if (exp.metadata) { 41 | if (exp.metadata.x < minX) { minX = exp.metadata.x; } 42 | if (exp.metadata.y < minY) { minY = exp.metadata.y; } 43 | if (exp.metadata.x > maxX) { maxX = exp.metadata.x; } 44 | if (exp.metadata.y > maxY) { maxY = exp.metadata.y; } 45 | } 46 | } 47 | } 48 | if (outports) { 49 | keys = Object.keys(outports); 50 | len = keys.length; 51 | for (let i = 0; i < len; i += 1) { 52 | exp = outports[keys[i]]; 53 | if (exp.metadata) { 54 | if (exp.metadata.x < minX) { minX = exp.metadata.x; } 55 | if (exp.metadata.y < minY) { minY = exp.metadata.y; } 56 | if (exp.metadata.x > maxX) { maxX = exp.metadata.x; } 57 | if (exp.metadata.y > maxY) { maxY = exp.metadata.y; } 58 | } 59 | } 60 | } 61 | 62 | if (!Number.isFinite(minX) 63 | || !Number.isFinite(minY) 64 | || !Number.isFinite(maxX) 65 | || !Number.isFinite(maxY)) { 66 | return null; 67 | } 68 | return { 69 | minX, 70 | minY, 71 | maxX, 72 | maxY, 73 | }; 74 | } 75 | 76 | function findFit(graph, width, height, sizeLimit) { 77 | const limits = findMinMax(graph); 78 | if (!limits) { 79 | return { x: 0, y: 0, scale: 1 }; 80 | } 81 | limits.minX -= sizeLimit; 82 | limits.minY -= sizeLimit; 83 | limits.maxX += sizeLimit * 2; 84 | limits.maxY += sizeLimit * 2; 85 | 86 | const gWidth = limits.maxX - limits.minX; 87 | const gHeight = limits.maxY - limits.minY; 88 | 89 | const scaleX = width / gWidth; 90 | const scaleY = height / gHeight; 91 | 92 | let scale; let x; let 93 | y; 94 | if (scaleX < scaleY) { 95 | scale = scaleX; 96 | x = 0 - limits.minX * scale; 97 | y = 0 - limits.minY * scale + (height - (gHeight * scale)) / 2; 98 | } else { 99 | scale = scaleY; 100 | x = 0 - limits.minX * scale + (width - (gWidth * scale)) / 2; 101 | y = 0 - limits.minY * scale; 102 | } 103 | 104 | return { 105 | x, 106 | y, 107 | scale, 108 | graphWidth: gWidth, 109 | graphHeight: gHeight, 110 | }; 111 | } 112 | 113 | function findAreaFit(point1, point2, width, height, sizeLimit) { 114 | const limits = { 115 | minX: point1.x < point2.x ? point1.x : point2.x, 116 | minY: point1.y < point2.y ? point1.y : point2.y, 117 | maxX: point1.x > point2.x ? point1.x : point2.x, 118 | maxY: point1.y > point2.y ? point1.y : point2.y, 119 | }; 120 | 121 | limits.minX -= sizeLimit; 122 | limits.minY -= sizeLimit; 123 | limits.maxX += sizeLimit * 2; 124 | limits.maxY += sizeLimit * 2; 125 | 126 | const gWidth = limits.maxX - limits.minX; 127 | const gHeight = limits.maxY - limits.minY; 128 | 129 | const scaleX = width / gWidth; 130 | const scaleY = height / gHeight; 131 | 132 | let scale; let x; let 133 | y; 134 | if (scaleX < scaleY) { 135 | scale = scaleX; 136 | x = 0 - limits.minX * scale; 137 | y = 0 - limits.minY * scale + (height - (gHeight * scale)) / 2; 138 | } else { 139 | scale = scaleY; 140 | x = 0 - limits.minX * scale + (width - (gWidth * scale)) / 2; 141 | y = 0 - limits.minY * scale; 142 | } 143 | 144 | return { 145 | x, 146 | y, 147 | scale, 148 | }; 149 | } 150 | 151 | function findNodeFit(node, width, height, sizeLimit) { 152 | const limits = { 153 | minX: node.metadata.x - sizeLimit, 154 | minY: node.metadata.y - sizeLimit, 155 | maxX: node.metadata.x + sizeLimit * 2, 156 | maxY: node.metadata.y + sizeLimit * 2, 157 | }; 158 | 159 | const gWidth = limits.maxX - limits.minX; 160 | const gHeight = limits.maxY - limits.minY; 161 | 162 | const scaleX = width / gWidth; 163 | const scaleY = height / gHeight; 164 | 165 | let scale; let x; let 166 | y; 167 | if (scaleX < scaleY) { 168 | scale = scaleX; 169 | x = 0 - limits.minX * scale; 170 | y = 0 - limits.minY * scale + (height - (gHeight * scale)) / 2; 171 | } else { 172 | scale = scaleY; 173 | x = 0 - limits.minX * scale + (width - (gWidth * scale)) / 2; 174 | y = 0 - limits.minY * scale; 175 | } 176 | 177 | return { 178 | x, 179 | y, 180 | scale, 181 | }; 182 | } 183 | 184 | module.exports = { 185 | findMinMax, 186 | findNodeFit, 187 | findAreaFit, 188 | findFit, 189 | }; 190 | -------------------------------------------------------------------------------- /the-graph/hammer.js: -------------------------------------------------------------------------------- 1 | const Hammer = require('hammerjs'); 2 | // Contains code from hammmer.js 3 | // https://github.com/hammerjs/hammer.js 4 | // The MIT License (MIT) 5 | // Copyright (C) 2011-2014 by Jorik Tangelder (Eight Media) 6 | // 7 | // With customizations to get it to work as we like/need, 8 | // particularly we track all events on the target element itself 9 | 10 | const VENDOR_PREFIXES = ['', 'webkit', 'Moz', 'MS', 'ms', 'o']; 11 | 12 | function prefixed(obj, property) { 13 | let prefix; let 14 | prop; 15 | const camelProp = property[0].toUpperCase() + property.slice(1); 16 | 17 | let i = 0; 18 | while (i < VENDOR_PREFIXES.length) { 19 | prefix = VENDOR_PREFIXES[i]; 20 | prop = (prefix) ? prefix + camelProp : property; 21 | 22 | if (prop in obj) { 23 | return prop; 24 | } 25 | i += 1; 26 | } 27 | return undefined; 28 | } 29 | 30 | const MOBILE_REGEX = /mobile|tablet|ip(ad|hone|od)|android/i; 31 | 32 | const SUPPORT_TOUCH = ('ontouchstart' in window); 33 | const SUPPORT_POINTER_EVENTS = prefixed(window, 'PointerEvent') !== undefined; 34 | const SUPPORT_ONLY_TOUCH = SUPPORT_TOUCH && MOBILE_REGEX.test(navigator.userAgent); 35 | 36 | let POINTER_ELEMENT_EVENTS = 'pointerdown'; 37 | let POINTER_WINDOW_EVENTS = 'pointermove pointerup pointercancel'; 38 | // IE10 has prefixed support, and case-sensitive 39 | if (window.MSPointerEvent && !window.PointerEvent) { 40 | POINTER_ELEMENT_EVENTS = 'MSPointerDown'; 41 | POINTER_WINDOW_EVENTS = 'MSPointerMove MSPointerUp MSPointerCancel'; 42 | } 43 | 44 | function PointerInput(...args) { 45 | // OVERRIDE: listen for all event on the element, not on window 46 | // This is needed for event propagation to get the right targets 47 | this.evEl = `${POINTER_ELEMENT_EVENTS} ${POINTER_WINDOW_EVENTS}`; 48 | this.evWin = ''; 49 | Hammer.Input.apply(this, args); 50 | this.manager.session.pointerEvents = []; 51 | this.store = this.manager.session.pointerEvents; 52 | } 53 | Hammer.inherit(PointerInput, Hammer.PointerEventInput, {}); 54 | PointerInput.prototype.constructor = function () { }; // STUB, avoids init() being called too early 55 | 56 | const MOUSE_ELEMENT_EVENTS = 'mousedown'; 57 | const MOUSE_WINDOW_EVENTS = 'mousemove mouseup'; 58 | 59 | function MouseInput(...args) { 60 | // OVERRIDE: listen for all event on the element, not on window 61 | // This is needed for event propagation to get the right targets 62 | this.evEl = `${MOUSE_ELEMENT_EVENTS} ${MOUSE_WINDOW_EVENTS}`; 63 | this.evWin = ''; 64 | 65 | this.pressed = false; // mousedown state 66 | Hammer.Input.apply(this, args); 67 | } 68 | Hammer.inherit(MouseInput, Hammer.MouseInput, {}); 69 | // STUB, avoids overridden constructor being called 70 | MouseInput.prototype.constructor = function () { }; 71 | 72 | function TouchMouseInput(...args) { 73 | Hammer.Input.apply(this, args); 74 | 75 | const handler = this.handler.bind(this); 76 | this.touch = new Hammer.TouchInput(this.manager, handler); 77 | this.mouse = new MouseInput(this.manager, handler); 78 | 79 | this.primaryTouch = null; 80 | this.lastTouches = []; 81 | } 82 | Hammer.inherit(TouchMouseInput, Hammer.TouchMouseInput, {}); 83 | // STUB, avoids overridden constructor being called 84 | TouchMouseInput.prototype.constructor = function () { }; 85 | 86 | let Input = null; 87 | if (SUPPORT_POINTER_EVENTS) { 88 | Input = PointerInput; 89 | } else if (SUPPORT_ONLY_TOUCH) { 90 | Input = Hammer.TouchInput; 91 | } else if (!SUPPORT_TOUCH) { 92 | Input = MouseInput; 93 | } else { 94 | Input = TouchMouseInput; 95 | } 96 | 97 | module.exports = { 98 | Input, 99 | }; 100 | -------------------------------------------------------------------------------- /the-graph/merge.js: -------------------------------------------------------------------------------- 1 | // The `merge` function provides simple property merging. 2 | module.exports = function (src, dest, overwrite) { 3 | // Do nothing if neither are true objects. 4 | if (Array.isArray(src) || Array.isArray(dest) || typeof src !== 'object' || typeof dest !== 'object') return dest; 5 | 6 | // Default overwriting of existing properties to false. 7 | overwrite = overwrite || false; 8 | 9 | Object.keys(src).forEach((key) => { 10 | // Only copy properties, not functions. 11 | if (typeof src[key] !== 'function' && (!dest[key] || overwrite)) { 12 | dest[key] = src[key]; 13 | } 14 | }); 15 | 16 | return dest; 17 | }; 18 | -------------------------------------------------------------------------------- /the-graph/mixins.js: -------------------------------------------------------------------------------- 1 | const ReactDOM = require('react-dom'); 2 | // React mixins 3 | 4 | // Show fake tooltip 5 | // Class must have getTooltipTrigger (dom node) and shouldShowTooltip (boolean) 6 | const Tooltip = { 7 | showTooltip(event) { 8 | if (!this.shouldShowTooltip()) { return; } 9 | 10 | const tooltipEvent = new CustomEvent('the-graph-tooltip', { 11 | detail: { 12 | tooltip: this.props.label, 13 | x: event.clientX, 14 | y: event.clientY, 15 | }, 16 | bubbles: true, 17 | }); 18 | ReactDOM.findDOMNode(this).dispatchEvent(tooltipEvent); 19 | }, 20 | hideTooltip() { 21 | if (!this.shouldShowTooltip()) { return; } 22 | 23 | const tooltipEvent = new CustomEvent('the-graph-tooltip-hide', { 24 | bubbles: true, 25 | }); 26 | if (this.mounted) { 27 | ReactDOM.findDOMNode(this).dispatchEvent(tooltipEvent); 28 | } 29 | }, 30 | componentDidMount() { 31 | this.mounted = true; 32 | if (navigator && navigator.userAgent.indexOf('Firefox') !== -1) { 33 | // HACK Ff does native tooltips on svg elements 34 | return; 35 | } 36 | const tooltipper = this.getTooltipTrigger(); 37 | tooltipper.addEventListener('tap', this.showTooltip); 38 | tooltipper.addEventListener('mouseenter', this.showTooltip); 39 | tooltipper.addEventListener('mouseleave', this.hideTooltip); 40 | }, 41 | componentWillUnmount() { 42 | this.mounted = false; 43 | }, 44 | }; 45 | 46 | module.exports = { 47 | Tooltip, 48 | }; 49 | -------------------------------------------------------------------------------- /the-graph/render.js: -------------------------------------------------------------------------------- 1 | const React = require('react'); 2 | const ReactDOM = require('react-dom'); 3 | 4 | const geometryutils = require('./geometryutils.js'); 5 | // var TheGraphApp = require('./the-graph-app.js'); 6 | 7 | // XXX: hack, goes away when the-graph-app.js can be CommonJS loaded 8 | let TheGraphApp = null; 9 | function register(context) { 10 | TheGraphApp = context.TheGraph.App; 11 | } 12 | 13 | function applyStyleManual(element) { 14 | const style = getComputedStyle(element); 15 | const transferToAttribute = [ 16 | 17 | ]; 18 | const transferToStyle = [ 19 | 'fill', 20 | 'stroke', 21 | 'stroke-width', 22 | 'opacity', 23 | 'text-anchor', 24 | 'font-size', 25 | 'visibility', 26 | ]; 27 | 28 | transferToAttribute.forEach((name) => { 29 | const s = style.getPropertyValue(name); 30 | if (s) { 31 | element.setAttribute(name, s); 32 | } 33 | }); 34 | transferToStyle.forEach((name) => { 35 | const s = style.getPropertyValue(name); 36 | if (s) { 37 | element.style[name] = s; 38 | } 39 | }); 40 | } 41 | 42 | // FIXME: icons are broken 43 | function applyStyle(tree) { 44 | const all = tree.getElementsByTagName('*'); 45 | for (let i = 0; i < all.length; i += 1) { 46 | applyStyleManual(all[i]); 47 | } 48 | return tree; 49 | } 50 | 51 | function renderImage(graphElement, options, callback) { 52 | if (!options) { options = {}; } 53 | if (!options.format) { options.format = 'png'; } 54 | if (typeof options.background === 'undefined') { options.background = true; } 55 | if (typeof options.quality === 'undefined') { options.quality = 0.9; } 56 | 57 | const svgNode = graphElement.getElementsByTagName('svg')[0]; 58 | const bgCanvas = graphElement.getElementsByTagName('canvas')[0]; 59 | if (svgNode.tagName.toLowerCase() !== 'svg') { 60 | callback(new Error(`renderImage input must be SVG, got ${svgNode.tagName}`)); 61 | return; 62 | } 63 | 64 | // FIXME: make copy 65 | // svgNode = svgNode.cloneNode(true, true); 66 | 67 | // Note: alternative to inlining style is to inject the CSS file into SVG file? 68 | // https://stackoverflow.com/questions/18434094/how-to-style-svg-with-external-css 69 | const withStyle = applyStyle(svgNode); 70 | 71 | // TODO: include background in SVG file 72 | // not that easy thougj, https://stackoverflow.com/questions/11293026/default-background-color-of-svg-root-element 73 | const serializer = new XMLSerializer(); 74 | const svgData = serializer.serializeToString(withStyle); 75 | 76 | if (options.format === 'svg') { 77 | callback(null, svgData); 78 | return; 79 | } 80 | 81 | const DOMURL = window.URL || window.webkitURL || window; 82 | 83 | const img = new Image(); 84 | const svg = new Blob([svgData], { type: 'image/svg+xml' }); 85 | const svgUrl = DOMURL.createObjectURL(svg); 86 | 87 | const canvas = document.createElement('canvas'); 88 | canvas.width = svgNode.getAttribute('width'); 89 | canvas.height = svgNode.getAttribute('height'); 90 | 91 | // TODO: allow resizing? 92 | const ctx = canvas.getContext('2d'); 93 | 94 | if (options.background) { 95 | const bgColor = getComputedStyle(graphElement)['background-color']; 96 | ctx.fillStyle = bgColor; 97 | ctx.fillRect(0, 0, canvas.width, canvas.height); 98 | ctx.drawImage(bgCanvas, 0, 0); 99 | } 100 | 101 | img.onerror = (err) => { 102 | callback(err); 103 | }; 104 | img.onload = () => { 105 | ctx.drawImage(img, 0, 0); 106 | DOMURL.revokeObjectURL(svgUrl); 107 | const out = canvas.toDataURL(`image/${options.format}`, options.quality); 108 | callback(null, out); 109 | }; 110 | img.src = svgUrl; 111 | } 112 | 113 | function libraryFromGraph(graph) { 114 | const components = {}; 115 | const processComponents = {}; 116 | 117 | graph.nodes.forEach((process) => { 118 | const name = process.component; 119 | processComponents[process.id] = name; 120 | components[name] = { 121 | name, 122 | description: name, 123 | icon: null, 124 | inports: [], 125 | outports: [], 126 | }; 127 | }); 128 | 129 | function addIfMissing(ports, name) { 130 | const found = ports.filter((p) => p.name === name); 131 | if (found.length === 0) { 132 | ports.push({ name, type: 'all' }); 133 | } 134 | } 135 | 136 | graph.edges.forEach((conn) => { 137 | const tgt = processComponents[conn.to.node]; 138 | addIfMissing(components[tgt].inports, conn.to.port); 139 | if (conn.from) { 140 | const src = processComponents[conn.from.node]; 141 | addIfMissing(components[src].outports, conn.from.port); 142 | } 143 | }); 144 | 145 | function componentsFromExports(exports, inports) { 146 | Object.keys(exports).forEach((exportedName) => { 147 | const internal = exports[exportedName]; 148 | const comp = components[processComponents[internal.process]]; 149 | const ports = (inports) ? comp.inports : comp.outports; 150 | addIfMissing(ports, internal.port); 151 | }); 152 | } 153 | componentsFromExports(graph.inports, true); 154 | componentsFromExports(graph.outports, false); 155 | 156 | return components; 157 | } 158 | 159 | function removeAllChildren(n) { 160 | while (n.firstChild) { 161 | n.removeChild(n.firstChild); 162 | } 163 | } 164 | 165 | function renderGraph(graph, options) { 166 | if (!options.library) { options.library = libraryFromGraph(graph); } 167 | if (!options.theme) { options.theme = 'the-graph-dark'; } 168 | if (!options.width) { options.width = 1200; } 169 | if (!options.margin) { options.margin = 72; } 170 | 171 | // TODO support doing autolayout. Default to on if graph is missing x/y positions 172 | // TODO: Set zoom-level, width,height so that whole graph shows with all info 173 | 174 | // fit based on width constrained (height near infinite) 175 | const fit = geometryutils.findFit(graph, options.width, options.width * 100, options.margin); 176 | const aspectRatio = fit.graphWidth / fit.graphHeight; 177 | if (!options.height) { 178 | // calculate needed aspect ratio 179 | options.height = options.width / aspectRatio; 180 | } 181 | console.log('f', aspectRatio, options.height, JSON.stringify(fit)); 182 | 183 | const props = { 184 | readonly: true, 185 | width: options.width, 186 | height: options.height, 187 | graph, 188 | library: options.library, 189 | }; 190 | // console.log('render', props); 191 | 192 | const wrapper = document.createElement('div'); 193 | wrapper.className = options.theme; 194 | wrapper.width = props.width; 195 | wrapper.height = props.height; 196 | 197 | // FIXME: find a less intrusive way 198 | const container = document.body; 199 | removeAllChildren(container); 200 | container.appendChild(wrapper); 201 | 202 | const element = React.createElement(TheGraphApp, props); 203 | ReactDOM.render(element, wrapper); 204 | 205 | const svgElement = wrapper.children[0]; 206 | return svgElement; 207 | } 208 | 209 | module.exports = { 210 | graphToDOM: renderGraph, 211 | exportImage: renderImage, 212 | register, 213 | }; 214 | -------------------------------------------------------------------------------- /the-graph/the-graph-autolayout.js: -------------------------------------------------------------------------------- 1 | // NOTE: caller should wrap in a graph transaction, to group all changes made to @graph 2 | function applyAutolayout(graph, keilerGraph, props) { 3 | console.error('DEPRECATED: TheGraph.autolayout.applyAutolayout() will be removed in next version'); 4 | 5 | // Update original graph nodes with the new coordinates from KIELER graph 6 | const children = keilerGraph.children.slice(); 7 | 8 | let i; let 9 | len; 10 | for (i = 0, len = children.length; i < len; i++) { 11 | const klayNode = children[i]; 12 | const fbpNode = graph.getNode(klayNode.id); 13 | 14 | // Encode nodes inside groups 15 | if (klayNode.children) { 16 | const klayChildren = klayNode.children; 17 | var idx; 18 | for (idx in klayChildren) { 19 | const klayChild = klayChildren[idx]; 20 | if (klayChild.id) { 21 | graph.setNodeMetadata(klayChild.id, { 22 | x: Math.round((klayNode.x + klayChild.x) / props.snap) * props.snap, 23 | y: Math.round((klayNode.y + klayChild.y) / props.snap) * props.snap, 24 | }); 25 | } 26 | } 27 | } 28 | 29 | // Encode nodes outside groups 30 | if (fbpNode) { 31 | graph.setNodeMetadata(klayNode.id, { 32 | x: Math.round(klayNode.x / props.snap) * props.snap, 33 | y: Math.round(klayNode.y / props.snap) * props.snap, 34 | }); 35 | } else { 36 | // Find inport or outport 37 | const idSplit = klayNode.id.split(':::'); 38 | const expDirection = idSplit[0]; 39 | const expKey = idSplit[1]; 40 | if (expDirection === 'inport' && graph.inports[expKey]) { 41 | graph.setInportMetadata(expKey, { 42 | x: Math.round(klayNode.x / props.snap) * props.snap, 43 | y: Math.round(klayNode.y / props.snap) * props.snap, 44 | }); 45 | } else if (expDirection === 'outport' && graph.outports[expKey]) { 46 | graph.setOutportMetadata(expKey, { 47 | x: Math.round(klayNode.x / props.snap) * props.snap, 48 | y: Math.round(klayNode.y / props.snap) * props.snap, 49 | }); 50 | } 51 | } 52 | } 53 | } 54 | 55 | module.exports = { 56 | applyToGraph: applyAutolayout, 57 | }; 58 | -------------------------------------------------------------------------------- /the-graph/the-graph-edge.js: -------------------------------------------------------------------------------- 1 | const React = require('react'); 2 | const ReactDOM = require('react-dom'); 3 | const createReactClass = require('create-react-class'); 4 | const TooltipMixin = require('./mixins').Tooltip; 5 | 6 | module.exports.register = function (context) { 7 | const { TheGraph } = context; 8 | 9 | TheGraph.config.edge = { 10 | curve: TheGraph.config.nodeSize, 11 | container: { 12 | className: 'edge', 13 | }, 14 | backgroundPath: { 15 | className: 'edge-bg', 16 | }, 17 | foregroundPath: { 18 | ref: 'route', 19 | className: 'edge-fg stroke route', 20 | }, 21 | touchPath: { 22 | className: 'edge-touch', 23 | ref: 'touch', 24 | }, 25 | }; 26 | 27 | TheGraph.factories.edge = { 28 | createEdgeGroup: TheGraph.factories.createGroup, 29 | createEdgeBackgroundPath: TheGraph.factories.createPath, 30 | createEdgeForegroundPath: TheGraph.factories.createPath, 31 | createEdgeTouchPath: TheGraph.factories.createPath, 32 | createEdgePathArray, 33 | createArrow: TheGraph.factories.createPolygon, 34 | }; 35 | 36 | function createEdgePathArray(sourceX, sourceY, c1X, c1Y, c2X, c2Y, targetX, targetY) { 37 | return [ 38 | 'M', 39 | sourceX, sourceY, 40 | 'C', 41 | c1X, c1Y, 42 | c2X, c2Y, 43 | targetX, targetY, 44 | ]; 45 | } 46 | 47 | // Const 48 | const CURVE = TheGraph.config.edge.curve; 49 | 50 | // Point along cubic bezier curve 51 | // See http://en.wikipedia.org/wiki/File:Bezier_3_big.gif 52 | const findPointOnCubicBezier = function (p, sx, sy, c1x, c1y, c2x, c2y, ex, ey) { 53 | // p is percentage from 0 to 1 54 | const op = 1 - p; 55 | // 3 green points between 4 points that define curve 56 | const g1x = sx * p + c1x * op; 57 | const g1y = sy * p + c1y * op; 58 | const g2x = c1x * p + c2x * op; 59 | const g2y = c1y * p + c2y * op; 60 | const g3x = c2x * p + ex * op; 61 | const g3y = c2y * p + ey * op; 62 | // 2 blue points between green points 63 | const b1x = g1x * p + g2x * op; 64 | const b1y = g1y * p + g2y * op; 65 | const b2x = g2x * p + g3x * op; 66 | const b2y = g2y * p + g3y * op; 67 | // Point on the curve between blue points 68 | const x = b1x * p + b2x * op; 69 | const y = b1y * p + b2y * op; 70 | return [x, y]; 71 | }; 72 | 73 | // Edge view 74 | 75 | TheGraph.Edge = React.createFactory(createReactClass({ 76 | displayName: 'TheGraphEdge', 77 | mixins: [ 78 | TooltipMixin, 79 | ], 80 | componentWillMount() { 81 | }, 82 | componentDidMount() { 83 | const domNode = ReactDOM.findDOMNode(this); 84 | 85 | // Select 86 | if (this.props.onEdgeSelection) { 87 | // Needs to be click (not tap) to get event.shiftKey 88 | domNode.addEventListener('tap', this.onEdgeSelection); 89 | } 90 | // Open menu 91 | if (this.props.showContext) { 92 | domNode.addEventListener('contextmenu', this.showContext); 93 | domNode.addEventListener('press', this.showContext); 94 | } 95 | }, 96 | onEdgeSelection(event) { 97 | // Don't click app 98 | event.stopPropagation(); 99 | 100 | const toggle = (TheGraph.metaKeyPressed || event.pointerType === 'touch'); 101 | this.props.onEdgeSelection(this.props.edgeID, this.props.edge, toggle); 102 | }, 103 | showContext(event) { 104 | // Don't show native context menu 105 | event.preventDefault(); 106 | 107 | // Don't tap graph on hold event 108 | if (event.stopPropagation) { event.stopPropagation(); } 109 | if (event.preventTap) { event.preventTap(); } 110 | 111 | // Get mouse position 112 | if (event.gesture) { 113 | event = event.gesture.srcEvent; // unpack hammer.js gesture event 114 | } 115 | let x = event.x || event.clientX || 0; 116 | let y = event.y || event.clientY || 0; 117 | if (event.touches && event.touches.length) { 118 | x = event.touches[0].clientX; 119 | y = event.touches[0].clientY; 120 | } 121 | 122 | // App.showContext 123 | this.props.showContext({ 124 | element: this, 125 | type: (this.props.export ? (this.props.isIn ? 'graphInport' : 'graphOutport') : 'edge'), 126 | x, 127 | y, 128 | graph: this.props.graph, 129 | itemKey: (this.props.export ? this.props.exportKey : null), 130 | item: (this.props.export ? this.props.export : this.props.edge), 131 | }); 132 | }, 133 | getContext(menu, options, hide) { 134 | return TheGraph.Menu({ 135 | menu, 136 | options, 137 | triggerHideContext: hide, 138 | label: this.props.label, 139 | iconColor: this.props.route, 140 | }); 141 | }, 142 | shouldComponentUpdate(nextProps, nextState) { 143 | // Only rerender if changed 144 | return ( 145 | nextProps.sX !== this.props.sX 146 | || nextProps.sY !== this.props.sY 147 | || nextProps.tX !== this.props.tX 148 | || nextProps.tY !== this.props.tY 149 | || nextProps.selected !== this.props.selected 150 | || nextProps.animated !== this.props.animated 151 | || nextProps.route !== this.props.route 152 | ); 153 | }, 154 | getTooltipTrigger() { 155 | return ReactDOM.findDOMNode(this.refs.touch); 156 | }, 157 | shouldShowTooltip() { 158 | return true; 159 | }, 160 | render() { 161 | const sourceX = this.props.sX; 162 | const sourceY = this.props.sY; 163 | const targetX = this.props.tX; 164 | const targetY = this.props.tY; 165 | 166 | // Organic / curved edge 167 | let c1X; let c1Y; let c2X; let 168 | c2Y; 169 | if (targetX - 5 < sourceX) { 170 | const curveFactor = (sourceX - targetX) * CURVE / 200; 171 | if (Math.abs(targetY - sourceY) < TheGraph.config.nodeSize / 2) { 172 | // Loopback 173 | c1X = sourceX + curveFactor; 174 | c1Y = sourceY - curveFactor; 175 | c2X = targetX - curveFactor; 176 | c2Y = targetY - curveFactor; 177 | } else { 178 | // Stick out some 179 | c1X = sourceX + curveFactor; 180 | c1Y = sourceY + (targetY > sourceY ? curveFactor : -curveFactor); 181 | c2X = targetX - curveFactor; 182 | c2Y = targetY + (targetY > sourceY ? -curveFactor : curveFactor); 183 | } 184 | } else { 185 | // Controls halfway between 186 | c1X = sourceX + (targetX - sourceX) / 2; 187 | c1Y = sourceY; 188 | c2X = c1X; 189 | c2Y = targetY; 190 | } 191 | 192 | // Make SVG path 193 | 194 | let path = TheGraph.factories.edge.createEdgePathArray(sourceX, sourceY, c1X, c1Y, c2X, c2Y, targetX, targetY); 195 | path = path.join(' '); 196 | 197 | const backgroundPathOptions = TheGraph.merge(TheGraph.config.edge.backgroundPath, { d: path }); 198 | const backgroundPath = TheGraph.factories.edge.createEdgeBackgroundPath(backgroundPathOptions); 199 | 200 | const foregroundPathClassName = TheGraph.config.edge.foregroundPath.className + this.props.route; 201 | const foregroundPathOptions = TheGraph.merge(TheGraph.config.edge.foregroundPath, { d: path, className: foregroundPathClassName }); 202 | const foregroundPath = TheGraph.factories.edge.createEdgeForegroundPath(foregroundPathOptions); 203 | 204 | const touchPathOptions = TheGraph.merge(TheGraph.config.edge.touchPath, { d: path }); 205 | const touchPath = TheGraph.factories.edge.createEdgeTouchPath(touchPathOptions); 206 | 207 | let containerOptions = { 208 | className: `edge${ 209 | this.props.selected ? ' selected' : '' 210 | }${this.props.animated ? ' animated' : ''}`, 211 | title: this.props.label, 212 | }; 213 | 214 | containerOptions = TheGraph.merge(TheGraph.config.edge.container, containerOptions); 215 | 216 | const epsilon = 0.01; 217 | let center = findPointOnCubicBezier(0.5, sourceX, sourceY, c1X, c1Y, c2X, c2Y, targetX, targetY); 218 | 219 | // estimate slope and intercept of tangent line 220 | const getShiftedPoint = function (epsilon) { 221 | return findPointOnCubicBezier( 222 | 0.5 + epsilon, sourceX, sourceY, c1X, c1Y, c2X, c2Y, targetX, targetY, 223 | ); 224 | }; 225 | const plus = getShiftedPoint(epsilon); 226 | const minus = getShiftedPoint(-epsilon); 227 | const m = 1 * (plus[1] - minus[1]) / (plus[0] - minus[0]); 228 | const b = center[1] - (m * center[0]); 229 | 230 | // find point on line y = mx + b that is `offset` away from x,y 231 | const findLinePoint = function (x, y, m, b, offset, flip) { 232 | const x1 = x + offset / Math.sqrt(1 + m * m); 233 | let y1; 234 | if (Math.abs(m) === Infinity) { 235 | y1 = y + (flip || 1) * offset; 236 | } else { 237 | y1 = (m * x1) + b; 238 | } 239 | return [x1, y1]; 240 | }; 241 | 242 | let arrowLength = 12; 243 | // Which direction should arrow point 244 | if (plus[0] > minus[0]) { 245 | arrowLength *= -1; 246 | } 247 | center = findLinePoint(center[0], center[1], m, b, -1 * arrowLength / 2); 248 | 249 | // find points of perpendicular line length l centered at x,y 250 | const perpendicular = function (x, y, oldM, l) { 251 | const m = -1 / oldM; 252 | const b = y - m * x; 253 | const point1 = findLinePoint(x, y, m, b, l / 2); 254 | const point2 = findLinePoint(x, y, m, b, l / -2); 255 | return [point1, point2]; 256 | }; 257 | 258 | const points = perpendicular(center[0], center[1], m, arrowLength * 0.9); 259 | // For m === 0, figure out if arrow should be straight up or down 260 | const flip = plus[1] > minus[1] ? -1 : 1; 261 | const arrowTip = findLinePoint(center[0], center[1], m, b, arrowLength, flip); 262 | points.push(arrowTip); 263 | 264 | const pointsArray = points.map( 265 | (point) => point.join(','), 266 | ).join(' '); 267 | const arrowBg = TheGraph.factories.edge.createArrow({ 268 | points: pointsArray, 269 | className: 'arrow-bg', 270 | }); 271 | 272 | const arrow = TheGraph.factories.edge.createArrow({ 273 | points: pointsArray, 274 | className: `arrow fill route${this.props.route}`, 275 | }); 276 | 277 | return TheGraph.factories.edge.createEdgeGroup(containerOptions, 278 | [backgroundPath, arrowBg, foregroundPath, touchPath, arrow]); 279 | }, 280 | })); 281 | }; 282 | -------------------------------------------------------------------------------- /the-graph/the-graph-group.js: -------------------------------------------------------------------------------- 1 | const React = require('react'); 2 | const ReactDOM = require('react-dom'); 3 | const createReactClass = require('create-react-class'); 4 | 5 | module.exports.register = function (context) { 6 | const { TheGraph } = context; 7 | 8 | TheGraph.config.group = { 9 | container: { 10 | className: 'group', 11 | }, 12 | boxRect: { 13 | rx: TheGraph.config.nodeRadius, 14 | ry: TheGraph.config.nodeRadius, 15 | }, 16 | labelText: { 17 | className: 'group-label', 18 | }, 19 | descriptionText: { 20 | className: 'group-description', 21 | }, 22 | }; 23 | 24 | TheGraph.factories.group = { 25 | createGroupGroup: TheGraph.factories.createGroup, 26 | createGroupBoxRect: TheGraph.factories.createRect, 27 | createGroupLabelText: TheGraph.factories.createText, 28 | createGroupDescriptionText: TheGraph.factories.createText, 29 | }; 30 | 31 | // Group view 32 | 33 | TheGraph.Group = React.createFactory(createReactClass({ 34 | displayName: 'TheGraphGroup', 35 | getInitialState() { 36 | return { 37 | moving: false, 38 | lastTrackX: null, 39 | lastTrackY: null, 40 | }; 41 | }, 42 | componentDidMount() { 43 | // Move group 44 | const dragNode = ReactDOM.findDOMNode(this.refs.events); 45 | dragNode.addEventListener('panstart', this.onTrackStart); 46 | 47 | // Context menu 48 | const domNode = ReactDOM.findDOMNode(this); 49 | if (this.props.showContext) { 50 | domNode.addEventListener('contextmenu', this.showContext); 51 | domNode.addEventListener('press', this.showContext); 52 | } 53 | }, 54 | showContext(event) { 55 | // Don't show native context menu 56 | event.preventDefault(); 57 | 58 | // Don't tap graph on hold event 59 | if (event.stopPropagation) { event.stopPropagation(); } 60 | if (event.preventTap) { event.preventTap(); } 61 | 62 | // Get mouse position 63 | if (event.gesture) { 64 | event = event.gesture.srcEvent; // unpack hammer.js gesture event 65 | } 66 | let x = event.x || event.clientX || 0; 67 | let y = event.y || event.clientY || 0; 68 | if (event.touches && event.touches.length) { 69 | x = event.touches[0].clientX; 70 | y = event.touches[0].clientY; 71 | } 72 | 73 | // App.showContext 74 | this.props.showContext({ 75 | element: this, 76 | type: (this.props.isSelectionGroup ? 'selection' : 'group'), 77 | x, 78 | y, 79 | graph: this.props.graph, 80 | itemKey: this.props.label, 81 | item: this.props.item, 82 | }); 83 | }, 84 | getContext(menu, options, hide) { 85 | return TheGraph.Menu({ 86 | menu, 87 | options, 88 | label: this.props.label, 89 | triggerHideContext: hide, 90 | }); 91 | }, 92 | onTrackStart(event) { 93 | // Don't pan graph 94 | event.stopPropagation(); 95 | this.setState({ moving: true }); 96 | this.setState({ lastTrackX: 0, lastTrackY: 0 }); 97 | 98 | const dragNode = ReactDOM.findDOMNode(this.refs.events); 99 | dragNode.addEventListener('panmove', this.onTrack); 100 | dragNode.addEventListener('panend', this.onTrackEnd); 101 | 102 | this.props.graph.startTransaction('movegroup'); 103 | }, 104 | onTrack(event) { 105 | // Don't pan graph 106 | event.stopPropagation(); 107 | 108 | // Reconstruct relative motion since last event 109 | const x = event.gesture.deltaX; 110 | const y = event.gesture.deltaY; 111 | const movementX = x - this.state.lastTrackX; 112 | const movementY = y - this.state.lastTrackY; 113 | 114 | const deltaX = Math.round(movementX / this.props.scale); 115 | const deltaY = Math.round(movementY / this.props.scale); 116 | 117 | this.setState({ lastTrackX: x, lastTrackY: y }); 118 | this.props.triggerMoveGroup(this.props.item.nodes, deltaX, deltaY); 119 | }, 120 | onTrackEnd(event) { 121 | this.setState({ moving: false }); 122 | // Don't pan graph 123 | event.stopPropagation(); 124 | 125 | // Snap to grid 126 | this.props.triggerMoveGroup(this.props.item.nodes); 127 | 128 | const dragNode = ReactDOM.findDOMNode(this.refs.events); 129 | dragNode.addEventListener('panmove', this.onTrack); 130 | dragNode.addEventListener('panend', this.onTrackEnd); 131 | 132 | this.setState({ lastTrackX: null, lastTrackY: null }); 133 | this.props.graph.endTransaction('movegroup'); 134 | }, 135 | render() { 136 | const x = this.props.minX - TheGraph.config.nodeWidth / 2; 137 | const y = this.props.minY - TheGraph.config.nodeHeight / 2; 138 | const color = (this.props.color ? this.props.color : 0); 139 | const selection = (this.props.isSelectionGroup ? ' selection drag' : ''); 140 | 141 | let boxRectOptions = { 142 | x, 143 | y, 144 | width: this.props.maxX - x + TheGraph.config.nodeWidth * 0.5, 145 | height: this.props.maxY - y + TheGraph.config.nodeHeight * 0.75, 146 | className: `group-box color${color}${selection}`, 147 | }; 148 | boxRectOptions = TheGraph.merge(TheGraph.config.group.boxRect, boxRectOptions); 149 | const boxRect = TheGraph.factories.group.createGroupBoxRect.call(this, boxRectOptions); 150 | 151 | let labelTextOptions = { 152 | x: x + TheGraph.config.nodeRadius, 153 | y: y + 9, 154 | children: this.props.label, 155 | }; 156 | labelTextOptions = TheGraph.merge(TheGraph.config.group.labelText, labelTextOptions); 157 | const labelText = TheGraph.factories.group.createGroupLabelText.call(this, labelTextOptions); 158 | 159 | let descriptionTextOptions = { 160 | x: x + TheGraph.config.nodeRadius, 161 | y: y + 24, 162 | children: this.props.description, 163 | }; 164 | descriptionTextOptions = TheGraph.merge(TheGraph.config.group.descriptionText, descriptionTextOptions); 165 | const descriptionText = TheGraph.factories.group.createGroupDescriptionText.call(this, descriptionTextOptions); 166 | 167 | // When moving, expand bounding box of element 168 | // to catch events when pointer moves faster than we can move the element 169 | const eventOptions = { 170 | className: 'eventcatcher drag', 171 | ref: 'events', 172 | }; 173 | if (this.props.isSelectionGroup) { 174 | eventOptions.x = boxRectOptions.x; 175 | eventOptions.y = boxRectOptions.y; 176 | eventOptions.width = boxRectOptions.width; 177 | eventOptions.height = boxRectOptions.height; 178 | } else { 179 | eventOptions.x = boxRectOptions.x; 180 | eventOptions.y = boxRectOptions.y; 181 | eventOptions.width = 24 * this.props.label.length * 0.75; 182 | eventOptions.height = 24 * 2; 183 | } 184 | if (this.state.moving) { 185 | const extend = 1000; 186 | eventOptions.width += extend * 2; 187 | eventOptions.height += extend * 2; 188 | eventOptions.x -= extend; 189 | eventOptions.y -= extend; 190 | } 191 | const eventCatcher = TheGraph.factories.createRect(eventOptions); 192 | 193 | const groupContents = [ 194 | boxRect, 195 | labelText, 196 | descriptionText, 197 | eventCatcher, 198 | ]; 199 | 200 | const containerOptions = TheGraph.merge(TheGraph.config.group.container, {}); 201 | return TheGraph.factories.group.createGroupGroup.call(this, containerOptions, groupContents); 202 | }, 203 | })); 204 | }; 205 | -------------------------------------------------------------------------------- /the-graph/the-graph-iip.js: -------------------------------------------------------------------------------- 1 | const React = require('react'); 2 | const createReactClass = require('create-react-class'); 3 | 4 | const TextBG = require('./TextBG'); 5 | 6 | module.exports.register = function (context) { 7 | const { TheGraph } = context; 8 | 9 | TheGraph.config.iip = { 10 | container: { 11 | className: 'iip', 12 | }, 13 | path: { 14 | className: 'iip-path', 15 | }, 16 | text: { 17 | className: 'iip-info', 18 | height: 5, 19 | halign: 'right', 20 | }, 21 | }; 22 | 23 | TheGraph.factories.iip = { 24 | createIIPContainer: TheGraph.factories.createGroup, 25 | createIIPPath: TheGraph.factories.createPath, 26 | createIIPText, 27 | }; 28 | 29 | function createIIPText(options) { 30 | return TextBG(options); 31 | } 32 | 33 | // Const 34 | const CURVE = 50; 35 | 36 | // Edge view 37 | 38 | TheGraph.IIP = React.createFactory(createReactClass({ 39 | displayName: 'TheGraphIIP', 40 | shouldComponentUpdate(nextProps, nextState) { 41 | // Only rerender if changed 42 | 43 | return ( 44 | nextProps.x !== this.props.x 45 | || nextProps.y !== this.props.y 46 | || nextProps.label !== this.props.label 47 | ); 48 | }, 49 | render() { 50 | const { x } = this.props; 51 | const { y } = this.props; 52 | 53 | const path = [ 54 | 'M', x, y, 55 | 'L', x - 10, y, 56 | ].join(' '); 57 | 58 | // Make a string 59 | let label = `${this.props.label}`; 60 | // TODO make this smarter with ZUI 61 | if (label.length > 12) { 62 | label = `${label.slice(0, 9)}...`; 63 | } 64 | 65 | const pathOptions = TheGraph.merge(TheGraph.config.iip.path, { d: path }); 66 | const iipPath = TheGraph.factories.iip.createIIPPath.call(this, pathOptions); 67 | 68 | const textOptions = TheGraph.merge(TheGraph.config.iip.text, { x: x - 10, y, text: label }); 69 | const text = TheGraph.factories.iip.createIIPText.call(this, textOptions); 70 | 71 | const containerContents = [iipPath, text]; 72 | 73 | const containerOptions = TheGraph.merge(TheGraph.config.iip.container, { title: this.props.label }); 74 | return TheGraph.factories.iip.createIIPContainer.call(this, containerOptions, containerContents); 75 | }, 76 | })); 77 | }; 78 | -------------------------------------------------------------------------------- /the-graph/the-graph-library.js: -------------------------------------------------------------------------------- 1 | // Component library functionality 2 | function mergeComponentDefinition(component, definition) { 3 | // In cases where a component / subgraph ports change, 4 | // we don't want the connections hanging in middle of node 5 | // TODO visually indicate that port is a ghost 6 | if (component === definition) { 7 | return definition; 8 | } 9 | let _i; let _j; let _len; let _len1; let 10 | exists; 11 | const cInports = component.inports; 12 | const dInports = definition.inports; 13 | 14 | if (cInports !== dInports) { 15 | for (_i = 0, _len = cInports.length; _i < _len; _i++) { 16 | const cInport = cInports[_i]; 17 | exists = false; 18 | for (_j = 0, _len1 = dInports.length; _j < _len1; _j++) { 19 | const dInport = dInports[_j]; 20 | if (cInport.name === dInport.name) { 21 | exists = true; 22 | } 23 | } 24 | if (!exists) { 25 | dInports.push(cInport); 26 | } 27 | } 28 | } 29 | 30 | const cOutports = component.outports; 31 | const dOutports = definition.outports; 32 | 33 | if (cOutports !== dOutports) { 34 | for (_i = 0, _len = cOutports.length; _i < _len; _i++) { 35 | const cOutport = cOutports[_i]; 36 | exists = false; 37 | for (_j = 0, _len1 = dOutports.length; _j < _len1; _j++) { 38 | const dOutport = dOutports[_j]; 39 | if (cOutport.name === dOutport.name) { 40 | exists = true; 41 | } 42 | } 43 | if (!exists) { 44 | dOutports.push(cOutport); 45 | } 46 | } 47 | } 48 | 49 | if (definition.icon !== 'cog') { 50 | // Use the latest icon given 51 | component.icon = definition.icon; 52 | } else { 53 | // we should use the icon from the library 54 | definition.icon = component.icon; 55 | } 56 | // a component could also define a svg icon 57 | definition.iconsvg = component.iconsvg; 58 | 59 | return definition; 60 | } 61 | 62 | function componentsFromGraph(fbpGraph) { 63 | const components = []; 64 | 65 | fbpGraph.nodes.forEach((node) => { 66 | const component = { 67 | name: node.component, 68 | icon: 'cog', 69 | description: '', 70 | inports: [], 71 | outports: [], 72 | }; 73 | 74 | Object.keys(fbpGraph.inports).forEach((pub) => { 75 | const exported = fbpGraph.inports[pub]; 76 | if (exported.process === node.id) { 77 | for (let i = 0; i < component.inports.length; i++) { 78 | if (component.inports[i].name === exported.port) { 79 | return; 80 | } 81 | } 82 | component.inports.push({ 83 | name: exported.port, 84 | type: 'all', 85 | }); 86 | } 87 | }); 88 | Object.keys(fbpGraph.outports).forEach((pub) => { 89 | const exported = fbpGraph.outports[pub]; 90 | if (exported.process === node.id) { 91 | for (let i = 0; i < component.outports.length; i++) { 92 | if (component.outports[i].name === exported.port) { 93 | return; 94 | } 95 | } 96 | component.outports.push({ 97 | name: exported.port, 98 | type: 'all', 99 | }); 100 | } 101 | }); 102 | fbpGraph.initializers.forEach((iip) => { 103 | if (iip.to.node === node.id) { 104 | for (let i = 0; i < component.inports.length; i++) { 105 | if (component.inports[i].name === iip.to.port) { 106 | return; 107 | } 108 | } 109 | component.inports.push({ 110 | name: iip.to.port, 111 | type: 'all', 112 | }); 113 | } 114 | }); 115 | 116 | fbpGraph.edges.forEach((edge) => { 117 | let i; 118 | if (edge.from.node === node.id) { 119 | for (i = 0; i < component.outports.length; i++) { 120 | if (component.outports[i].name === edge.from.port) { 121 | return; 122 | } 123 | } 124 | component.outports.push({ 125 | name: edge.from.port, 126 | type: 'all', 127 | }); 128 | } 129 | if (edge.to.node === node.id) { 130 | for (i = 0; i < component.inports.length; i++) { 131 | if (component.inports[i].name === edge.to.port) { 132 | return; 133 | } 134 | } 135 | component.inports.push({ 136 | name: edge.to.port, 137 | type: 'all', 138 | }); 139 | } 140 | }); 141 | components.push(component); 142 | }); 143 | return components; 144 | } 145 | 146 | function libraryFromGraph(fbpGraph) { 147 | const library = {}; 148 | const components = componentsFromGraph(fbpGraph); 149 | components.forEach((c) => { 150 | library[c.name] = c; 151 | }); 152 | return library; 153 | } 154 | 155 | module.exports = { 156 | mergeComponentDefinition, 157 | componentsFromGraph, 158 | libraryFromGraph, 159 | }; 160 | -------------------------------------------------------------------------------- /the-graph/the-graph-menu.js: -------------------------------------------------------------------------------- 1 | const React = require('react'); 2 | const ReactDOM = require('react-dom'); 3 | const createReactClass = require('create-react-class'); 4 | 5 | const arcs = require('./arcs'); 6 | const merge = require('./merge'); 7 | const FONT_AWESOME = require('./font-awesome-unicode-map.js'); 8 | const baseFactories = require('./factories'); 9 | 10 | const config = { 11 | radius: 72, 12 | positions: { 13 | n4IconX: 0, 14 | n4IconY: -52, 15 | n4LabelX: 0, 16 | n4LabelY: -35, 17 | s4IconX: 0, 18 | s4IconY: 52, 19 | s4LabelX: 0, 20 | s4LabelY: 35, 21 | e4IconX: 45, 22 | e4IconY: -5, 23 | e4LabelX: 45, 24 | e4LabelY: 15, 25 | w4IconX: -45, 26 | w4IconY: -5, 27 | w4LabelX: -45, 28 | w4LabelY: 15, 29 | }, 30 | container: { 31 | className: 'context-menu', 32 | }, 33 | arcPath: { 34 | className: 'context-arc context-node-info-bg', 35 | }, 36 | sliceIconText: { 37 | className: 'icon context-icon context-node-info-icon', 38 | }, 39 | sliceLabelText: { 40 | className: 'context-arc-label', 41 | }, 42 | sliceIconLabelText: { 43 | className: 'context-arc-icon-label', 44 | }, 45 | circleXPath: { 46 | className: 'context-circle-x', 47 | d: 'M -51 -51 L 51 51 M -51 51 L 51 -51', 48 | }, 49 | outlineCircle: { 50 | className: 'context-circle', 51 | }, 52 | labelText: { 53 | className: 'context-node-label', 54 | }, 55 | iconRect: { 56 | className: 'context-node-rect', 57 | x: -24, 58 | y: -24, 59 | width: 48, 60 | height: 48, 61 | rx: 8, 62 | ry: 8, 63 | }, 64 | }; 65 | 66 | const factories = { 67 | createMenuGroup: baseFactories.createGroup, 68 | createMenuSlice, 69 | createMenuSliceArcPath: baseFactories.createPath, 70 | createMenuSliceText: baseFactories.createText, 71 | createMenuSliceIconText: baseFactories.createText, 72 | createMenuSliceLabelText: baseFactories.createText, 73 | createMenuSliceIconLabelText: baseFactories.createText, 74 | createMenuCircleXPath: baseFactories.createPath, 75 | createMenuOutlineCircle: baseFactories.createCircle, 76 | createMenuLabelText: baseFactories.createText, 77 | createMenuMiddleIconRect: baseFactories.createRect, 78 | createMenuMiddleIconText: baseFactories.createText, 79 | }; 80 | 81 | function createMenuSlice(options) { 82 | /* jshint validthis:true */ 83 | const { direction } = options; 84 | const arcPathOptions = merge(config.arcPath, { d: arcs[direction] }); 85 | const children = [ 86 | factories.createMenuSliceArcPath(arcPathOptions), 87 | ]; 88 | 89 | if (this.props.menu[direction]) { 90 | const slice = this.props.menu[direction]; 91 | if (slice.icon) { 92 | let sliceIconTextOptions = { 93 | x: config.positions[`${direction}IconX`], 94 | y: config.positions[`${direction}IconY`], 95 | children: FONT_AWESOME[slice.icon], 96 | }; 97 | sliceIconTextOptions = merge(config.sliceIconText, sliceIconTextOptions); 98 | children.push(factories.createMenuSliceIconText.call(this, sliceIconTextOptions)); 99 | } 100 | if (slice.label) { 101 | let sliceLabelTextOptions = { 102 | x: config.positions[`${direction}IconX`], 103 | y: config.positions[`${direction}IconY`], 104 | children: slice.label, 105 | }; 106 | sliceLabelTextOptions = merge(config.sliceLabelText, sliceLabelTextOptions); 107 | children.push(factories.createMenuSliceLabelText.call(this, sliceLabelTextOptions)); 108 | } 109 | if (slice.iconLabel) { 110 | let sliceIconLabelTextOptions = { 111 | x: config.positions[`${direction}LabelX`], 112 | y: config.positions[`${direction}LabelY`], 113 | children: slice.iconLabel, 114 | }; 115 | sliceIconLabelTextOptions = merge(config.sliceIconLabelText, sliceIconLabelTextOptions); 116 | children.push(factories.createMenuSliceIconLabelText.call(this, sliceIconLabelTextOptions)); 117 | } 118 | } 119 | 120 | let containerOptions = { 121 | ref: direction, 122 | className: `context-slice context-node-info${this.state[`${direction}tappable`] ? ' click' : ''}`, 123 | children, 124 | }; 125 | containerOptions = merge(config.container, containerOptions); 126 | return factories.createMenuGroup.call(this, containerOptions); 127 | } 128 | 129 | const Menu = React.createFactory(createReactClass({ 130 | displayName: 'TheGraphMenu', 131 | radius: config.radius, 132 | getInitialState() { 133 | // Use these in CSS for cursor and hover, and to attach listeners 134 | return { 135 | n4tappable: (this.props.menu.n4 && this.props.menu.n4.action), 136 | s4tappable: (this.props.menu.s4 && this.props.menu.s4.action), 137 | e4tappable: (this.props.menu.e4 && this.props.menu.e4.action), 138 | w4tappable: (this.props.menu.w4 && this.props.menu.w4.action), 139 | }; 140 | }, 141 | onTapN4() { 142 | const { options } = this.props; 143 | this.props.menu.n4.action(options.graph, options.itemKey, options.item); 144 | this.props.triggerHideContext(); 145 | }, 146 | onTapS4() { 147 | const { options } = this.props; 148 | this.props.menu.s4.action(options.graph, options.itemKey, options.item); 149 | this.props.triggerHideContext(); 150 | }, 151 | onTapE4() { 152 | const { options } = this.props; 153 | this.props.menu.e4.action(options.graph, options.itemKey, options.item); 154 | this.props.triggerHideContext(); 155 | }, 156 | onTapW4() { 157 | const { options } = this.props; 158 | this.props.menu.w4.action(options.graph, options.itemKey, options.item); 159 | this.props.triggerHideContext(); 160 | }, 161 | componentDidMount() { 162 | if (this.state.n4tappable) { 163 | this.refs.n4.addEventListener('tap', this.onTapN4); 164 | } 165 | if (this.state.s4tappable) { 166 | this.refs.s4.addEventListener('tap', this.onTapS4); 167 | } 168 | if (this.state.e4tappable) { 169 | this.refs.e4.addEventListener('tap', this.onTapE4); 170 | } 171 | if (this.state.w4tappable) { 172 | this.refs.w4.addEventListener('tap', this.onTapW4); 173 | } 174 | 175 | // Prevent context menu 176 | ReactDOM.findDOMNode(this).addEventListener('contextmenu', (event) => { 177 | if (event) { 178 | event.stopPropagation(); 179 | event.preventDefault(); 180 | } 181 | }, false); 182 | }, 183 | getPosition() { 184 | return { 185 | x: this.props.x !== undefined ? this.props.x : this.props.options.x || 0, 186 | y: this.props.y !== undefined ? this.props.y : this.props.options.y || 0, 187 | }; 188 | }, 189 | render() { 190 | const { menu } = this.props; 191 | const { options } = this.props; 192 | const position = this.getPosition(); 193 | 194 | const circleXOptions = merge(config.circleXPath, {}); 195 | const outlineCircleOptions = merge(config.outlineCircle, { r: this.radius }); 196 | 197 | const children = [ 198 | // Directional slices 199 | factories.createMenuSlice.call(this, { direction: 'n4' }), 200 | factories.createMenuSlice.call(this, { direction: 's4' }), 201 | factories.createMenuSlice.call(this, { direction: 'e4' }), 202 | factories.createMenuSlice.call(this, { direction: 'w4' }), 203 | // Outline and X 204 | factories.createMenuCircleXPath.call(this, circleXOptions), 205 | factories.createMenuOutlineCircle.call(this, outlineCircleOptions), 206 | ]; 207 | // Menu label 208 | if (this.props.label || menu.icon) { 209 | let labelTextOptions = { 210 | x: 0, 211 | y: 0 - this.radius - 15, 212 | children: (this.props.label ? this.props.label : menu.label), 213 | }; 214 | 215 | labelTextOptions = merge(config.labelText, labelTextOptions); 216 | children.push(factories.createMenuLabelText.call(this, labelTextOptions)); 217 | } 218 | // Middle icon 219 | if (this.props.icon || menu.icon) { 220 | const iconColor = (this.props.iconColor !== undefined ? this.props.iconColor : menu.iconColor); 221 | let iconStyle = ''; 222 | if (iconColor) { 223 | iconStyle = ` fill route${iconColor}`; 224 | } 225 | 226 | const middleIconRectOptions = merge(config.iconRect, {}); 227 | const middleIcon = factories.createMenuMiddleIconRect.call(this, middleIconRectOptions); 228 | 229 | let middleIconTextOptions = { 230 | className: `icon context-node-icon${iconStyle}`, 231 | children: FONT_AWESOME[(this.props.icon ? this.props.icon : menu.icon)], 232 | }; 233 | middleIconTextOptions = merge(config.iconText, middleIconTextOptions); 234 | const iconText = factories.createMenuMiddleIconText.call(this, middleIconTextOptions); 235 | 236 | children.push(middleIcon, iconText); 237 | } 238 | 239 | let containerOptions = { 240 | transform: `translate(${position.x},${position.y})`, 241 | children, 242 | }; 243 | 244 | containerOptions = merge(config.container, containerOptions); 245 | return factories.createMenuGroup.call(this, containerOptions); 246 | }, 247 | })); 248 | 249 | module.exports = { 250 | Menu, 251 | config, 252 | factories, 253 | }; 254 | -------------------------------------------------------------------------------- /the-graph/the-graph-modalbg.js: -------------------------------------------------------------------------------- 1 | const React = require('react'); 2 | const ReactDOM = require('react-dom'); 3 | const createReactClass = require('create-react-class'); 4 | 5 | const baseFactories = require('./factories'); 6 | const merge = require('./merge'); 7 | 8 | const config = { 9 | container: {}, 10 | rect: { 11 | ref: 'rect', 12 | className: 'context-modal-bg', 13 | }, 14 | }; 15 | 16 | const factories = { 17 | createModalBackgroundGroup: baseFactories.createGroup, 18 | createModalBackgroundRect: baseFactories.createRect, 19 | }; 20 | 21 | const ModalBG = React.createFactory(createReactClass({ 22 | displayName: 'TheGraphModalBG', 23 | componentDidMount() { 24 | const domNode = ReactDOM.findDOMNode(this); 25 | const rectNode = this.refs.rect; 26 | 27 | // Right-click on another item will show its menu 28 | domNode.addEventListener('mousedown', (event) => { 29 | // Only if outside of menu 30 | if (event && event.target === rectNode) { 31 | this.hideModal(); 32 | } 33 | }); 34 | }, 35 | hideModal(event) { 36 | this.props.triggerHideContext(); 37 | }, 38 | render() { 39 | let rectOptions = { 40 | width: this.props.width, 41 | height: this.props.height, 42 | }; 43 | 44 | rectOptions = merge(config.rect, rectOptions); 45 | const rect = factories.createModalBackgroundRect.call(this, rectOptions); 46 | 47 | const containerContents = [rect, this.props.children]; 48 | const containerOptions = merge(config.container, {}); 49 | return factories.createModalBackgroundGroup.call(this, containerOptions, containerContents); 50 | }, 51 | })); 52 | 53 | module.exports = { 54 | ModalBG, 55 | config, 56 | factories, 57 | }; 58 | -------------------------------------------------------------------------------- /the-graph/the-graph-node-menu-port.js: -------------------------------------------------------------------------------- 1 | const React = require('react'); 2 | const ReactDOM = require('react-dom'); 3 | const createReactClass = require('create-react-class'); 4 | 5 | module.exports.register = function (context) { 6 | const { TheGraph } = context; 7 | 8 | TheGraph.config.nodeMenuPort = { 9 | container: {}, 10 | backgroundRect: { 11 | rx: TheGraph.config.nodeRadius, 12 | ry: TheGraph.config.nodeRadius, 13 | height: TheGraph.contextPortSize - 1, 14 | }, 15 | circle: { 16 | r: 10, 17 | }, 18 | text: {}, 19 | }; 20 | 21 | TheGraph.factories.nodeMenuPort = { 22 | createNodeMenuPortGroup: TheGraph.factories.createGroup, 23 | createNodeMenuBackgroundRect: TheGraph.factories.createRect, 24 | createNodeMenuPortCircle: TheGraph.factories.createCircle, 25 | createNodeMenuPortText: TheGraph.factories.createText, 26 | }; 27 | 28 | TheGraph.NodeMenuPort = React.createFactory(createReactClass({ 29 | displayName: 'TheGraphNodeMenuPort', 30 | componentDidMount() { 31 | ReactDOM.findDOMNode(this).addEventListener('tap', this.edgeStart); 32 | }, 33 | edgeStart(event) { 34 | // Don't tap graph 35 | event.stopPropagation(); 36 | 37 | const port = { 38 | process: this.props.processKey, 39 | port: this.props.label, 40 | type: this.props.port.type, 41 | }; 42 | 43 | const edgeStartEvent = new CustomEvent('the-graph-edge-start', { 44 | detail: { 45 | isIn: this.props.isIn, 46 | port, 47 | route: this.props.route, 48 | }, 49 | bubbles: true, 50 | }); 51 | ReactDOM.findDOMNode(this).dispatchEvent(edgeStartEvent); 52 | }, 53 | render() { 54 | const labelLen = this.props.label.length; 55 | const bgWidth = (labelLen > 12 ? labelLen * 8 + 40 : 120); 56 | // Highlight compatible port 57 | const { highlightPort } = this.props; 58 | const highlight = (highlightPort && highlightPort.isIn === this.props.isIn && highlightPort.type === this.props.port.type); 59 | 60 | let rectOptions = { 61 | className: `context-port-bg${highlight ? ' highlight' : ''}`, 62 | x: this.props.x + (this.props.isIn ? -bgWidth : 0), 63 | y: this.props.y - TheGraph.contextPortSize / 2, 64 | width: bgWidth, 65 | }; 66 | 67 | rectOptions = TheGraph.merge(TheGraph.config.nodeMenuPort.backgroundRect, rectOptions); 68 | const rect = TheGraph.factories.nodeMenuPort.createNodeMenuBackgroundRect.call(this, rectOptions); 69 | 70 | let circleOptions = { 71 | className: `context-port-hole stroke route${this.props.route}`, 72 | cx: this.props.x, 73 | cy: this.props.y, 74 | }; 75 | circleOptions = TheGraph.merge(TheGraph.config.nodeMenuPort.circle, circleOptions); 76 | const circle = TheGraph.factories.nodeMenuPort.createNodeMenuPortCircle.call(this, circleOptions); 77 | 78 | let textOptions = { 79 | className: `context-port-label fill route${this.props.route}`, 80 | x: this.props.x + (this.props.isIn ? -20 : 20), 81 | y: this.props.y, 82 | children: this.props.label.replace(/(.*)\/(.*)(_.*)\.(.*)/, '$2.$4'), 83 | }; 84 | 85 | textOptions = TheGraph.merge(TheGraph.config.nodeMenuPort.text, textOptions); 86 | const text = TheGraph.factories.nodeMenuPort.createNodeMenuPortText.call(this, textOptions); 87 | 88 | const containerContents = [rect, circle, text]; 89 | 90 | const containerOptions = TheGraph.merge(TheGraph.config.nodeMenuPort.container, { className: `context-port click context-port-${this.props.isIn ? 'in' : 'out'}` }); 91 | return TheGraph.factories.nodeMenuPort.createNodeMenuPortGroup.call(this, containerOptions, containerContents); 92 | }, 93 | })); 94 | }; 95 | -------------------------------------------------------------------------------- /the-graph/the-graph-node-menu-ports.js: -------------------------------------------------------------------------------- 1 | const React = require('react'); 2 | const createReactClass = require('create-react-class'); 3 | 4 | module.exports.register = function (context) { 5 | const { TheGraph } = context; 6 | 7 | TheGraph.config.nodeMenuPorts = { 8 | container: {}, 9 | linesGroup: { 10 | className: 'context-ports-lines', 11 | }, 12 | portsGroup: { 13 | className: 'context-ports-ports', 14 | }, 15 | portPath: { 16 | className: 'context-port-path', 17 | }, 18 | nodeMenuPort: {}, 19 | }; 20 | 21 | TheGraph.factories.menuPorts = { 22 | createNodeMenuPortsGroup: TheGraph.factories.createGroup, 23 | createNodeMenuPortsLinesGroup: TheGraph.factories.createGroup, 24 | createNodeMenuPortsPortsGroup: TheGraph.factories.createGroup, 25 | createNodeMenuPortsPortPath: TheGraph.factories.createPath, 26 | createNodeMenuPortsNodeMenuPort: createNodeMenuPort, 27 | }; 28 | 29 | function createNodeMenuPort(options) { 30 | return TheGraph.NodeMenuPort(options); 31 | } 32 | 33 | TheGraph.NodeMenuPorts = React.createFactory(createReactClass({ 34 | displayName: 'TheGraphNodeMenuPorts', 35 | render() { 36 | const portViews = []; 37 | const lines = []; 38 | 39 | const { scale } = this.props; 40 | const { ports } = this.props; 41 | const { deltaX } = this.props; 42 | const { deltaY } = this.props; 43 | 44 | const keys = Object.keys(this.props.ports); 45 | const h = keys.length * TheGraph.contextPortSize; 46 | const len = keys.length; 47 | for (let i = 0; i < len; i++) { 48 | const key = keys[i]; 49 | const port = ports[key]; 50 | 51 | const x = (this.props.isIn ? -100 : 100); 52 | const y = 0 - h / 2 + i * TheGraph.contextPortSize + TheGraph.contextPortSize / 2; 53 | const ox = (port.x - this.props.nodeWidth / 2) * scale + deltaX; 54 | const oy = (port.y - this.props.nodeHeight / 2) * scale + deltaY; 55 | 56 | // Make path from graph port to menu port 57 | const lineOptions = TheGraph.merge(TheGraph.config.nodeMenuPorts.portPath, { d: ['M', ox, oy, 'L', x, y].join(' ') }); 58 | const line = TheGraph.factories.menuPorts.createNodeMenuPortsPortPath.call(this, lineOptions); 59 | 60 | let portViewOptions = { 61 | label: key, 62 | port, 63 | processKey: this.props.processKey, 64 | isIn: this.props.isIn, 65 | x, 66 | y, 67 | route: port.route, 68 | highlightPort: this.props.highlightPort, 69 | }; 70 | portViewOptions = TheGraph.merge(TheGraph.config.nodeMenuPorts.nodeMenuPort, portViewOptions); 71 | const portView = TheGraph.factories.menuPorts.createNodeMenuPortsNodeMenuPort.call(this, portViewOptions); 72 | 73 | lines.push(line); 74 | portViews.push(portView); 75 | } 76 | 77 | let transform = ''; 78 | if (this.props.translateX !== undefined) { 79 | transform = `translate(${this.props.translateX},${this.props.translateY})`; 80 | } 81 | 82 | const linesGroupOptions = TheGraph.merge(TheGraph.config.nodeMenuPorts.linesGroup, { children: lines }); 83 | const linesGroup = TheGraph.factories.menuPorts.createNodeMenuPortsLinesGroup.call(this, linesGroupOptions); 84 | 85 | const portsGroupOptions = TheGraph.merge(TheGraph.config.nodeMenuPorts.portsGroup, { children: portViews }); 86 | const portsGroup = TheGraph.factories.menuPorts.createNodeMenuPortsGroup.call(this, portsGroupOptions); 87 | 88 | const containerContents = [linesGroup, portsGroup]; 89 | let containerOptions = { 90 | className: `context-ports context-ports-${this.props.isIn ? 'in' : 'out'}`, 91 | transform, 92 | }; 93 | containerOptions = TheGraph.merge(TheGraph.config.nodeMenuPorts.container, containerOptions); 94 | return TheGraph.factories.menuPorts.createNodeMenuPortsGroup.call(this, containerOptions, containerContents); 95 | }, 96 | })); 97 | }; 98 | -------------------------------------------------------------------------------- /the-graph/the-graph-node-menu.js: -------------------------------------------------------------------------------- 1 | const React = require('react'); 2 | const ReactDOM = require('react-dom'); 3 | const createReactClass = require('create-react-class'); 4 | 5 | module.exports.register = function (context) { 6 | const { TheGraph } = context; 7 | 8 | TheGraph.config.nodeMenu = { 9 | container: { 10 | className: 'context-node', 11 | }, 12 | inports: {}, 13 | outports: {}, 14 | menu: { 15 | x: 0, 16 | y: 0, 17 | }, 18 | }; 19 | 20 | TheGraph.factories.nodeMenu = { 21 | createNodeMenuGroup: TheGraph.factories.createGroup, 22 | createNodeMenuInports: createNodeMenuPorts, 23 | createNodeMenuOutports: createNodeMenuPorts, 24 | createNodeMenuMenu, 25 | }; 26 | 27 | function createNodeMenuPorts(options) { 28 | return TheGraph.NodeMenuPorts(options); 29 | } 30 | 31 | function createNodeMenuMenu(options) { 32 | return TheGraph.Menu(options); 33 | } 34 | 35 | TheGraph.NodeMenu = React.createFactory(createReactClass({ 36 | displayName: 'TheGraphNodeMenu', 37 | radius: 72, 38 | stopPropagation(event) { 39 | // Don't drag graph 40 | event.stopPropagation(); 41 | }, 42 | componentDidMount() { 43 | // Prevent context menu 44 | ReactDOM.findDOMNode(this).addEventListener('contextmenu', (event) => { 45 | event.stopPropagation(); 46 | event.preventDefault(); 47 | }, false); 48 | }, 49 | render() { 50 | const { scale } = this.props.node.props.app.state; 51 | const { ports } = this.props; 52 | const { deltaX } = this.props; 53 | const { deltaY } = this.props; 54 | 55 | let inportsOptions = { 56 | ports: ports.inports, 57 | isIn: true, 58 | scale, 59 | processKey: this.props.processKey, 60 | deltaX, 61 | deltaY, 62 | nodeWidth: this.props.nodeWidth, 63 | nodeHeight: this.props.nodeHeight, 64 | highlightPort: this.props.highlightPort, 65 | }; 66 | 67 | inportsOptions = TheGraph.merge(TheGraph.config.nodeMenu.inports, inportsOptions); 68 | const inports = TheGraph.factories.nodeMenu.createNodeMenuInports.call(this, inportsOptions); 69 | 70 | let outportsOptions = { 71 | ports: ports.outports, 72 | isIn: false, 73 | scale, 74 | processKey: this.props.processKey, 75 | deltaX, 76 | deltaY, 77 | nodeWidth: this.props.nodeWidth, 78 | nodeHeight: this.props.nodeHeight, 79 | highlightPort: this.props.highlightPort, 80 | }; 81 | 82 | outportsOptions = TheGraph.merge(TheGraph.config.nodeMenu.outports, outportsOptions); 83 | const outports = TheGraph.factories.nodeMenu.createNodeMenuOutports.call(this, outportsOptions); 84 | 85 | let menuOptions = { 86 | menu: this.props.menu, 87 | options: this.props.options, 88 | triggerHideContext: this.props.triggerHideContext, 89 | icon: this.props.icon, 90 | label: this.props.label, 91 | }; 92 | 93 | menuOptions = TheGraph.merge(TheGraph.config.nodeMenu.menu, menuOptions); 94 | const menu = TheGraph.factories.nodeMenu.createNodeMenuMenu.call(this, menuOptions); 95 | 96 | const children = [ 97 | inports, outports, menu, 98 | ]; 99 | 100 | let containerOptions = { 101 | transform: `translate(${this.props.x},${this.props.y})`, 102 | children, 103 | }; 104 | containerOptions = TheGraph.merge(TheGraph.config.nodeMenu.container, containerOptions); 105 | return TheGraph.factories.nodeMenu.createNodeMenuGroup.call(this, containerOptions); 106 | }, 107 | })); 108 | }; 109 | -------------------------------------------------------------------------------- /the-graph/the-graph-port.js: -------------------------------------------------------------------------------- 1 | const React = require('react'); 2 | const ReactDOM = require('react-dom'); 3 | const createReactClass = require('create-react-class'); 4 | const TooltipMixin = require('./mixins').Tooltip; 5 | const arcs = require('./arcs.js'); 6 | 7 | module.exports.register = function (context) { 8 | const { TheGraph } = context; 9 | 10 | // Initialize configuration for the Port view. 11 | TheGraph.config.port = { 12 | container: { 13 | className: 'port arrow', 14 | }, 15 | backgroundCircle: { 16 | className: 'port-circle-bg', 17 | }, 18 | arc: { 19 | className: 'port-arc', 20 | }, 21 | innerCircle: { 22 | ref: 'circleSmall', 23 | }, 24 | text: { 25 | ref: 'label', 26 | className: 'port-label drag', 27 | }, 28 | }; 29 | 30 | TheGraph.factories.port = { 31 | createPortGroup: TheGraph.factories.createGroup, 32 | createPortBackgroundCircle: TheGraph.factories.createCircle, 33 | createPortArc: TheGraph.factories.createPath, 34 | createPortInnerCircle: TheGraph.factories.createCircle, 35 | createPortLabelText: TheGraph.factories.createText, 36 | }; 37 | 38 | // Port view 39 | 40 | TheGraph.Port = React.createFactory(createReactClass({ 41 | displayName: 'TheGraphPort', 42 | mixins: [ 43 | TooltipMixin, 44 | ], 45 | componentDidMount() { 46 | const domNode = ReactDOM.findDOMNode(this); 47 | 48 | // Preview edge start 49 | domNode.addEventListener('tap', this.edgeStart); 50 | domNode.addEventListener('panstart', this.edgeStart); 51 | // Make edge 52 | domNode.addEventListener('panend', this.triggerDropOnTarget); 53 | domNode.addEventListener('the-graph-edge-drop', this.edgeStart); 54 | 55 | // Show context menu 56 | if (this.props.showContext) { 57 | domNode.addEventListener('contextmenu', this.showContext); 58 | domNode.addEventListener('press', this.showContext); 59 | } 60 | }, 61 | getTooltipTrigger() { 62 | return ReactDOM.findDOMNode(this); 63 | }, 64 | shouldShowTooltip() { 65 | return ( 66 | this.props.app.state.scale < TheGraph.zbpBig 67 | || this.props.label.length > 8 68 | ); 69 | }, 70 | showContext(event) { 71 | // Don't show port menu on export node port 72 | if (this.props.isExport) { 73 | return; 74 | } 75 | // Click on label, pass context menu to node 76 | if (event && (event.target === ReactDOM.findDOMNode(this.refs.label))) { 77 | return; 78 | } 79 | // Don't show native context menu 80 | event.preventDefault(); 81 | 82 | // Don't tap graph on hold event 83 | if (event.stopPropagation) { event.stopPropagation(); } 84 | if (event.preventTap) { event.preventTap(); } 85 | 86 | // Get mouse position 87 | if (event.gesture) { 88 | event = event.gesture.srcEvent; // unpack hammer.js gesture event 89 | } 90 | let x = event.x || event.clientX || 0; 91 | let y = event.y || event.clientY || 0; 92 | if (event.touches && event.touches.length) { 93 | x = event.touches[0].clientX; 94 | y = event.touches[0].clientY; 95 | } 96 | 97 | // App.showContext 98 | this.props.showContext({ 99 | element: this, 100 | type: (this.props.isIn ? 'nodeInport' : 'nodeOutport'), 101 | x, 102 | y, 103 | graph: this.props.graph, 104 | itemKey: this.props.label, 105 | item: this.props.port, 106 | }); 107 | }, 108 | getContext(menu, options, hide) { 109 | return TheGraph.Menu({ 110 | menu, 111 | options, 112 | label: this.props.label, 113 | triggerHideContext: hide, 114 | }); 115 | }, 116 | edgeStart(event) { 117 | // Don't start edge on export node port 118 | if (this.props.isExport) { 119 | return; 120 | } 121 | if (!this.props.allowEdgeStart) { 122 | return; 123 | } 124 | 125 | // Click on label, allow node context menu 126 | if (event && (event.target === ReactDOM.findDOMNode(this.refs.label))) { 127 | return; 128 | } 129 | // Don't tap graph 130 | if (event.stopPropagation) { event.stopPropagation(); } 131 | 132 | const edgeStartEvent = new CustomEvent('the-graph-edge-start', { 133 | detail: { 134 | isIn: this.props.isIn, 135 | port: this.props.port, 136 | // process: this.props.processKey, 137 | route: this.props.route, 138 | }, 139 | bubbles: true, 140 | }); 141 | ReactDOM.findDOMNode(this).dispatchEvent(edgeStartEvent); 142 | }, 143 | triggerDropOnTarget(event) { 144 | // If dropped on a child element will bubble up to port 145 | // FIXME: broken, is never set, neither on event.srcEvent 146 | if (!event.relatedTarget) { return; } 147 | const dropEvent = new CustomEvent('the-graph-edge-drop', { 148 | detail: null, 149 | bubbles: true, 150 | }); 151 | event.relatedTarget.dispatchEvent(dropEvent); 152 | }, 153 | render() { 154 | let style; 155 | if (this.props.label.length > 7) { 156 | const fontSize = 6 * (30 / (4 * this.props.label.length)); 157 | style = { fontSize: `${fontSize}px` }; 158 | } 159 | let r = 4; 160 | // Highlight matching ports 161 | const { highlightPort } = this.props; 162 | let inArc = arcs.inport; 163 | let outArc = arcs.outport; 164 | if (highlightPort && highlightPort.isIn === this.props.isIn && (highlightPort.type === this.props.port.type || this.props.port.type === 'any')) { 165 | r = 6; 166 | inArc = arcs.inportBig; 167 | outArc = arcs.outportBig; 168 | } 169 | 170 | const backgroundCircleOptions = TheGraph.merge(TheGraph.config.port.backgroundCircle, { r: r + 1 }); 171 | const backgroundCircle = TheGraph.factories.port.createPortBackgroundCircle.call(this, backgroundCircleOptions); 172 | 173 | const arcOptions = TheGraph.merge(TheGraph.config.port.arc, { d: (this.props.isIn ? inArc : outArc) }); 174 | const arc = TheGraph.factories.port.createPortArc.call(this, arcOptions); 175 | 176 | let innerCircleOptions = { 177 | className: `port-circle-small fill route${this.props.route}`, 178 | r: r - 1.5, 179 | }; 180 | 181 | innerCircleOptions = TheGraph.merge(TheGraph.config.port.innerCircle, innerCircleOptions); 182 | const innerCircle = TheGraph.factories.port.createPortInnerCircle.call(this, innerCircleOptions); 183 | 184 | let labelTextOptions = { 185 | x: (this.props.isIn ? 5 : -5), 186 | style, 187 | children: this.props.label, 188 | }; 189 | labelTextOptions = TheGraph.merge(TheGraph.config.port.text, labelTextOptions); 190 | const labelText = TheGraph.factories.port.createPortLabelText.call(this, labelTextOptions); 191 | 192 | const portContents = [ 193 | backgroundCircle, 194 | arc, 195 | innerCircle, 196 | labelText, 197 | ]; 198 | 199 | const containerOptions = TheGraph.merge(TheGraph.config.port.container, { title: this.props.label, transform: `translate(${this.props.x},${this.props.y})` }); 200 | return TheGraph.factories.port.createPortGroup.call(this, containerOptions, portContents); 201 | }, 202 | })); 203 | 204 | TheGraph.Port.defaultProps = { 205 | allowEdgeStart: true, 206 | }; 207 | }; 208 | -------------------------------------------------------------------------------- /the-graph/the-graph-tooltip.js: -------------------------------------------------------------------------------- 1 | const React = require('react'); 2 | const createReactClass = require('create-react-class'); 3 | const defaultFactories = require('./factories.js'); 4 | const merge = require('./merge.js'); 5 | 6 | const config = { 7 | container: {}, 8 | rect: { 9 | className: 'tooltip-bg', 10 | x: 0, 11 | y: -7, 12 | rx: 3, 13 | ry: 3, 14 | height: 16, 15 | }, 16 | text: { 17 | className: 'tooltip-label', 18 | ref: 'label', 19 | }, 20 | }; 21 | 22 | const factories = { 23 | createTooltipGroup: defaultFactories.createGroup, 24 | createTooltipRect: defaultFactories.createRect, 25 | createTooltipText: defaultFactories.createText, 26 | }; 27 | 28 | // Port view 29 | const Tooltip = React.createFactory(createReactClass({ 30 | displayName: 'TheGraphTooltip', 31 | render() { 32 | const rectOptions = merge(config.rect, { width: this.props.label.length * 6 }); 33 | const rect = factories.createTooltipRect.call(this, rectOptions); 34 | 35 | const textOptions = merge(config.text, { children: this.props.label }); 36 | const text = factories.createTooltipText.call(this, textOptions); 37 | 38 | const containerContents = [rect, text]; 39 | 40 | let containerOptions = { 41 | className: `tooltip${this.props.visible ? '' : ' hidden'}`, 42 | transform: `translate(${this.props.x},${this.props.y})`, 43 | }; 44 | containerOptions = merge(config.container, containerOptions); 45 | return factories.createTooltipGroup.call(this, containerOptions, containerContents); 46 | }, 47 | })); 48 | 49 | module.exports = { 50 | config, 51 | factories, 52 | Tooltip, 53 | }; 54 | -------------------------------------------------------------------------------- /themes/dark/the-graph-spectrum.styl: -------------------------------------------------------------------------------- 1 | /* white, red, orange, yellow, green, mint, cyan, blue, purple, pink, fuchsia */ 2 | 3 | var-route00 = white 4 | var-route01 = hsl( 0, 98%, 46%) 5 | var-route02 = hsl( 35, 98%, 46%) 6 | var-route03 = hsl( 60, 98%, 46%) 7 | var-route04 = hsl(135, 98%, 46%) 8 | var-route05 = hsl(160, 98%, 46%) 9 | var-route06 = hsl(185, 98%, 46%) 10 | var-route07 = hsl(210, 98%, 46%) 11 | var-route08 = hsl(285, 98%, 46%) 12 | var-route09 = hsl(310, 98%, 46%) 13 | var-route10 = hsl(335, 98%, 46%) 14 | 15 | var-route00-fade = hsl( 0, 0%, 46%) 16 | var-route01-fade = hsl( 0, 98%, 16%) 17 | var-route02-fade = hsl( 35, 98%, 16%) 18 | var-route03-fade = hsl( 60, 98%, 16%) 19 | var-route04-fade = hsl(135, 98%, 16%) 20 | var-route05-fade = hsl(160, 98%, 16%) 21 | var-route06-fade = hsl(185, 98%, 16%) 22 | var-route07-fade = hsl(210, 98%, 16%) 23 | var-route08-fade = hsl(285, 98%, 16%) 24 | var-route09-fade = hsl(310, 98%, 16%) 25 | var-route10-fade = hsl(335, 98%, 16%) 26 | 27 | var-group00 = hsla( 0, 0%, 75%, 0.15) 28 | var-group01 = hsla( 0, 100%, 75%, 0.15) 29 | var-group02 = hsla( 35, 100%, 75%, 0.15) 30 | var-group03 = hsla( 60, 100%, 75%, 0.15) 31 | var-group04 = hsla(135, 100%, 75%, 0.15) 32 | var-group05 = hsla(160, 100%, 75%, 0.15) 33 | var-group06 = hsla(185, 100%, 75%, 0.15) 34 | var-group07 = hsla(210, 100%, 75%, 0.15) 35 | var-group08 = hsla(285, 100%, 75%, 0.15) 36 | var-group09 = hsla(310, 100%, 75%, 0.15) 37 | var-group10 = hsla(335, 100%, 75%, 0.15) 38 | 39 | var-group00-stroke = none 40 | var-group01-stroke = none 41 | var-group02-stroke = none 42 | var-group03-stroke = none 43 | var-group04-stroke = none 44 | var-group05-stroke = none 45 | var-group06-stroke = none 46 | var-group07-stroke = none 47 | var-group08-stroke = none 48 | var-group09-stroke = none 49 | var-group10-stroke = none 50 | 51 | var-group00-hover = hsla( 0, 0%, 75%, 0.25) 52 | var-group01-hover = hsla( 0, 100%, 75%, 0.25) 53 | var-group02-hover = hsla( 35, 100%, 75%, 0.25) 54 | var-group03-hover = hsla( 60, 100%, 75%, 0.25) 55 | var-group04-hover = hsla(135, 100%, 75%, 0.25) 56 | var-group05-hover = hsla(160, 100%, 75%, 0.25) 57 | var-group06-hover = hsla(185, 100%, 75%, 0.25) 58 | var-group07-hover = hsla(210, 100%, 75%, 0.25) 59 | var-group08-hover = hsla(285, 100%, 75%, 0.25) 60 | var-group09-hover = hsla(310, 100%, 75%, 0.25) 61 | var-group10-hover = hsla(335, 100%, 75%, 0.25) 62 | 63 | var-group00-fade = hsla( 0, 0%, 75%, 0.1) 64 | var-group01-fade = hsla( 0, 100%, 75%, 0.1) 65 | var-group02-fade = hsla( 35, 100%, 75%, 0.1) 66 | var-group03-fade = hsla( 60, 100%, 75%, 0.1) 67 | var-group04-fade = hsla(135, 100%, 75%, 0.1) 68 | var-group05-fade = hsla(160, 100%, 75%, 0.1) 69 | var-group06-fade = hsla(185, 100%, 75%, 0.1) 70 | var-group07-fade = hsla(210, 100%, 75%, 0.1) 71 | var-group08-fade = hsla(285, 100%, 75%, 0.1) 72 | var-group09-fade = hsla(310, 100%, 75%, 0.1) 73 | var-group10-fade = hsla(335, 100%, 75%, 0.1) 74 | -------------------------------------------------------------------------------- /themes/dark/the-graph.styl: -------------------------------------------------------------------------------- 1 | var-bg = black 2 | 3 | var-node = hsla(192, 25%, 92%, 0.94) 4 | var-node-error = hsla(0, 100%, 50%, 0.94) 5 | var-node-hover = hsla(192, 25%, 92%, 0.97) 6 | var-node-border = hsl(0, 0%, 40%) 7 | var-node-border-bg = hsla(0, 0%, 0%, 0.5) 8 | var-node-hover-border = hsl(0, 0%, 50%) 9 | var-node-icon = hsl(194, 8%, 40%) 10 | var-node-icon-big = hsl(194, 8%, 80%) 11 | var-node-icon-small = hsl(194, 8%, 20%) 12 | 13 | var-node-selected = hsla(192, 25%, 92%, 0.94) 14 | var-node-selected-border = hsl(0, 0%, 50%) 15 | var-node-selected-icon = hsl(194, 8%, 40%) 16 | var-node-deselected = hsla(192, 5%, 25%, 0.94) 17 | var-node-deselected-border = hsl(194, 8%, 15%) 18 | var-node-deselected-icon = hsl(194, 8%, 15%) 19 | 20 | var-export-bg = hsl(0, 0%, 10%) 21 | 22 | var-type-a = white 23 | var-type-b = hsl(188, 6%, 5%) 24 | var-text-bg = hsla(0, 0%, 0%, 0.5) 25 | var-tooltip-bg = hsla(0, 0%, 0%, 0.9) 26 | 27 | var-selection-box = hsla(0, 0%, 100%, 0.15) 28 | var-selection-box-hover = hsla(0, 0%, 100%, 0.25) 29 | var-selection-box-border = hsl(0, 0%, 90%) 30 | 31 | var-menu-modal-bg = rgba(0, 0, 0, 0.5) 32 | var-menu-bg = hsla(211, 12%, 19%, 0.95) 33 | var-menu-bg-hover = hsla(211, 12%, 40%, 0.95) 34 | var-menu-border = hsl(0, 0%, 75%) 35 | var-menu-icon-bg = hsl(211, 12%, 8%) 36 | 37 | var-edge-bg = black 38 | var-edge-hover-bg = hsl(0, 0%, 50%) 39 | var-edge-width = 3px 40 | 41 | @require "./the-graph-spectrum" 42 | -------------------------------------------------------------------------------- /themes/default/the-graph.styl: -------------------------------------------------------------------------------- 1 | 2 | .the-graph-app { 3 | background-color: var-bg; 4 | position: relative; 5 | } 6 | 7 | svg { 8 | -webkit-touch-callout: none; 9 | -webkit-user-select: none; 10 | -khtml-user-select: none; 11 | -moz-user-select: none; 12 | -ms-user-select: none; 13 | user-select: none; 14 | } 15 | .view { 16 | } 17 | .arrow { 18 | cursor: default; 19 | } 20 | .click { 21 | cursor: pointer; 22 | } 23 | .drag { 24 | cursor: pointer; 25 | cursor: -moz-grab; 26 | cursor: -webkit-grab; 27 | cursor: grab; 28 | } 29 | 30 | /* defaults */ 31 | 32 | .node-border { 33 | fill: var-node-border-bg; 34 | stroke: var-node-border; 35 | stroke-width: 2px; 36 | }; 37 | .node-rect { 38 | fill: var-node; 39 | stroke: none; 40 | } 41 | 42 | @-webkit-keyframes error { 43 | 0% { fill: var-node; } 44 | 100% { fill: var-node-error; } 45 | } 46 | @-moz-keyframes error { 47 | 0% { fill: var-node; } 48 | 100% { fill: var-node-error; } 49 | } 50 | @-o-keyframes error { 51 | 0% { fill: var-node; } 52 | 100% { fill: var-node-error; } 53 | } 54 | @keyframes error { 55 | 0% { fill: var-node; } 56 | 100% { fill: var-node-error; } 57 | } 58 | 59 | .node.error .node-rect { 60 | -webkit-animation: error 1s linear infinite alternate; 61 | -moz-animation: error 1s linear infinite alternate; 62 | -o-animation: error 1s linear infinite alternate; 63 | animation: error 1s linear infinite alternate; 64 | stroke: none; 65 | } 66 | 67 | .eventcatcher { 68 | fill: var-type-a; 69 | fill-opacity: 0.1; 70 | } 71 | 72 | .node:hover 73 | .node-rect 74 | fill: var-node-hover 75 | .node-border 76 | stroke: var-node-hover-border 77 | .port-arc 78 | fill: var-node-hover-border 79 | 80 | .ex-inports .node-border { 81 | fill: var-export-bg; 82 | stroke: none; 83 | } 84 | .ex-outports .node-border { 85 | fill: var-export-bg; 86 | stroke: none; 87 | } 88 | .ex-inports .node .node-rect, 89 | .ex-outports .node .node-rect, 90 | .ex-inports .node:hover .node-rect, 91 | .ex-outports .node:hover .node-rect { 92 | fill: none; 93 | } 94 | 95 | .selection 96 | .ex-inports .node .node-border 97 | .ex-outports .node .node-border 98 | fill: var-export-bg 99 | 100 | .small 101 | .node-border 102 | fill: var-node 103 | stroke: none 104 | .ex-inports .node .node-border 105 | .ex-outports .node .node-border 106 | fill: var-export-bg 107 | 108 | 109 | .node-bg { 110 | opacity: 0; 111 | } 112 | 113 | /* de-emphasizing the rest of the nodes */ 114 | /* emphasizing the selection */ 115 | 116 | .selection 117 | .node 118 | .node-rect 119 | fill: var-node-deselected 120 | .node-border 121 | stroke: var-node-deselected-border 122 | .port-arc 123 | fill: var-node-deselected-border 124 | .node-icon 125 | fill: var-node-deselected-icon 126 | .node.selected 127 | .node-rect 128 | fill: var-node-selected 129 | .node-border 130 | stroke: var-node-selected-border 131 | .port-arc 132 | fill: var-node-selected-border 133 | .node-icon 134 | fill: var-node-selected-icon 135 | .ex-inports .node .node-icon 136 | fill: var-route02-fade 137 | .ex-outports .node .node-icon 138 | fill: var-route04-fade 139 | 140 | 141 | .small .selection 142 | .node 143 | .node-rect 144 | fill: none 145 | .node-border 146 | fill: var-node-deselected 147 | stroke: none 148 | .node.selected 149 | .node-rect 150 | fill: none 151 | .node-border 152 | fill: var-node-selected 153 | stroke: none 154 | 155 | 156 | 157 | 158 | path { 159 | fill: none; 160 | } 161 | 162 | .arrow-bg { 163 | stroke: var-edge-bg; 164 | stroke-width: ((var-edge-width+1)/2); 165 | } 166 | .edge-bg { 167 | stroke: var-edge-bg; 168 | stroke-width: (var-edge-width + 2); 169 | } 170 | .edge-fg { 171 | stroke: var-route00; 172 | stroke-width: var-edge-width; 173 | 174 | transition-property: stroke-width; 175 | transition-duration: 0.5s; 176 | } 177 | .edge-touch { 178 | stroke-width: (var-edge-width + 4); 179 | opacity: 0; 180 | } 181 | .edge:hover .edge-bg, .edge:hover .arrow-bg { 182 | stroke: var-edge-hover-bg; 183 | } 184 | .edge.selected .arrow-bg { 185 | stroke: var-selection-box-border; 186 | stroke-width: var-edge-width + 1; 187 | } 188 | .edge.selected .edge-bg { 189 | stroke-width: (var-edge-width * 2 + 2); 190 | stroke: var-selection-box-border; 191 | } 192 | .small .edge-bg { 193 | stroke-width: (var-edge-width * 2 + 2); 194 | } 195 | .small .edge-fg { 196 | stroke-width: (var-edge-width * 2); 197 | } 198 | 199 | 200 | /* marching ants for animated arrows */ 201 | 202 | @-webkit-keyframes arrow-heartbeats { 203 | 0% { stroke-width: (var-edge-width + 1); } 204 | 10% { stroke-width: ((var-edge-width + 1) * 2); } 205 | 30% { stroke-width: (var-edge-width + 1); } 206 | 40% { stroke-width: ((var-edge-width + 1) * 2); } 207 | 60% { stroke-width: (var-edge-width + 1); } 208 | } 209 | @-moz-keyframes arrow-heartbeats { 210 | 0% { stroke-width: (var-edge-width + 1); } 211 | 10% { stroke-width: ((var-edge-width + 1) * 2); } 212 | 30% { stroke-width: (var-edge-width + 1); } 213 | 40% { stroke-width: ((var-edge-width + 1) * 2); } 214 | 60% { stroke-width: (var-edge-width + 1); } 215 | } 216 | @-o-keyframes arrow-heartbeats { 217 | 0% { stroke-width: (var-edge-width + 1); } 218 | 10% { stroke-width: ((var-edge-width + 1) * 2); } 219 | 30% { stroke-width: (var-edge-width + 1); } 220 | 40% { stroke-width: ((var-edge-width + 1) * 2); } 221 | 60% { stroke-width: (var-edge-width + 1); } 222 | } 223 | @keyframes arrow-heartbeats { 224 | 0% { stroke-width: (var-edge-width + 1); } 225 | 10% { stroke-width: ((var-edge-width + 1) * 2); } 226 | 30% { stroke-width: (var-edge-width + 1); } 227 | 40% { stroke-width: ((var-edge-width + 1) * 2); } 228 | 60% { stroke-width: (var-edge-width + 1); } 229 | } 230 | .edge.animated .arrow-bg { 231 | stroke-linecap: round; 232 | stroke-width: (var-edge-width + 1); 233 | stroke: var-selection-box-border; 234 | 235 | -webkit-animation: arrow-heartbeats 4s linear infinite; 236 | -moz-animation: arrow-heartbeats 4s linear infinite; 237 | -o-animation: arrow-heartbeats 4s linear infinite; 238 | animation: arrow-heartbeats 4s linear infinite; 239 | } 240 | 241 | 242 | /* marching ants for animated edges */ 243 | 244 | @-webkit-keyframes heartbeats { 245 | 0% { stroke-width: (var-edge-width * 2); } 246 | 10% { stroke-width: (var-edge-width * 4); } 247 | 30% { stroke-width: (var-edge-width * 2); } 248 | 40% { stroke-width: (var-edge-width * 4); } 249 | 60% { stroke-width: (var-edge-width * 2); } 250 | } 251 | @-moz-keyframes heartbeats { 252 | 0% { stroke-width: (var-edge-width * 2); } 253 | 10% { stroke-width: (var-edge-width * 4); } 254 | 30% { stroke-width: (var-edge-width * 2); } 255 | 40% { stroke-width: (var-edge-width * 4); } 256 | 60% { stroke-width: (var-edge-width * 2); } 257 | } 258 | @-o-keyframes heartbeats { 259 | 0% { stroke-width: (var-edge-width * 2); } 260 | 10% { stroke-width: (var-edge-width * 4); } 261 | 30% { stroke-width: (var-edge-width * 2); } 262 | 40% { stroke-width: (var-edge-width * 4); } 263 | 60% { stroke-width: (var-edge-width * 2); } 264 | } 265 | @keyframes heartbeats { 266 | 0% { stroke-width: (var-edge-width * 2); } 267 | 10% { stroke-width: (var-edge-width * 4); } 268 | 30% { stroke-width: (var-edge-width * 2); } 269 | 40% { stroke-width: (var-edge-width * 4); } 270 | 60% { stroke-width: (var-edge-width * 2); } 271 | } 272 | .edge.animated .edge-bg { 273 | stroke-linecap: round; 274 | stroke-width: (var-edge-width * 2); 275 | stroke: var-selection-box-border; 276 | 277 | -webkit-animation: heartbeats 4s linear infinite; 278 | -moz-animation: heartbeats 4s linear infinite; 279 | -o-animation: heartbeats 4s linear infinite; 280 | animation: heartbeats 4s linear infinite; 281 | } 282 | 283 | 284 | text { 285 | font-family: "SourceCodePro", "Source Code Pro", Helvetica, Arial, sans-serif; 286 | text-rendering: geometricPrecision; /* makes text scale smoothly */ 287 | font-size: 14px; 288 | fill: var-type-a; 289 | text-anchor: middle; 290 | dominant-baseline: central; 291 | } 292 | 293 | .text-bg-rect { 294 | fill: var-text-bg; 295 | } 296 | 297 | .node-label, 298 | .node-sublabel { 299 | text-anchor: middle; 300 | } 301 | .small .node-label-bg { 302 | visibility: hidden; 303 | } 304 | 305 | .node-sublabel { 306 | font-size: 9px; 307 | } 308 | .node-sublabel-bg { 309 | visibility: hidden; 310 | } 311 | .big .node-sublabel-bg { 312 | visibility: visible; 313 | } 314 | 315 | 316 | /* IIPs */ 317 | .iip-path { 318 | stroke-width: 1px; 319 | stroke: var-route00; 320 | } 321 | .iip-info { 322 | visibility: hidden; 323 | } 324 | .iip-info .text-bg-text { 325 | font-size: 5px; 326 | text-anchor: end; 327 | } 328 | .big .iip-info { 329 | visibility: visible; 330 | } 331 | 332 | 333 | /* Context menu - node */ 334 | .context-modal-bg { 335 | fill: var-menu-modal-bg; 336 | } 337 | .context-node-rect { 338 | fill: var-menu-icon-bg; 339 | stroke: var-menu-border; 340 | stroke-width: 0.5px; 341 | } 342 | .context-node-icon { 343 | font-size: 30px; 344 | fill: var-type-a; 345 | } 346 | .context-icon { 347 | font-size: 20px; 348 | fill: var-type-a; 349 | } 350 | .context-node-label { 351 | text-anchor: middle; 352 | } 353 | .context-arc { 354 | stroke-width: 72px; 355 | stroke: var-menu-bg; 356 | } 357 | .context-slice.click:hover .context-arc { 358 | stroke: var-menu-bg-hover; 359 | } 360 | .context-circle { 361 | stroke: var-menu-border; 362 | fill: none; 363 | stroke-width: 1.5px; 364 | } 365 | .context-circle-x { 366 | stroke: var-menu-border; 367 | fill: none; 368 | stroke-width: 1px; 369 | } 370 | 371 | .context-arc-icon-label { 372 | font-size: 12px; 373 | } 374 | 375 | 376 | /* Context menu - port */ 377 | .context-port-bg { 378 | fill: var-menu-bg; 379 | } 380 | .context-port-bg.highlight { 381 | stroke: var-menu-border; 382 | stroke-width: 1px; 383 | } 384 | .context-port-hole { 385 | stroke-width: 2px; 386 | fill: var-menu-bg; 387 | stroke: var-menu-border; 388 | } 389 | .context-port-path { 390 | stroke: var-menu-border; 391 | } 392 | .context-port-label { 393 | fill: var-type-a; 394 | dominant-baseline: central; 395 | } 396 | .context-port-in .context-port-label { 397 | text-anchor: end; 398 | } 399 | .context-port-out .context-port-label { 400 | text-anchor: start; 401 | } 402 | 403 | 404 | 405 | /* Context menu - edge */ 406 | .context-edge-label-out { 407 | text-anchor: end; 408 | dominant-baseline: central; 409 | } 410 | .context-edge-label-in { 411 | text-anchor: start; 412 | dominant-baseline: central; 413 | } 414 | 415 | 416 | .tooltip { 417 | opacity: 1.0; 418 | 419 | transition-property: opacity; 420 | transition-duration: 0.3s; 421 | } 422 | .tooltip.hidden { 423 | opacity: 0; 424 | } 425 | .tooltip-bg { 426 | fill: var-tooltip-bg; 427 | opacity: 0.75; 428 | } 429 | .tooltip-label { 430 | text-anchor: start; 431 | font-size: 10px; 432 | } 433 | 434 | .icon { 435 | font-family: FontAwesome; 436 | text-anchor: middle; 437 | dominant-baseline: central; 438 | } 439 | .node-icon { 440 | font-size: 45px; 441 | fill: var-node-icon; 442 | 443 | transition-property: font-size, fill; 444 | transition-duration: 0.5s, 0.3s; 445 | } 446 | .small .node-icon { 447 | fill: var-node-icon-small; 448 | font-size: 66px; 449 | } 450 | .big .node-icon { 451 | fill: var-node-icon-big; 452 | } 453 | 454 | .ex-inports .node-icon, 455 | .small .ex-inports .node-icon { 456 | fill: var-route02; 457 | } 458 | .ex-outports .node-icon, 459 | .small .ex-outports .node-icon { 460 | fill: var-route04; 461 | } 462 | 463 | .port-circle-bg { 464 | fill: var-node-border; 465 | opacity: 0; 466 | } 467 | .port-arc { 468 | fill: var-node-border; 469 | } 470 | .port-circle-small { 471 | fill: none; 472 | } 473 | .small .port-circle { 474 | visibility: hidden; 475 | } 476 | .port-label { 477 | fill: var-type-b; 478 | visibility: hidden; 479 | font-size: 6px; 480 | dominant-baseline: central; 481 | } 482 | 483 | .inports .port-label { 484 | text-anchor: start; 485 | } 486 | .outports .port-label { 487 | text-anchor: end; 488 | } 489 | .big .port-label { 490 | visibility: visible; 491 | } 492 | .big .ex-inports .port-label, 493 | .big .ex-outports .port-label { 494 | visibility: hidden; 495 | } 496 | 497 | 498 | /* Groups */ 499 | .group-box { 500 | fill: var-group00; 501 | 502 | transition-property: fill; 503 | transition-duration: 0.3s; 504 | } 505 | .group-box:hover { 506 | fill: var-group00-hover; 507 | } 508 | .small .group-box { 509 | stroke-width: 8px; 510 | } 511 | 512 | .group-box.color0 513 | fill: var-group00 514 | stroke: var-group00-stroke 515 | .group-box.color1 516 | fill: var-group01 517 | stroke: var-group01-stroke 518 | .group-box.color2 519 | fill: var-group02 520 | stroke: var-group02-stroke 521 | .group-box.color3 522 | fill: var-group03 523 | stroke: var-group03-stroke 524 | .group-box.color4 525 | fill: var-group04 526 | stroke: var-group04-stroke 527 | .group-box.color5 528 | fill: var-group05 529 | stroke: var-group05-stroke 530 | .group-box.color6 531 | fill: var-group06 532 | stroke: var-group06-stroke 533 | .group-box.color7 534 | fill: var-group07 535 | stroke: var-group07-stroke 536 | .group-box.color8 537 | fill: var-group08 538 | stroke: var-group08-stroke 539 | .group-box.color9 540 | fill: var-group09 541 | stroke: var-group09-stroke 542 | .group-box.color10 543 | fill: var-group10 544 | stroke: var-group10-stroke 545 | 546 | .group-box.color0:hover 547 | fill: var-group00-hover 548 | .group-box.color1:hover 549 | fill: var-group01-hover 550 | .group-box.color2:hover 551 | fill: var-group02-hover 552 | .group-box.color3:hover 553 | fill: var-group03-hover 554 | .group-box.color4:hover 555 | fill: var-group04-hover 556 | .group-box.color5:hover 557 | fill: var-group05-hover 558 | .group-box.color6:hover 559 | fill: var-group06-hover 560 | .group-box.color7:hover 561 | fill: var-group07-hover 562 | .group-box.color8:hover 563 | fill: var-group08-hover 564 | .group-box.color9:hover 565 | fill: var-group09-hover 566 | .group-box.color10:hover 567 | fill: var-group10-hover 568 | 569 | /* deselected group */ 570 | .selection 571 | .group-box.color0 572 | fill: var-group00-fade 573 | .group-box.color1 574 | fill: var-group01-fade 575 | .group-box.color2 576 | fill: var-group02-fade 577 | .group-box.color3 578 | fill: var-group03-fade 579 | .group-box.color4 580 | fill: var-group04-fade 581 | .group-box.color5 582 | fill: var-group05-fade 583 | .group-box.color6 584 | fill: var-group06-fade 585 | .group-box.color7 586 | fill: var-group07-fade 587 | .group-box.color8 588 | fill: var-group08-fade 589 | .group-box.color9 590 | fill: var-group09-fade 591 | .group-box.color10 592 | fill: var-group10-fade 593 | 594 | /* selected pseudo-group */ 595 | .selection 596 | .group-box.selection 597 | fill: var-selection-box 598 | stroke: var-selection-box-border 599 | stroke-width: 1px 600 | .group-box.selection:hover 601 | fill: var-selection-box-hover 602 | 603 | 604 | .group-label { 605 | text-anchor: start; 606 | fill: var-type-a; 607 | font-size: 20px; 608 | transition-property: font-size; 609 | transition-duration: 0.5s; 610 | } 611 | .small .group-label { 612 | font-size: 30px; 613 | transition-property: font-size; 614 | transition-duration: 0.5s; 615 | } 616 | 617 | .group-description { 618 | fill: var-type-a; 619 | font-size: 12px; 620 | text-anchor: start; 621 | } 622 | .small .group-description { 623 | visibility: hidden; 624 | } 625 | 626 | .stroke 627 | .stroke.route0 628 | .selection .selected .stroke 629 | .selection .selected .stroke.route0 630 | stroke: var-route00 631 | .stroke.route1 632 | .selection .selected .stroke.route1 633 | stroke: var-route01 634 | .stroke.route2 635 | .selection .selected .stroke.route2 636 | stroke: var-route02 637 | .stroke.route3 638 | .selection .selected .stroke.route3 639 | stroke: var-route03 640 | .stroke.route4 641 | .selection .selected .stroke.route4 642 | stroke: var-route04 643 | .stroke.route5 644 | .selection .selected .stroke.route5 645 | stroke: var-route05 646 | .stroke.route6 647 | .selection .selected .stroke.route6 648 | stroke: var-route06 649 | .stroke.route7 650 | .selection .selected .stroke.route7 651 | stroke: var-route07 652 | .stroke.route8 653 | .selection .selected .stroke.route8 654 | stroke: var-route08 655 | .stroke.route9 656 | .selection .selected .stroke.route9 657 | stroke: var-route09 658 | .stroke.route10 659 | .selection .selected .stroke.route10 660 | stroke: var-route10 661 | 662 | .selection 663 | .stroke 664 | .stroke.route0 665 | stroke: var-route00-fade 666 | .stroke.route1 667 | stroke: var-route01-fade 668 | .stroke.route2 669 | stroke: var-route02-fade 670 | .stroke.route3 671 | stroke: var-route03-fade 672 | .stroke.route4 673 | stroke: var-route04-fade 674 | .stroke.route5 675 | stroke: var-route05-fade 676 | .stroke.route6 677 | stroke: var-route06-fade 678 | .stroke.route7 679 | stroke: var-route07-fade 680 | .stroke.route8 681 | stroke: var-route08-fade 682 | .stroke.route9 683 | stroke: var-route09-fade 684 | .stroke.route10 685 | stroke: var-route10-fade 686 | 687 | .fill 688 | .fill.route0 689 | .selection .selected .fill 690 | .selection .selected .fill.route0 691 | fill: var-route00 692 | .fill.route1 693 | .selection .selected .fill.route1 694 | fill: var-route01 695 | .fill.route2 696 | .selection .selected .fill.route2 697 | fill: var-route02 698 | .fill.route3 699 | .selection .selected .fill.route3 700 | fill: var-route03 701 | .fill.route4 702 | .selection .selected .fill.route4 703 | fill: var-route04 704 | .fill.route5 705 | .selection .selected .fill.route5 706 | fill: var-route05 707 | .fill.route6 708 | .selection .selected .fill.route6 709 | fill: var-route06 710 | .fill.route7 711 | .selection .selected .fill.route7 712 | fill: var-route07 713 | .fill.route8 714 | .selection .selected .fill.route8 715 | fill: var-route08 716 | .fill.route9 717 | .selection .selected .fill.route9 718 | fill: var-route09 719 | .fill.route10 720 | .selection .selected .fill.route10 721 | fill: var-route10 722 | 723 | .selection 724 | .fill 725 | .fill.route0 726 | fill: var-route00-fade 727 | .fill.route1 728 | fill: var-route01-fade 729 | .fill.route2 730 | fill: var-route02-fade 731 | .fill.route3 732 | fill: var-route03-fade 733 | .fill.route4 734 | fill: var-route04-fade 735 | .fill.route5 736 | fill: var-route05-fade 737 | .fill.route6 738 | fill: var-route06-fade 739 | .fill.route7 740 | fill: var-route07-fade 741 | .fill.route8 742 | fill: var-route08-fade 743 | .fill.route9 744 | fill: var-route09-fade 745 | .fill.route10 746 | fill: var-route10-fade 747 | -------------------------------------------------------------------------------- /themes/light/the-graph-spectrum.styl: -------------------------------------------------------------------------------- 1 | var-route00 = hsl( 0, 0%, 75%) 2 | var-route01 = hsl( 0, 100%, 40%) 3 | var-route02 = hsl( 29, 100%, 40%) 4 | var-route03 = hsl( 47, 100%, 50%) 5 | var-route04 = hsl(138, 100%, 40%) 6 | var-route05 = hsl(160, 73%, 81%) 7 | var-route06 = hsl(181, 100%, 40%) 8 | var-route07 = hsl(216, 100%, 40%) 9 | var-route08 = hsl(260, 100%, 40%) 10 | var-route09 = hsl(348, 100%, 83%) 11 | var-route10 = hsl(328, 100%, 40%) 12 | 13 | var-route00-fade = hsl( 0, 0%, 90%) 14 | var-route01-fade = hsl( 0, 100%, 90%) 15 | var-route02-fade = hsl( 29, 100%, 90%) 16 | var-route03-fade = hsl( 47, 100%, 90%) 17 | var-route04-fade = hsl(138, 100%, 90%) 18 | var-route05-fade = hsl(160, 73%, 90%) 19 | var-route06-fade = hsl(181, 100%, 90%) 20 | var-route07-fade = hsl(216, 100%, 90%) 21 | var-route08-fade = hsl(260, 100%, 90%) 22 | var-route09-fade = hsl(348, 100%, 90%) 23 | var-route10-fade = hsl(328, 100%, 90%) 24 | 25 | var-group00 = hsla( 0, 0%, 75%, 0.05) 26 | var-group01 = hsla( 0, 100%, 40%, 0.05) 27 | var-group02 = hsla( 29, 100%, 40%, 0.05) 28 | var-group03 = hsla( 47, 100%, 50%, 0.05) 29 | var-group04 = hsla(138, 100%, 40%, 0.05) 30 | var-group05 = hsla(160, 73%, 81%, 0.05) 31 | var-group06 = hsla(181, 100%, 40%, 0.05) 32 | var-group07 = hsla(216, 100%, 40%, 0.05) 33 | var-group08 = hsla(260, 100%, 40%, 0.05) 34 | var-group09 = hsla(348, 100%, 83%, 0.05) 35 | var-group10 = hsla(328, 100%, 40%, 0.05) 36 | 37 | var-group00-stroke = hsla( 0, 0%, 75%, 0.15) 38 | var-group01-stroke = hsla( 0, 100%, 40%, 0.15) 39 | var-group02-stroke = hsla( 29, 100%, 40%, 0.15) 40 | var-group03-stroke = hsla( 47, 100%, 50%, 0.15) 41 | var-group04-stroke = hsla(138, 100%, 40%, 0.15) 42 | var-group05-stroke = hsla(160, 73%, 81%, 0.15) 43 | var-group06-stroke = hsla(181, 100%, 40%, 0.15) 44 | var-group07-stroke = hsla(216, 100%, 40%, 0.15) 45 | var-group08-stroke = hsla(260, 100%, 40%, 0.15) 46 | var-group09-stroke = hsla(348, 100%, 83%, 0.15) 47 | var-group10-stroke = hsla(328, 100%, 40%, 0.15) 48 | 49 | var-group00-hover = hsla( 0, 0%, 75%, 0.15) 50 | var-group01-hover = hsla( 0, 100%, 40%, 0.15) 51 | var-group02-hover = hsla( 29, 100%, 40%, 0.15) 52 | var-group03-hover = hsla( 47, 100%, 50%, 0.15) 53 | var-group04-hover = hsla(138, 100%, 40%, 0.15) 54 | var-group05-hover = hsla(160, 73%, 81%, 0.15) 55 | var-group06-hover = hsla(181, 100%, 40%, 0.15) 56 | var-group07-hover = hsla(216, 100%, 40%, 0.15) 57 | var-group08-hover = hsla(260, 100%, 40%, 0.15) 58 | var-group09-hover = hsla(348, 100%, 83%, 0.15) 59 | var-group10-hover = hsla(328, 100%, 40%, 0.15) 60 | 61 | var-group00-fade = hsla( 0, 0%, 75%, 0.01) 62 | var-group01-fade = hsla( 0, 100%, 40%, 0.01) 63 | var-group02-fade = hsla( 29, 100%, 40%, 0.01) 64 | var-group03-fade = hsla( 47, 100%, 50%, 0.01) 65 | var-group04-fade = hsla(138, 100%, 40%, 0.01) 66 | var-group05-fade = hsla(160, 73%, 81%, 0.01) 67 | var-group06-fade = hsla(181, 100%, 40%, 0.01) 68 | var-group07-fade = hsla(216, 100%, 40%, 0.01) 69 | var-group08-fade = hsla(260, 100%, 40%, 0.01) 70 | var-group09-fade = hsla(348, 100%, 83%, 0.01) 71 | var-group10-fade = hsla(328, 100%, 40%, 0.01) 72 | -------------------------------------------------------------------------------- /themes/light/the-graph.styl: -------------------------------------------------------------------------------- 1 | var-bg = white 2 | 3 | var-node = hsla(192, 25%, 92%, 0.94) 4 | var-node-error = hsla(0, 100%, 50%, 0.94) 5 | var-node-hover = hsla(192, 25%, 92%, 0.97) 6 | var-node-border = hsl(194, 8%, 25%) 7 | var-node-border-bg = hsla(0, 0%, 100%, 0.5) 8 | var-node-hover-border = hsl(0, 0%, 50%) 9 | var-node-icon = hsl(194, 8%, 25%) 10 | var-node-icon-big = hsl(194, 8%, 80%) 11 | var-node-icon-small = hsl(194, 8%, 10%) 12 | 13 | var-node-selected = hsla(192, 25%, 92%, 0.94) 14 | var-node-selected-border = hsl(0, 0%, 50%) 15 | var-node-selected-icon = hsl(194, 8%, 20%) 16 | var-node-deselected = hsla(192, 25%, 97%, 0.94) 17 | var-node-deselected-border = hsl(194, 8%, 85%) 18 | var-node-deselected-icon = hsl(194, 8%, 75%) 19 | 20 | var-export-bg = hsl(0, 0%, 90%) 21 | 22 | var-type-a = black 23 | var-type-b = hsl(188, 6%, 5%) 24 | var-text-bg = hsla(0, 0%, 100%, 0.5) 25 | var-tooltip-bg = hsla(0, 0%, 100%, 0.9) 26 | 27 | var-selection-box = hsla(0, 0%, 50%, 0.15) 28 | var-selection-box-hover = hsla(0, 0%, 40%, 0.25) 29 | var-selection-box-border = hsl(0, 0%, 50%) 30 | 31 | var-menu-modal-bg = hsla(0, 0%, 100%, 0.5) 32 | var-menu-bg = hsla(211, 12%, 89%, 0.95) 33 | var-menu-bg-hover = hsla(211, 12%, 79%, 0.95) 34 | var-menu-border = hsl(0, 0%, 25%) 35 | var-menu-icon-bg = hsl(211, 12%, 89%) 36 | 37 | var-edge-bg = hsl(0, 0%, 90%) 38 | var-edge-hover-bg = hsl(0, 0%, 50%) 39 | var-edge-width = 4px 40 | 41 | @require "./the-graph-spectrum" 42 | -------------------------------------------------------------------------------- /themes/the-graph-dark.styl: -------------------------------------------------------------------------------- 1 | .the-graph-dark 2 | @require "./dark/the-graph" 3 | @require "./default/the-graph" -------------------------------------------------------------------------------- /themes/the-graph-light.styl: -------------------------------------------------------------------------------- 1 | .the-graph-light 2 | @require "./light/the-graph" 3 | @require "./default/the-graph" -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 3 | const { Z_FULL_FLUSH } = require('zlib'); 4 | 5 | module.exports = { 6 | entry: { 7 | 'the-graph': './index.js', 8 | 'the-graph-render': './render.jsjob.js', 9 | 'demo-full': './examples/demo-full.js', 10 | 'demo-simple': './examples/demo-simple.js', 11 | 'demo-thumbnail': './examples/demo-thumbnail.js', 12 | }, 13 | output: { 14 | path: path.resolve(__dirname, './dist/'), 15 | filename: '[name].js', 16 | sourceMapFilename: '[name].js.map', 17 | }, 18 | mode: 'production', 19 | devtool: 'source-map', 20 | module: { 21 | rules: [ 22 | { 23 | test: /\.css$/i, 24 | use: ['style-loader', 'css-loader'], 25 | }, 26 | { 27 | test: /\.styl$/, 28 | use: ['style-loader', 'css-loader', 'stylus-loader'], 29 | }, 30 | { 31 | test: /\.(woff(2)?|ttf|eot|svg)(\?v=\d+\.\d+\.\d+)?$/, 32 | use: [ 33 | { 34 | loader: 'file-loader', 35 | options: { 36 | name: '[name].[ext]', 37 | outputPath: 'fonts/', 38 | }, 39 | }, 40 | ], 41 | }, 42 | ], 43 | }, 44 | plugins: [ 45 | new HtmlWebpackPlugin({ 46 | filename: 'demo-full.html', 47 | template: 'examples/demo-full.html', 48 | chunks: ['demo-full'], 49 | }), 50 | new HtmlWebpackPlugin({ 51 | filename: 'demo-simple.html', 52 | template: 'examples/demo-simple.html', 53 | chunks: ['demo-simple'], 54 | }), 55 | new HtmlWebpackPlugin({ 56 | filename: 'demo-thumbnail.html', 57 | template: 'examples/demo-thumbnail.html', 58 | chunks: ['demo-thumbnail'], 59 | }), 60 | ], 61 | resolve: { 62 | fallback: { 63 | events: require.resolve('events/'), 64 | fs: false, 65 | }, 66 | }, 67 | }; 68 | --------------------------------------------------------------------------------