├── .gitattributes ├── .gitignore ├── .pre-commit-config.yaml ├── .prettierrc.js ├── .tool-versions ├── CHANGELOG.md ├── LICENSE ├── README.md ├── commitlint.config.js ├── eslint.config.mjs ├── example ├── clos.html ├── clos.json ├── images │ ├── bundle01.png │ ├── bundle02.png │ ├── clos.png │ ├── group_size.png │ ├── internal_group.png │ ├── ix.png │ ├── link_label.png │ ├── link_width.png │ ├── nat.png │ ├── node_label.png │ ├── node_size.png │ ├── openflow.png │ ├── position_hints.png │ ├── router.png │ ├── shownet.png │ ├── switch.png │ └── tooltip.png ├── index.html ├── index.json ├── shownet.html ├── shownet.json └── style.css ├── inet-henge.js ├── inet-henge.js.map ├── inet-henge.min.js ├── inet-henge.min.js.LICENSE.txt ├── inet-henge.min.js.map ├── package-lock.json ├── package.json ├── plugins ├── arrows_link │ ├── README.md │ ├── docs │ │ └── images │ │ │ └── screenshot01.png │ ├── plugin.js │ ├── plugin.js.map │ ├── plugin.min.js │ ├── plugin.min.js.map │ ├── src │ │ └── plugin.ts │ ├── style.css │ ├── tsconfig.json │ ├── webpack.common.js │ ├── webpack.dev.js │ └── webpack.prod.js └── removable_node │ ├── README.md │ ├── docs │ └── images │ │ └── screenshot01.gif │ ├── plugin.js │ ├── plugin.js.map │ ├── plugin.min.js │ ├── plugin.min.js.map │ ├── src │ └── plugin.ts │ ├── tsconfig.json │ ├── webpack.common.js │ ├── webpack.dev.js │ └── webpack.prod.js ├── src ├── bundle.ts ├── diagram.ts ├── group.ts ├── hack_cola.js ├── link.ts ├── link_tooltip.ts ├── meta_data.ts ├── node.ts ├── node_tooltip.ts ├── plugin.ts ├── position_cache.ts ├── tooltip.ts └── util.ts ├── tsconfig.json ├── types └── WebCola.d.ts ├── vendor ├── cola.min.js └── cola.min.js.map ├── webpack.common.js ├── webpack.dev.js └── webpack.prod.js /.gitattributes: -------------------------------------------------------------------------------- 1 | /inet-henge.* diff=skip 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | 3 | /WebCola/ 4 | /.idea/ 5 | /bak/ 6 | .ignore 7 | tags 8 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/alessandrojcm/commitlint-pre-commit-hook 3 | rev: v9.13.0 4 | hooks: 5 | - id: commitlint 6 | stages: [commit-msg] 7 | additional_dependencies: ["@commitlint/config-conventional"] 8 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | semi: true, 3 | trailingComma: "all", 4 | singleQuote: false, 5 | printWidth: 120, 6 | tabWidth: 2, 7 | useTabs: false, 8 | }; 9 | -------------------------------------------------------------------------------- /.tool-versions: -------------------------------------------------------------------------------- 1 | nodejs 20.17.0 2 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [1.4.7] - 2024-08-23 2 | 3 | ### Fixed 4 | 5 | - Link.tick() unexpectedly threw TypeError 6 | 7 | 8 | ## [1.4.6] - 2024-08-18 9 | 10 | ### Fixed 11 | 12 | - Hide link labels when the link having them is too short. 13 | Otherwise, Chrome ( and Edge ) show them at unexpected positions. 14 | 15 | 16 | ## [1.4.4] - 2024-03-09 17 | 18 | ### Fixed 19 | 20 | - Source node was shown in "target" field of LinkTooltip 21 | 22 | 23 | ## [1.4.3] - 2023-09-24 24 | 25 | ### Fixed 26 | 27 | - Define Diagram.tickCallback to call in custom "tick" event listener 28 | 29 | 30 | ## [1.4.2] - 2023-08-21 31 | 32 | ### Fixed 33 | 34 | - Link tooltips not following 35 | 36 | 37 | ## [1.4.1] - 2023-08-20 38 | 39 | ### Fixed 40 | 41 | - Tooltips not following dragged group 42 | 43 | 44 | ## [1.4.0] - 2023-08-16 45 | 46 | ### Added 47 | 48 | - "bundle" option of link to show bundle "tie" marks over bundled links 49 | - "positionConstraints" option of Diagram 50 | - Link tooltip 51 | 52 | 53 | ## [1.3.1] - 2023-06-10 54 | 55 | ### Fixed 56 | 57 | - Zoom origin when initial translate is specified 58 | 59 | 60 | ## [1.3.0] - 2023-04-12 61 | 62 | ### Added 63 | 64 | - Introduce "nodeCallback" option for position hinting 65 | 66 | 67 | ## [1.2.5] - 2022-12-11 68 | 69 | ### Added 70 | 71 | - Introduce "groupPadding" option to increase the size of groups 72 | 73 | 74 | ## [1.2.4] - 2022-12-01 75 | 76 | ### Fixed 77 | 78 | - Tooltip visibility control along with Bootstrap CSS 79 | 80 | 81 | ## [1.2.2] - 2022-09-26 82 | 83 | ### Added 84 | 85 | - Introduce "nodeWidth" and "nodeHeight" options of `Diagram` 86 | 87 | 88 | ## [1.2.1] - 2022-03-09 89 | 90 | ### Added 91 | 92 | - Introduce "href" option to show `` in tooltips 93 | 94 | 95 | ## [1.2.0] - 2021-09-27 96 | 97 | ### Added 98 | 99 | - Introduce plugin system. 100 | - Removable Node Plugin 101 | - Arrows Link Plugin 102 | 103 | ### Fixed 104 | 105 | - Use md5 instead of sha1 for positionCache hash. Bundling crypto results in an unexpectedly huge bundle size. 106 | 107 | ### Removed 108 | 109 | - Remove a deprecated method `Diagram.prototype.link_width`. 110 | 111 | 112 | ## [1.1.1] - 2021-02-01 113 | 114 | ### Fixed 115 | 116 | - CSS escape to avoid "Uncaught DOMException: Failed to execute 'querySelector' on 'Document'" 117 | 118 | 119 | ## [1.1.0] - 2020-11-09 120 | 121 | ### Added 122 | 123 | - Introduce "initialTicks" option for unconstrained initial layout iterations. 124 | 125 | 126 | ## [1.0.2] - 2020-06-29 127 | 128 | ### Added 129 | 130 | - Create an npm package of inet-henge so that users can use it in other projects, even customize and rebuild. 131 | - Rewrote all .js with .ts to reuse in typescript projects. 132 | - Update the build environment, which was .js + babel + browserify, with .ts + webpack + ts-loader. 133 | 134 | 135 | ## [1.0.0] - 2020-02-25 136 | 137 | ### Added 138 | 139 | - Start versioning. 140 | 141 | ### Fixed 142 | 143 | - Change SVG DOM structure to render link labels in front of nodes. 144 | - :warning: This change breaks backward compatibility in CSS. Use `.link line` instead of `.link`, `.link text` instead of `.path-label`, and `.link text.hover` instead of `.link:hover ~ .path-label ` for instance. 145 | 146 | #### Previous SVG DOM 147 | 148 | ```svg 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | POP03 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | ge-0/0/0 167 | 168 | 169 | 170 | 171 | Te0/0/0/0 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | POP03-bb02 181 | 182 | 183 | ... 184 | 185 | 186 | 187 | ``` 188 | 189 | #### v1.0.0's SVG DOM 190 | 191 | ```svg 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | ... 200 | 201 | 202 | POP03 203 | 204 | 205 | 206 | 207 | 208 | ... 209 | 210 | 211 | 212 | 213 | ... 214 | 215 | 216 | 217 | 218 | ... 219 | 220 | 221 | 222 | POP03-bb02 223 | 224 | 225 | ... 226 | 227 | 228 | 229 | 230 | ... 231 | 232 | 233 | 234 | ge-0/0/0 235 | 236 | 237 | 238 | 239 | Te0/0/0/0 240 | 241 | 242 | 243 | ... 244 | 245 | 246 | 247 | 248 | ``` 249 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016-2024 Shintaro Kojima 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # inet-henge.js 2 | 3 | inet-henge.js generates d3.js based Auto Layout Network Diagram from JSON data. 4 | inet-henge helps you draw it by calculating coordinates automatically, placing nodes and links in SVG format. 5 | 6 | Each object is draggable and zoomable. 7 | 8 | ![stone-henge](https://c3.staticflickr.com/6/5480/11307043746_b3b36ccf34_h.jpg) 9 | 10 | All you have to do are: 11 | 12 | 1. Define nodes identified by name 13 | 2. Define links by specifying both end nodes 14 | 3. Show in a browser. That's it. 15 | 16 | JSON example: 17 | 18 | ```json 19 | { 20 | "nodes": [ 21 | { "name": "A" }, 22 | { "name": "B" } 23 | ], 24 | 25 | "links": [ 26 | { "source": "A", "target": "B" } 27 | ] 28 | } 29 | ``` 30 | 31 | ## Getting Started 32 | 33 | ```zsh 34 | npm install inet-henge 35 | 36 | # or 37 | 38 | git clone https://github.com/codeout/inet-henge.git 39 | ``` 40 | 41 | Then host the root directory in your favorite web server. 42 | 43 | ``` 44 | ruby -run -e httpd . -p 8000 45 | ``` 46 | 47 | Now you can see `http://localhost:8000/example`. 48 | 49 | ``` 50 | python -m SimpleHTTPServer # python2 51 | python -m http.server # python3 52 | 53 | or 54 | 55 | php -S 127.0.0.1:8000 56 | ``` 57 | 58 | are also available to start a web server. 59 | 60 | ## Demo 61 | 62 | - [Shownet 2017 Network](https://codeout.github.io/inet-henge/shownet2017.html) 63 | - [Shownet 2016 Network](https://codeout.github.io/inet-henge/shownet2016.html) 64 | 65 | ## Usage 66 | 67 | In example [here](example/shownet.html), load related assets at first: 68 | 69 | - d3.js v3 70 | - cola.js 71 | - :warning: **It doesn't support d3.js v4** :warning: 72 | - inet-henge.js 73 | 74 | ```html 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | ``` 88 | 89 | define a blank container: 90 | 91 | ```html 92 | 93 |
94 | 95 | ``` 96 | 97 | and render your network diagram: 98 | 99 | ```html 100 | 103 | 104 | ``` 105 | 106 | Object is also acceptable: 107 | 108 | ```html 109 | 116 | 117 | ``` 118 | 119 | inet-henge.js renders your network diagram as SVG within `
`. In the example above the diagram also displays metadata labelled `"interface"` which defined in JSON data. 120 | 121 | ![Shownet2016 example](example/images/shownet.png) 122 | 123 | ### JSON Data 124 | 125 | Minimal json looks like: 126 | 127 | ```json 128 | { 129 | "nodes": [ 130 | { "name": "A" }, 131 | { "name": "B" } 132 | ], 133 | 134 | "links": [ 135 | { "source": "A", "target": "B" } 136 | ] 137 | } 138 | ``` 139 | 140 | You can specify node icon by URL: 141 | 142 | ```json 143 | "nodes": [ 144 | { "name": "dceast-ne40e", "icon": "./images/router.png" } 145 | ] 146 | ``` 147 | 148 | Metadata to display on network diagrams: 149 | 150 | ```js 151 | new Diagram("#diagram", "index.json").init("interface"); 152 | ``` 153 | 154 | ```json 155 | "links": [ 156 | { 157 | "source": "noc-asr9904", "target": "noc-ax8616r", 158 | "meta": { 159 | "interface": { "source": "0-0-0-2", "target": "1-1" } 160 | } 161 | } 162 | ] 163 | ``` 164 | 165 | :point_up: This will render metadata on both ends of links. 166 | 167 | ### Node Group 168 | 169 | Nodes get rendered in groups when you specify which node belongs to which group by regular expression. 170 | 171 | When the first three characters describe POP name, you can group nodes by doing this: 172 | 173 | ```javascript 174 | const diagram = new Diagram("#diagram", "data.json", { pop: /^.{3}/ }); 175 | ``` 176 | 177 | ### Labels 178 | 179 | When `init()` API is called with arguments, inet-henge finds corresponding metadata and show them as labels. 180 | 181 | To place a loopback address on nodes: 182 | 183 | ```js 184 | new Diagram("#diagram", "index.json").init("loopback"); 185 | ``` 186 | 187 | ```json 188 | { 189 | "nodes": [ 190 | { "name": "Node 1", "meta": { "loopback": "10.0.0.1" } }, 191 | ... 192 | ], 193 | ... 194 | } 195 | ``` 196 | 197 | ![Label on node](example/images/node_label.png) 198 | 199 | To place link and interface names: 200 | 201 | ```js 202 | new Diagram("#diagram", "index.json").init("bandwidth", "intf-name"); 203 | ``` 204 | 205 | ```js 206 | { 207 | ... 208 | "links": [ 209 | { 210 | "source": "Node 1", "target": "Node 2", 211 | "meta": { 212 | "bandwidth": "10G", 213 | "intf-name": { "source": "interface A", "target": "interface B" } 214 | } 215 | }, 216 | ... 217 | ] 218 | ``` 219 | 220 | ![Label on link](example/images/link_label.png) 221 | 222 | ### Node Size 223 | 224 | You can change node width and height: 225 | 226 | ```js 227 | const diagram = new Diagram("#diagram", "data.json", { nodeWidth: 120, nodeHeight: 30 }); 228 | ``` 229 | 230 | ![Node Size](example/images/node_size.png) 231 | 232 | Width `60` and heigh `40` (px) by default. 233 | 234 | ### Link Width 235 | 236 | You can use `linkWidth()` API to customize link widths. The argument should be a function which calculates metadata and returns value for `stroke-width` of SVG. 237 | 238 | ```js 239 | const diagram = new Diagram("#diagram", "index.json"); 240 | diagram.linkWidth(function (link) { 241 | if (!link) 242 | return 1; // px 243 | else if (link.bandwidth === "100G") 244 | return 10; // px 245 | else if (link.bandwidth === "10G") 246 | return 3; // px 247 | else if (link.bandwidth === "1G") 248 | return 1; // px 249 | }); 250 | diagram.init("bandwidth"); 251 | ``` 252 | 253 | ```json 254 | "links": [ 255 | { "source": "Node 1", "target": "Node 2", "meta": { "bandwidth": "1G" }}, 256 | { "source": "Node 1", "target": "Node 3", "meta": { "bandwidth": "10G" }}, 257 | { "source": "Node 2", "target": "Node 3", "meta": { "bandwidth": "100G" }} 258 | ] 259 | ``` 260 | 261 | ![Link width](example/images/link_width.png) 262 | 263 | :warning: Make sure no stylesheet overrides customized link widths. :warning: 264 | 265 | ### Group Size 266 | 267 | You can specify padding to increase the size of groups (default: 1): 268 | 269 | ```js 270 | const diagram = new Diagram("#diagram", "data.json", { groupPadding: 30 }); 271 | ``` 272 | 273 | ![Group Size](example/images/group_size.png) 274 | 275 | :bulb: Position calculation sometimes gets stuck when increasing `groupPadding`. [initialTicks](#ticks) may help in such cases. 276 | 277 | ### Ticks 278 | 279 | You can specify the number of steps (called as ticks) to calculate with [d3-force](https://github.com/d3/d3-force/blob/master/README.md) layout. Bigger ticks typically converge on a better layout, but it will take much longer until settlement. The default value is 1000. 280 | 281 | ```javascript 282 | const diagram = new Diagram("#diagram", "data.json", { ticks: 3000 }); 283 | ``` 284 | 285 | For large scale network diagrams, you can also specify the number of initial unconstrained ticks. 286 | 287 | ```javascript 288 | const diagram = new Diagram("#diagram", "data.json", { initialTicks: 100, ticks: 100 }); 289 | ``` 290 | 291 | inet-henge calculates the layout in two iteration phases: 292 | 293 | 1. Initial iteration with no constraints. ( default: 0 tick ) 294 | 2. The main iteration with constraints that apply groups as bounding boxes, prevent nodes and groups from overlapping with each other, and so on. ( default: 1000 ticks ) 295 | 296 | If you increase `initialTicks`, inet-henge calculates faster in exchange for network diagram precision so that you can decrease `ticks` which is the number of main iteration steps. 297 | 298 | 20 ~ 100 `initialTicks` and 70 ~ 100 `ticks` should be good start for 800 nodes with 950 links for example. It takes 20 ~ 30 seconds to render in the benchmark environment. 299 | 300 | ### Position Cache 301 | 302 | inet-henge caches a calculated position of nodes, groups, and links for the next rendering. If you load the same JSON data, the cache will be used as a position hint. You can disable this behavior with `positionCache` option. 303 | 304 | ```javascript 305 | const diagram = new Diagram("#diagram", "data.json", { positionCache: false }); 306 | ``` 307 | 308 | ### SVG viewport size 309 | 310 | You can change svg's viewport size: 311 | 312 | ```js 313 | const diagram = new Diagram("#diagram", "data.json", { width: 1200, height: 600 }); 314 | ``` 315 | 316 | This will generate: 317 | 318 | ```html 319 | 320 | ``` 321 | 322 | ### Style 323 | 324 | inet-henge generates an SVG image, so you can customize the style by using CSS. 325 | 326 | ### Display bundles 327 | 328 | You can display multiple links between nodes by setting `bundle: true` in the constructor like: 329 | 330 | ```html 331 | 335 | ``` 336 | 337 | ![Bundle example](example/images/bundle01.png) 338 | 339 | Nodes are connected to each other with a single link by default. 340 | 341 | ### Save positions after dragging nodes 342 | 343 | You can save positions of all nodes in browser even after dragging them by setting `positionCache: "fixed"` in the constructor like: 344 | 345 | ```html 346 | 350 | ``` 351 | 352 | ### Position hinting 353 | 354 | You can provide the coordinates of nodes as position hints to place them where you want. inet-henge calculates the layout by considering them. It always refers to position cache over hint when there is a cache. 355 | 356 | :bulb: They are "hints". Nodes won't be strictly placed there. 357 | 358 | ```js 359 | const diagram = new Diagram("#diagram", "index.json", { 360 | pop: /^([^\s-]+)-/, 361 | bundle: true, 362 | positionHint: { 363 | nodeCallback: (node) => { 364 | const [_, pop] = node.name.match(/^([^\s-]+)-/); 365 | 366 | // specify the position of nodes in POP01 367 | if (pop === "POP01") { 368 | return { x: 600, y: 330 }; 369 | } 370 | 371 | // unspecified 372 | return null; 373 | }, 374 | }, 375 | }); 376 | 377 | diagram.init("loopback", "interface", "description", "type"); 378 | ``` 379 | 380 | - Use `nodeCallback` option to specify per node. 381 | - The `Node` object is passed as an argument. 382 | - Return value should be an object like `{x: 600, y: 330}`. 383 | - If the callback returns `null`, this means that the node position is unspecified. 384 | 385 | ![Position hinting](example/images/position_hints.png) 386 | 387 | #### :bulb: How position hinting works 388 | 389 | The position hints are initial positions technically. 390 | 391 | 1. inet-henge places nodes according to the hints. 392 | - When no hint is specified, the node will be placed in the center of the diagram. 393 | 2. Then, it starts [the ticks calculation](#Ticks). 394 | 395 | ### Metadata tooltip 396 | 397 | You can display node metadata in the tooltip, instead of always showing as node text, by setting `tooltip: "click"` in the constructor like: 398 | 399 | ```html 400 | 404 | ``` 405 | 406 | In the example above, `description` and `type` will be displayed. 407 | 408 | ```json 409 | "nodes": [ 410 | { "name": "POP01-bb01", "meta": {"description": "This is a router", "type": "Backbone"}, "icon": "./images/router.png" }, 411 | { "name": "POP01-bb02", "meta": {"description": "This is a router", "type": "Backbone"}, "icon": "./images/router.png" }, 412 | { "name": "POP01-ag01", "meta": {"description": "This is a router", "type": "Aggregation"}, "icon": "./images/switch.png" }, 413 | { "name": "POP01-ag02", "meta": {"description": "This is a router", "type": "Aggregation"}, "icon": "./images/switch.png" }, 414 | ``` 415 | 416 | ![Metadata tooltip](example/images/tooltip.png) 417 | 418 | :bulb: `tooltip: "hover"` is also available. 419 | 420 | #### Hyperlink in tooltop 421 | 422 | You can show `...` in node metadata tooltips. 423 | 424 | ```html 425 | 451 | ``` 452 | 453 | This example above will generate `...`. 454 | 455 | :bulb: Use `tooltip: "click"` to make tooltips sticky. 456 | 457 | ### Initial Position and Scale 458 | 459 | You can specify initial position and scale of diagram. 460 | 461 | ```html 462 | 469 | ``` 470 | 471 | ## Experimental Features 472 | 473 | :warning: Those features may work, but still under development. The behavior might be changed :warning: 474 | 475 | ### Position constraints 476 | 477 | You can apply x-axis or y-axis based position constraints to nodes. 478 | 479 | Here is [an example](https://codeout.github.io/inet-henge/clos.html). 480 | 481 | ![Position constraints](example/images/clos.png) 482 | 483 | ```html 484 | 511 | ``` 512 | 513 | - Use `nodesCallback` option to create node groups. 514 | - The Node object is passed as an argument. 515 | - Individual constraints will be applied to each group. 516 | 517 | You may want to define [Position hinting](#position-hinting) besides constraints. 518 | Please note that hint is just a hint and nodes won't be strictly placed there, while constraint is always satisfied. 519 | 520 | ### Internal groups 521 | 522 | You can display node type based groups in POP-based [Node group](#Node-Group) by `group` definition in each node. 523 | 524 | ```json 525 | "nodes": [ 526 | { "name": "POP01-bb01", "group": "core", "icon": "./images/router.png" }, 527 | { "name": "POP01-bb02", "group": "core", "icon": "./images/router.png" }, 528 | ... 529 | ``` 530 | 531 | ![Internal group](example/images/internal_group.png) 532 | 533 | ### Bundle Mark 534 | 535 | You can show a "tie" over bundled links by `bundle:` definition in each link. 536 | 537 | ```json 538 | "links": [ 539 | { "source": "POP01-bb01", "target": "POP01-bb02", "bundle": "lag 1" }, 540 | { "source": "POP01-bb01", "target": "POP01-bb02", "bundle": "lag 1" }, 541 | { "source": "POP01-bb01", "target": "POP01-bb02"} 542 | ] 543 | ``` 544 | 545 | ![Bundle example](example/images/bundle02.png) 546 | 547 | - Define bundle name as `bundle:` to specify which link belongs to the bundle ( integer or string value ) 548 | - Set `bundle: true` when initializing `Diagram`. See [Display bundles](#display-bundles) section for details. 549 | - inet-henge draws a "tie" when there are multiple links in bundle among the same node pair. It doesn't draw the mark between different node pairs, even when bundle names are the same. 550 | 551 | :warning: Multi-chassis bundling is not supported. 552 | 553 | ## Plugins 554 | 555 | | Name | Note | 556 | | --------------------------------------------------------------- | --------------------------------- | 557 | | [Removable Node Plugin](../../tree/main/plugins/removable_node) | Hide and show nodes by key inputs | 558 | | [Arrows Link Plugin](../../tree/main/plugins/arrows_link) | Make links bidirectional arrows | 559 | 560 | ## Contributing 561 | 562 | Please report issues or enhancement requests to [GitHub issues](https://github.com/codeout/inet-henge/issues). 563 | For questions or feedbacks write to my twitter @codeout. 564 | 565 | Or send a pull request to fix. 566 | 567 | ## Copyright and License 568 | 569 | Copyright (c) 2016-2024 Shintaro Kojima. Code released under the [MIT license](LICENSE). 570 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ["@commitlint/config-conventional"], 3 | rules: { 4 | "subject-case": [0, "always"], 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import prettierPlugin from "eslint-config-prettier"; 2 | import simpleImportSortPlugin from "eslint-plugin-simple-import-sort"; 3 | import eslint from "@eslint/js"; 4 | import tsEslint from "typescript-eslint"; 5 | 6 | 7 | /** @type {import("eslint").Linter.FlatConfig[]} */ 8 | export default tsEslint.config( 9 | eslint.configs.recommended, 10 | ...tsEslint.configs.recommended, 11 | prettierPlugin, 12 | { 13 | plugins: { 14 | "simple-import-sort": simpleImportSortPlugin 15 | }, 16 | rules: { 17 | "@typescript-eslint/consistent-type-assertions": [ 18 | "error", 19 | { 20 | assertionStyle: "as" 21 | } 22 | ], 23 | "simple-import-sort/imports": "error", 24 | "simple-import-sort/exports": "error" 25 | } 26 | } 27 | ); 28 | 29 | 30 | -------------------------------------------------------------------------------- /example/clos.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 14 | 15 | 16 | 17 |
18 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /example/clos.json: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": [ 3 | { "name": "spine01" }, 4 | { "name": "spine02" }, 5 | { "name": "leaf01" }, 6 | { "name": "leaf02" }, 7 | { "name": "leaf03" }, 8 | { "name": "leaf04" }, 9 | { "name": "leaf05" }, 10 | { "name": "leaf06" }, 11 | { "name": "server01" }, 12 | { "name": "server02" }, 13 | { "name": "server03" }, 14 | { "name": "server04" }, 15 | { "name": "server05" }, 16 | { "name": "server06" } 17 | ], 18 | 19 | "links": [ 20 | {"source": "spine01", "target": "leaf01" }, 21 | {"source": "spine01", "target": "leaf02" }, 22 | {"source": "spine01", "target": "leaf03" }, 23 | {"source": "spine01", "target": "leaf04" }, 24 | {"source": "spine01", "target": "leaf05" }, 25 | {"source": "spine01", "target": "leaf06" }, 26 | {"source": "spine02", "target": "leaf01" }, 27 | {"source": "spine02", "target": "leaf02" }, 28 | {"source": "spine02", "target": "leaf03" }, 29 | {"source": "spine02", "target": "leaf04" }, 30 | {"source": "spine02", "target": "leaf05" }, 31 | {"source": "spine02", "target": "leaf06" }, 32 | {"source": "server01", "target": "leaf01"}, 33 | {"source": "server01", "target": "leaf02"}, 34 | {"source": "server02", "target": "leaf01"}, 35 | {"source": "server02", "target": "leaf02"}, 36 | {"source": "server03", "target": "leaf03"}, 37 | {"source": "server03", "target": "leaf04"}, 38 | {"source": "server04", "target": "leaf03"}, 39 | {"source": "server04", "target": "leaf04"}, 40 | {"source": "server05", "target": "leaf05"}, 41 | {"source": "server05", "target": "leaf06"}, 42 | {"source": "server06", "target": "leaf05"}, 43 | {"source": "server06", "target": "leaf06"} 44 | ] 45 | } 46 | -------------------------------------------------------------------------------- /example/images/bundle01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeout/inet-henge/c849bcd96952662db594b9f89a3722c6eb21300f/example/images/bundle01.png -------------------------------------------------------------------------------- /example/images/bundle02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeout/inet-henge/c849bcd96952662db594b9f89a3722c6eb21300f/example/images/bundle02.png -------------------------------------------------------------------------------- /example/images/clos.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeout/inet-henge/c849bcd96952662db594b9f89a3722c6eb21300f/example/images/clos.png -------------------------------------------------------------------------------- /example/images/group_size.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeout/inet-henge/c849bcd96952662db594b9f89a3722c6eb21300f/example/images/group_size.png -------------------------------------------------------------------------------- /example/images/internal_group.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeout/inet-henge/c849bcd96952662db594b9f89a3722c6eb21300f/example/images/internal_group.png -------------------------------------------------------------------------------- /example/images/ix.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeout/inet-henge/c849bcd96952662db594b9f89a3722c6eb21300f/example/images/ix.png -------------------------------------------------------------------------------- /example/images/link_label.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeout/inet-henge/c849bcd96952662db594b9f89a3722c6eb21300f/example/images/link_label.png -------------------------------------------------------------------------------- /example/images/link_width.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeout/inet-henge/c849bcd96952662db594b9f89a3722c6eb21300f/example/images/link_width.png -------------------------------------------------------------------------------- /example/images/nat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeout/inet-henge/c849bcd96952662db594b9f89a3722c6eb21300f/example/images/nat.png -------------------------------------------------------------------------------- /example/images/node_label.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeout/inet-henge/c849bcd96952662db594b9f89a3722c6eb21300f/example/images/node_label.png -------------------------------------------------------------------------------- /example/images/node_size.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeout/inet-henge/c849bcd96952662db594b9f89a3722c6eb21300f/example/images/node_size.png -------------------------------------------------------------------------------- /example/images/openflow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeout/inet-henge/c849bcd96952662db594b9f89a3722c6eb21300f/example/images/openflow.png -------------------------------------------------------------------------------- /example/images/position_hints.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeout/inet-henge/c849bcd96952662db594b9f89a3722c6eb21300f/example/images/position_hints.png -------------------------------------------------------------------------------- /example/images/router.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeout/inet-henge/c849bcd96952662db594b9f89a3722c6eb21300f/example/images/router.png -------------------------------------------------------------------------------- /example/images/shownet.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeout/inet-henge/c849bcd96952662db594b9f89a3722c6eb21300f/example/images/shownet.png -------------------------------------------------------------------------------- /example/images/switch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeout/inet-henge/c849bcd96952662db594b9f89a3722c6eb21300f/example/images/switch.png -------------------------------------------------------------------------------- /example/images/tooltip.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeout/inet-henge/c849bcd96952662db594b9f89a3722c6eb21300f/example/images/tooltip.png -------------------------------------------------------------------------------- /example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /example/index.json: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": [ 3 | { 4 | "name": "POP01-bb01", 5 | "meta": { "description": "This is a router", "type": "Backbone" }, 6 | "icon": "./images/router.png" 7 | }, 8 | { 9 | "name": "POP01-bb02", 10 | "meta": { "description": "This is a router", "type": "Backbone" }, 11 | "icon": "./images/router.png" 12 | }, 13 | { 14 | "name": "POP01-ag01", 15 | "meta": { "description": "This is a router", "type": "Aggregation" }, 16 | "icon": "./images/switch.png" 17 | }, 18 | { 19 | "name": "POP01-ag02", 20 | "meta": { "description": "This is a router", "type": "Aggregation" }, 21 | "icon": "./images/switch.png" 22 | }, 23 | { "name": "POP02-bb01", "meta": { "loopback": "10.0.0.1" } }, 24 | { "name": "POP02-bb02" }, 25 | { "name": "POP02-ag01" }, 26 | { "name": "POP02-ag02" }, 27 | { "name": "POP03-bb01" }, 28 | { "name": "POP03-bb02" }, 29 | { "name": "POP03-ag01" }, 30 | { "name": "POP03-ag02" } 31 | ], 32 | 33 | "links": [ 34 | { "source": "POP01-bb01", "target": "POP01-bb02" }, 35 | { "source": "POP02-bb01", "target": "POP02-bb02" }, 36 | { 37 | "source": "POP03-bb01", 38 | "target": "POP03-bb02", 39 | "meta": { "interface": { "source": "ge-0/0/0", "target": "Te0/0/0/0" } } 40 | }, 41 | { "source": "POP01-bb01", "target": "POP02-bb01" }, 42 | { "source": "POP01-bb01", "target": "POP03-bb01" }, 43 | { "source": "POP01-bb02", "target": "POP02-bb02" }, 44 | { 45 | "source": "POP01-bb02", 46 | "target": "POP03-bb02", 47 | "meta": { "interface": { "source": "ge-0/0/0", "target": "Te0/0/0/0" } } 48 | }, 49 | { 50 | "source": "POP01-bb02", 51 | "target": "POP03-bb02", 52 | "meta": { "interface": { "source": "ge-0/0/1", "target": "Te0/0/0/1" } } 53 | }, 54 | { "source": "POP02-bb01", "target": "POP03-bb02" }, 55 | { "source": "POP02-bb02", "target": "POP03-bb01" }, 56 | { "source": "POP01-ag01", "target": "POP01-ag02" }, 57 | { "source": "POP02-ag01", "target": "POP02-ag02" }, 58 | { "source": "POP03-ag01", "target": "POP03-ag02" }, 59 | { "source": "POP01-bb01", "target": "POP01-ag01" }, 60 | { "source": "POP01-bb01", "target": "POP01-ag02" }, 61 | { "source": "POP01-bb02", "target": "POP01-ag01" }, 62 | { "source": "POP01-bb02", "target": "POP01-ag02" }, 63 | { "source": "POP02-bb01", "target": "POP02-ag01" }, 64 | { "source": "POP02-bb01", "target": "POP02-ag02" }, 65 | { "source": "POP02-bb02", "target": "POP02-ag01" }, 66 | { "source": "POP02-bb02", "target": "POP02-ag02" }, 67 | { "source": "POP03-bb01", "target": "POP03-ag01" }, 68 | { "source": "POP03-bb01", "target": "POP03-ag02" }, 69 | { "source": "POP03-bb02", "target": "POP03-ag01" }, 70 | { "source": "POP03-bb02", "target": "POP03-ag02" } 71 | ] 72 | } 73 | -------------------------------------------------------------------------------- /example/shownet.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /example/shownet.json: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": [ 3 | { "name": "dceast-ne40e", "icon": "./images/router.png" }, 4 | { "name": "dceast-fx1-1", "icon": "./images/router.png" }, 5 | { "name": "dceast-fx1-2", "icon": "./images/router.png" }, 6 | { "name": "dceast-pf5240", "icon": "./images/openflow.png" }, 7 | { "name": "dceast-ce6851", "icon": "./images/router.png" }, 8 | { "name": "dceast-nx92k", "icon": "./images/router.png" }, 9 | 10 | { "name": "noc-asr9904", "icon": "./images/router.png" }, 11 | { "name": "noc-mx2010", "icon": "./images/router.png" }, 12 | { "name": "noc-ax8616r", "icon": "./images/router.png" }, 13 | { "name": "noc-ne5000e", "icon": "./images/router.png" }, 14 | { "name": "noc-asr9006", "icon": "./images/router.png" }, 15 | { "name": "noc-ce12k", "icon": "./images/router.png" }, 16 | { "name": "noc-mx240", "icon": "./images/router.png" }, 17 | { "name": "noc-ptx3k", "icon": "./images/router.png" }, 18 | { "name": "noc-qfx10k", "icon": "./images/router.png" }, 19 | { "name": "noc-nx7706", "icon": "./images/router.png" }, 20 | { "name": "noc-cat6807-1", "icon": "./images/router.png" }, 21 | { "name": "noc-cat6807-2", "icon": "./images/router.png" }, 22 | 23 | { "name": "noc-ex3300-0", "icon": "./images/switch.png" }, 24 | { "name": "pod4-ex3300", "icon": "./images/switch.png" }, 25 | { "name": "pod5-1-s3048", "icon": "./images/switch.png" }, 26 | { "name": "pod5-2-s3048", "icon": "./images/switch.png" }, 27 | 28 | { "name": "service-ax8308s", "icon": "./images/router.png" }, 29 | { "name": "service-ip88s86", "icon": "./images/router.png" }, 30 | 31 | { "name": "conf-s4048-1", "icon": "./images/router.png" }, 32 | { "name": "conf-s4048-2", "icon": "./images/router.png" }, 33 | 34 | { "name": "life-ip88s46-1", "icon": "./images/router.png" }, 35 | { "name": "life-ip88s46-2", "icon": "./images/router.png" }, 36 | 37 | { "name": "BBIX", "icon": "./images/ix.png" }, 38 | { "name": "JPIX", "icon": "./images/ix.png" }, 39 | { "name": "EQUINIX", "icon": "./images/ix.png" }, 40 | { "name": "NTTCom", "icon": "./images/ix.png" }, 41 | 42 | { "name": "dcwest-z9100", "icon": "./images/router.png" }, 43 | { "name": "dcwest-nx3232c", "icon": "./images/router.png" }, 44 | { "name": "dcwest-ce8860", "icon": "./images/router.png" }, 45 | { "name": "dcwest-qfx5100", "icon": "./images/router.png" }, 46 | { "name": "dcwest-nx9504", "icon": "./images/router.png" }, 47 | { "name": "dcwest-nx9272", "icon": "./images/router.png" }, 48 | { "name": "dcwest-fx1-3", "icon": "./images/router.png" } 49 | ], 50 | 51 | "links": [ 52 | { 53 | "source": "noc-asr9904", 54 | "target": "noc-ax8616r", 55 | "meta": { "interface": { "source": "0-0-0-2", "target": "1-1" } } 56 | }, 57 | { 58 | "source": "noc-asr9904", 59 | "target": "noc-ne5000e", 60 | "meta": { "interface": { "source": "0-0-0-3", "target": "3-0-0" } } 61 | }, 62 | { 63 | "source": "noc-mx2010", 64 | "target": "noc-ax8616r", 65 | "meta": { "interface": { "source": "0-0-1", "target": "5-1" } } 66 | }, 67 | { 68 | "source": "noc-mx2010", 69 | "target": "noc-ne5000e", 70 | "meta": { "interface": { "source": "0-0-2", "target": "3-0-1" } } 71 | }, 72 | { 73 | "source": "noc-ax8616r", 74 | "target": "noc-ne5000e", 75 | "meta": { "interface": { "source": "13-1", "target": "3-0-2" } } 76 | }, 77 | 78 | { 79 | "source": "noc-ax8616r", 80 | "target": "noc-asr9006", 81 | "meta": { "interface": { "source": "9-1", "target": "0-2-0-5" } } 82 | }, 83 | { 84 | "source": "noc-ne5000e", 85 | "target": "noc-ce12k", 86 | "meta": { "interface": { "source": "3-0-4", "target": "1-0-0" } } 87 | }, 88 | { 89 | "source": "noc-asr9006", 90 | "target": "noc-ce12k", 91 | "meta": { "interface": { "source": "0-2-0-1", "target": "1-0-1" } } 92 | }, 93 | 94 | { 95 | "source": "noc-asr9006", 96 | "target": "noc-mx240", 97 | "meta": { "interface": { "source": "2-0-2", "target": "1-1-0-0" } } 98 | }, 99 | { "source": "noc-ce12k", "target": "noc-ptx3k", "meta": { "interface": { "source": "1-0-2", "target": "0-0-0" } } }, 100 | { "source": "noc-mx240", "target": "noc-ptx3k", "meta": { "interface": { "source": "1-3-0", "target": "0-0-1" } } }, 101 | 102 | { 103 | "source": "noc-mx240", 104 | "target": "noc-qfx10k", 105 | "meta": { "interface": { "source": "2-1-0", "target": "0-0-1" } } 106 | }, 107 | { "source": "noc-ptx3k", "target": "noc-nx7706", "meta": { "interface": { "source": "2-0-0", "target": "1-1" } } }, 108 | { "source": "noc-qfx10k", "target": "noc-nx7706", "meta": { "interface": { "source": "0-0-5", "target": "1-2" } } }, 109 | 110 | { 111 | "source": "noc-qfx10k", 112 | "target": "noc-cat6807-1", 113 | "meta": { "interface": { "source": "0-0-19-1", "target": "1-2-1" } } 114 | }, 115 | { 116 | "source": "noc-nx7706", 117 | "target": "noc-cat6807-2", 118 | "meta": { "interface": { "source": "2-2", "target": "2-2-1" } } 119 | }, 120 | 121 | { 122 | "source": "noc-cat6807-1", 123 | "target": "noc-ex3300-0", 124 | "meta": { "interface": { "source": "1-2-2", "target": "0-1-0" } } 125 | }, 126 | { 127 | "source": "noc-cat6807-1", 128 | "target": "pod5-1-s3048", 129 | "meta": { "interface": { "source": "1-2-3", "target": "1-49" } } 130 | }, 131 | { 132 | "source": "noc-cat6807-2", 133 | "target": "pod4-ex3300", 134 | "meta": { "interface": { "source": "2-2-2", "target": "1-1-0" } } 135 | }, 136 | { 137 | "source": "noc-cat6807-2", 138 | "target": "pod5-2-s3048", 139 | "meta": { "interface": { "source": "2-2-3", "target": "2-49" } } 140 | }, 141 | 142 | { 143 | "source": "noc-ex3300-0", 144 | "target": "pod4-ex3300", 145 | "meta": { "interface": { "source": "0-1-1", "target": "1-1-1" } } 146 | }, 147 | { 148 | "source": "pod5-1-s3048", 149 | "target": "pod5-2-s3048", 150 | "meta": { "interface": { "source": "1-51", "target": "2-51" } } 151 | }, 152 | 153 | { 154 | "source": "noc-nx7706", 155 | "target": "service-ip88s86", 156 | "meta": { "interface": { "source": "2-4", "target": "2-1" } } 157 | }, 158 | { 159 | "source": "noc-qfx10k", 160 | "target": "service-ax8308s", 161 | "meta": { "interface": { "source": "0-0-19-3", "target": "1-1" } } 162 | }, 163 | { 164 | "source": "service-ip88s86", 165 | "target": "service-ax8308s", 166 | "meta": { "interface": { "source": "1-2", "target": "2-2" } } 167 | }, 168 | 169 | { "source": "noc-nx7706", "target": "conf-s4048-2", "meta": { "interface": { "source": "2-3", "target": "2-1" } } }, 170 | { 171 | "source": "noc-qfx10k", 172 | "target": "conf-s4048-1", 173 | "meta": { "interface": { "source": "0-0-19-2", "target": "1-1" } } 174 | }, 175 | { 176 | "source": "conf-s4048-1", 177 | "target": "conf-s4048-2", 178 | "meta": { "interface": { "source": "1-53", "target": "2-53" } } 179 | }, 180 | { 181 | "source": "conf-s4048-1", 182 | "target": "conf-s4048-2", 183 | "meta": { "interface": { "source": "1-54", "target": "2-54" } } 184 | }, 185 | 186 | { 187 | "source": "noc-nx7706", 188 | "target": "life-ip88s46-1", 189 | "meta": { "interface": { "source": "2-1", "target": "2-1-1" } } 190 | }, 191 | { 192 | "source": "noc-qfx10k", 193 | "target": "life-ip88s46-2", 194 | "meta": { "interface": { "source": "0-0-19-0", "target": "1-1-1" } } 195 | }, 196 | { 197 | "source": "life-ip88s46-1", 198 | "target": "life-ip88s46-2", 199 | "meta": { "interface": { "source": "2-1-2", "target": "1-1-2" } } 200 | }, 201 | 202 | { 203 | "source": "noc-ce12k", 204 | "target": "dceast-ne40e", 205 | "meta": { "interface": { "source": "2-0-0", "target": "0-3-0" } } 206 | }, 207 | { 208 | "source": "noc-ce12k", 209 | "target": "dceast-fx1-1", 210 | "meta": { "interface": { "source": "2-0-1", "target": "1-1" } } 211 | }, 212 | { 213 | "source": "noc-ptx3k", 214 | "target": "dceast-ne40e", 215 | "meta": { "interface": { "source": "4-0-0", "target": "0-3-1" } } 216 | }, 217 | { 218 | "source": "noc-ptx3k", 219 | "target": "dceast-fx1-2", 220 | "meta": { "interface": { "source": "4-0-1", "target": "1-1" } } 221 | }, 222 | 223 | { 224 | "source": "dceast-ne40e", 225 | "target": "dceast-pf5240", 226 | "meta": { "interface": { "source": "0-3-3", "target": "0-49" } } 227 | }, 228 | { 229 | "source": "dceast-fx1-1", 230 | "target": "dceast-pf5240", 231 | "meta": { "interface": { "source": "1-3", "target": "0-1" } } 232 | }, 233 | { 234 | "source": "dceast-fx1-1", 235 | "target": "dceast-ce6851", 236 | "meta": { "interface": { "source": "1-2", "target": "1-0-1" } } 237 | }, 238 | { 239 | "source": "dceast-fx1-1", 240 | "target": "dceast-nx92k", 241 | "meta": { "interface": { "source": "1-2", "target": "1-46" } } 242 | }, 243 | { 244 | "source": "dceast-fx1-2", 245 | "target": "dceast-pf5240", 246 | "meta": { "interface": { "source": "1-3", "target": "0-2" } } 247 | }, 248 | { 249 | "source": "dceast-fx1-2", 250 | "target": "dceast-ce6851", 251 | "meta": { "interface": { "source": "1-2", "target": "1-0-2" } } 252 | }, 253 | { 254 | "source": "dceast-fx1-2", 255 | "target": "dceast-nx92k", 256 | "meta": { "interface": { "source": "1-2", "target": "1-47" } } 257 | }, 258 | 259 | { 260 | "source": "dceast-pf5240", 261 | "target": "dceast-ce6851", 262 | "meta": { "interface": { "source": "0-51", "target": "1-48" } } 263 | }, 264 | { 265 | "source": "dceast-pf5240", 266 | "target": "dceast-nx92k", 267 | "meta": { "interface": { "source": "0-50", "target": "1-0-3" } } 268 | }, 269 | 270 | { "source": "noc-asr9904", "target": "BBIX", "meta": { "interface": { "source": "0-0-0-1" } } }, 271 | { "source": "noc-asr9904", "target": "JPIX", "meta": { "interface": { "source": "0-0-0-0" } } }, 272 | { "source": "noc-mx2010", "target": "EQUINIX", "meta": { "interface": { "source": "1-0-0" } } }, 273 | { "source": "noc-mx2010", "target": "NTTCom", "meta": { "interface": { "source": "0-0-0" } } }, 274 | 275 | { 276 | "source": "noc-asr9006", 277 | "target": "dcwest-z9100", 278 | "meta": { "interface": { "source": "0-2-0-3", "target": "1-1" } } 279 | }, 280 | { 281 | "source": "noc-mx240", 282 | "target": "dcwest-nx3232c", 283 | "meta": { "interface": { "source": "2-3-0", "target": "1-1" } } 284 | }, 285 | 286 | { 287 | "source": "dcwest-z9100", 288 | "target": "dcwest-ce8860", 289 | "meta": { "interface": { "source": "1-2", "target": "1-1-1" } } 290 | }, 291 | { 292 | "source": "dcwest-z9100", 293 | "target": "dcwest-qfx5100", 294 | "meta": { "interface": { "source": "1-3-1", "target": "0-0-48" } } 295 | }, 296 | { 297 | "source": "dcwest-z9100", 298 | "target": "dcwest-nx9504", 299 | "meta": { "interface": { "source": "1-4-1", "target": "1-49" } } 300 | }, 301 | { 302 | "source": "dcwest-z9100", 303 | "target": "dcwest-nx9272", 304 | "meta": { "interface": { "source": "1-5-1", "target": "1-1" } } 305 | }, 306 | { 307 | "source": "dcwest-z9100", 308 | "target": "dcwest-fx1-3", 309 | "meta": { "interface": { "source": "1-33", "target": "1-1" } } 310 | }, 311 | 312 | { 313 | "source": "dcwest-nx3232c", 314 | "target": "dcwest-ce8860", 315 | "meta": { "interface": { "source": "1-2", "target": "1-1-2" } } 316 | }, 317 | { 318 | "source": "dcwest-nx3232c", 319 | "target": "dcwest-qfx5100", 320 | "meta": { "interface": { "source": "1-3", "target": "0-0-49" } } 321 | }, 322 | { 323 | "source": "dcwest-nx3232c", 324 | "target": "dcwest-nx9504", 325 | "meta": { "interface": { "source": "1-4", "target": "1-50" } } 326 | }, 327 | { 328 | "source": "dcwest-nx3232c", 329 | "target": "dcwest-nx9272", 330 | "meta": { "interface": { "source": "1-5", "target": "1-2" } } 331 | }, 332 | { 333 | "source": "dcwest-nx3232c", 334 | "target": "dcwest-fx1-3", 335 | "meta": { "interface": { "source": "1-33", "target": "1-2" } } 336 | } 337 | ] 338 | } 339 | -------------------------------------------------------------------------------- /example/style.css: -------------------------------------------------------------------------------- 1 | .group rect { 2 | opacity: 0.5; 3 | } 4 | 5 | .node, 6 | .group rect { 7 | cursor: move; 8 | } 9 | 10 | .link text { 11 | cursor: default; 12 | } 13 | 14 | .node rect, 15 | .group rect { 16 | stroke: #fff; 17 | stroke-width: 1.5px; 18 | } 19 | 20 | .node text { 21 | font-size: 10px; 22 | } 23 | 24 | .link line { 25 | stroke: #7a4e4e; 26 | stroke-width: 3px; 27 | stroke-opacity: 1; 28 | } 29 | 30 | .link text { 31 | font-size: 7px; 32 | } 33 | 34 | .link text.hover:not(.short) tspan { 35 | fill: #00b0e8; 36 | font-weight: bold; 37 | visibility: visible !important; 38 | } 39 | 40 | .link text.hover tspan { 41 | font-size: 10px; 42 | } 43 | 44 | .link line:hover { 45 | stroke: #00b0e8; 46 | } 47 | 48 | .tooltip { 49 | font-size: 12px; 50 | font-weight: bold; 51 | --label-color: #8d7966; 52 | --hovered-link-color: #58a6ff; 53 | } 54 | 55 | .tooltip > path { 56 | stroke: #e2ddd9; 57 | stroke-width: 3px; 58 | } 59 | 60 | .tooltip .name { 61 | fill: var(--label-color); 62 | } 63 | 64 | .tooltip .icon { 65 | stroke: var(--label-color); 66 | } 67 | .tooltip .icon:hover { 68 | stroke: var(--hovered-link-color); 69 | } 70 | 71 | .tooltip a:hover { 72 | fill: var(--hovered-link-color); 73 | } 74 | 75 | .loopback, 76 | .interface { 77 | fill: #333; 78 | } 79 | -------------------------------------------------------------------------------- /inet-henge.min.js.LICENSE.txt: -------------------------------------------------------------------------------- 1 | /*! 2 | * inet-henge v1.4.7 3 | * @author Shintaro Kojima 4 | * @license MIT 5 | * Copyright (c) 2016-2024 Shintaro Kojima 6 | */ 7 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "inet-henge", 3 | "version": "1.4.7", 4 | "description": "Generate d3.js based Network Diagram from JSON data.", 5 | "license": "MIT", 6 | "keywords": [ 7 | "network", 8 | "diagram", 9 | "d3.js", 10 | "cola.js" 11 | ], 12 | "repository": { 13 | "type": "git", 14 | "url": "git://github.com/codeout/inet-henge.git" 15 | }, 16 | "bugs": { 17 | "url": "https://github.com/codeout/inet-henge/issues" 18 | }, 19 | "author": { 20 | "name": "Shintaro Kojima", 21 | "email": "goodies@codeout.net", 22 | "url": "http://github.com/codeout" 23 | }, 24 | "main": "inet-henge.js", 25 | "devDependencies": { 26 | "@eslint/js": "^9.13.0", 27 | "@types/crypto-js": "^4.2.2", 28 | "@types/d3": "~3", 29 | "@types/node": "^20.17.1", 30 | "crypto-js": "^4.2.0", 31 | "d3": "~3", 32 | "eslint": "^9.13.0", 33 | "eslint-config-prettier": "^9.1.0", 34 | "eslint-plugin-simple-import-sort": "^12.1.1", 35 | "prettier": "^3.3.3", 36 | "ts-loader": "^9.5.1", 37 | "typescript": "^5.6.3", 38 | "typescript-eslint": "^8.11.0", 39 | "webcola": "^3.4.0", 40 | "webpack": "^5.95.0", 41 | "webpack-cli": "^5.1.4", 42 | "webpack-merge": "^6.0.1" 43 | }, 44 | "scripts": { 45 | "lint": "npx eslint $INIT_CWD/src", 46 | "build": "cd $INIT_CWD && npm run clean && npm run dev && npm run prod", 47 | "type-check": "npx tsc --project $INIT_CWD/tsconfig.json --noEmit", 48 | "check": "npm run lint && npm run type-check", 49 | "dev": "npx webpack --config $INIT_CWD/webpack.dev.js", 50 | "prod": "npx webpack --config $INIT_CWD/webpack.prod.js", 51 | "clean": "rm -rf $INIT_CWD/{inet-henge*.js*, plugin*.js*, dist}", 52 | "watch": "npx webpack --config $INIT_CWD/webpack.dev.js --watch", 53 | "format": "npx prettier --ignore-unknown --ignore-path .gitignore --write", 54 | "build-all": "npm run build && for i in plugins/*; do cd $i; echo 🔵 $i; npm run build; cd -; done", 55 | "type-check-all": "npm run type-check && for i in plugins/*; do cd $i; echo 🔵 $i; npm run type-check; cd -; done", 56 | "lint-all": "npm run lint && for i in plugins/*; do cd $i; echo 🔵 $i; npm run lint; cd -; done", 57 | "check-all": "npm run lint-all && npm run type-check-all", 58 | "format-all": "npx prettier --ignore-unknown --ignore-path .gitignore --write **/*.ts" 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /plugins/arrows_link/README.md: -------------------------------------------------------------------------------- 1 | # Arrows Link Plugin 2 | 3 | Make links bidirectional arrows. 4 | 5 | ![screenshot](docs/images/screenshot01.png) 6 | 7 | 8 | ## Usage 9 | 10 | ```html 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 |
27 | 34 | 35 | 36 | 37 | ``` 38 | 39 | :memo: Links in JSON data should be bidirectional. 40 | 41 | ```js 42 | { 43 | "nodes": [ 44 | { "name": "POP01-bb01" }, 45 | { "name": "POP01-bb02" } 46 | ], 47 | 48 | // A unidirectional link results in a unidirectional arrow 49 | "links": [ 50 | { "source": "POP01-bb01", "target": "POP01-bb02" } 51 | ] 52 | 53 | // So do this 54 | "links": [ 55 | { "source": "POP01-bb01", "target": "POP01-bb02" }, 56 | { "source": "POP01-bb02", "target": "POP01-bb01" } 57 | ] 58 | } 59 | ``` 60 | 61 | :warning: This plugin conflicts with `bundle: true` option. 62 | 63 | ```js 64 | const diagram = new Diagram("#diagram", "index.json", { 65 | // Don't do this 66 | bundle: true, 67 | }); 68 | ``` -------------------------------------------------------------------------------- /plugins/arrows_link/docs/images/screenshot01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeout/inet-henge/c849bcd96952662db594b9f89a3722c6eb21300f/plugins/arrows_link/docs/images/screenshot01.png -------------------------------------------------------------------------------- /plugins/arrows_link/plugin.min.js: -------------------------------------------------------------------------------- 1 | !function(t,e){if("object"==typeof exports&&"object"==typeof module)module.exports=e(require("d3"));else if("function"==typeof define&&define.amd)define(["d3"],e);else{var s="object"==typeof exports?e(require("d3")):e(t.d3);for(var r in s)("object"==typeof exports?exports:t)[r]=s[r]}}(self,(t=>(()=>{"use strict";var e={893:e=>{e.exports=t}},s={};function r(t){var a=s[t];if(void 0!==a)return a.exports;var i=s[t]={exports:{}};return e[t](i,i.exports,r),i.exports}r.d=(t,e)=>{for(var s in e)r.o(e,s)&&!r.o(t,s)&&Object.defineProperty(t,s,{enumerable:!0,get:e[s]})},r.o=(t,e)=>Object.prototype.hasOwnProperty.call(t,e),r.r=t=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(t,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(t,"__esModule",{value:!0})};var a={};r.r(a),r.d(a,{ArrowsLinkPlugin:()=>v,default:()=>M});var i=r(893);class n{constructor(t,e){this.id=e,this.links=t,this.color="#7a4e4e",this.width=2,this.space=4}static divide(t){n.groups={};for(const e of t)if(e.bundle){const t=this.groupKey(e);(n.groups[t]||(n.groups[t]=[])).push(e.id)}return Object.values(n.groups).map(((e,s)=>new n(e.map((e=>t[e])),s)))}static groupKey(t){return JSON.stringify([t.source,t.target,t.bundle])}static render(t,e){return t.selectAll(".bundle").data(e).enter().append("g").attr("class",(t=>t.class())).append("path").attr("d",(t=>t.d())).attr("stroke",(t=>t.color)).attr("stroke-width",(t=>t.width)).attr("fill","none").attr("id",(t=>t.bundleId()))}static reset(){n.groups=null}static sortByBundle(t){return t.sort(((t,e)=>{switch(!0){case!!t.bundle&&!e.bundle:return-1;case!t.bundle&&!!e.bundle:return 1;case!t.bundle&&!e.bundle:return 0;case t.bundle.toString()e.bundle.toString():return 1;default:return 0}}))}d(){const t=this.links[0].centerCoordinates(),e=this.links[this.links.length-1].centerCoordinates(),s=Math.sqrt(Math.pow(t[0]-e[0],2)+Math.pow(t[1]-e[1],2));if(0===s)return"";const r=this.links[0].angle()+90,a=[(t[0]-e[0])*this.space/s+t[0],(t[1]-e[1])*this.space/s+t[1]],i=[(e[0]-t[0])*this.space/s+e[0],(e[1]-t[1])*this.space/s+e[1]];return`M ${a[0]} ${a[1]} A ${s/2+10},5 ${r} 1,0 ${i[0]} ${i[1]}`}shiftMultiplier(){if(!this._shiftMultiplier){const t=this.links[0].group()||[];this._shiftMultiplier=this.links.reduce(((e,s)=>e+(s.id-(t.length-1)/2)),0)/2}return this._shiftMultiplier}static shiftBundle(t){t.attr("d",(t=>t.d()))}class(){return this.links[0].class().replace(/^link/,"bundle")}bundleId(){return`bundle${this.id}`}}class o{constructor(t,e){this.data=t,this.extraKey=e}get(t){return this.slice(t).filter((t=>"string"==typeof t.value))}slice(t){return this.data?this.extraKey?this.sliceWithExtraKey(t):this.sliceWithoutExtraKey(t):[]}sliceWithExtraKey(t){const e=[];return t.forEach((t=>{this.data[t]&&this.data[t][this.extraKey]&&e.push({class:t,value:this.data[t][this.extraKey]})})),e}sliceWithoutExtraKey(t){const e=[];return t.forEach((t=>{this.data[t]&&e.push({class:t,value:this.data[t]})})),e}}function l(t){return t.replace(" ","-").toLowerCase()}class h{constructor(t,e,s){this.id=e,this.options=s,this.name=t.name,this.group="string"==typeof t.group?[t.group]:t.group||[],this.icon=t.icon,this.metaList=new o(t.meta).get(s.metaKeys),this.meta=t.meta,this.extraClass=t.class||"",this.width=s.width||60,this.height=s.height||40,this.padding=3,this.tspanOffset="1.1em",this.register(e)}register(t){p.all=p.all||{},p.all[this.name]=t}transform(){return`translate(${this.x-this.width/2+this.padding}, ${this.y-this.height/2+this.padding})`}nodeWidth(){return this.width-2*this.padding}nodeHeight(){return this.height-2*this.padding}xForText(){return this.nodeWidth()/2}yForText(){return this.height/2}static idByName(t){if(void 0===p.all[t])throw`Unknown node "${t}"`;return p.all[t]}nodeId(){return l(this.name)}static render(t,e){const s=t.selectAll(".node").data(e).enter().append("g").attr("id",(t=>t.nodeId())).attr("name",(t=>t.name)).attr("transform",(t=>t.transform()));return s.each((function(t){t.icon?p.appendImage(this):p.appendRect(this),p.appendText(this)})),s}static appendText(t){const e=i.select(t).append("text").attr("text-anchor","middle").attr("x",(t=>t.xForText())).attr("y",(t=>t.yForText()));e.append("tspan").text((t=>t.name)).attr("x",(t=>t.xForText())),e.each((t=>{t.options.tooltip||p.appendMetaText(e,t.metaList)}))}static appendMetaText(t,e){e.forEach((e=>{t.append("tspan").attr("x",(t=>t.xForText())).attr("dy",(t=>t.tspanOffset)).attr("class",e.class).text(e.value)}))}static appendImage(t){i.select(t).attr("class",(t=>`node image ${l(t.name)} ${t.extraClass}`)).append("image").attr("xlink:href",(t=>t.icon)).attr("width",(t=>t.nodeWidth())).attr("height",(t=>t.nodeHeight()))}static appendRect(t){i.select(t).attr("class",(t=>`node rect ${l(t.name)} ${t.extraClass}`)).append("rect").attr("width",(t=>t.nodeWidth())).attr("height",(t=>t.nodeHeight())).attr("rx",5).attr("ry",5).style("fill",(t=>t.options.color(void 0)))}static tick(t){t.attr("transform",(t=>t.transform()))}static setPosition(t,e){t.attr("transform",((t,s)=>{var r,a,i,n;return null!==(null===(r=e[s])||void 0===r?void 0:r.x)&&void 0!==(null===(a=e[s])||void 0===a?void 0:a.x)&&null!==(null===(i=e[s])||void 0===i?void 0:i.y)&&void 0!==(null===(n=e[s])||void 0===n?void 0:n.y)&&(t.x=e[s].x,t.y=e[s].y),t.transform()}))}static reset(){p.all=null}}const d=t=>class extends t{constructor(t,e,s){super(t,e,s),this.dispatch=i.dispatch("rendered")}static render(t,e){const s=super.render(t,e);return s.each((function(t){t.dispatch.rendered(this)})),s}on(t,e){this.dispatch.on(t,e)}},c=t=>{class e extends t{constructor(t,s,r){super(t,s,r);for(const a of e.pluginConstructors)a.bind(this)(t,s,r)}static registerConstructor(t){e.pluginConstructors.push(t)}}return e.pluginConstructors=[],e};class u extends(d(h)){}class p extends(c(u)){}class g{constructor(t,e,s){this.id=e,this.options=s,this.source=p.idByName(t.source),this.target=p.idByName(t.target),this.bundle=t.bundle,this.metaList=new o(t.meta).get(s.metaKeys),this.sourceMeta=new o(t.meta,"source").get(s.metaKeys),this.targetMeta=new o(t.meta,"target").get(s.metaKeys),this.extraClass=t.class||"","function"==typeof s.linkWidth?this.width=s.linkWidth(t.meta)||3:this.width=s.linkWidth||3,this.defaultMargin=15,this.labelXOffset=20,this.labelYOffset=1.5,this.color="#7a4e4e",this.register(e)}register(t){y.groups=y.groups||{};const e=[this.source,this.target].sort().toString();(y.groups[e]||(y.groups[e]=[])).push(t)}isLabelledPath(){return this.metaList.length>0}isReversePath(){return this.targetMeta.length>0}d(){return`M ${this.source.x} ${this.source.y} L ${this.target.x} ${this.target.y}`}pathId(){return`path${this.id}`}linkId(){return`link${this.id}`}margin(){if(!this._margin){const t=window.getComputedStyle(document.getElementById(this.linkId())).margin;this._margin=t&&"0px"!==t?parseInt(t):this.defaultMargin}return this._margin}isLabelVisible(){const t=document.getElementById(this.pathId()).getTotalLength(),e=Array.from(document.getElementsByClassName(this.pathId())).some((e=>{const s=e.firstChild;return s.classList.contains("center")?s.getComputedTextLength()>t:s.getComputedTextLength()+this.labelXOffset>t}));return i.selectAll(`text.${this.pathId()}`).classed("short",e),y.scale>1.5&&!e}group(){return y.groups[[this.source.id,this.target.id].sort().toString()]}tspanXOffset(){switch(!0){case this.isLabelledPath():return 0;case this.isReversePath():return-this.labelXOffset;default:return this.labelXOffset}}tspanYOffset(){return this.isLabelledPath()?.7-this.labelYOffset+"em":`${this.labelYOffset}em`}rotate(t){return this.source.x>this.target.x?`rotate(180 ${t.x+t.width/2} ${t.y+t.height/2})`:"rotate(0)"}split(){if(!this.metaList&&!this.sourceMeta&&!this.targetMeta)return[this];const t=[];return["metaList","sourceMeta","targetMeta"].forEach(((e,s,r)=>{if(this[e]){const s=Object.assign(Object.create(this),this);r.filter((t=>t!==e)).forEach((t=>s[t]=[])),t.push(s)}})),t}hasMeta(){return this.metaList.length>0||this.sourceMeta.length>0||this.targetMeta.length>0}class(){return`link ${l(this.source.name)} ${l(this.target.name)} ${l(this.source.name)}-${l(this.target.name)} ${this.extraClass}`}centerCoordinates(){const t=i.select(`.link #${this.linkId()}`).node(),e=t.getBBox(),s=t.transform.baseVal.consolidate();return[e.x+e.width/2+((null==s?void 0:s.matrix.e)||0),e.y+e.height/2+((null==s?void 0:s.matrix.f)||0)]}angle(){const t=i.select(`.link #${this.linkId()}`).node();return 180*Math.atan2(t.y2.baseVal.value-t.y1.baseVal.value,t.x2.baseVal.value-t.x1.baseVal.value)/Math.PI}static render(t,e,s){const r=t.selectAll(".link").data(s).enter().append("g").attr("class",(t=>t.class())),a=r.append("line").attr("x1",(t=>t.source.x)).attr("y1",(t=>t.source.y)).attr("x2",(t=>t.target.x)).attr("y2",(t=>t.target.y)).attr("stroke",(t=>t.color)).attr("stroke-width",(t=>t.width)).attr("id",(t=>t.linkId())).on("mouseover.line",(t=>n.selectAll(`text.${t.pathId()}`).classed("hover",!0))).on("mouseout.line",(t=>n.selectAll(`text.${t.pathId()}`).classed("hover",!1))),i=r.append("path").attr("d",(t=>t.d())).attr("id",(t=>t.pathId())),n=e.selectAll(".link").data(s).enter().append("g").attr("class",(t=>t.class())),o=n.selectAll("text").data((t=>t.split().filter((t=>t.hasMeta())))).enter().append("text").attr("class",(t=>t.pathId()));return o.append("textPath").attr("xlink:href",(t=>`#${t.pathId()}`)).each((function(t){y.appendMetaText(this,t.metaList),y.appendMetaText(this,t.sourceMeta),y.appendMetaText(this,t.targetMeta),t.isLabelledPath()&&y.center(this),t.isReversePath()&&y.theOtherEnd(this)})),y.zoom(),[a,i,o]}static theOtherEnd(t){i.select(t).attr("class","reverse").attr("text-anchor","end").attr("startOffset","100%")}static center(t){i.select(t).attr("class","center").attr("text-anchor","middle").attr("startOffset","50%")}static appendMetaText(t,e){e.forEach((e=>{i.select(t).append("tspan").attr("x",(t=>t.tspanXOffset())).attr("dy",(t=>t.tspanYOffset())).attr("class",e.class).text(e.value)}))}static tick(t,e,s){t.attr("x1",(t=>t.source.x)).attr("y1",(t=>t.source.y)).attr("x2",(t=>t.target.x)).attr("y2",(t=>t.target.y)),e&&e.attr("d",(t=>t.d())),s&&s.attr("transform",(function(t){return t.rotate(this.getBBox())})),i.selectAll(".link text").style("visibility",(t=>t.isLabelVisible()?"visible":"hidden"))}static zoom(t){y.scale=t,i.selectAll(".link text").style("visibility",(t=>t.isLabelVisible()?"visible":"hidden"))}static setPosition(t,e){t.attr("x1",((t,s)=>e[s].x1)).attr("y1",((t,s)=>e[s].y1)).attr("x2",((t,s)=>e[s].x2)).attr("y2",((t,s)=>e[s].y2))}shiftMultiplier(){if(!this._shiftMultiplier){const t=this.group()||[];this._shiftMultiplier=t.indexOf(this.id)-(t.length-1)/2}return this._shiftMultiplier}static shiftBundle(t,e,s,r){const a=t=>t.shiftBundle(t.shiftMultiplier());t.attr("transform",a),e.attr("transform",a),s.attr("transform",a),n.shiftBundle(r)}shiftBundle(t){const e=this.margin()*t,s=this.target.x-this.source.x,r=this.target.y-this.source.y,a=Math.sqrt(Math.pow(s,2)+Math.pow(r,2));return`translate(${-e*r/a}, ${e*s/a})`}static reset(){y.groups=null}}const f=t=>class extends t{constructor(t,e,s){super(t,e,s),this.dispatch=i.dispatch("rendered")}static render(t,e,s){const[r,a,i]=super.render(t,e,s);return r.each((function(t){t.dispatch.rendered(this)})),[r,a,i]}on(t,e){this.dispatch.on(t,e)}},x=t=>{class e extends t{constructor(t,s,r){super(t,s,r);for(const a of e.pluginConstructors)a.bind(this)(t,s,r)}static registerConstructor(t){e.pluginConstructors.push(t)}}return e.pluginConstructors=[],e};class m extends(f(g)){}class y extends(x(m)){}var b;class k extends y{static tick(t,e,s){super.tick(t,e,s),t.attr("x2",(t=>t.x2())),t.attr("y2",(t=>t.y2()))}length(){return Math.sqrt((this.source.x-this.target.x)**2+(this.source.y-this.target.y)**2)}x2(){return this.source.x+(.5-5/this.length())*(this.target.x-this.source.x)}y2(){return this.source.y+(.5-5/this.length())*(this.target.y-this.source.y)}}const v=(b=class{static load(t,e,s){s.registerConstructor((function(t,e,s,r){this.selected=!1,this.on("rendered",(t=>{b.appendMarker(t),b.isMarkerDefined||b.defineMarkers()}))})),s.tick=k.tick,s.prototype.length=k.prototype.length,s.prototype.x2=k.prototype.x2,s.prototype.y2=k.prototype.y2}static defineMarkers(){const t=i.select("svg").append("defs"),e=e=>{t.append("marker").attr("id",e).attr("markerWidth",10).attr("markerHeight",7).attr("refX",7.5).attr("orient","auto").attr("viewBox","0 -5 10 10").append("path").attr("d","M0,-5 L10,0 L0,5")};e("marker-odd"),e("marker-even"),b.isMarkerDefined=!0}static appendMarker(t){i.select(t).attr("marker-end",(t=>t.id%2==0?"url(#marker-odd)":"url(#marker-even)"))}},b.isMarkerDefined=!1,b),M=v;return a})())); 2 | //# sourceMappingURL=plugin.min.js.map -------------------------------------------------------------------------------- /plugins/arrows_link/src/plugin.ts: -------------------------------------------------------------------------------- 1 | import * as d3 from "d3"; 2 | 3 | import { Constructor as LinkConstructor, Link, LinkDataType } from "../../../src/link"; 4 | import { Node } from "../../../src/node"; 5 | import { PluginClass } from "../../../src/plugin"; 6 | 7 | class ArrowsLink extends Link { 8 | public readonly source: number | Node; 9 | public readonly target: number | Node; 10 | 11 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 12 | static tick(link: d3.Selection, path: d3.Selection, label: d3.Selection) { 13 | super.tick(link, path, label); 14 | 15 | link.attr("x2", (d) => d.x2()); 16 | link.attr("y2", (d) => d.y2()); 17 | } 18 | 19 | length() { 20 | return Math.sqrt( 21 | ((this.source as Node).x - (this.target as Node).x) ** 2 + 22 | ((this.source as Node).y - (this.target as Node).y) ** 2, 23 | ); 24 | } 25 | 26 | x2() { 27 | return (this.source as Node).x + (0.5 - 5 / this.length()) * ((this.target as Node).x - (this.source as Node).x); 28 | } 29 | 30 | y2() { 31 | return (this.source as Node).y + (0.5 - 5 / this.length()) * ((this.target as Node).y - (this.source as Node).y); 32 | } 33 | } 34 | 35 | export const ArrowsLinkPlugin: PluginClass = class ArrowsLinkPlugin { 36 | private static isMarkerDefined = false; 37 | 38 | static load(Group, Node, Link) { 39 | Link.registerConstructor(function ( 40 | /* eslint-disable @typescript-eslint/no-unused-vars */ 41 | data: LinkDataType, 42 | id: number, 43 | metaKeys: string[], 44 | linkWidth: (object) => number, 45 | /* eslint-enable @typescript-eslint/no-unused-vars */ 46 | ) { 47 | this.selected = false; 48 | 49 | this.on("rendered", (element: SVGLineElement) => { 50 | ArrowsLinkPlugin.appendMarker(element); 51 | if (!ArrowsLinkPlugin.isMarkerDefined) { 52 | ArrowsLinkPlugin.defineMarkers(); 53 | } 54 | }); 55 | } as LinkConstructor); 56 | 57 | // Copy methods 58 | Link.tick = ArrowsLink.tick; 59 | Link.prototype.length = ArrowsLink.prototype.length; 60 | Link.prototype.x2 = ArrowsLink.prototype.x2; 61 | Link.prototype.y2 = ArrowsLink.prototype.y2; 62 | } 63 | 64 | private static defineMarkers() { 65 | const defs = d3.select("svg").append("defs"); 66 | const define = (id: string) => { 67 | defs 68 | .append("marker") 69 | .attr("id", id) 70 | .attr("markerWidth", 10) 71 | .attr("markerHeight", 7) 72 | .attr("refX", 7.5) 73 | .attr("orient", "auto") 74 | .attr("viewBox", "0 -5 10 10") 75 | .append("path") 76 | .attr("d", "M0,-5 L10,0 L0,5"); 77 | }; 78 | 79 | define("marker-odd"); 80 | define("marker-even"); 81 | ArrowsLinkPlugin.isMarkerDefined = true; 82 | } 83 | 84 | private static appendMarker(element: SVGLineElement) { 85 | d3.select(element).attr( 86 | "marker-end", 87 | // For consistency with #links :nth-child(odd), it's one-based 88 | (d: ArrowsLink) => (d.id % 2 === 0 ? "url(#marker-odd)" : "url(#marker-even)"), 89 | ); 90 | } 91 | }; 92 | 93 | export default ArrowsLinkPlugin; 94 | -------------------------------------------------------------------------------- /plugins/arrows_link/style.css: -------------------------------------------------------------------------------- 1 | #links :nth-child(odd) line { 2 | stroke: #7a4e4e; 3 | } 4 | 5 | #links :nth-child(even) line { 6 | stroke: #a68c6f; 7 | } 8 | 9 | marker#marker-odd { 10 | fill: #7a4e4e; 11 | } 12 | 13 | marker#marker-even { 14 | fill: #a68c6f; 15 | } 16 | -------------------------------------------------------------------------------- /plugins/arrows_link/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "ES6", 4 | "target": "ES2017", 5 | "outDir": "dist", 6 | "sourceMap": true 7 | }, 8 | "include": [ 9 | "src/*" 10 | ], 11 | "lib": [ 12 | "ES2017" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /plugins/arrows_link/webpack.common.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | entry: "./plugins/arrows_link/src/plugin", 3 | output: { 4 | path: __dirname, 5 | libraryTarget: "umd", 6 | }, 7 | resolve: { 8 | extensions: [".ts", ".js"] 9 | }, 10 | module: { 11 | rules: [ 12 | { 13 | test: /\.ts$/, 14 | use: ["ts-loader"] 15 | } 16 | ] 17 | }, 18 | externals: { 19 | d3: "d3" 20 | }, 21 | devtool: "source-map" 22 | }; 23 | -------------------------------------------------------------------------------- /plugins/arrows_link/webpack.dev.js: -------------------------------------------------------------------------------- 1 | const { merge } = require("webpack-merge"); 2 | const common = require("./webpack.common.js"); 3 | 4 | module.exports = merge(common, { 5 | mode: "development", 6 | output: { 7 | filename: "plugin.js" 8 | } 9 | } 10 | ); 11 | -------------------------------------------------------------------------------- /plugins/arrows_link/webpack.prod.js: -------------------------------------------------------------------------------- 1 | const { merge } = require("webpack-merge"); 2 | const common = require("./webpack.common.js"); 3 | 4 | module.exports = merge(common, { 5 | mode: "production", 6 | output: { 7 | filename: "plugin.min.js" 8 | } 9 | } 10 | ); 11 | -------------------------------------------------------------------------------- /plugins/removable_node/README.md: -------------------------------------------------------------------------------- 1 | # Removable Node Plugin 2 | 3 | Hide and show by key inputs. 4 | 5 | 1. Click node to select 6 | 2. Hit "d" to hide selected nodes 7 | 3. Hit "escape" to show hidden nodes 8 | 9 | ![screenshot](docs/images/screenshot01.gif) 10 | 11 | 12 | ## Usage 13 | 14 | ```html 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 |
30 | 37 | 38 | 39 | 40 | ``` 41 | 42 | 43 | ## Customize keybinds 44 | 45 | You can customize keybinds like this: 46 | 47 | ```js 48 | Diagram.plugin(RemovableNodePlugin, { 49 | showKey: "s", 50 | hideKey: "h", 51 | }); 52 | ``` 53 | -------------------------------------------------------------------------------- /plugins/removable_node/docs/images/screenshot01.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeout/inet-henge/c849bcd96952662db594b9f89a3722c6eb21300f/plugins/removable_node/docs/images/screenshot01.gif -------------------------------------------------------------------------------- /plugins/removable_node/plugin.js: -------------------------------------------------------------------------------- 1 | (function webpackUniversalModuleDefinition(root, factory) { 2 | if(typeof exports === 'object' && typeof module === 'object') 3 | module.exports = factory(require("d3")); 4 | else if(typeof define === 'function' && define.amd) 5 | define(["d3"], factory); 6 | else { 7 | var a = typeof exports === 'object' ? factory(require("d3")) : factory(root["d3"]); 8 | for(var i in a) (typeof exports === 'object' ? exports : root)[i] = a[i]; 9 | } 10 | })(self, (__WEBPACK_EXTERNAL_MODULE_d3__) => { 11 | return /******/ (() => { // webpackBootstrap 12 | /******/ "use strict"; 13 | /******/ var __webpack_modules__ = ({ 14 | 15 | /***/ "./src/meta_data.ts": 16 | /*!**************************!*\ 17 | !*** ./src/meta_data.ts ***! 18 | \**************************/ 19 | /***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { 20 | 21 | __webpack_require__.r(__webpack_exports__); 22 | /* harmony export */ __webpack_require__.d(__webpack_exports__, { 23 | /* harmony export */ MetaData: () => (/* binding */ MetaData) 24 | /* harmony export */ }); 25 | class MetaData { 26 | constructor( 27 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 28 | data, extraKey) { 29 | this.data = data; 30 | this.extraKey = extraKey; 31 | } 32 | get(keys) { 33 | return this.slice(keys).filter((k) => typeof k.value === "string"); 34 | } 35 | slice(keys) { 36 | if (!this.data) 37 | return []; 38 | if (this.extraKey) 39 | return this.sliceWithExtraKey(keys); 40 | else 41 | return this.sliceWithoutExtraKey(keys); 42 | } 43 | sliceWithExtraKey(keys) { 44 | const data = []; 45 | keys.forEach((k) => { 46 | if (this.data[k] && this.data[k][this.extraKey]) 47 | data.push({ class: k, value: this.data[k][this.extraKey] }); 48 | }); 49 | return data; 50 | } 51 | sliceWithoutExtraKey(keys) { 52 | const data = []; 53 | keys.forEach((k) => { 54 | if (this.data[k]) 55 | data.push({ class: k, value: this.data[k] }); 56 | }); 57 | return data; 58 | } 59 | } 60 | 61 | 62 | /***/ }), 63 | 64 | /***/ "./src/node.ts": 65 | /*!*********************!*\ 66 | !*** ./src/node.ts ***! 67 | \*********************/ 68 | /***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { 69 | 70 | __webpack_require__.r(__webpack_exports__); 71 | /* harmony export */ __webpack_require__.d(__webpack_exports__, { 72 | /* harmony export */ Node: () => (/* binding */ Node) 73 | /* harmony export */ }); 74 | /* harmony import */ var d3__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! d3 */ "d3"); 75 | /* harmony import */ var d3__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(d3__WEBPACK_IMPORTED_MODULE_0__); 76 | /* harmony import */ var _meta_data__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./meta_data */ "./src/meta_data.ts"); 77 | /* harmony import */ var _util__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ./util */ "./src/util.ts"); 78 | 79 | 80 | 81 | class NodeBase { 82 | constructor(data, id, options) { 83 | this.id = id; 84 | this.options = options; 85 | this.name = data.name; 86 | this.group = typeof data.group === "string" ? [data.group] : data.group || []; 87 | this.icon = data.icon; 88 | this.metaList = new _meta_data__WEBPACK_IMPORTED_MODULE_1__.MetaData(data.meta).get(options.metaKeys); 89 | this.meta = data.meta; 90 | this.extraClass = data.class || ""; 91 | this.width = options.width || 60; 92 | this.height = options.height || 40; 93 | this.padding = 3; 94 | this.tspanOffset = "1.1em"; 95 | this.register(id); 96 | } 97 | register(id) { 98 | Node.all = Node.all || {}; 99 | Node.all[this.name] = id; 100 | } 101 | transform() { 102 | const x = this.x - this.width / 2 + this.padding; 103 | const y = this.y - this.height / 2 + this.padding; 104 | return `translate(${x}, ${y})`; 105 | } 106 | nodeWidth() { 107 | return this.width - 2 * this.padding; 108 | } 109 | nodeHeight() { 110 | return this.height - 2 * this.padding; 111 | } 112 | xForText() { 113 | return this.nodeWidth() / 2; 114 | } 115 | yForText() { 116 | // svg ignores padding for some reason 117 | return this.height / 2; 118 | } 119 | static idByName(name) { 120 | if (Node.all[name] === undefined) 121 | throw `Unknown node "${name}"`; 122 | return Node.all[name]; 123 | } 124 | nodeId() { 125 | return (0,_util__WEBPACK_IMPORTED_MODULE_2__.classify)(this.name); 126 | } 127 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 128 | static render(layer, nodes) { 129 | const node = layer 130 | .selectAll(".node") 131 | .data(nodes) 132 | .enter() 133 | .append("g") 134 | .attr("id", (d) => d.nodeId()) 135 | .attr("name", (d) => d.name) 136 | .attr("transform", (d) => d.transform()); 137 | node.each(function (d) { 138 | if (d.icon) 139 | Node.appendImage(this); 140 | else 141 | Node.appendRect(this); 142 | Node.appendText(this); 143 | }); 144 | return node; 145 | } 146 | static appendText(container) { 147 | const text = d3__WEBPACK_IMPORTED_MODULE_0__.select(container) 148 | .append("text") 149 | .attr("text-anchor", "middle") 150 | .attr("x", (d) => d.xForText()) 151 | .attr("y", (d) => d.yForText()); 152 | text 153 | .append("tspan") 154 | .text((d) => d.name) 155 | .attr("x", (d) => d.xForText()); 156 | text.each((d) => { 157 | // Show meta only when "tooltip" option is not configured 158 | if (!d.options.tooltip) { 159 | Node.appendMetaText(text, d.metaList); 160 | } 161 | }); 162 | } 163 | static appendMetaText(container, meta) { 164 | meta.forEach((m) => { 165 | container 166 | .append("tspan") 167 | .attr("x", (d) => d.xForText()) 168 | .attr("dy", (d) => d.tspanOffset) 169 | .attr("class", m.class) 170 | .text(m.value); 171 | }); 172 | } 173 | static appendImage(container) { 174 | d3__WEBPACK_IMPORTED_MODULE_0__.select(container) 175 | .attr("class", (d) => `node image ${(0,_util__WEBPACK_IMPORTED_MODULE_2__.classify)(d.name)} ${d.extraClass}`) 176 | .append("image") 177 | .attr("xlink:href", (d) => d.icon) 178 | .attr("width", (d) => d.nodeWidth()) 179 | .attr("height", (d) => d.nodeHeight()); 180 | } 181 | static appendRect(container) { 182 | d3__WEBPACK_IMPORTED_MODULE_0__.select(container) 183 | .attr("class", (d) => `node rect ${(0,_util__WEBPACK_IMPORTED_MODULE_2__.classify)(d.name)} ${d.extraClass}`) 184 | .append("rect") 185 | .attr("width", (d) => d.nodeWidth()) 186 | .attr("height", (d) => d.nodeHeight()) 187 | .attr("rx", 5) 188 | .attr("ry", 5) 189 | .style("fill", (d) => d.options.color(undefined)); 190 | } 191 | static tick(node) { 192 | node.attr("transform", (d) => d.transform()); 193 | } 194 | static setPosition(node, position) { 195 | node.attr("transform", (d, i) => { 196 | var _a, _b, _c, _d; 197 | if (((_a = position[i]) === null || _a === void 0 ? void 0 : _a.x) !== null && 198 | ((_b = position[i]) === null || _b === void 0 ? void 0 : _b.x) !== undefined && 199 | ((_c = position[i]) === null || _c === void 0 ? void 0 : _c.y) !== null && 200 | ((_d = position[i]) === null || _d === void 0 ? void 0 : _d.y) !== undefined) { 201 | d.x = position[i].x; 202 | d.y = position[i].y; 203 | } 204 | return d.transform(); 205 | }); 206 | } 207 | static reset() { 208 | Node.all = null; 209 | } 210 | } 211 | const Eventable = (Base) => { 212 | class EventableNode extends Base { 213 | constructor(data, id, options) { 214 | super(data, id, options); 215 | this.dispatch = d3__WEBPACK_IMPORTED_MODULE_0__.dispatch("rendered"); 216 | } 217 | static render(layer, nodes) { 218 | const node = super.render(layer, nodes); 219 | node.each(function (d) { 220 | d.dispatch.rendered(this); 221 | }); 222 | return node; 223 | } 224 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 225 | on(name, callback) { 226 | this.dispatch.on(name, callback); 227 | } 228 | } 229 | return EventableNode; 230 | }; 231 | const Pluggable = (Base) => { 232 | class Node extends Base { 233 | constructor(data, id, options) { 234 | super(data, id, options); 235 | for (const constructor of Node.pluginConstructors) { 236 | // Call Pluggable at last as constructor may call methods defined in other classes 237 | constructor.bind(this)(data, id, options); 238 | } 239 | } 240 | static registerConstructor(func) { 241 | Node.pluginConstructors.push(func); 242 | } 243 | } 244 | Node.pluginConstructors = []; 245 | return Node; 246 | }; 247 | class EventableNode extends Eventable(NodeBase) { 248 | } 249 | // Call Pluggable at last as constructor may call methods defined in other classes 250 | class Node extends Pluggable(EventableNode) { 251 | } 252 | 253 | 254 | 255 | /***/ }), 256 | 257 | /***/ "./src/util.ts": 258 | /*!*********************!*\ 259 | !*** ./src/util.ts ***! 260 | \*********************/ 261 | /***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { 262 | 263 | __webpack_require__.r(__webpack_exports__); 264 | /* harmony export */ __webpack_require__.d(__webpack_exports__, { 265 | /* harmony export */ classify: () => (/* binding */ classify) 266 | /* harmony export */ }); 267 | function classify(string) { 268 | return string.replace(" ", "-").toLowerCase(); 269 | } 270 | 271 | 272 | /***/ }), 273 | 274 | /***/ "d3": 275 | /*!*********************!*\ 276 | !*** external "d3" ***! 277 | \*********************/ 278 | /***/ ((module) => { 279 | 280 | module.exports = __WEBPACK_EXTERNAL_MODULE_d3__; 281 | 282 | /***/ }) 283 | 284 | /******/ }); 285 | /************************************************************************/ 286 | /******/ // The module cache 287 | /******/ var __webpack_module_cache__ = {}; 288 | /******/ 289 | /******/ // The require function 290 | /******/ function __webpack_require__(moduleId) { 291 | /******/ // Check if module is in cache 292 | /******/ var cachedModule = __webpack_module_cache__[moduleId]; 293 | /******/ if (cachedModule !== undefined) { 294 | /******/ return cachedModule.exports; 295 | /******/ } 296 | /******/ // Create a new module (and put it into the cache) 297 | /******/ var module = __webpack_module_cache__[moduleId] = { 298 | /******/ // no module.id needed 299 | /******/ // no module.loaded needed 300 | /******/ exports: {} 301 | /******/ }; 302 | /******/ 303 | /******/ // Execute the module function 304 | /******/ __webpack_modules__[moduleId](module, module.exports, __webpack_require__); 305 | /******/ 306 | /******/ // Return the exports of the module 307 | /******/ return module.exports; 308 | /******/ } 309 | /******/ 310 | /************************************************************************/ 311 | /******/ /* webpack/runtime/compat get default export */ 312 | /******/ (() => { 313 | /******/ // getDefaultExport function for compatibility with non-harmony modules 314 | /******/ __webpack_require__.n = (module) => { 315 | /******/ var getter = module && module.__esModule ? 316 | /******/ () => (module['default']) : 317 | /******/ () => (module); 318 | /******/ __webpack_require__.d(getter, { a: getter }); 319 | /******/ return getter; 320 | /******/ }; 321 | /******/ })(); 322 | /******/ 323 | /******/ /* webpack/runtime/define property getters */ 324 | /******/ (() => { 325 | /******/ // define getter functions for harmony exports 326 | /******/ __webpack_require__.d = (exports, definition) => { 327 | /******/ for(var key in definition) { 328 | /******/ if(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) { 329 | /******/ Object.defineProperty(exports, key, { enumerable: true, get: definition[key] }); 330 | /******/ } 331 | /******/ } 332 | /******/ }; 333 | /******/ })(); 334 | /******/ 335 | /******/ /* webpack/runtime/hasOwnProperty shorthand */ 336 | /******/ (() => { 337 | /******/ __webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop)) 338 | /******/ })(); 339 | /******/ 340 | /******/ /* webpack/runtime/make namespace object */ 341 | /******/ (() => { 342 | /******/ // define __esModule on exports 343 | /******/ __webpack_require__.r = (exports) => { 344 | /******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) { 345 | /******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' }); 346 | /******/ } 347 | /******/ Object.defineProperty(exports, '__esModule', { value: true }); 348 | /******/ }; 349 | /******/ })(); 350 | /******/ 351 | /************************************************************************/ 352 | var __webpack_exports__ = {}; 353 | /*!**********************************************!*\ 354 | !*** ./plugins/removable_node/src/plugin.ts ***! 355 | \**********************************************/ 356 | __webpack_require__.r(__webpack_exports__); 357 | /* harmony export */ __webpack_require__.d(__webpack_exports__, { 358 | /* harmony export */ RemovableNodePlugin: () => (/* binding */ RemovableNodePlugin), 359 | /* harmony export */ "default": () => (__WEBPACK_DEFAULT_EXPORT__) 360 | /* harmony export */ }); 361 | /* harmony import */ var d3__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! d3 */ "d3"); 362 | /* harmony import */ var d3__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(d3__WEBPACK_IMPORTED_MODULE_0__); 363 | /* harmony import */ var _src_node__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ../../../src/node */ "./src/node.ts"); 364 | /* harmony import */ var _src_util__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ../../../src/util */ "./src/util.ts"); 365 | var _a; 366 | 367 | 368 | 369 | class RemovableNode extends _src_node__WEBPACK_IMPORTED_MODULE_1__.Node { 370 | toggleSelected() { 371 | this.selected = !this.selected; 372 | } 373 | reset() { 374 | this.selected = false; 375 | } 376 | textColor() { 377 | return this.selected ? "red" : "black"; 378 | } 379 | } 380 | const RemovableNodePlugin = (_a = class RemovableNodePlugin { 381 | static load(Group, Node, Link, options = {}) { 382 | if (options.showKey) { 383 | _a.showKey = options.showKey; 384 | } 385 | if (options.hideKey) { 386 | _a.hideKey = options.hideKey; 387 | } 388 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 389 | Node.registerConstructor(function (data, id, options) { 390 | this.selected = false; 391 | this.on("rendered", (element) => { 392 | _a.configureRemovableNode(element); 393 | }); 394 | }); 395 | _a.configureRemovableNodes(); 396 | // Copy methods 397 | Node.prototype.toggleSelected = RemovableNode.prototype.toggleSelected; 398 | Node.prototype.reset = RemovableNode.prototype.reset; 399 | Node.prototype.textColor = RemovableNode.prototype.textColor; 400 | } 401 | /** 402 | * Configure keyboard event listener to show or hide Nodes and Links 403 | */ 404 | static configureRemovableNodes() { 405 | d3__WEBPACK_IMPORTED_MODULE_0__.select("body").on("keydown", () => { 406 | switch (d3__WEBPACK_IMPORTED_MODULE_0__.event.key) { 407 | case _a.showKey: 408 | _a.show(); 409 | break; 410 | case _a.hideKey: 411 | _a.hide(); 412 | } 413 | }); 414 | } 415 | /** 416 | * Configure click event listener to select Nodes 417 | */ 418 | static configureRemovableNode(element) { 419 | const d3Element = d3__WEBPACK_IMPORTED_MODULE_0__.select(element); 420 | d3Element.on("click.removableNode", function (d) { 421 | // Do nothing for dragging 422 | if (d3__WEBPACK_IMPORTED_MODULE_0__.event.defaultPrevented) { 423 | return; 424 | } 425 | d.toggleSelected(); 426 | _a.applyColor(this); 427 | }); 428 | } 429 | static applyColor(element) { 430 | d3__WEBPACK_IMPORTED_MODULE_0__.select(element) 431 | .select("text tspan") 432 | .style("fill", (d) => d.textColor()); 433 | } 434 | static show() { 435 | d3__WEBPACK_IMPORTED_MODULE_0__.selectAll(".node") 436 | .style("display", "inline") 437 | .each(function (d) { 438 | d.reset(); 439 | _a.applyColor(this); 440 | }); 441 | _a.showLinks(); 442 | } 443 | static hide() { 444 | d3__WEBPACK_IMPORTED_MODULE_0__.selectAll(".node").style("display", (d) => { 445 | if (d.selected) { 446 | // Hide connected elements 447 | _a.hideLinks(d.name); 448 | _a.hideToolTips(d.name); 449 | return "none"; 450 | } 451 | return "inline"; 452 | }); 453 | } 454 | static showLinks() { 455 | d3__WEBPACK_IMPORTED_MODULE_0__.selectAll(`.link`).style("display", "inline"); 456 | } 457 | static hideLinks(nodeName) { 458 | d3__WEBPACK_IMPORTED_MODULE_0__.selectAll(`.link.${(0,_src_util__WEBPACK_IMPORTED_MODULE_2__.classify)(nodeName)}`).style("display", "none"); 459 | } 460 | static hideToolTips(nodeName) { 461 | d3__WEBPACK_IMPORTED_MODULE_0__.selectAll(`.tooltip.${(0,_src_util__WEBPACK_IMPORTED_MODULE_2__.classify)(nodeName)}`).attr("visibility", "hidden"); 462 | } 463 | }, 464 | _a.showKey = "Escape", 465 | _a.hideKey = "d", 466 | _a); 467 | /* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = (RemovableNodePlugin); 468 | 469 | /******/ return __webpack_exports__; 470 | /******/ })() 471 | ; 472 | }); 473 | //# sourceMappingURL=plugin.js.map -------------------------------------------------------------------------------- /plugins/removable_node/plugin.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"plugin.js","mappings":"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","sources":["webpack://inet-henge/webpack/universalModuleDefinition","webpack://inet-henge/./src/meta_data.ts","webpack://inet-henge/./src/node.ts","webpack://inet-henge/./src/util.ts","webpack://inet-henge/external umd \"d3\"","webpack://inet-henge/webpack/bootstrap","webpack://inet-henge/webpack/runtime/compat get default export","webpack://inet-henge/webpack/runtime/define property getters","webpack://inet-henge/webpack/runtime/hasOwnProperty shorthand","webpack://inet-henge/webpack/runtime/make namespace object","webpack://inet-henge/./plugins/removable_node/src/plugin.ts"],"sourcesContent":["(function webpackUniversalModuleDefinition(root, factory) {\n\tif(typeof exports === 'object' && typeof module === 'object')\n\t\tmodule.exports = factory(require(\"d3\"));\n\telse if(typeof define === 'function' && define.amd)\n\t\tdefine([\"d3\"], factory);\n\telse {\n\t\tvar a = typeof exports === 'object' ? factory(require(\"d3\")) : factory(root[\"d3\"]);\n\t\tfor(var i in a) (typeof exports === 'object' ? exports : root)[i] = a[i];\n\t}\n})(self, (__WEBPACK_EXTERNAL_MODULE_d3__) => {\nreturn ","export type MetaDataType = { class: string; value: any }; // eslint-disable-line @typescript-eslint/no-explicit-any\n\nexport class MetaData {\n constructor(\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n private data: Record,\n private extraKey?: string,\n ) {}\n\n get(keys: string[]) {\n return this.slice(keys).filter((k) => typeof k.value === \"string\");\n }\n\n private slice(keys: string[]) {\n if (!this.data) return [];\n\n if (this.extraKey) return this.sliceWithExtraKey(keys);\n else return this.sliceWithoutExtraKey(keys);\n }\n\n private sliceWithExtraKey(keys: string[]) {\n const data: MetaDataType[] = [];\n\n keys.forEach((k) => {\n if (this.data[k] && this.data[k][this.extraKey]) data.push({ class: k, value: this.data[k][this.extraKey] });\n });\n\n return data;\n }\n\n private sliceWithoutExtraKey(keys: string[]) {\n const data: MetaDataType[] = [];\n\n keys.forEach((k) => {\n if (this.data[k]) data.push({ class: k, value: this.data[k] });\n });\n\n return data;\n }\n}\n","import * as d3 from \"d3\";\n\nimport { Color } from \"./diagram\";\nimport { MetaData, MetaDataType } from \"./meta_data\";\nimport { NodePosition } from \"./position_cache\";\nimport { classify } from \"./util\";\n\nexport type Constructor = (data: NodeDataType, id: number, options: NodeOptions) => void;\n\nexport type NodeDataType = {\n name: string;\n group: string[];\n icon: string;\n meta: Record; // eslint-disable-line @typescript-eslint/no-explicit-any\n class: string;\n};\n\nexport type NodeOptions = {\n width: number;\n height: number;\n metaKeys: string[];\n color: Color;\n tooltip: boolean;\n};\n\nclass NodeBase {\n private static all: Record; // eslint-disable-line @typescript-eslint/no-explicit-any\n\n public name: string;\n public group: string[];\n public metaList: MetaDataType[];\n public meta: Record; // eslint-disable-line @typescript-eslint/no-explicit-any\n public x: number;\n public y: number;\n\n private icon: string;\n private extraClass: string;\n private width: number;\n private height: number;\n private padding: number;\n private tspanOffset: string;\n\n constructor(\n data: NodeDataType,\n public id: number,\n private options: NodeOptions,\n ) {\n this.name = data.name;\n this.group = typeof data.group === \"string\" ? [data.group] : data.group || [];\n this.icon = data.icon;\n this.metaList = new MetaData(data.meta).get(options.metaKeys);\n this.meta = data.meta;\n this.extraClass = data.class || \"\";\n\n this.width = options.width || 60;\n this.height = options.height || 40;\n this.padding = 3;\n this.tspanOffset = \"1.1em\";\n\n this.register(id);\n }\n\n private register(id: number) {\n Node.all = Node.all || {};\n Node.all[this.name] = id;\n }\n\n transform() {\n const x = this.x - this.width / 2 + this.padding;\n const y = this.y - this.height / 2 + this.padding;\n return `translate(${x}, ${y})`;\n }\n\n private nodeWidth() {\n return this.width - 2 * this.padding;\n }\n\n private nodeHeight() {\n return this.height - 2 * this.padding;\n }\n\n private xForText() {\n return this.nodeWidth() / 2;\n }\n\n private yForText() {\n // svg ignores padding for some reason\n return this.height / 2;\n }\n\n static idByName(name: string) {\n if (Node.all[name] === undefined) throw `Unknown node \"${name}\"`;\n return Node.all[name];\n }\n\n public nodeId() {\n return classify(this.name);\n }\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n static render(layer: d3.Selection, nodes: Node[]) {\n const node: d3.Selection = layer\n .selectAll(\".node\")\n .data(nodes)\n .enter()\n .append(\"g\")\n .attr(\"id\", (d) => d.nodeId())\n .attr(\"name\", (d) => d.name)\n .attr(\"transform\", (d) => d.transform());\n\n node.each(function (this: SVGGElement, d) {\n if (d.icon) Node.appendImage(this);\n else Node.appendRect(this);\n\n Node.appendText(this);\n });\n\n return node;\n }\n\n private static appendText(container: SVGGElement) {\n const text = (d3.select(container) as d3.Selection)\n .append(\"text\")\n .attr(\"text-anchor\", \"middle\")\n .attr(\"x\", (d) => d.xForText())\n .attr(\"y\", (d) => d.yForText());\n text\n .append(\"tspan\")\n .text((d) => d.name)\n .attr(\"x\", (d) => d.xForText());\n\n text.each((d) => {\n // Show meta only when \"tooltip\" option is not configured\n if (!d.options.tooltip) {\n Node.appendMetaText(text, d.metaList);\n }\n });\n }\n\n private static appendMetaText(container: d3.Selection, meta: MetaDataType[]) {\n meta.forEach((m) => {\n container\n .append(\"tspan\")\n .attr(\"x\", (d) => d.xForText())\n .attr(\"dy\", (d) => d.tspanOffset)\n .attr(\"class\", m.class)\n .text(m.value);\n });\n }\n\n private static appendImage(container: SVGGElement) {\n (d3.select(container) as d3.Selection)\n .attr(\"class\", (d) => `node image ${classify(d.name)} ${d.extraClass}`)\n .append(\"image\")\n .attr(\"xlink:href\", (d) => d.icon)\n .attr(\"width\", (d) => d.nodeWidth())\n .attr(\"height\", (d) => d.nodeHeight());\n }\n\n private static appendRect(container: SVGGElement) {\n (d3.select(container) as d3.Selection)\n .attr(\"class\", (d) => `node rect ${classify(d.name)} ${d.extraClass}`)\n .append(\"rect\")\n .attr(\"width\", (d) => d.nodeWidth())\n .attr(\"height\", (d) => d.nodeHeight())\n .attr(\"rx\", 5)\n .attr(\"ry\", 5)\n .style(\"fill\", (d) => d.options.color(undefined));\n }\n\n static tick(node: d3.Selection) {\n node.attr(\"transform\", (d) => d.transform());\n }\n\n static setPosition(node: d3.Selection, position: NodePosition[]) {\n node.attr(\"transform\", (d, i) => {\n if (\n position[i]?.x !== null &&\n position[i]?.x !== undefined &&\n position[i]?.y !== null &&\n position[i]?.y !== undefined\n ) {\n d.x = position[i].x;\n d.y = position[i].y;\n }\n return d.transform();\n });\n }\n\n static reset() {\n Node.all = null;\n }\n}\n\nconst Eventable = (Base: typeof NodeBase) => {\n class EventableNode extends Base {\n private dispatch: d3.Dispatch;\n\n constructor(data: NodeDataType, id: number, options: NodeOptions) {\n super(data, id, options);\n\n this.dispatch = d3.dispatch(\"rendered\");\n }\n\n static render(layer, nodes) {\n const node = super.render(layer, nodes);\n\n node.each(function (this: SVGGElement, d: Node & EventableNode) {\n d.dispatch.rendered(this);\n });\n\n return node;\n }\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n on(name: string, callback: (element: SVGGElement) => any) {\n this.dispatch.on(name, callback);\n }\n }\n\n return EventableNode;\n};\n\nconst Pluggable = (Base: typeof NodeBase) => {\n class Node extends Base {\n private static pluginConstructors: Constructor[] = [];\n\n constructor(data: NodeDataType, id: number, options: NodeOptions) {\n super(data, id, options);\n\n for (const constructor of Node.pluginConstructors) {\n // Call Pluggable at last as constructor may call methods defined in other classes\n constructor.bind(this)(data, id, options);\n }\n }\n\n static registerConstructor(func: Constructor) {\n Node.pluginConstructors.push(func);\n }\n }\n\n return Node;\n};\n\nclass EventableNode extends Eventable(NodeBase) {}\n\n// Call Pluggable at last as constructor may call methods defined in other classes\nclass Node extends Pluggable(EventableNode) {}\n\nexport { Node };\n","export function classify(string: string) {\n return string.replace(\" \", \"-\").toLowerCase();\n}\n","module.exports = __WEBPACK_EXTERNAL_MODULE_d3__;","// The module cache\nvar __webpack_module_cache__ = {};\n\n// The require function\nfunction __webpack_require__(moduleId) {\n\t// Check if module is in cache\n\tvar cachedModule = __webpack_module_cache__[moduleId];\n\tif (cachedModule !== undefined) {\n\t\treturn cachedModule.exports;\n\t}\n\t// Create a new module (and put it into the cache)\n\tvar module = __webpack_module_cache__[moduleId] = {\n\t\t// no module.id needed\n\t\t// no module.loaded needed\n\t\texports: {}\n\t};\n\n\t// Execute the module function\n\t__webpack_modules__[moduleId](module, module.exports, __webpack_require__);\n\n\t// Return the exports of the module\n\treturn module.exports;\n}\n\n","// getDefaultExport function for compatibility with non-harmony modules\n__webpack_require__.n = (module) => {\n\tvar getter = module && module.__esModule ?\n\t\t() => (module['default']) :\n\t\t() => (module);\n\t__webpack_require__.d(getter, { a: getter });\n\treturn getter;\n};","// define getter functions for harmony exports\n__webpack_require__.d = (exports, definition) => {\n\tfor(var key in definition) {\n\t\tif(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {\n\t\t\tObject.defineProperty(exports, key, { enumerable: true, get: definition[key] });\n\t\t}\n\t}\n};","__webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))","// define __esModule on exports\n__webpack_require__.r = (exports) => {\n\tif(typeof Symbol !== 'undefined' && Symbol.toStringTag) {\n\t\tObject.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });\n\t}\n\tObject.defineProperty(exports, '__esModule', { value: true });\n};","import * as d3 from \"d3\";\n\nimport { Constructor as NodeConstructor, Node, NodeDataType, NodeOptions } from \"../../../src/node\";\nimport { PluginClass } from \"../../../src/plugin\";\nimport { classify } from \"../../../src/util\";\n\ntype Options = {\n showKey?: string;\n hideKey?: string;\n};\n\nclass RemovableNode extends Node {\n public selected;\n\n public toggleSelected() {\n this.selected = !this.selected;\n }\n\n public reset() {\n this.selected = false;\n }\n\n public textColor() {\n return this.selected ? \"red\" : \"black\";\n }\n}\n\nexport const RemovableNodePlugin: PluginClass = class RemovableNodePlugin {\n private static showKey = \"Escape\";\n private static hideKey = \"d\";\n\n static load(Group, Node, Link, options: Options = {}) {\n if (options.showKey) {\n RemovableNodePlugin.showKey = options.showKey;\n }\n if (options.hideKey) {\n RemovableNodePlugin.hideKey = options.hideKey;\n }\n\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n Node.registerConstructor(function (data: NodeDataType, id: number, options: NodeOptions) {\n this.selected = false;\n\n this.on(\"rendered\", (element: SVGGElement) => {\n RemovableNodePlugin.configureRemovableNode(element);\n });\n } as NodeConstructor);\n\n RemovableNodePlugin.configureRemovableNodes();\n\n // Copy methods\n Node.prototype.toggleSelected = RemovableNode.prototype.toggleSelected;\n Node.prototype.reset = RemovableNode.prototype.reset;\n Node.prototype.textColor = RemovableNode.prototype.textColor;\n }\n\n /**\n * Configure keyboard event listener to show or hide Nodes and Links\n */\n private static configureRemovableNodes() {\n d3.select(\"body\").on(\"keydown\", () => {\n switch ((d3.event as KeyboardEvent).key) {\n case RemovableNodePlugin.showKey:\n RemovableNodePlugin.show();\n break;\n case RemovableNodePlugin.hideKey:\n RemovableNodePlugin.hide();\n }\n });\n }\n\n /**\n * Configure click event listener to select Nodes\n */\n private static configureRemovableNode(element: SVGGElement) {\n const d3Element = d3.select(element);\n d3Element.on(\"click.removableNode\", function (this: SVGGElement, d: RemovableNode) {\n // Do nothing for dragging\n if ((d3.event as MouseEvent).defaultPrevented) {\n return;\n }\n\n d.toggleSelected();\n RemovableNodePlugin.applyColor(this);\n });\n }\n\n private static applyColor(element: SVGGElement) {\n d3.select(element)\n .select(\"text tspan\")\n .style(\"fill\", (d: RemovableNode) => d.textColor());\n }\n\n private static show() {\n d3.selectAll(\".node\")\n .style(\"display\", \"inline\")\n .each(function (this: SVGGElement, d: RemovableNode) {\n d.reset();\n RemovableNodePlugin.applyColor(this);\n });\n\n RemovableNodePlugin.showLinks();\n }\n\n private static hide() {\n d3.selectAll(\".node\").style(\"display\", (d: RemovableNode) => {\n if (d.selected) {\n // Hide connected elements\n RemovableNodePlugin.hideLinks(d.name);\n RemovableNodePlugin.hideToolTips(d.name);\n return \"none\";\n }\n\n return \"inline\";\n });\n }\n\n private static showLinks() {\n d3.selectAll(`.link`).style(\"display\", \"inline\");\n }\n\n private static hideLinks(nodeName: string) {\n d3.selectAll(`.link.${classify(nodeName)}`).style(\"display\", \"none\");\n }\n\n private static hideToolTips(nodeName: string) {\n d3.selectAll(`.tooltip.${classify(nodeName)}`).attr(\"visibility\", \"hidden\");\n }\n};\n\nexport default RemovableNodePlugin;\n"],"names":[],"sourceRoot":""} -------------------------------------------------------------------------------- /plugins/removable_node/plugin.min.js: -------------------------------------------------------------------------------- 1 | !function(t,e){if("object"==typeof exports&&"object"==typeof module)module.exports=e(require("d3"));else if("function"==typeof define&&define.amd)define(["d3"],e);else{var s="object"==typeof exports?e(require("d3")):e(t.d3);for(var i in s)("object"==typeof exports?exports:t)[i]=s[i]}}(self,(t=>(()=>{"use strict";var e={893:e=>{e.exports=t}},s={};function i(t){var r=s[t];if(void 0!==r)return r.exports;var o=s[t]={exports:{}};return e[t](o,o.exports,i),o.exports}i.d=(t,e)=>{for(var s in e)i.o(e,s)&&!i.o(t,s)&&Object.defineProperty(t,s,{enumerable:!0,get:e[s]})},i.o=(t,e)=>Object.prototype.hasOwnProperty.call(t,e),i.r=t=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(t,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(t,"__esModule",{value:!0})};var r={};i.r(r),i.d(r,{RemovableNodePlugin:()=>x,default:()=>f});var o=i(893);class a{constructor(t,e){this.data=t,this.extraKey=e}get(t){return this.slice(t).filter((t=>"string"==typeof t.value))}slice(t){return this.data?this.extraKey?this.sliceWithExtraKey(t):this.sliceWithoutExtraKey(t):[]}sliceWithExtraKey(t){const e=[];return t.forEach((t=>{this.data[t]&&this.data[t][this.extraKey]&&e.push({class:t,value:this.data[t][this.extraKey]})})),e}sliceWithoutExtraKey(t){const e=[];return t.forEach((t=>{this.data[t]&&e.push({class:t,value:this.data[t]})})),e}}function n(t){return t.replace(" ","-").toLowerCase()}class l{constructor(t,e,s){this.id=e,this.options=s,this.name=t.name,this.group="string"==typeof t.group?[t.group]:t.group||[],this.icon=t.icon,this.metaList=new a(t.meta).get(s.metaKeys),this.meta=t.meta,this.extraClass=t.class||"",this.width=s.width||60,this.height=s.height||40,this.padding=3,this.tspanOffset="1.1em",this.register(e)}register(t){p.all=p.all||{},p.all[this.name]=t}transform(){return`translate(${this.x-this.width/2+this.padding}, ${this.y-this.height/2+this.padding})`}nodeWidth(){return this.width-2*this.padding}nodeHeight(){return this.height-2*this.padding}xForText(){return this.nodeWidth()/2}yForText(){return this.height/2}static idByName(t){if(void 0===p.all[t])throw`Unknown node "${t}"`;return p.all[t]}nodeId(){return n(this.name)}static render(t,e){const s=t.selectAll(".node").data(e).enter().append("g").attr("id",(t=>t.nodeId())).attr("name",(t=>t.name)).attr("transform",(t=>t.transform()));return s.each((function(t){t.icon?p.appendImage(this):p.appendRect(this),p.appendText(this)})),s}static appendText(t){const e=o.select(t).append("text").attr("text-anchor","middle").attr("x",(t=>t.xForText())).attr("y",(t=>t.yForText()));e.append("tspan").text((t=>t.name)).attr("x",(t=>t.xForText())),e.each((t=>{t.options.tooltip||p.appendMetaText(e,t.metaList)}))}static appendMetaText(t,e){e.forEach((e=>{t.append("tspan").attr("x",(t=>t.xForText())).attr("dy",(t=>t.tspanOffset)).attr("class",e.class).text(e.value)}))}static appendImage(t){o.select(t).attr("class",(t=>`node image ${n(t.name)} ${t.extraClass}`)).append("image").attr("xlink:href",(t=>t.icon)).attr("width",(t=>t.nodeWidth())).attr("height",(t=>t.nodeHeight()))}static appendRect(t){o.select(t).attr("class",(t=>`node rect ${n(t.name)} ${t.extraClass}`)).append("rect").attr("width",(t=>t.nodeWidth())).attr("height",(t=>t.nodeHeight())).attr("rx",5).attr("ry",5).style("fill",(t=>t.options.color(void 0)))}static tick(t){t.attr("transform",(t=>t.transform()))}static setPosition(t,e){t.attr("transform",((t,s)=>{var i,r,o,a;return null!==(null===(i=e[s])||void 0===i?void 0:i.x)&&void 0!==(null===(r=e[s])||void 0===r?void 0:r.x)&&null!==(null===(o=e[s])||void 0===o?void 0:o.y)&&void 0!==(null===(a=e[s])||void 0===a?void 0:a.y)&&(t.x=e[s].x,t.y=e[s].y),t.transform()}))}static reset(){p.all=null}}const d=t=>class extends t{constructor(t,e,s){super(t,e,s),this.dispatch=o.dispatch("rendered")}static render(t,e){const s=super.render(t,e);return s.each((function(t){t.dispatch.rendered(this)})),s}on(t,e){this.dispatch.on(t,e)}},c=t=>{class e extends t{constructor(t,s,i){super(t,s,i);for(const r of e.pluginConstructors)r.bind(this)(t,s,i)}static registerConstructor(t){e.pluginConstructors.push(t)}}return e.pluginConstructors=[],e};class h extends(d(l)){}class p extends(c(h)){}var u;class y extends p{toggleSelected(){this.selected=!this.selected}reset(){this.selected=!1}textColor(){return this.selected?"red":"black"}}const x=(u=class{static load(t,e,s,i={}){i.showKey&&(u.showKey=i.showKey),i.hideKey&&(u.hideKey=i.hideKey),e.registerConstructor((function(t,e,s){this.selected=!1,this.on("rendered",(t=>{u.configureRemovableNode(t)}))})),u.configureRemovableNodes(),e.prototype.toggleSelected=y.prototype.toggleSelected,e.prototype.reset=y.prototype.reset,e.prototype.textColor=y.prototype.textColor}static configureRemovableNodes(){o.select("body").on("keydown",(()=>{switch(o.event.key){case u.showKey:u.show();break;case u.hideKey:u.hide()}}))}static configureRemovableNode(t){o.select(t).on("click.removableNode",(function(t){o.event.defaultPrevented||(t.toggleSelected(),u.applyColor(this))}))}static applyColor(t){o.select(t).select("text tspan").style("fill",(t=>t.textColor()))}static show(){o.selectAll(".node").style("display","inline").each((function(t){t.reset(),u.applyColor(this)})),u.showLinks()}static hide(){o.selectAll(".node").style("display",(t=>t.selected?(u.hideLinks(t.name),u.hideToolTips(t.name),"none"):"inline"))}static showLinks(){o.selectAll(".link").style("display","inline")}static hideLinks(t){o.selectAll(`.link.${n(t)}`).style("display","none")}static hideToolTips(t){o.selectAll(`.tooltip.${n(t)}`).attr("visibility","hidden")}},u.showKey="Escape",u.hideKey="d",u),f=x;return r})())); 2 | //# sourceMappingURL=plugin.min.js.map -------------------------------------------------------------------------------- /plugins/removable_node/plugin.min.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"plugin.min.js","mappings":"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","sources":["webpack://inet-henge/webpack/universalModuleDefinition","webpack://inet-henge/external umd \"d3\"","webpack://inet-henge/webpack/bootstrap","webpack://inet-henge/webpack/runtime/define property getters","webpack://inet-henge/webpack/runtime/hasOwnProperty shorthand","webpack://inet-henge/webpack/runtime/make namespace object","webpack://inet-henge/./src/meta_data.ts","webpack://inet-henge/./src/util.ts","webpack://inet-henge/./src/node.ts","webpack://inet-henge/./plugins/removable_node/src/plugin.ts"],"sourcesContent":["(function webpackUniversalModuleDefinition(root, factory) {\n\tif(typeof exports === 'object' && typeof module === 'object')\n\t\tmodule.exports = factory(require(\"d3\"));\n\telse if(typeof define === 'function' && define.amd)\n\t\tdefine([\"d3\"], factory);\n\telse {\n\t\tvar a = typeof exports === 'object' ? factory(require(\"d3\")) : factory(root[\"d3\"]);\n\t\tfor(var i in a) (typeof exports === 'object' ? exports : root)[i] = a[i];\n\t}\n})(self, (__WEBPACK_EXTERNAL_MODULE__893__) => {\nreturn ","module.exports = __WEBPACK_EXTERNAL_MODULE__893__;","// The module cache\nvar __webpack_module_cache__ = {};\n\n// The require function\nfunction __webpack_require__(moduleId) {\n\t// Check if module is in cache\n\tvar cachedModule = __webpack_module_cache__[moduleId];\n\tif (cachedModule !== undefined) {\n\t\treturn cachedModule.exports;\n\t}\n\t// Create a new module (and put it into the cache)\n\tvar module = __webpack_module_cache__[moduleId] = {\n\t\t// no module.id needed\n\t\t// no module.loaded needed\n\t\texports: {}\n\t};\n\n\t// Execute the module function\n\t__webpack_modules__[moduleId](module, module.exports, __webpack_require__);\n\n\t// Return the exports of the module\n\treturn module.exports;\n}\n\n","// define getter functions for harmony exports\n__webpack_require__.d = (exports, definition) => {\n\tfor(var key in definition) {\n\t\tif(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {\n\t\t\tObject.defineProperty(exports, key, { enumerable: true, get: definition[key] });\n\t\t}\n\t}\n};","__webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))","// define __esModule on exports\n__webpack_require__.r = (exports) => {\n\tif(typeof Symbol !== 'undefined' && Symbol.toStringTag) {\n\t\tObject.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });\n\t}\n\tObject.defineProperty(exports, '__esModule', { value: true });\n};","export type MetaDataType = { class: string; value: any }; // eslint-disable-line @typescript-eslint/no-explicit-any\n\nexport class MetaData {\n constructor(\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n private data: Record,\n private extraKey?: string,\n ) {}\n\n get(keys: string[]) {\n return this.slice(keys).filter((k) => typeof k.value === \"string\");\n }\n\n private slice(keys: string[]) {\n if (!this.data) return [];\n\n if (this.extraKey) return this.sliceWithExtraKey(keys);\n else return this.sliceWithoutExtraKey(keys);\n }\n\n private sliceWithExtraKey(keys: string[]) {\n const data: MetaDataType[] = [];\n\n keys.forEach((k) => {\n if (this.data[k] && this.data[k][this.extraKey]) data.push({ class: k, value: this.data[k][this.extraKey] });\n });\n\n return data;\n }\n\n private sliceWithoutExtraKey(keys: string[]) {\n const data: MetaDataType[] = [];\n\n keys.forEach((k) => {\n if (this.data[k]) data.push({ class: k, value: this.data[k] });\n });\n\n return data;\n }\n}\n","export function classify(string: string) {\n return string.replace(\" \", \"-\").toLowerCase();\n}\n","import * as d3 from \"d3\";\n\nimport { Color } from \"./diagram\";\nimport { MetaData, MetaDataType } from \"./meta_data\";\nimport { NodePosition } from \"./position_cache\";\nimport { classify } from \"./util\";\n\nexport type Constructor = (data: NodeDataType, id: number, options: NodeOptions) => void;\n\nexport type NodeDataType = {\n name: string;\n group: string[];\n icon: string;\n meta: Record; // eslint-disable-line @typescript-eslint/no-explicit-any\n class: string;\n};\n\nexport type NodeOptions = {\n width: number;\n height: number;\n metaKeys: string[];\n color: Color;\n tooltip: boolean;\n};\n\nclass NodeBase {\n private static all: Record; // eslint-disable-line @typescript-eslint/no-explicit-any\n\n public name: string;\n public group: string[];\n public metaList: MetaDataType[];\n public meta: Record; // eslint-disable-line @typescript-eslint/no-explicit-any\n public x: number;\n public y: number;\n\n private icon: string;\n private extraClass: string;\n private width: number;\n private height: number;\n private padding: number;\n private tspanOffset: string;\n\n constructor(\n data: NodeDataType,\n public id: number,\n private options: NodeOptions,\n ) {\n this.name = data.name;\n this.group = typeof data.group === \"string\" ? [data.group] : data.group || [];\n this.icon = data.icon;\n this.metaList = new MetaData(data.meta).get(options.metaKeys);\n this.meta = data.meta;\n this.extraClass = data.class || \"\";\n\n this.width = options.width || 60;\n this.height = options.height || 40;\n this.padding = 3;\n this.tspanOffset = \"1.1em\";\n\n this.register(id);\n }\n\n private register(id: number) {\n Node.all = Node.all || {};\n Node.all[this.name] = id;\n }\n\n transform() {\n const x = this.x - this.width / 2 + this.padding;\n const y = this.y - this.height / 2 + this.padding;\n return `translate(${x}, ${y})`;\n }\n\n private nodeWidth() {\n return this.width - 2 * this.padding;\n }\n\n private nodeHeight() {\n return this.height - 2 * this.padding;\n }\n\n private xForText() {\n return this.nodeWidth() / 2;\n }\n\n private yForText() {\n // svg ignores padding for some reason\n return this.height / 2;\n }\n\n static idByName(name: string) {\n if (Node.all[name] === undefined) throw `Unknown node \"${name}\"`;\n return Node.all[name];\n }\n\n public nodeId() {\n return classify(this.name);\n }\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n static render(layer: d3.Selection, nodes: Node[]) {\n const node: d3.Selection = layer\n .selectAll(\".node\")\n .data(nodes)\n .enter()\n .append(\"g\")\n .attr(\"id\", (d) => d.nodeId())\n .attr(\"name\", (d) => d.name)\n .attr(\"transform\", (d) => d.transform());\n\n node.each(function (this: SVGGElement, d) {\n if (d.icon) Node.appendImage(this);\n else Node.appendRect(this);\n\n Node.appendText(this);\n });\n\n return node;\n }\n\n private static appendText(container: SVGGElement) {\n const text = (d3.select(container) as d3.Selection)\n .append(\"text\")\n .attr(\"text-anchor\", \"middle\")\n .attr(\"x\", (d) => d.xForText())\n .attr(\"y\", (d) => d.yForText());\n text\n .append(\"tspan\")\n .text((d) => d.name)\n .attr(\"x\", (d) => d.xForText());\n\n text.each((d) => {\n // Show meta only when \"tooltip\" option is not configured\n if (!d.options.tooltip) {\n Node.appendMetaText(text, d.metaList);\n }\n });\n }\n\n private static appendMetaText(container: d3.Selection, meta: MetaDataType[]) {\n meta.forEach((m) => {\n container\n .append(\"tspan\")\n .attr(\"x\", (d) => d.xForText())\n .attr(\"dy\", (d) => d.tspanOffset)\n .attr(\"class\", m.class)\n .text(m.value);\n });\n }\n\n private static appendImage(container: SVGGElement) {\n (d3.select(container) as d3.Selection)\n .attr(\"class\", (d) => `node image ${classify(d.name)} ${d.extraClass}`)\n .append(\"image\")\n .attr(\"xlink:href\", (d) => d.icon)\n .attr(\"width\", (d) => d.nodeWidth())\n .attr(\"height\", (d) => d.nodeHeight());\n }\n\n private static appendRect(container: SVGGElement) {\n (d3.select(container) as d3.Selection)\n .attr(\"class\", (d) => `node rect ${classify(d.name)} ${d.extraClass}`)\n .append(\"rect\")\n .attr(\"width\", (d) => d.nodeWidth())\n .attr(\"height\", (d) => d.nodeHeight())\n .attr(\"rx\", 5)\n .attr(\"ry\", 5)\n .style(\"fill\", (d) => d.options.color(undefined));\n }\n\n static tick(node: d3.Selection) {\n node.attr(\"transform\", (d) => d.transform());\n }\n\n static setPosition(node: d3.Selection, position: NodePosition[]) {\n node.attr(\"transform\", (d, i) => {\n if (\n position[i]?.x !== null &&\n position[i]?.x !== undefined &&\n position[i]?.y !== null &&\n position[i]?.y !== undefined\n ) {\n d.x = position[i].x;\n d.y = position[i].y;\n }\n return d.transform();\n });\n }\n\n static reset() {\n Node.all = null;\n }\n}\n\nconst Eventable = (Base: typeof NodeBase) => {\n class EventableNode extends Base {\n private dispatch: d3.Dispatch;\n\n constructor(data: NodeDataType, id: number, options: NodeOptions) {\n super(data, id, options);\n\n this.dispatch = d3.dispatch(\"rendered\");\n }\n\n static render(layer, nodes) {\n const node = super.render(layer, nodes);\n\n node.each(function (this: SVGGElement, d: Node & EventableNode) {\n d.dispatch.rendered(this);\n });\n\n return node;\n }\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n on(name: string, callback: (element: SVGGElement) => any) {\n this.dispatch.on(name, callback);\n }\n }\n\n return EventableNode;\n};\n\nconst Pluggable = (Base: typeof NodeBase) => {\n class Node extends Base {\n private static pluginConstructors: Constructor[] = [];\n\n constructor(data: NodeDataType, id: number, options: NodeOptions) {\n super(data, id, options);\n\n for (const constructor of Node.pluginConstructors) {\n // Call Pluggable at last as constructor may call methods defined in other classes\n constructor.bind(this)(data, id, options);\n }\n }\n\n static registerConstructor(func: Constructor) {\n Node.pluginConstructors.push(func);\n }\n }\n\n return Node;\n};\n\nclass EventableNode extends Eventable(NodeBase) {}\n\n// Call Pluggable at last as constructor may call methods defined in other classes\nclass Node extends Pluggable(EventableNode) {}\n\nexport { Node };\n","import * as d3 from \"d3\";\n\nimport { Constructor as NodeConstructor, Node, NodeDataType, NodeOptions } from \"../../../src/node\";\nimport { PluginClass } from \"../../../src/plugin\";\nimport { classify } from \"../../../src/util\";\n\ntype Options = {\n showKey?: string;\n hideKey?: string;\n};\n\nclass RemovableNode extends Node {\n public selected;\n\n public toggleSelected() {\n this.selected = !this.selected;\n }\n\n public reset() {\n this.selected = false;\n }\n\n public textColor() {\n return this.selected ? \"red\" : \"black\";\n }\n}\n\nexport const RemovableNodePlugin: PluginClass = class RemovableNodePlugin {\n private static showKey = \"Escape\";\n private static hideKey = \"d\";\n\n static load(Group, Node, Link, options: Options = {}) {\n if (options.showKey) {\n RemovableNodePlugin.showKey = options.showKey;\n }\n if (options.hideKey) {\n RemovableNodePlugin.hideKey = options.hideKey;\n }\n\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n Node.registerConstructor(function (data: NodeDataType, id: number, options: NodeOptions) {\n this.selected = false;\n\n this.on(\"rendered\", (element: SVGGElement) => {\n RemovableNodePlugin.configureRemovableNode(element);\n });\n } as NodeConstructor);\n\n RemovableNodePlugin.configureRemovableNodes();\n\n // Copy methods\n Node.prototype.toggleSelected = RemovableNode.prototype.toggleSelected;\n Node.prototype.reset = RemovableNode.prototype.reset;\n Node.prototype.textColor = RemovableNode.prototype.textColor;\n }\n\n /**\n * Configure keyboard event listener to show or hide Nodes and Links\n */\n private static configureRemovableNodes() {\n d3.select(\"body\").on(\"keydown\", () => {\n switch ((d3.event as KeyboardEvent).key) {\n case RemovableNodePlugin.showKey:\n RemovableNodePlugin.show();\n break;\n case RemovableNodePlugin.hideKey:\n RemovableNodePlugin.hide();\n }\n });\n }\n\n /**\n * Configure click event listener to select Nodes\n */\n private static configureRemovableNode(element: SVGGElement) {\n const d3Element = d3.select(element);\n d3Element.on(\"click.removableNode\", function (this: SVGGElement, d: RemovableNode) {\n // Do nothing for dragging\n if ((d3.event as MouseEvent).defaultPrevented) {\n return;\n }\n\n d.toggleSelected();\n RemovableNodePlugin.applyColor(this);\n });\n }\n\n private static applyColor(element: SVGGElement) {\n d3.select(element)\n .select(\"text tspan\")\n .style(\"fill\", (d: RemovableNode) => d.textColor());\n }\n\n private static show() {\n d3.selectAll(\".node\")\n .style(\"display\", \"inline\")\n .each(function (this: SVGGElement, d: RemovableNode) {\n d.reset();\n RemovableNodePlugin.applyColor(this);\n });\n\n RemovableNodePlugin.showLinks();\n }\n\n private static hide() {\n d3.selectAll(\".node\").style(\"display\", (d: RemovableNode) => {\n if (d.selected) {\n // Hide connected elements\n RemovableNodePlugin.hideLinks(d.name);\n RemovableNodePlugin.hideToolTips(d.name);\n return \"none\";\n }\n\n return \"inline\";\n });\n }\n\n private static showLinks() {\n d3.selectAll(`.link`).style(\"display\", \"inline\");\n }\n\n private static hideLinks(nodeName: string) {\n d3.selectAll(`.link.${classify(nodeName)}`).style(\"display\", \"none\");\n }\n\n private static hideToolTips(nodeName: string) {\n d3.selectAll(`.tooltip.${classify(nodeName)}`).attr(\"visibility\", \"hidden\");\n }\n};\n\nexport default RemovableNodePlugin;\n"],"names":["root","factory","exports","module","require","define","amd","a","i","self","__WEBPACK_EXTERNAL_MODULE__893__","__webpack_module_cache__","__webpack_require__","moduleId","cachedModule","undefined","__webpack_modules__","d","definition","key","o","Object","defineProperty","enumerable","get","obj","prop","prototype","hasOwnProperty","call","r","Symbol","toStringTag","value","MetaData","constructor","data","extraKey","keys","this","slice","filter","k","sliceWithExtraKey","sliceWithoutExtraKey","forEach","push","class","classify","string","replace","toLowerCase","NodeBase","id","options","name","group","icon","metaList","meta","metaKeys","extraClass","width","height","padding","tspanOffset","register","Node","all","transform","x","y","nodeWidth","nodeHeight","xForText","yForText","idByName","nodeId","render","layer","nodes","node","selectAll","enter","append","attr","each","appendImage","appendRect","appendText","container","text","tooltip","appendMetaText","m","style","color","tick","setPosition","position","reset","Eventable","Base","super","dispatch","rendered","on","callback","Pluggable","pluginConstructors","bind","registerConstructor","func","EventableNode","RemovableNode","toggleSelected","selected","textColor","RemovableNodePlugin","load","Group","Link","showKey","hideKey","element","configureRemovableNode","configureRemovableNodes","show","hide","defaultPrevented","applyColor","select","showLinks","hideLinks","hideToolTips","nodeName"],"sourceRoot":""} -------------------------------------------------------------------------------- /plugins/removable_node/src/plugin.ts: -------------------------------------------------------------------------------- 1 | import * as d3 from "d3"; 2 | 3 | import { Constructor as NodeConstructor, Node, NodeDataType, NodeOptions } from "../../../src/node"; 4 | import { PluginClass } from "../../../src/plugin"; 5 | import { classify } from "../../../src/util"; 6 | 7 | type Options = { 8 | showKey?: string; 9 | hideKey?: string; 10 | }; 11 | 12 | class RemovableNode extends Node { 13 | public selected; 14 | 15 | public toggleSelected() { 16 | this.selected = !this.selected; 17 | } 18 | 19 | public reset() { 20 | this.selected = false; 21 | } 22 | 23 | public textColor() { 24 | return this.selected ? "red" : "black"; 25 | } 26 | } 27 | 28 | export const RemovableNodePlugin: PluginClass = class RemovableNodePlugin { 29 | private static showKey = "Escape"; 30 | private static hideKey = "d"; 31 | 32 | static load(Group, Node, Link, options: Options = {}) { 33 | if (options.showKey) { 34 | RemovableNodePlugin.showKey = options.showKey; 35 | } 36 | if (options.hideKey) { 37 | RemovableNodePlugin.hideKey = options.hideKey; 38 | } 39 | 40 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 41 | Node.registerConstructor(function (data: NodeDataType, id: number, options: NodeOptions) { 42 | this.selected = false; 43 | 44 | this.on("rendered", (element: SVGGElement) => { 45 | RemovableNodePlugin.configureRemovableNode(element); 46 | }); 47 | } as NodeConstructor); 48 | 49 | RemovableNodePlugin.configureRemovableNodes(); 50 | 51 | // Copy methods 52 | Node.prototype.toggleSelected = RemovableNode.prototype.toggleSelected; 53 | Node.prototype.reset = RemovableNode.prototype.reset; 54 | Node.prototype.textColor = RemovableNode.prototype.textColor; 55 | } 56 | 57 | /** 58 | * Configure keyboard event listener to show or hide Nodes and Links 59 | */ 60 | private static configureRemovableNodes() { 61 | d3.select("body").on("keydown", () => { 62 | switch ((d3.event as KeyboardEvent).key) { 63 | case RemovableNodePlugin.showKey: 64 | RemovableNodePlugin.show(); 65 | break; 66 | case RemovableNodePlugin.hideKey: 67 | RemovableNodePlugin.hide(); 68 | } 69 | }); 70 | } 71 | 72 | /** 73 | * Configure click event listener to select Nodes 74 | */ 75 | private static configureRemovableNode(element: SVGGElement) { 76 | const d3Element = d3.select(element); 77 | d3Element.on("click.removableNode", function (this: SVGGElement, d: RemovableNode) { 78 | // Do nothing for dragging 79 | if ((d3.event as MouseEvent).defaultPrevented) { 80 | return; 81 | } 82 | 83 | d.toggleSelected(); 84 | RemovableNodePlugin.applyColor(this); 85 | }); 86 | } 87 | 88 | private static applyColor(element: SVGGElement) { 89 | d3.select(element) 90 | .select("text tspan") 91 | .style("fill", (d: RemovableNode) => d.textColor()); 92 | } 93 | 94 | private static show() { 95 | d3.selectAll(".node") 96 | .style("display", "inline") 97 | .each(function (this: SVGGElement, d: RemovableNode) { 98 | d.reset(); 99 | RemovableNodePlugin.applyColor(this); 100 | }); 101 | 102 | RemovableNodePlugin.showLinks(); 103 | } 104 | 105 | private static hide() { 106 | d3.selectAll(".node").style("display", (d: RemovableNode) => { 107 | if (d.selected) { 108 | // Hide connected elements 109 | RemovableNodePlugin.hideLinks(d.name); 110 | RemovableNodePlugin.hideToolTips(d.name); 111 | return "none"; 112 | } 113 | 114 | return "inline"; 115 | }); 116 | } 117 | 118 | private static showLinks() { 119 | d3.selectAll(`.link`).style("display", "inline"); 120 | } 121 | 122 | private static hideLinks(nodeName: string) { 123 | d3.selectAll(`.link.${classify(nodeName)}`).style("display", "none"); 124 | } 125 | 126 | private static hideToolTips(nodeName: string) { 127 | d3.selectAll(`.tooltip.${classify(nodeName)}`).attr("visibility", "hidden"); 128 | } 129 | }; 130 | 131 | export default RemovableNodePlugin; 132 | -------------------------------------------------------------------------------- /plugins/removable_node/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "ES6", 4 | "target": "ES2017", 5 | "outDir": "dist", 6 | "sourceMap": true 7 | }, 8 | "include": [ 9 | "src/*" 10 | ], 11 | "lib": [ 12 | "ES2017" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /plugins/removable_node/webpack.common.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | entry: "./plugins/removable_node/src/plugin", 3 | output: { 4 | path: __dirname, 5 | libraryTarget: "umd", 6 | }, 7 | resolve: { 8 | extensions: [".ts", ".js"] 9 | }, 10 | module: { 11 | rules: [ 12 | { 13 | test: /\.ts$/, 14 | use: ["ts-loader"] 15 | } 16 | ] 17 | }, 18 | externals: { 19 | d3: "d3" 20 | }, 21 | devtool: "source-map" 22 | }; 23 | -------------------------------------------------------------------------------- /plugins/removable_node/webpack.dev.js: -------------------------------------------------------------------------------- 1 | const { merge } = require("webpack-merge"); 2 | const common = require("./webpack.common.js"); 3 | 4 | module.exports = merge(common, { 5 | mode: "development", 6 | output: { 7 | filename: "plugin.js" 8 | } 9 | } 10 | ); 11 | -------------------------------------------------------------------------------- /plugins/removable_node/webpack.prod.js: -------------------------------------------------------------------------------- 1 | const { merge } = require("webpack-merge"); 2 | const common = require("./webpack.common.js"); 3 | 4 | module.exports = merge(common, { 5 | mode: "production", 6 | output: { 7 | filename: "plugin.min.js" 8 | } 9 | } 10 | ); 11 | -------------------------------------------------------------------------------- /src/bundle.ts: -------------------------------------------------------------------------------- 1 | import * as d3 from "d3"; 2 | 3 | import { Link, LinkDataType } from "./link"; 4 | 5 | export class Bundle { 6 | // Bundle group in the whole graph 7 | // { 8 | // "[, , \"\"]": [, ...], 9 | // } 10 | private static groups: Record; 11 | 12 | // member Links of this Bundle 13 | protected readonly links: Link[]; 14 | 15 | private color: string; 16 | private width: number; 17 | private space: number; 18 | private _shiftMultiplier: number; 19 | 20 | constructor( 21 | links: Link[], 22 | public id: number, 23 | ) { 24 | this.links = links; 25 | this.color = "#7a4e4e"; 26 | this.width = 2; 27 | this.space = 4; 28 | } 29 | 30 | static divide(links: Link[]) { 31 | Bundle.groups = {}; 32 | 33 | for (const l of links) { 34 | if (l.bundle) { 35 | const key = this.groupKey(l); 36 | (Bundle.groups[key] || (Bundle.groups[key] = [])).push(l.id); 37 | } 38 | } 39 | 40 | return Object.values(Bundle.groups).map((ids, i) => { 41 | return new Bundle( 42 | ids.map((id) => links[id]), 43 | i, 44 | ); 45 | }); 46 | } 47 | 48 | private static groupKey(link: Link) { 49 | return JSON.stringify([link.source, link.target, link.bundle]); 50 | } 51 | 52 | static render( 53 | linkLayer: d3.Selection, // eslint-disable-line @typescript-eslint/no-explicit-any 54 | bundles: Bundle[], 55 | ) { 56 | const bundleGroup = linkLayer 57 | .selectAll(".bundle") 58 | .data(bundles) 59 | .enter() 60 | .append("g") 61 | .attr("class", (d) => d.class()); 62 | 63 | const bundle = bundleGroup 64 | .append("path") 65 | .attr("d", (d) => d.d()) 66 | .attr("stroke", (d) => d.color) 67 | .attr("stroke-width", (d) => d.width) 68 | .attr("fill", "none") 69 | .attr("id", (d) => d.bundleId()); 70 | 71 | return bundle; 72 | } 73 | 74 | static reset() { 75 | Bundle.groups = null; 76 | } 77 | 78 | // sort by bundle with preserving order 79 | static sortByBundle(links: LinkDataType[]) { 80 | return links.sort((a, b) => { 81 | switch (true) { 82 | case !!a.bundle && !b.bundle: 83 | return -1; 84 | case !a.bundle && !!b.bundle: 85 | return 1; 86 | case !a.bundle && !b.bundle: 87 | return 0; 88 | 89 | // !!a.bundle && !!b.bundle === true 90 | case a.bundle.toString() < b.bundle.toString(): 91 | return -1; 92 | case a.bundle.toString() > b.bundle.toString(): 93 | return 1; 94 | 95 | default: 96 | return 0; 97 | } 98 | }); 99 | } 100 | 101 | d() { 102 | const first = this.links[0].centerCoordinates(); 103 | const last = this.links[this.links.length - 1].centerCoordinates(); 104 | const gap = Math.sqrt(Math.pow(first[0] - last[0], 2) + Math.pow(first[1] - last[1], 2)); 105 | 106 | if (gap === 0) { 107 | return ""; 108 | } 109 | 110 | const angle = this.links[0].angle() + 90; 111 | const start = [ 112 | ((first[0] - last[0]) * this.space) / gap + first[0], 113 | ((first[1] - last[1]) * this.space) / gap + first[1], 114 | ]; 115 | const end = [ 116 | ((last[0] - first[0]) * this.space) / gap + last[0], 117 | ((last[1] - first[1]) * this.space) / gap + last[1], 118 | ]; 119 | 120 | return `M ${start[0]} ${start[1]} A ${gap / 2 + 10},5 ${angle} 1,0 ${end[0]} ${end[1]}`; 121 | } 122 | 123 | private shiftMultiplier() { 124 | if (!this._shiftMultiplier) { 125 | const members = this.links[0].group() || []; 126 | this._shiftMultiplier = this.links.reduce((sum, l) => (sum += l.id - (members.length - 1) / 2), 0) / 2; 127 | } 128 | 129 | return this._shiftMultiplier; 130 | } 131 | 132 | static shiftBundle(bundle: d3.Selection) { 133 | bundle.attr("d", (d) => d.d()); 134 | } 135 | 136 | private class() { 137 | // modified link's class 138 | return this.links[0].class().replace(/^link/, "bundle"); 139 | } 140 | 141 | private bundleId() { 142 | return `bundle${this.id}`; 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /src/diagram.ts: -------------------------------------------------------------------------------- 1 | import "./hack_cola"; 2 | 3 | import * as d3 from "d3"; 4 | 5 | import { WebColaConstraint } from "../types/WebCola"; 6 | import { Bundle } from "./bundle"; 7 | import { Group, GroupOptions } from "./group"; 8 | import { Link, LinkDataType } from "./link"; 9 | import { LinkTooltip } from "./link_tooltip"; 10 | import { Node, NodeDataType, NodeOptions } from "./node"; 11 | import { NodeTooltip } from "./node_tooltip"; 12 | import { NodePosition, PositionCache } from "./position_cache"; 13 | 14 | const cola = require("cola"); // eslint-disable-line @typescript-eslint/no-require-imports 15 | 16 | type LinkWidthFunction = (object) => number; 17 | export type HrefFunction = (object, type?: "node" | "link") => string; 18 | export type InetHengeDataType = { nodes: NodeDataType[]; links: LinkDataType[] }; 19 | // Fix @types/d3/index.d.ts. Should be "d3.scale.Ordinal" but "d3.scale.Ordinal" somehow 20 | export type Color = d3.scale.Ordinal; 21 | type PositionHint = { 22 | nodeCallback?: (node: Node) => NodePosition; 23 | }; 24 | type PositionConstraint = { 25 | axis: "x" | "y"; 26 | nodesCallback: (nodes: Node[]) => Node[][]; 27 | }; 28 | type DiagramOptionType = { 29 | // Options publicly available 30 | width: number; 31 | height: number; 32 | nodeWidth: number; 33 | nodeHeight: number; 34 | groupPadding: number; 35 | initialTicks: number; 36 | ticks: number; 37 | positionCache: boolean | string; 38 | positionHint: PositionHint; 39 | positionConstraints: PositionConstraint[]; 40 | bundle: boolean; 41 | pop: RegExp; 42 | distance: LinkWidthFunction; 43 | tooltip: string; 44 | href: HrefFunction; 45 | 46 | // Internal options 47 | selector: string; 48 | urlOrData: string | InetHengeDataType; 49 | groupPattern: RegExp | undefined; 50 | color: Color; 51 | maxTicks: number; 52 | 53 | meta: string[]; 54 | }; 55 | 56 | class DiagramBase { 57 | public tickCallback: () => void; 58 | 59 | private options: DiagramOptionType; 60 | private readonly setDistance: (object) => number; 61 | private getLinkWidth: LinkWidthFunction; 62 | private zoom: d3.behavior.Zoom; 63 | private cola; 64 | private uniqueUrl: string; 65 | private positionCache: PositionCache; 66 | private indicator: d3.Selection; // eslint-disable-line @typescript-eslint/no-explicit-any 67 | private initialTranslate: [number, number]; 68 | private initialScale: number; 69 | private svg: d3.Selection; // eslint-disable-line @typescript-eslint/no-explicit-any 70 | 71 | constructor(container: string, urlOrData: string | InetHengeDataType, options: DiagramOptionType) { 72 | options ||= {} as DiagramOptionType; 73 | this.options = Object.assign({}, options); 74 | this.options.selector = container; 75 | this.options.urlOrData = urlOrData; 76 | this.options.groupPattern = options.pop; 77 | this.options.width = options.width || 960; 78 | this.options.height = options.height || 600; 79 | this.options.positionHint = options.positionHint || {}; 80 | this.options.positionConstraints = options.positionConstraints || []; 81 | 82 | this.options.color = d3.scale.category20(); 83 | this.options.initialTicks = options.initialTicks || 0; 84 | this.options.maxTicks = options.ticks || 1000; 85 | // NOTE: true or 'fixed' (experimental) affects behavior 86 | this.options.positionCache = "positionCache" in options ? options.positionCache : true; 87 | // NOTE: This is an experimental option 88 | this.options.bundle = "bundle" in options ? options.bundle : false; 89 | this.options.tooltip = options.tooltip; 90 | 91 | this.setDistance = this.linkDistance(options.distance || 150); 92 | NodeTooltip.setHref(options.href); 93 | LinkTooltip.setHref(options.href); 94 | } 95 | 96 | init(...meta: string[]) { 97 | this.options.meta = meta; 98 | this.cola = this.initCola(); 99 | this.svg = this.initSvg(); 100 | 101 | this.displayLoadMessage(); 102 | 103 | if (typeof this.options.urlOrData === "object") { 104 | setTimeout(() => { 105 | // Run asynchronously 106 | this.render(this.options.urlOrData as InetHengeDataType); 107 | }); 108 | } else { 109 | d3.json(this.url(), (error, data) => { 110 | if (error) { 111 | console.error(error); 112 | this.showMessage(`Failed to load "${this.url()}"`); 113 | } 114 | 115 | this.render(data); 116 | }); 117 | } 118 | } 119 | 120 | initCola() { 121 | return cola 122 | .d3adaptor() 123 | .avoidOverlaps(true) 124 | .handleDisconnected(false) 125 | .size([this.options.width, this.options.height]); 126 | } 127 | 128 | initSvg() { 129 | this.zoom = d3.behavior.zoom(); 130 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 131 | const container: d3.Selection = d3 132 | .select(this.options.selector) 133 | .append("svg") 134 | .attr("width", this.options.width) 135 | .attr("height", this.options.height) 136 | .append("g") 137 | .call(this.zoom.on("zoom", () => this.zoomCallback(container))) 138 | .append("g"); 139 | 140 | container 141 | .append("rect") 142 | .attr("width", this.options.width * 10) // 10 is huge enough 143 | .attr("height", this.options.height * 10) 144 | .attr("transform", `translate(-${this.options.width * 5}, -${this.options.height * 5})`) 145 | .style("opacity", 0); 146 | 147 | return container; 148 | } 149 | 150 | render(data: InetHengeDataType) { 151 | try { 152 | const nodes = data.nodes 153 | ? data.nodes.map( 154 | (n, i) => 155 | new Node(n, i, { 156 | width: this.options.nodeWidth, 157 | height: this.options.nodeHeight, 158 | metaKeys: this.options.meta, 159 | color: this.options.color, 160 | tooltip: !!this.options.tooltip, 161 | } as NodeOptions), 162 | ) 163 | : []; 164 | const links = data.links 165 | ? Bundle.sortByBundle(data.links).map( 166 | (l, i) => 167 | new Link(l, i, { 168 | metaKeys: this.options.meta, 169 | linkWidth: this.getLinkWidth, 170 | }), 171 | ) 172 | : []; 173 | const groups = Group.divide(nodes, this.options.groupPattern, { 174 | color: this.options.color, 175 | padding: this.options.groupPadding, 176 | } as GroupOptions); 177 | const nodeTooltips = nodes.map((n) => new NodeTooltip(n, this.options.tooltip)); 178 | const linkTooltips = links.map((l) => new LinkTooltip(l, this.options.tooltip)); 179 | const bundles = Bundle.divide(links); 180 | 181 | this.cola.nodes(nodes).links(links).groups(groups); 182 | this.applyConstraints(this.options.positionConstraints, nodes); 183 | this.setDistance(this.cola); 184 | 185 | // Start to update Link.source and Link.target with Node object after 186 | // initial layout iterations without any constraints. 187 | this.cola.start(this.options.initialTicks); 188 | 189 | const groupLayer = this.svg.append("g").attr("id", "groups"); 190 | const linkLayer = this.svg.append("g").attr("id", "links"); 191 | const nodeLayer = this.svg.append("g").attr("id", "nodes"); 192 | const linkLabelLayer = this.svg.append("g").attr("id", "link-labels"); 193 | const tooltipLayer = this.svg.append("g").attr("id", "tooltips"); 194 | 195 | const [link, path, label] = Link.render(linkLayer, linkLabelLayer, links); 196 | const bundle = Bundle.render(linkLayer, bundles); 197 | 198 | const group = Group.render(groupLayer, groups).call( 199 | this.cola 200 | .drag() 201 | .on("dragstart", DiagramBase.dragstartCallback) 202 | .on("drag", () => { 203 | if (this.options.bundle) { 204 | Link.shiftBundle(link, path, label, bundle); 205 | } 206 | 207 | NodeTooltip.followObject(nodeTooltip); 208 | LinkTooltip.followObject(linkTooltip); 209 | }), 210 | ); 211 | 212 | const node = Node.render(nodeLayer, nodes).call( 213 | this.cola 214 | .drag() 215 | .on("dragstart", DiagramBase.dragstartCallback) 216 | .on("drag", () => { 217 | if (this.options.bundle) { 218 | Link.shiftBundle(link, path, label, bundle); 219 | } 220 | 221 | NodeTooltip.followObject(nodeTooltip); 222 | LinkTooltip.followObject(linkTooltip); 223 | }), 224 | ); 225 | 226 | // without path calculation 227 | this.configureTick(group, node, link); 228 | 229 | this.positionCache = PositionCache.load(data, this.options.groupPattern); 230 | if (this.options.positionCache && this.positionCache) { 231 | // NOTE: Evaluate only when positionCache: true or 'fixed', and 232 | // when the stored position cache matches a pair of given data and pop 233 | Group.setPosition(group, this.positionCache.group); 234 | Node.setPosition(node, this.positionCache.node); 235 | Link.setPosition(link, this.positionCache.link); 236 | } else { 237 | if (this.options.positionHint.nodeCallback) { 238 | Node.setPosition( 239 | node, 240 | node.data().map((d) => this.options.positionHint.nodeCallback(d)), 241 | ); 242 | this.cola.start(); // update internal positions of objects before ticks forward 243 | } 244 | 245 | this.ticksForward(); 246 | this.positionCache = new PositionCache(data, this.options.groupPattern); 247 | this.savePosition(group, node, link); 248 | } 249 | 250 | this.hideLoadMessage(); 251 | 252 | this.configureTick(group, node, link, path, label); // render path 253 | this.removeConstraints(); 254 | 255 | this.cola.start(); 256 | if (this.options.bundle) { 257 | Link.shiftBundle(link, path, label, bundle); 258 | } 259 | 260 | path.attr("d", (d) => d.d()); // make sure path calculation is done 261 | DiagramBase.freeze(node); 262 | 263 | const nodeTooltip = NodeTooltip.render(tooltipLayer, nodeTooltips); 264 | const linkTooltip = LinkTooltip.render(tooltipLayer, linkTooltips); 265 | 266 | // NOTE: This is an experimental option 267 | if (this.options.positionCache === "fixed") { 268 | this.cola.on("end", () => { 269 | this.savePosition(group, node, link); 270 | }); 271 | } 272 | } catch (e) { 273 | this.showMessage(e); 274 | throw e; 275 | } 276 | } 277 | 278 | linkWidth(func: LinkWidthFunction) { 279 | this.getLinkWidth = func; 280 | } 281 | 282 | attr(name: string, value: string) { 283 | if (!this.initialTranslate) { 284 | this.saveInitialTranslate(); 285 | } 286 | 287 | this.svg.attr(name, value); 288 | 289 | const transform = d3.transform(this.svg.attr("transform")); // FIXME: This is valid only for d3.js v3 290 | this.zoom.scale(transform.scale[0]); // NOTE: Assuming ky = kx 291 | this.zoom.translate(transform.translate); 292 | } 293 | 294 | destroy() { 295 | d3.select("body svg").remove(); 296 | Node.reset(); 297 | Link.reset(); 298 | Bundle.reset(); 299 | } 300 | 301 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 302 | private static freeze(container: d3.Selection) { 303 | container.each((d) => (d.fixed = true)); 304 | } 305 | 306 | private static dragstartCallback() { 307 | (d3.event as d3.ZoomEvent).sourceEvent.stopPropagation(); 308 | } 309 | 310 | private linkDistance(distance: number | ((any) => number)) { 311 | if (typeof distance === "function") return distance; 312 | else return (cola) => cola.linkDistance(distance); 313 | } 314 | 315 | private url() { 316 | if (this.uniqueUrl) { 317 | return this.uniqueUrl; 318 | } 319 | 320 | this.uniqueUrl = `${this.options.urlOrData}?${new Date().getTime()}`; 321 | return this.uniqueUrl; 322 | } 323 | 324 | private configureTick( 325 | group: d3.Selection, 326 | node: d3.Selection, 327 | link: d3.Selection, 328 | path?: d3.Selection, 329 | label?: d3.Selection, // eslint-disable-line @typescript-eslint/no-explicit-any 330 | ) { 331 | // this.cola.on() overrides existing listener, not additionally register it. 332 | // May need to call it manually. 333 | this.tickCallback = () => { 334 | Node.tick(node); 335 | Link.tick(link, path, label); 336 | Group.tick(group); 337 | }; 338 | 339 | this.cola.on("tick", this.tickCallback); 340 | } 341 | 342 | private ticksForward(count?: number) { 343 | count = count || this.options.maxTicks; 344 | 345 | for (let i = 0; i < count; i++) this.cola.tick(); 346 | } 347 | 348 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 349 | private zoomCallback(container: d3.Selection) { 350 | if (!this.initialTranslate) { 351 | this.saveInitialTranslate(); 352 | } 353 | 354 | const event = d3.event as d3.ZoomEvent; 355 | 356 | event.scale *= this.initialScale; 357 | event.translate[0] += this.initialTranslate[0]; 358 | event.translate[1] += this.initialTranslate[1]; 359 | 360 | Link.zoom(event.scale); 361 | container.attr("transform", `translate(${event.translate}) scale(${event.scale})`); 362 | } 363 | 364 | private displayLoadMessage() { 365 | this.indicator = this.svg 366 | .append("text") 367 | .attr("x", this.options.width / 2) 368 | .attr("y", this.options.height / 2) 369 | .attr("dy", ".35em") 370 | .style("text-anchor", "middle") 371 | .text("Simulating. Just a moment ..."); 372 | } 373 | 374 | private hideLoadMessage() { 375 | if (this.indicator) this.indicator.remove(); 376 | } 377 | 378 | private showMessage(message: string) { 379 | if (this.indicator) this.indicator.text(message); 380 | } 381 | 382 | private saveInitialTranslate() { 383 | const transform = d3.transform(this.svg.attr("transform")); // FIXME: This is valid only for d3.js v3 384 | this.initialScale = transform.scale[0]; // NOTE: Assuming ky = kx 385 | this.initialTranslate = transform.translate; 386 | } 387 | 388 | private savePosition(group: d3.Selection, node: d3.Selection, link: d3.Selection) { 389 | this.positionCache.save(group, node, link); 390 | } 391 | 392 | private applyConstraints(constraints: PositionConstraint[], nodes: Node[]) { 393 | const colaConstraints: WebColaConstraint[] = []; 394 | 395 | for (const constraint of constraints) { 396 | for (const ns of constraint.nodesCallback(nodes)) { 397 | colaConstraints.push({ 398 | type: "alignment", 399 | axis: constraint.axis, 400 | offsets: ns.map((n) => ({ node: n.id, offset: 0 })), 401 | }); 402 | } 403 | } 404 | 405 | this.cola.constraints(colaConstraints); 406 | } 407 | 408 | private removeConstraints() { 409 | this.cola.constraints([]); 410 | } 411 | } 412 | 413 | const Pluggable = (Base: typeof DiagramBase) => { 414 | class Diagram extends Base { 415 | static plugin(cls, options = {}) { 416 | cls.load(Group, Node, Link, options); 417 | } 418 | } 419 | 420 | return Diagram; 421 | }; 422 | 423 | const Eventable = (Base: typeof DiagramBase) => { 424 | class Diagram extends Base { 425 | private dispatch: d3.Dispatch; 426 | 427 | constructor(container: string, urlOrData: string | InetHengeDataType, options: DiagramOptionType) { 428 | super(container, urlOrData, options); 429 | 430 | this.dispatch = d3.dispatch("rendered"); 431 | } 432 | 433 | render(arg) { 434 | super.render(arg); 435 | this.dispatch.rendered(); 436 | } 437 | 438 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 439 | on(name: string, callback: () => any) { 440 | this.dispatch.on(name, callback); 441 | } 442 | } 443 | 444 | return Diagram; 445 | }; 446 | 447 | class PluggableDiagram extends Pluggable(DiagramBase) {} 448 | 449 | export class Diagram extends Eventable(PluggableDiagram) {} 450 | -------------------------------------------------------------------------------- /src/group.ts: -------------------------------------------------------------------------------- 1 | import * as d3 from "d3"; 2 | 3 | import { Color } from "./diagram"; 4 | import { Node } from "./node"; 5 | import { GroupPosition } from "./position_cache"; 6 | import { classify } from "./util"; 7 | 8 | export type Constructor = (name: string, color: Color) => void; 9 | 10 | export type GroupOptions = { 11 | color: Color; 12 | padding: number; 13 | }; 14 | 15 | export class GroupBase { 16 | private padding: number; 17 | 18 | // Not appropriately defined in @types/d3/index.d.ts 19 | private bounds: any; // eslint-disable-line @typescript-eslint/no-explicit-any 20 | 21 | constructor( 22 | private name: string, 23 | private options: GroupOptions, 24 | ) { 25 | this.padding = options.padding; 26 | } 27 | 28 | transform() { 29 | return `translate(${this.bounds.x}, ${this.bounds.y})`; 30 | } 31 | 32 | private groupWidth() { 33 | return this.bounds.width(); 34 | } 35 | 36 | private groupHeight() { 37 | return this.bounds.height(); 38 | } 39 | 40 | static divide(nodes: Node[], pattern: RegExp, options: GroupOptions) { 41 | const groups = {}; 42 | const register = (name: string, node: Node, parent?: string) => { 43 | const key = `${parent}:${name}`; 44 | groups[key] = groups[key] || new Group(name, options); 45 | // hacky but required due to WebCola implementation 46 | groups[key].push(node); 47 | }; 48 | 49 | nodes.forEach((node) => { 50 | let result = null; 51 | 52 | if (pattern) { 53 | result = node.name.match(pattern); 54 | if (result) { 55 | register(result[1] || result[0], node); 56 | } 57 | } 58 | 59 | // Node type based group 60 | node.group.forEach((name) => register(name, node, String(result))); 61 | }); 62 | 63 | return Object.values(groups as Record); 64 | } 65 | 66 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 67 | static render(layer: d3.Selection, groups: Group[]) { 68 | const group: d3.Selection = layer 69 | .selectAll(".group") 70 | .data(groups) 71 | .enter() 72 | .append("g") 73 | .attr("class", (d) => `group ${classify(d.name)}`) 74 | .attr("transform", (d) => d.transform()); 75 | 76 | group 77 | .append("rect") 78 | .attr("rx", 8) 79 | .attr("ry", 8) 80 | .attr("width", (d) => d.groupWidth()) 81 | .attr("height", (d) => d.groupHeight()) 82 | // Fix @types/d3/index.d.ts. Should be "d3.scale.Ordinal" but "d3.scale.Ordinal" somehow 83 | .style("fill", (d, i) => d.options.color(i.toString())); 84 | 85 | group.append("text").text((d) => d.name); 86 | 87 | return group; 88 | } 89 | 90 | static tick(group: d3.Selection) { 91 | group.attr("transform", (d) => d.transform()); 92 | group 93 | .selectAll("rect") 94 | .attr("width", (d) => d.groupWidth()) 95 | .attr("height", (d) => d.groupHeight()); 96 | } 97 | 98 | static setPosition(group: d3.Selection, position: GroupPosition[]) { 99 | group.attr("transform", (d, i) => { 100 | d.bounds.x = position[i].x; 101 | d.bounds.y = position[i].y; 102 | return d.transform(); 103 | }); 104 | group 105 | .selectAll("rect") 106 | .attr("width", (d, i) => position[i].width) 107 | .attr("height", (d, i) => position[i].height); 108 | } 109 | } 110 | 111 | const WebColable = (Base: typeof GroupBase) => { 112 | class Group extends Base { 113 | private leaves: number[]; // WebCola requires this 114 | 115 | constructor(name: string, options: GroupOptions) { 116 | super(name, options); 117 | 118 | this.leaves = []; 119 | } 120 | 121 | push(node: Node) { 122 | this.leaves.push(node.id); 123 | } 124 | } 125 | 126 | return Group; 127 | }; 128 | 129 | const Eventable = (Base: typeof GroupBase) => { 130 | class EventableGroup extends Base { 131 | private dispatch: d3.Dispatch; 132 | 133 | constructor(name: string, options: GroupOptions) { 134 | super(name, options); 135 | 136 | this.dispatch = d3.dispatch("rendered"); 137 | } 138 | 139 | static render(layer, groups) { 140 | const group = super.render(layer, groups); 141 | 142 | group.each(function (this: SVGGElement, d: Group & EventableGroup) { 143 | d.dispatch.rendered(this); 144 | }); 145 | 146 | return group; 147 | } 148 | 149 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 150 | on(name: string, callback: (element: SVGGElement) => any) { 151 | this.dispatch.on(name, callback); 152 | } 153 | } 154 | 155 | return EventableGroup; 156 | }; 157 | 158 | const Pluggable = (Base: typeof GroupBase) => { 159 | class Group extends Base { 160 | private static pluginConstructors: Constructor[] = []; 161 | 162 | constructor(name: string, options: GroupOptions) { 163 | super(name, options); 164 | 165 | for (const constructor of Group.pluginConstructors) { 166 | // Call Pluggable at last as constructor may call methods defined in other classes 167 | constructor.bind(this)(name, options); 168 | } 169 | } 170 | 171 | static registerConstructor(func: Constructor) { 172 | Group.pluginConstructors.push(func); 173 | } 174 | } 175 | 176 | return Group; 177 | }; 178 | 179 | class WebColableGroup extends WebColable(GroupBase) {} 180 | 181 | class EventableGroup extends Eventable(WebColableGroup) {} 182 | 183 | // Call Pluggable at last as constructor may call methods defined in other classes 184 | class Group extends Pluggable(EventableGroup) {} 185 | 186 | export { Group }; 187 | -------------------------------------------------------------------------------- /src/hack_cola.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | 3 | // Ported from WebCola/cola.js and overrode jaccardLinkLengths() 4 | 5 | function unionCount(a, b) { 6 | var u = {}; 7 | for (var i in a) u[i] = {}; 8 | for (var i in b) u[i] = {}; 9 | return Object.keys(u).length; 10 | } 11 | 12 | function intersectionCount(a, b) { 13 | var n = 0; 14 | for (var i in a) if (typeof b[i] !== "undefined") ++n; 15 | return n; 16 | } 17 | 18 | function getNeighbours(links, la) { 19 | var neighbours = {}; 20 | var addNeighbours = function (u, v) { 21 | if (typeof neighbours[u] === "undefined") neighbours[u] = {}; 22 | neighbours[u][v] = {}; 23 | }; 24 | links.forEach(function (e) { 25 | var u = la.getSourceIndex(e), 26 | v = la.getTargetIndex(e); 27 | addNeighbours(u, v); 28 | addNeighbours(v, u); 29 | }); 30 | return neighbours; 31 | } 32 | 33 | function computeLinkLengths(links, w, f, la) { 34 | var neighbours = getNeighbours(links, la); 35 | links.forEach(function (l) { 36 | var a = neighbours[la.getSourceIndex(l)]; 37 | var b = neighbours[la.getTargetIndex(l)]; 38 | la.setLength(l, 1 + w * f(a, b)); 39 | }); 40 | } 41 | 42 | function jaccardLinkLengths(links, la, w) { 43 | if (w === void 0) { 44 | w = 1; 45 | } 46 | computeLinkLengths( 47 | links, 48 | w, 49 | function (a, b) { 50 | return Math.min(Object.keys(a).length, Object.keys(b).length) < 1.1 51 | ? 0 52 | : 1 - intersectionCount(a, b) / unionCount(a, b); 53 | }, 54 | la, 55 | ); 56 | } 57 | 58 | cola.Layout.prototype.jaccardLinkLengths = function (idealLength, w) { 59 | var _this = this; 60 | if (w === void 0) { 61 | w = 1; 62 | } 63 | this.linkDistance(function (l) { 64 | return idealLength * l.length; 65 | }); 66 | this._linkLengthCalculator = function () { 67 | return jaccardLinkLengths(_this._links, _this.linkAccessor, w); 68 | }; 69 | return this; 70 | }; 71 | -------------------------------------------------------------------------------- /src/link.ts: -------------------------------------------------------------------------------- 1 | import * as d3 from "d3"; 2 | 3 | import { Bundle } from "./bundle"; 4 | import { MetaData, MetaDataType } from "./meta_data"; 5 | import { Node } from "./node"; 6 | import { LinkPosition } from "./position_cache"; 7 | import { classify } from "./util"; 8 | 9 | export type Constructor = (data: LinkDataType, id: number, metaKeys: string[], linkWidth: (object) => number) => void; 10 | 11 | export type LinkDataType = { 12 | source: string; 13 | target: string; 14 | bundle?: number | string; 15 | meta: Record; // eslint-disable-line @typescript-eslint/no-explicit-any 16 | class: string; 17 | }; 18 | 19 | export type LinkOptions = { 20 | metaKeys: string[]; 21 | linkWidth: (object) => number; 22 | }; 23 | 24 | export class LinkBase { 25 | private static groups: Record; 26 | private static scale?: number; 27 | 28 | public readonly bundle?: number | string; 29 | public readonly source: number | Node; 30 | public readonly target: number | Node; 31 | public readonly metaList: MetaDataType[]; 32 | public readonly sourceMeta: MetaDataType[]; 33 | public readonly targetMeta: MetaDataType[]; 34 | 35 | private readonly extraClass: string; 36 | private width: number; 37 | private readonly defaultMargin: number; 38 | private readonly labelXOffset: number; 39 | private readonly labelYOffset: number; 40 | private color: string; 41 | private _margin: number; 42 | private _shiftMultiplier: number; 43 | 44 | constructor( 45 | data: LinkDataType, 46 | public id: number, 47 | private options: LinkOptions, 48 | ) { 49 | this.source = Node.idByName(data.source); 50 | this.target = Node.idByName(data.target); 51 | this.bundle = data.bundle; 52 | this.metaList = new MetaData(data.meta).get(options.metaKeys); 53 | this.sourceMeta = new MetaData(data.meta, "source").get(options.metaKeys); 54 | this.targetMeta = new MetaData(data.meta, "target").get(options.metaKeys); 55 | this.extraClass = data.class || ""; 56 | 57 | if (typeof options.linkWidth === "function") this.width = options.linkWidth(data.meta) || 3; 58 | else this.width = options.linkWidth || 3; 59 | 60 | this.defaultMargin = 15; 61 | this.labelXOffset = 20; 62 | this.labelYOffset = 1.5; // em 63 | this.color = "#7a4e4e"; 64 | 65 | this.register(id); 66 | } 67 | 68 | private register(id: number) { 69 | Link.groups = Link.groups || {}; 70 | 71 | // source and target 72 | const key = [this.source, this.target].sort().toString(); 73 | (Link.groups[key] || (Link.groups[key] = [])).push(id); 74 | } 75 | 76 | private isLabelledPath() { 77 | return this.metaList.length > 0; 78 | } 79 | 80 | private isReversePath() { 81 | return this.targetMeta.length > 0; 82 | } 83 | 84 | d() { 85 | return `M ${(this.source as Node).x} ${(this.source as Node).y} L ${(this.target as Node).x} ${ 86 | (this.target as Node).y 87 | }`; 88 | } 89 | 90 | private pathId() { 91 | return `path${this.id}`; 92 | } 93 | 94 | public linkId() { 95 | return `link${this.id}`; 96 | } 97 | 98 | private margin() { 99 | if (!this._margin) { 100 | const margin = window.getComputedStyle(document.getElementById(this.linkId())).margin; 101 | 102 | // NOTE: Assuming that window.getComputedStyle() returns some value link "10px" 103 | // or "0px" even when not defined in .css 104 | if (!margin || margin === "0px") { 105 | this._margin = this.defaultMargin; 106 | } else { 107 | this._margin = parseInt(margin); 108 | } 109 | } 110 | 111 | return this._margin; 112 | } 113 | 114 | private isLabelVisible() { 115 | const pathLength = (document.getElementById(this.pathId()) as unknown as SVGPathElement).getTotalLength(); 116 | 117 | const isShort = Array.from(document.getElementsByClassName(this.pathId())).some((p) => { 118 | // has only one 119 | const tp = p.firstChild as SVGTextPathElement; 120 | // center label 121 | if (tp.classList.contains("center")) { 122 | return tp.getComputedTextLength() > pathLength; 123 | } else { 124 | return tp.getComputedTextLength() + this.labelXOffset > pathLength; 125 | } 126 | }); 127 | 128 | d3.selectAll(`text.${this.pathId()}`).classed("short", isShort); 129 | 130 | // Link.scale is initially undefined 131 | return Link.scale > 1.5 && !isShort; 132 | } 133 | 134 | group(): number[] { 135 | return Link.groups[[(this.source as Node).id, (this.target as Node).id].sort().toString()]; 136 | } 137 | 138 | // OPTIMIZE: Implement better right-alignment of the path, especially for multi tspans 139 | private tspanXOffset() { 140 | switch (true) { 141 | case this.isLabelledPath(): 142 | return 0; 143 | case this.isReversePath(): 144 | return -this.labelXOffset; 145 | default: 146 | return this.labelXOffset; 147 | } 148 | } 149 | 150 | private tspanYOffset() { 151 | if (this.isLabelledPath()) return `${-this.labelYOffset + 0.7}em`; 152 | else return `${this.labelYOffset}em`; 153 | } 154 | 155 | private rotate(bbox: SVGRect) { 156 | if ((this.source as Node).x > (this.target as Node).x) 157 | return `rotate(180 ${bbox.x + bbox.width / 2} ${bbox.y + bbox.height / 2})`; 158 | else return "rotate(0)"; 159 | } 160 | 161 | private split() { 162 | if (!this.metaList && !this.sourceMeta && !this.targetMeta) return [this]; 163 | 164 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 165 | const meta: Record[] = []; 166 | ["metaList", "sourceMeta", "targetMeta"].forEach((key, i, keys) => { 167 | if (this[key]) { 168 | const duped = Object.assign(Object.create(this), this); 169 | 170 | keys.filter((k) => k !== key).forEach((k) => (duped[k] = [])); 171 | meta.push(duped); 172 | } 173 | }); 174 | 175 | return meta; 176 | } 177 | 178 | private hasMeta() { 179 | return this.metaList.length > 0 || this.sourceMeta.length > 0 || this.targetMeta.length > 0; 180 | } 181 | 182 | class() { 183 | return `link ${classify((this.source as Node).name)} ${classify((this.target as Node).name)} ${classify( 184 | (this.source as Node).name, 185 | )}-${classify((this.target as Node).name)} ${this.extraClass}`; 186 | } 187 | 188 | // after transform is applied 189 | centerCoordinates() { 190 | const link = d3.select(`.link #${this.linkId()}`).node() as SVGLineElement; 191 | const bbox = link.getBBox(); 192 | const transform = link.transform.baseVal.consolidate(); 193 | 194 | return [ 195 | bbox.x + bbox.width / 2 + (transform?.matrix.e || 0), 196 | bbox.y + bbox.height / 2 + (transform?.matrix.f || 0), 197 | ]; 198 | } 199 | 200 | angle() { 201 | const link = d3.select(`.link #${this.linkId()}`).node() as SVGLineElement; 202 | return ( 203 | (Math.atan2(link.y2.baseVal.value - link.y1.baseVal.value, link.x2.baseVal.value - link.x1.baseVal.value) * 180) / 204 | Math.PI 205 | ); 206 | } 207 | 208 | static render( 209 | linkLayer: d3.Selection, // eslint-disable-line @typescript-eslint/no-explicit-any 210 | labelLayer: d3.Selection, // eslint-disable-line @typescript-eslint/no-explicit-any 211 | links: Link[], 212 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 213 | ): [d3.Selection, d3.Selection, d3.Selection] { 214 | // Render lines 215 | const pathGroup = linkLayer 216 | .selectAll(".link") 217 | .data(links) 218 | .enter() 219 | .append("g") 220 | .attr("class", (d) => d.class()); 221 | 222 | const link = pathGroup 223 | .append("line") 224 | .attr("x1", (d) => (d.source as Node).x) 225 | .attr("y1", (d) => (d.source as Node).y) 226 | .attr("x2", (d) => (d.target as Node).x) 227 | .attr("y2", (d) => (d.target as Node).y) 228 | .attr("stroke", (d) => d.color) 229 | .attr("stroke-width", (d) => d.width) 230 | .attr("id", (d) => d.linkId()) 231 | .on("mouseover.line", (d) => textGroup.selectAll(`text.${d.pathId()}`).classed("hover", true)) 232 | .on("mouseout.line", (d) => textGroup.selectAll(`text.${d.pathId()}`).classed("hover", false)); 233 | 234 | const path = pathGroup 235 | .append("path") 236 | .attr("d", (d) => d.d()) 237 | .attr("id", (d) => d.pathId()); 238 | 239 | // Render texts 240 | const textGroup = labelLayer 241 | .selectAll(".link") 242 | .data(links) 243 | .enter() 244 | .append("g") 245 | .attr("class", (d) => d.class()); 246 | 247 | const text = textGroup 248 | .selectAll("text") 249 | .data((d: Link) => d.split().filter((l: Link) => l.hasMeta())) 250 | .enter() 251 | .append("text") 252 | .attr("class", (d: Link) => d.pathId()); // Bind text with pathId as class 253 | 254 | const textPath = text.append("textPath").attr("xlink:href", (d: Link) => `#${d.pathId()}`); 255 | 256 | textPath.each(function (d: Link) { 257 | Link.appendMetaText(this, d.metaList); 258 | Link.appendMetaText(this, d.sourceMeta); 259 | Link.appendMetaText(this, d.targetMeta); 260 | 261 | if (d.isLabelledPath()) Link.center(this); 262 | 263 | if (d.isReversePath()) Link.theOtherEnd(this); 264 | }); 265 | 266 | Link.zoom(); // Initialize 267 | return [link, path, text]; 268 | } 269 | 270 | private static theOtherEnd(container: SVGGElement) { 271 | d3.select(container).attr("class", "reverse").attr("text-anchor", "end").attr("startOffset", "100%"); 272 | } 273 | 274 | private static center(container: SVGGElement) { 275 | d3.select(container).attr("class", "center").attr("text-anchor", "middle").attr("startOffset", "50%"); 276 | } 277 | 278 | private static appendMetaText(container: SVGGElement, meta: MetaDataType[]) { 279 | meta.forEach((m) => { 280 | d3.select(container) 281 | .append("tspan") 282 | .attr("x", (d: Link) => d.tspanXOffset()) 283 | .attr("dy", (d: Link) => d.tspanYOffset()) 284 | .attr("class", m.class) 285 | .text(m.value); 286 | }); 287 | } 288 | 289 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 290 | static tick(link: d3.Selection, path?: d3.Selection, label?: d3.Selection) { 291 | link 292 | .attr("x1", (d) => (d.source as Node).x) 293 | .attr("y1", (d) => (d.source as Node).y) 294 | .attr("x2", (d) => (d.target as Node).x) 295 | .attr("y2", (d) => (d.target as Node).y); 296 | 297 | if(path) { 298 | path.attr("d", (d) => d.d()); 299 | } 300 | 301 | if(label) { 302 | label.attr("transform", function(d: Link) { 303 | return d.rotate(this.getBBox()); 304 | }); 305 | } 306 | 307 | // hide labels when the path is too short 308 | d3.selectAll(".link text").style("visibility", (d: Link) => (d.isLabelVisible() ? "visible" : "hidden")); 309 | } 310 | 311 | static zoom(scale?: number) { 312 | Link.scale = scale; 313 | 314 | d3.selectAll(".link text").style("visibility", (d: Link) => (d.isLabelVisible() ? "visible" : "hidden")); 315 | } 316 | 317 | static setPosition(link: d3.Selection, position: LinkPosition[]) { 318 | link 319 | .attr("x1", (d, i) => position[i].x1) 320 | .attr("y1", (d, i) => position[i].y1) 321 | .attr("x2", (d, i) => position[i].x2) 322 | .attr("y2", (d, i) => position[i].y2); 323 | } 324 | 325 | private shiftMultiplier() { 326 | if (!this._shiftMultiplier) { 327 | const members = this.group() || []; 328 | this._shiftMultiplier = members.indexOf(this.id) - (members.length - 1) / 2; 329 | } 330 | 331 | return this._shiftMultiplier; 332 | } 333 | 334 | static shiftBundle( 335 | link: d3.Selection, 336 | path: d3.Selection, 337 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 338 | label: d3.Selection, 339 | bundle: d3.Selection, 340 | ) { 341 | const transform = (d: Link) => d.shiftBundle(d.shiftMultiplier()); 342 | 343 | link.attr("transform", transform); 344 | path.attr("transform", transform); 345 | label.attr("transform", transform); 346 | Bundle.shiftBundle(bundle); 347 | } 348 | 349 | shiftBundle(multiplier: number) { 350 | const gap = this.margin() * multiplier; 351 | 352 | const x = (this.target as Node).x - (this.source as Node).x; 353 | const y = (this.target as Node).y - (this.source as Node).y; 354 | const length = Math.sqrt(Math.pow(x, 2) + Math.pow(y, 2)); 355 | 356 | return `translate(${(-gap * y) / length}, ${(gap * x) / length})`; 357 | } 358 | 359 | static reset() { 360 | Link.groups = null; 361 | } 362 | } 363 | 364 | const Eventable = (Base: typeof LinkBase) => { 365 | class EventableLink extends Base { 366 | private dispatch: d3.Dispatch; 367 | 368 | constructor(data: LinkDataType, id: number, options: LinkOptions) { 369 | super(data, id, options); 370 | 371 | this.dispatch = d3.dispatch("rendered"); 372 | } 373 | 374 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 375 | static render(linkLayer, labelLayer, links): [d3.Selection, d3.Selection, d3.Selection] { 376 | const [link, path, text] = super.render(linkLayer, labelLayer, links); 377 | 378 | link.each(function (this: SVGLineElement, d: Link & EventableLink) { 379 | d.dispatch.rendered(this); 380 | }); 381 | 382 | return [link, path, text]; 383 | } 384 | 385 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 386 | on(name: string, callback: (element: SVGGElement) => any) { 387 | this.dispatch.on(name, callback); 388 | } 389 | } 390 | 391 | return EventableLink; 392 | }; 393 | 394 | const Pluggable = (Base: typeof LinkBase) => { 395 | class Link extends Base { 396 | private static pluginConstructors: Constructor[] = []; 397 | 398 | constructor(data: LinkDataType, id: number, options: LinkOptions) { 399 | super(data, id, options); 400 | 401 | for (const constructor of Link.pluginConstructors) { 402 | // Call Pluggable at last as constructor may call methods defined in other classes 403 | constructor.bind(this)(data, id, options); 404 | } 405 | } 406 | 407 | static registerConstructor(func: Constructor) { 408 | Link.pluginConstructors.push(func); 409 | } 410 | } 411 | 412 | return Link; 413 | }; 414 | 415 | class EventableLink extends Eventable(LinkBase) {} 416 | 417 | // Call Pluggable at last as constructor may call methods defined in other classes 418 | class Link extends Pluggable(EventableLink) {} 419 | 420 | export { Link }; 421 | -------------------------------------------------------------------------------- /src/link_tooltip.ts: -------------------------------------------------------------------------------- 1 | import * as d3 from "d3"; 2 | 3 | import { Link } from "./link"; 4 | import { Node } from "./node"; 5 | import { Tooltip } from "./tooltip"; 6 | import { classify } from "./util"; 7 | 8 | export class LinkTooltip extends Tooltip { 9 | protected static type = "link" as const; 10 | 11 | constructor( 12 | private link: Link, 13 | eventType: string, 14 | ) { 15 | super(eventType, { offsetX: 10 }); 16 | } 17 | 18 | transform() { 19 | const [x, y] = this.link.centerCoordinates(); 20 | return `translate(${x}, ${y})`; 21 | } 22 | 23 | protected objectId(escape = false) { 24 | let id = classify(this.link.linkId()); 25 | 26 | if (escape) { 27 | id = CSS.escape(id); 28 | } 29 | 30 | return id; 31 | } 32 | 33 | protected static appendText(container: SVGGElement) { 34 | const path = d3.select(container).append("path") as d3.Selection; 35 | const text = d3.select(container).append("text") as d3.Selection; 36 | 37 | LinkTooltip.appendNameValue(text, "source", (d) => (d.link.source as Node).name); 38 | text.each(function (d) { 39 | d.link.sourceMeta.forEach((m) => { 40 | LinkTooltip.appendNameValue(text, m.class, m.value, false); 41 | }); 42 | }); 43 | 44 | LinkTooltip.appendNameValue(text, "target", (d) => (d.link.target as Node).name, true); 45 | text.each(function (d) { 46 | d.link.targetMeta.forEach((m) => { 47 | LinkTooltip.appendNameValue(text, m.class, m.value, false); 48 | }); 49 | }); 50 | 51 | text.each(function (d) { 52 | d.link.metaList.forEach((m, i) => { 53 | LinkTooltip.appendNameValue(text, m.class, m.value, i === 0); 54 | }); 55 | 56 | // Add "d" after bbox calculation 57 | const bbox = this.getBBox(); 58 | path 59 | .attr("d", (d) => LinkTooltip.pathD(d.offsetX, 0, bbox.width + 40, bbox.height + 20)) 60 | .style("fill", function () { 61 | return LinkTooltip.fill(this); 62 | }); 63 | }); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/meta_data.ts: -------------------------------------------------------------------------------- 1 | export type MetaDataType = { class: string; value: any }; // eslint-disable-line @typescript-eslint/no-explicit-any 2 | 3 | export class MetaData { 4 | constructor( 5 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 6 | private data: Record, 7 | private extraKey?: string, 8 | ) {} 9 | 10 | get(keys: string[]) { 11 | return this.slice(keys).filter((k) => typeof k.value === "string"); 12 | } 13 | 14 | private slice(keys: string[]) { 15 | if (!this.data) return []; 16 | 17 | if (this.extraKey) return this.sliceWithExtraKey(keys); 18 | else return this.sliceWithoutExtraKey(keys); 19 | } 20 | 21 | private sliceWithExtraKey(keys: string[]) { 22 | const data: MetaDataType[] = []; 23 | 24 | keys.forEach((k) => { 25 | if (this.data[k] && this.data[k][this.extraKey]) data.push({ class: k, value: this.data[k][this.extraKey] }); 26 | }); 27 | 28 | return data; 29 | } 30 | 31 | private sliceWithoutExtraKey(keys: string[]) { 32 | const data: MetaDataType[] = []; 33 | 34 | keys.forEach((k) => { 35 | if (this.data[k]) data.push({ class: k, value: this.data[k] }); 36 | }); 37 | 38 | return data; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/node.ts: -------------------------------------------------------------------------------- 1 | import * as d3 from "d3"; 2 | 3 | import { Color } from "./diagram"; 4 | import { MetaData, MetaDataType } from "./meta_data"; 5 | import { NodePosition } from "./position_cache"; 6 | import { classify } from "./util"; 7 | 8 | export type Constructor = (data: NodeDataType, id: number, options: NodeOptions) => void; 9 | 10 | export type NodeDataType = { 11 | name: string; 12 | group: string[]; 13 | icon: string; 14 | meta: Record; // eslint-disable-line @typescript-eslint/no-explicit-any 15 | class: string; 16 | }; 17 | 18 | export type NodeOptions = { 19 | width: number; 20 | height: number; 21 | metaKeys: string[]; 22 | color: Color; 23 | tooltip: boolean; 24 | }; 25 | 26 | class NodeBase { 27 | private static all: Record; // eslint-disable-line @typescript-eslint/no-explicit-any 28 | 29 | public name: string; 30 | public group: string[]; 31 | public metaList: MetaDataType[]; 32 | public meta: Record; // eslint-disable-line @typescript-eslint/no-explicit-any 33 | public x: number; 34 | public y: number; 35 | 36 | private icon: string; 37 | private extraClass: string; 38 | private width: number; 39 | private height: number; 40 | private padding: number; 41 | private tspanOffset: string; 42 | 43 | constructor( 44 | data: NodeDataType, 45 | public id: number, 46 | private options: NodeOptions, 47 | ) { 48 | this.name = data.name; 49 | this.group = typeof data.group === "string" ? [data.group] : data.group || []; 50 | this.icon = data.icon; 51 | this.metaList = new MetaData(data.meta).get(options.metaKeys); 52 | this.meta = data.meta; 53 | this.extraClass = data.class || ""; 54 | 55 | this.width = options.width || 60; 56 | this.height = options.height || 40; 57 | this.padding = 3; 58 | this.tspanOffset = "1.1em"; 59 | 60 | this.register(id); 61 | } 62 | 63 | private register(id: number) { 64 | Node.all = Node.all || {}; 65 | Node.all[this.name] = id; 66 | } 67 | 68 | transform() { 69 | const x = this.x - this.width / 2 + this.padding; 70 | const y = this.y - this.height / 2 + this.padding; 71 | return `translate(${x}, ${y})`; 72 | } 73 | 74 | private nodeWidth() { 75 | return this.width - 2 * this.padding; 76 | } 77 | 78 | private nodeHeight() { 79 | return this.height - 2 * this.padding; 80 | } 81 | 82 | private xForText() { 83 | return this.nodeWidth() / 2; 84 | } 85 | 86 | private yForText() { 87 | // svg ignores padding for some reason 88 | return this.height / 2; 89 | } 90 | 91 | static idByName(name: string) { 92 | if (Node.all[name] === undefined) throw `Unknown node "${name}"`; 93 | return Node.all[name]; 94 | } 95 | 96 | public nodeId() { 97 | return classify(this.name); 98 | } 99 | 100 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 101 | static render(layer: d3.Selection, nodes: Node[]) { 102 | const node: d3.Selection = layer 103 | .selectAll(".node") 104 | .data(nodes) 105 | .enter() 106 | .append("g") 107 | .attr("id", (d) => d.nodeId()) 108 | .attr("name", (d) => d.name) 109 | .attr("transform", (d) => d.transform()); 110 | 111 | node.each(function (this: SVGGElement, d) { 112 | if (d.icon) Node.appendImage(this); 113 | else Node.appendRect(this); 114 | 115 | Node.appendText(this); 116 | }); 117 | 118 | return node; 119 | } 120 | 121 | private static appendText(container: SVGGElement) { 122 | const text = (d3.select(container) as d3.Selection) 123 | .append("text") 124 | .attr("text-anchor", "middle") 125 | .attr("x", (d) => d.xForText()) 126 | .attr("y", (d) => d.yForText()); 127 | text 128 | .append("tspan") 129 | .text((d) => d.name) 130 | .attr("x", (d) => d.xForText()); 131 | 132 | text.each((d) => { 133 | // Show meta only when "tooltip" option is not configured 134 | if (!d.options.tooltip) { 135 | Node.appendMetaText(text, d.metaList); 136 | } 137 | }); 138 | } 139 | 140 | private static appendMetaText(container: d3.Selection, meta: MetaDataType[]) { 141 | meta.forEach((m) => { 142 | container 143 | .append("tspan") 144 | .attr("x", (d) => d.xForText()) 145 | .attr("dy", (d) => d.tspanOffset) 146 | .attr("class", m.class) 147 | .text(m.value); 148 | }); 149 | } 150 | 151 | private static appendImage(container: SVGGElement) { 152 | (d3.select(container) as d3.Selection) 153 | .attr("class", (d) => `node image ${classify(d.name)} ${d.extraClass}`) 154 | .append("image") 155 | .attr("xlink:href", (d) => d.icon) 156 | .attr("width", (d) => d.nodeWidth()) 157 | .attr("height", (d) => d.nodeHeight()); 158 | } 159 | 160 | private static appendRect(container: SVGGElement) { 161 | (d3.select(container) as d3.Selection) 162 | .attr("class", (d) => `node rect ${classify(d.name)} ${d.extraClass}`) 163 | .append("rect") 164 | .attr("width", (d) => d.nodeWidth()) 165 | .attr("height", (d) => d.nodeHeight()) 166 | .attr("rx", 5) 167 | .attr("ry", 5) 168 | .style("fill", (d) => d.options.color(undefined)); 169 | } 170 | 171 | static tick(node: d3.Selection) { 172 | node.attr("transform", (d) => d.transform()); 173 | } 174 | 175 | static setPosition(node: d3.Selection, position: NodePosition[]) { 176 | node.attr("transform", (d, i) => { 177 | if ( 178 | position[i]?.x !== null && 179 | position[i]?.x !== undefined && 180 | position[i]?.y !== null && 181 | position[i]?.y !== undefined 182 | ) { 183 | d.x = position[i].x; 184 | d.y = position[i].y; 185 | } 186 | return d.transform(); 187 | }); 188 | } 189 | 190 | static reset() { 191 | Node.all = null; 192 | } 193 | } 194 | 195 | const Eventable = (Base: typeof NodeBase) => { 196 | class EventableNode extends Base { 197 | private dispatch: d3.Dispatch; 198 | 199 | constructor(data: NodeDataType, id: number, options: NodeOptions) { 200 | super(data, id, options); 201 | 202 | this.dispatch = d3.dispatch("rendered"); 203 | } 204 | 205 | static render(layer, nodes) { 206 | const node = super.render(layer, nodes); 207 | 208 | node.each(function (this: SVGGElement, d: Node & EventableNode) { 209 | d.dispatch.rendered(this); 210 | }); 211 | 212 | return node; 213 | } 214 | 215 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 216 | on(name: string, callback: (element: SVGGElement) => any) { 217 | this.dispatch.on(name, callback); 218 | } 219 | } 220 | 221 | return EventableNode; 222 | }; 223 | 224 | const Pluggable = (Base: typeof NodeBase) => { 225 | class Node extends Base { 226 | private static pluginConstructors: Constructor[] = []; 227 | 228 | constructor(data: NodeDataType, id: number, options: NodeOptions) { 229 | super(data, id, options); 230 | 231 | for (const constructor of Node.pluginConstructors) { 232 | // Call Pluggable at last as constructor may call methods defined in other classes 233 | constructor.bind(this)(data, id, options); 234 | } 235 | } 236 | 237 | static registerConstructor(func: Constructor) { 238 | Node.pluginConstructors.push(func); 239 | } 240 | } 241 | 242 | return Node; 243 | }; 244 | 245 | class EventableNode extends Eventable(NodeBase) {} 246 | 247 | // Call Pluggable at last as constructor may call methods defined in other classes 248 | class Node extends Pluggable(EventableNode) {} 249 | 250 | export { Node }; 251 | -------------------------------------------------------------------------------- /src/node_tooltip.ts: -------------------------------------------------------------------------------- 1 | import * as d3 from "d3"; 2 | 3 | import { Node } from "./node"; 4 | import { Tooltip } from "./tooltip"; 5 | 6 | export class NodeTooltip extends Tooltip { 7 | protected static type = "node" as const; 8 | 9 | constructor( 10 | private node: Node, 11 | eventType: string, 12 | ) { 13 | super(eventType); 14 | } 15 | 16 | transform() { 17 | return `translate(${this.node.x}, ${this.node.y})`; 18 | } 19 | 20 | protected objectId(escape = false) { 21 | let id = this.node.nodeId(); 22 | 23 | if (escape) { 24 | id = CSS.escape(id); 25 | } 26 | 27 | return id; 28 | } 29 | 30 | protected static appendText(container: SVGGElement) { 31 | const path = d3.select(container).append("path") as d3.Selection; 32 | const text = d3.select(container).append("text") as d3.Selection; 33 | 34 | NodeTooltip.appendNameValue(text, "node", (d) => d.node.name); 35 | 36 | text.each(function (d) { 37 | d.node.metaList.forEach((m, i) => { 38 | NodeTooltip.appendNameValue(text, m.class, m.value, i === 0); 39 | }); 40 | 41 | // Add "d" after bbox calculation 42 | const bbox = this.getBBox(); 43 | path 44 | .attr("d", (d) => NodeTooltip.pathD(d.offsetX, 0, bbox.width + 40, bbox.height + 20)) 45 | .style("fill", function () { 46 | return NodeTooltip.fill(this); 47 | }); 48 | }); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/plugin.ts: -------------------------------------------------------------------------------- 1 | export interface PluginClass { 2 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 3 | load(groupClass, nodeClass, linkClass, options: Record): void; 4 | } 5 | -------------------------------------------------------------------------------- /src/position_cache.ts: -------------------------------------------------------------------------------- 1 | import * as md5 from "crypto-js/md5"; 2 | import * as d3 from "d3"; 3 | 4 | import { InetHengeDataType } from "./diagram"; 5 | import { Group } from "./group"; 6 | import { Link } from "./link"; 7 | import { Node } from "./node"; 8 | 9 | export type GroupPosition = { x: number; y: number; width: number; height: number }; 10 | export type NodePosition = { x: number; y: number }; 11 | export type LinkPosition = { x1: number; y1: number; x2: number; y2: number }; 12 | type ExtendedInetHengeDataType = InetHengeDataType & { pop: string }; 13 | type CacheDataType = { 14 | md5: string; 15 | group: GroupPosition[]; 16 | node: NodePosition[]; 17 | link: LinkPosition[]; 18 | }; 19 | 20 | export class PositionCache { 21 | private readonly cachedMd5: string; 22 | public group: GroupPosition[]; 23 | public node: NodePosition[]; 24 | public link: LinkPosition[]; 25 | 26 | constructor( 27 | public data: InetHengeDataType, 28 | public pop?: RegExp, 29 | md5?: string, 30 | ) { 31 | // NOTE: properties below can be undefined 32 | this.cachedMd5 = md5; 33 | } 34 | 35 | private static getAll(): Record { 36 | return JSON.parse(localStorage.getItem("positionCache")) || {}; 37 | } 38 | 39 | private static key() { 40 | return `${location.pathname}${location.search}`; 41 | } 42 | 43 | private static get() { 44 | return this.getAll()[this.key()] || ({} as CacheDataType); 45 | } 46 | 47 | save(group: d3.Selection, node: d3.Selection, link: d3.Selection) { 48 | const cache = PositionCache.getAll(); 49 | cache[PositionCache.key()] = { 50 | md5: this.md5(), 51 | group: this.groupPosition(group), 52 | node: this.nodePosition(node), 53 | link: this.linkPosition(link), 54 | }; 55 | 56 | localStorage.setItem("positionCache", JSON.stringify(cache)); 57 | } 58 | 59 | private md5(data?: ExtendedInetHengeDataType, pop?: RegExp) { 60 | data = structuredClone(data || this.data) as ExtendedInetHengeDataType; 61 | data.pop = String(pop || this.pop); 62 | if (data.pop === "undefined") { 63 | data.pop = "null"; // NOTE: unify undefined with null 64 | } 65 | 66 | if (data.nodes) { 67 | data.nodes.forEach((i) => { 68 | delete i.icon; 69 | delete i.meta; 70 | }); 71 | } 72 | if (data.links) { 73 | data.links.forEach((i) => { 74 | delete i.meta; 75 | }); 76 | } 77 | 78 | return md5(JSON.stringify(data)).toString(); 79 | } 80 | 81 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 82 | private groupPosition(group: d3.Selection) { 83 | const position: GroupPosition[] = []; 84 | 85 | group.each((d) => { 86 | position.push({ 87 | x: d.bounds.x, 88 | y: d.bounds.y, 89 | width: d.bounds.width(), 90 | height: d.bounds.height(), 91 | }); 92 | }); 93 | 94 | return position; 95 | } 96 | 97 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 98 | private nodePosition(node: d3.Selection) { 99 | const position: NodePosition[] = []; 100 | 101 | node.each((d: Node) => { 102 | position.push({ 103 | x: d.x, 104 | y: d.y, 105 | }); 106 | }); 107 | 108 | return position; 109 | } 110 | 111 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 112 | private linkPosition(link: d3.Selection) { 113 | const position: LinkPosition[] = []; 114 | 115 | link.each((d) => { 116 | position.push({ 117 | x1: d.source.x, 118 | y1: d.source.y, 119 | x2: d.target.x, 120 | y2: d.target.y, 121 | }); 122 | }); 123 | 124 | return position; 125 | } 126 | 127 | private match(data: InetHengeDataType, pop: RegExp) { 128 | return this.cachedMd5 === this.md5(data as ExtendedInetHengeDataType, pop); 129 | } 130 | 131 | static load(data: InetHengeDataType, pop: RegExp) { 132 | const cache = this.get(); 133 | if (cache) { 134 | const position = new PositionCache(data, pop, cache.md5); 135 | if (position.match(data, pop)) { 136 | // if data and pop match saved md5 137 | position.group = cache.group; 138 | position.node = cache.node; 139 | position.link = cache.link; 140 | 141 | return position; 142 | } 143 | } 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /src/tooltip.ts: -------------------------------------------------------------------------------- 1 | import * as d3 from "d3"; 2 | 3 | import { HrefFunction } from "./diagram"; 4 | 5 | type TooltipOptions = { 6 | offsetX?: number; 7 | }; 8 | 9 | export abstract class Tooltip { 10 | static href: HrefFunction; 11 | protected static type: "node" | "link"; 12 | 13 | protected offsetX: number; 14 | private visibility: string; 15 | 16 | constructor( 17 | private eventType: string, 18 | options: TooltipOptions = {}, 19 | ) { 20 | this.offsetX = options.offsetX !== undefined ? options.offsetX : 30; 21 | this.visibility = "hidden"; 22 | } 23 | 24 | protected tspanOffsetY(marginTop: boolean) { 25 | return marginTop ? "2em" : "1.1em"; 26 | } 27 | 28 | transform(): string { 29 | throw new Error("not implemented"); 30 | } 31 | 32 | private class() { 33 | return `tooltip ${(this.constructor as typeof Tooltip).type}-tooltip ${this.objectId()}`; 34 | } 35 | 36 | // Object id which has this tooltip 37 | protected abstract objectId(boolean?): string; 38 | 39 | private setVisibility(visibility: string | null) { 40 | this.visibility = visibility === "visible" ? "visible" : "hidden"; 41 | } 42 | 43 | // This doesn't actually toggle visibility, but returns string for toggled visibility 44 | private toggleVisibility() { 45 | this.visibility = this.visibility === "hidden" ? "visible" : "hidden"; 46 | return this.visibility; 47 | } 48 | 49 | private toggleVisibilityCallback(element: SVGGElement) { 50 | return () => { 51 | // Do nothing for dragging 52 | if ((d3.event as MouseEvent).defaultPrevented) { 53 | return; 54 | } 55 | 56 | d3.select(element) 57 | .attr("visibility", function (d) { 58 | // Sync visibility before toggling. External script may change the visibility. 59 | d.setVisibility(this.getAttribute("visibility")); 60 | return d.toggleVisibility(); 61 | }) 62 | // bootstrap.css unexpectedly sets "opacity: 0". Reset if it's visible. 63 | .style("opacity", function (d) { 64 | return d.visibility === "visible" ? 1 : null; 65 | }); 66 | }; 67 | } 68 | 69 | private configureObjectClickCallback(element: SVGGElement) { 70 | d3.select(`#${this.objectId(true)}`).on("click.tooltip", this.toggleVisibilityCallback(element)); 71 | } 72 | 73 | private configureObjectHoverCallback(element: SVGGElement) { 74 | d3.select(`#${this.objectId(true)}`).on("mouseenter.tooltip", this.toggleVisibilityCallback(element)); 75 | d3.select(`#${this.objectId(true)}`).on("mouseleave.tooltip", this.toggleVisibilityCallback(element)); 76 | } 77 | 78 | // Make tooltip selectable 79 | private disableZoom(element: SVGGElement) { 80 | d3.select(element).on("mousedown.tooltip", () => { 81 | (d3.event as MouseEvent).stopPropagation(); 82 | }); 83 | } 84 | 85 | static setHref(callback: HrefFunction) { 86 | if (callback) this.href = callback; 87 | } 88 | 89 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 90 | static render(layer: d3.Selection, tooltips: T[]) { 91 | // eslint-disable-next-line @typescript-eslint/no-this-alias 92 | const cls = this; 93 | const tooltip = layer 94 | .selectAll(`.tooltip.${cls.type}-tooltip`) 95 | .data(tooltips) 96 | .enter() 97 | .append("g") 98 | .attr("visibility", (d) => d.visibility) 99 | .attr("class", (d) => d.class()) 100 | .attr("transform", (d) => d.transform()); 101 | 102 | tooltip.each(function (d) { 103 | cls.appendText(this); 104 | 105 | if (typeof (d.constructor as typeof Tooltip).href === "function") { 106 | cls.appendExternalLinkIcon(this); 107 | } 108 | 109 | if (d.eventType === "hover") { 110 | d.configureObjectHoverCallback(this); 111 | } else { 112 | d.configureObjectClickCallback(this); 113 | } 114 | 115 | d.disableZoom(this); 116 | }); 117 | 118 | return tooltip; 119 | } 120 | 121 | protected static fill(element: SVGPathElement) { 122 | // If no "fill" style is defined 123 | if (getComputedStyle(element).fill.match(/\(0,\s*0,\s*0\)/)) { 124 | return "#f8f1e9"; 125 | } 126 | } 127 | 128 | protected static pathD(x: number, y: number, width: number, height: number) { 129 | const round = 8; 130 | 131 | return ( 132 | `M ${x},${y} L ${x + 20},${y - 10} ${x + 20},${y - 20}` + 133 | `Q ${x + 20},${y - 20 - round} ${x + 20 + round},${y - 20 - round}` + 134 | `L ${x + 20 + width - round},${y - 20 - round}` + 135 | `Q ${x + 20 + width},${y - 20 - round} ${x + 20 + width},${y - 20}` + 136 | `L ${x + 20 + width},${y - 20 + height}` + 137 | `Q ${x + 20 + width},${y - 20 + height + round} ${x + 20 + width - round},${y - 20 + height + round}` + 138 | `L ${x + 20 + round},${y - 20 + height + round}` + 139 | `Q ${x + 20},${y - 20 + height + round} ${x + 20},${y - 20 + height}` + 140 | `L ${x + 20},${y + 10} Z` 141 | ); 142 | } 143 | 144 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 145 | protected static appendText(container: SVGGElement) { 146 | throw new Error("not implemented"); 147 | } 148 | 149 | /** 150 | * Append "name: value" to the container 151 | * @param container 152 | * @param name 153 | * @param value 154 | * @param marginTop Render wide margin if true, ordinary marin if false, and no margin if undefined 155 | * @protected 156 | */ 157 | protected static appendNameValue( 158 | container: d3.Selection, 159 | name: string, 160 | value: (d: T) => string, 161 | marginTop?: boolean, 162 | ) { 163 | container 164 | .append("tspan") 165 | .attr("x", (d) => d.offsetX + 40) 166 | .attr("dy", (d) => (marginTop === undefined ? undefined : d.tspanOffsetY(marginTop))) 167 | .attr("class", "name") 168 | .text(`${name}:`); 169 | 170 | container.append("tspan").attr("dx", 10).attr("class", "value").text(value); 171 | } 172 | 173 | // modified https://tabler-icons.io/i/external-link 174 | protected static appendExternalLinkIcon(container: SVGGElement) { 175 | const bbox = container.getBBox(); 176 | const a = d3 177 | .select(container) 178 | .append("a") 179 | .attr("href", (d) => (d.constructor as typeof Tooltip).href(d, (d.constructor as typeof Tooltip).type)); 180 | const size = 20; 181 | const svg = a 182 | .append("svg") 183 | .attr("x", (d) => bbox.width + d.offsetX - 2 - size) 184 | .attr("y", bbox.height - 30 - size) 185 | .attr("width", size) 186 | .attr("height", size) 187 | .attr("viewBox", `0 0 24 24`) 188 | .attr("stroke-width", 2) 189 | .attr("fill", "none") 190 | .attr("stroke-linecap", "round") 191 | .attr("stroke-linejoin", "round") 192 | .attr("class", "icon external-link"); 193 | svg.append("path").attr("d", `M0 0h24v24H0z`).attr("stroke", "none").attr("fill", "#fff").attr("fill-opacity", 0); 194 | svg.append("path").attr("d", "M12 6h-6a2 2 0 0 0 -2 2v10a2 2 0 0 0 2 2h10a2 2 0 0 0 2 -2v-6"); 195 | svg.append("path").attr("d", "M11 13l9 -9"); 196 | svg.append("path").attr("d", "M15 4h5v5"); 197 | } 198 | 199 | static followObject(tooltip: d3.Selection) { 200 | tooltip.attr("transform", (d) => d.transform()); 201 | } 202 | } 203 | -------------------------------------------------------------------------------- /src/util.ts: -------------------------------------------------------------------------------- 1 | export function classify(string: string) { 2 | return string.replace(" ", "-").toLowerCase(); 3 | } 4 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "ES6", 4 | "target": "ES2017", 5 | "outDir": "dist", 6 | "sourceMap": true 7 | }, 8 | "include": ["src/*"], 9 | "lib": ["ES2017"] 10 | } 11 | -------------------------------------------------------------------------------- /types/WebCola.d.ts: -------------------------------------------------------------------------------- 1 | type NodeOffset = { 2 | node: number; 3 | offset: number; 4 | }; 5 | 6 | export type WebColaConstraint = { 7 | type: "alignment"; 8 | axis: "x" | "y"; 9 | offsets: NodeOffset[]; 10 | }; 11 | -------------------------------------------------------------------------------- /webpack.common.js: -------------------------------------------------------------------------------- 1 | const webpack = require("webpack"); 2 | 3 | const package = require("./package.json"); 4 | 5 | const banner = ` 6 | /*! 7 | * inet-henge v${package.version} 8 | * @author ${package.author.name} 9 | * @license ${package.license} 10 | * Copyright (c) 2016-2024 Shintaro Kojima 11 | */ 12 | `; 13 | 14 | module.exports = { 15 | entry: "./src/diagram", 16 | output: { 17 | path: __dirname, 18 | libraryTarget: "umd", 19 | }, 20 | resolve: { 21 | extensions: [".ts", ".js"], 22 | }, 23 | module: { 24 | rules: [ 25 | { 26 | test: /\.ts$/, 27 | use: ["ts-loader"], 28 | }, 29 | ], 30 | }, 31 | externals: { 32 | cola: "cola", 33 | d3: "d3", 34 | }, 35 | devtool: "source-map", 36 | plugins: [new webpack.BannerPlugin({ banner: banner.trim(), raw: true })], 37 | }; 38 | -------------------------------------------------------------------------------- /webpack.dev.js: -------------------------------------------------------------------------------- 1 | const { merge } = require("webpack-merge"); 2 | const common = require("./webpack.common.js"); 3 | 4 | module.exports = merge(common, { 5 | mode: "development", 6 | output: { 7 | filename: "inet-henge.js", 8 | }, 9 | }); 10 | -------------------------------------------------------------------------------- /webpack.prod.js: -------------------------------------------------------------------------------- 1 | const { merge } = require("webpack-merge"); 2 | const common = require("./webpack.common.js"); 3 | 4 | module.exports = merge(common, { 5 | mode: "production", 6 | output: { 7 | filename: "inet-henge.min.js", 8 | }, 9 | }); 10 | --------------------------------------------------------------------------------