├── .babelrc ├── .eslintrc.json ├── .gitattributes ├── .github └── workflows │ └── tests.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── dist ├── xdsmjs.js └── xdsmjs.js.map ├── examples ├── bliss.json ├── dummy.json ├── idf.json ├── mdf.json └── nested.json ├── fontello.css ├── fontello ├── LICENSE.txt ├── README.txt ├── config.json ├── css │ ├── animation.css │ ├── fontello-codes.css │ ├── fontello-embedded.css │ ├── fontello-ie7-codes.css │ ├── fontello-ie7.css │ └── fontello.css ├── demo.html └── font │ ├── fontello.eot │ ├── fontello.svg │ ├── fontello.ttf │ ├── fontello.woff │ └── fontello.woff2 ├── gallery ├── xdsm_bliss_anim.gif ├── xdsm_idf.png ├── xdsm_mdf.png └── xdsm_v1_v2.gif ├── package.json ├── py_xdsmjs ├── make_pyxdsmjs.py ├── setup.py └── xdsmjs │ ├── __init__.py │ └── tests │ └── test_xdsmjs.py ├── src ├── animation.js ├── controls.js ├── graph.js ├── index.js ├── labelizer.js ├── selectable.js ├── xdsm-factory.js └── xdsm.js ├── test ├── xdsmjs-test.html └── xdsmjs-test.mjs ├── webpack.config.cjs ├── xdsm.html ├── xdsm.json ├── xdsmjs.css └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { "presets": ["@babel/preset-env"] } 2 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es6": true 5 | }, 6 | "extends": [ 7 | "eslint:recommended", 8 | "airbnb" 9 | ], 10 | "globals": { 11 | "Atomics": "readonly", 12 | "SharedArrayBuffer": "readonly" 13 | }, 14 | "parserOptions": { 15 | "ecmaFeatures": {}, 16 | "ecmaVersion": 2018, 17 | "sourceType": "module" 18 | }, 19 | "plugins": [], 20 | "rules": { 21 | "no-underscore-dangle": "off", 22 | "no-console": "off", 23 | "global-require": "off", 24 | "react/forbid-prop-types": "off", 25 | "no-restricted-syntax": "off", 26 | "linebreak-style": "off", 27 | "max-classes-per-file": "off", 28 | "import/no-unresolved": "off", 29 | "react/no-this-in-sfc": "off", 30 | "import/extensions": "off" 31 | } 32 | } -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | *.gif -text 3 | *.{cmd,[cC][mM][dD]} text eol=crlf 4 | *.{bat,[bB][aA][tT]} text eol=crlf -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: ubuntu-latest 13 | 14 | strategy: 15 | matrix: 16 | node-version: [16.x] 17 | # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ 18 | 19 | steps: 20 | - uses: actions/checkout@v2 21 | - name: Use Node.js ${{ matrix.node-version }} 22 | uses: actions/setup-node@v2 23 | with: 24 | node-version: ${{ matrix.node-version }} 25 | - run: npm install 26 | - run: npm run build 27 | - run: npm test 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Eclipse 2 | /.project 3 | 4 | # npm 5 | /node_modules 6 | 7 | # gems execution 8 | *.pdf 9 | *.h5 10 | *.xml 11 | 12 | # Python 13 | py_xdsmjs/**/dist 14 | *egg-info 15 | **/__pycache__ 16 | .pytest_cache 17 | 18 | #safe-buffer licensing 19 | dist/xdsmjs-test.js.LICENSE.txt -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## Unreleased 9 | 10 | ## 2.0.0 - 2023-02-13 11 | 12 | ### Changed 13 | 14 | - Update dependencies: d3 v7 15 | - Use `viewBox` attribute to make the `svg` really scalable in its container element 16 | - Declare `xdsmjs` an ESM module only (`type: 'module'` in `package.json`, ie require `node v12+`) 17 | 18 | ## 1.0.0 - 2020-09-23 19 | 20 | ### Changed 21 | 22 | - Update dependencies: d3 v6 23 | 24 | ## 0.8.1 - 2020-04-28 25 | 26 | ### Fixed 27 | 28 | - Fix packaging to publish on npm 29 | - Fix XDSMjs API : add `createSelectableXdsm(xdsmFormat, callback)` 30 | 31 | ## 0.8.0 - 2019-04-25 32 | 33 | ### Added 34 | 35 | - Add configuration and xdsm data passing from ` 47 | ``` 48 | 49 | * add the place-holder div element that will contain the XDSM diagram : 50 | 51 | ```html 52 |
53 | ``` 54 | 55 | You can either use the attribute data-mdo to specify MDO data in the XDSMjs JSON format in an HTML escaped string 56 | 57 | ```html 58 |
59 | ``` 60 | 61 | or use the attribute data-mdo-file to specify another MDO filename 62 | 63 | ```html 64 |
65 | ``` 66 | If no data attribute is specified, the default file xdsm.json is expected. 67 | 68 | As of 0.7.0, you can use XDSM v2 notation by using xdsm2 class instead of xdsm. 69 | 70 | ```html 71 |
72 | ``` 73 | 74 | As of 0.8.0, you can specify configuration and MDO data directly from the 83 | 84 | 85 | ## Example 86 | Below an example describing BLISS formulation inspired from XDSM description given in [Martins and Lambe MDO architecture survey](http://arc.aiaa.org/doi/pdf/10.2514/1.J051895). While the formulation could have been described in one diagram as in the survey, the example below use XDSMjs multi-level diagram capability to separate system and discipline optimization levels. 87 | The corresponding [xdsm.json](./examples/bliss.json) file is available in the example directory. 88 | 89 | ![](gallery/xdsm_bliss_anim.gif) 90 | 91 | ## Licence 92 | Copyright 2020 Rémi Lafage 93 | 94 | Licensed under the Apache License, Version 2.0 (the "License"); 95 | you may not use this file except in compliance with the License. 96 | You may obtain a copy of the License at 97 | 98 | http://www.apache.org/licenses/LICENSE-2.0 99 | 100 | Unless required by applicable law or agreed to in writing, software 101 | distributed under the License is distributed on an "AS IS" BASIS, 102 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 103 | See the License for the specific language governing permissions and 104 | limitations under the License. 105 | -------------------------------------------------------------------------------- /examples/bliss.json: -------------------------------------------------------------------------------- 1 | { 2 | "subopt-i": { 3 | "nodes": [ 4 | { 5 | "type": "optimization", 6 | "id": "Opt", 7 | "name": "DisciplineOptimization_i" 8 | }, 9 | { 10 | "type": "function", 11 | "id": "Dis1", 12 | "name": "Analysis_i" 13 | }, 14 | { 15 | "type": "function", 16 | "id": "Dis2", 17 | "name": "SystemFunctions" 18 | }, 19 | { 20 | "type": "function", 21 | "id": "Dis3", 22 | "name": "DisciplineFunctions" 23 | }, 24 | { 25 | "type": "function", 26 | "id": "Dis4", 27 | "name": "DisciplineVarDerivatives_i" 28 | } 29 | ], 30 | "edges": [ 31 | { 32 | "to": "Opt", 33 | "from": "_U_", 34 | "name": "x_i" 35 | }, 36 | { 37 | "to": "_U_", 38 | "from": "Opt", 39 | "name": "x_i^*, y_i^*" 40 | }, 41 | { 42 | "to": "Dis1", 43 | "from": "Opt", 44 | "name": "x_i" 45 | }, 46 | { 47 | "to": "Opt", 48 | "from": "Dis1", 49 | "name": "y_i" 50 | }, 51 | { 52 | "to": "Dis2", 53 | "from": "Dis1", 54 | "name": "x_i, y_i" 55 | }, 56 | { 57 | "to": "Dis3", 58 | "from": "Dis1", 59 | "name": "x_i, y_i" 60 | }, 61 | { 62 | "to": "Dis4", 63 | "from": "Dis1", 64 | "name": "x_i, y_i" 65 | }, 66 | { 67 | "to": "Opt", 68 | "from": "Dis2", 69 | "name": "f_0, c_0" 70 | }, 71 | { 72 | "to": "Opt", 73 | "from": "Dis3", 74 | "name": "f_i, c_i" 75 | }, 76 | { 77 | "to": "Opt", 78 | "from": "Dis4", 79 | "name": "df_xi, dc_xi" 80 | }, 81 | { 82 | "to": "Dis1", 83 | "from": "_U_", 84 | "name": "yest_j" 85 | }, 86 | { 87 | "to": "Dis2", 88 | "from": "_U_", 89 | "name": "yest_j" 90 | }, 91 | { 92 | "to": "Dis3", 93 | "from": "_U_", 94 | "name": "yest_j" 95 | }, 96 | { 97 | "to": "Dis4", 98 | "from": "_U_", 99 | "name": "yest_j" 100 | }, 101 | { 102 | "to": "_U_", 103 | "from": "Dis2", 104 | "name": "f_0, c_0" 105 | }, 106 | { 107 | "to": "_U_", 108 | "from": "Dis3", 109 | "name": "f_i, c_i" 110 | } 111 | ], 112 | "workflow": [ 113 | "_U_", 114 | [ 115 | "Opt", 116 | [ 117 | "Dis1", 118 | { 119 | "parallel": [ 120 | "Dis2", 121 | "Dis3", 122 | "Dis4" 123 | ] 124 | } 125 | ] 126 | ] 127 | ], 128 | "optpb": "Minimize (f0)0 + (df/dxi)Dxi\nwith respect to Dxi\nsubject to (c0)0 + (dc0/dxi)Dxi >= 0\n (ci)0 + (dci/dxi)Dxi >= 0\n Dxi_L <= Dxi <= Dxi_U " 129 | }, 130 | "root": { 131 | "nodes": [ 132 | { 133 | "type": "mda", 134 | "id": "Dis1", 135 | "name": "ConvergenceCheck" 136 | }, 137 | { 138 | "type": "mda", 139 | "id": "Mda", 140 | "name": "MDA" 141 | }, 142 | { 143 | "type": "optimization", 144 | "id": "Opt", 145 | "name": "SystemOptimization" 146 | }, 147 | { 148 | "type": "sub-optimization_multi", 149 | "id": "Mdo", 150 | "name": "DisciplineOptimization_i", 151 | "subxdsm": "subopt-i" 152 | }, 153 | { 154 | "type": "function", 155 | "id": "Dis2", 156 | "name": "SystemFunctions" 157 | }, 158 | { 159 | "type": "function", 160 | "id": "Dis4", 161 | "name": "DisciplineFunctions" 162 | }, 163 | { 164 | "type": "function", 165 | "id": "Dis3", 166 | "name": "SharedVarDerivatives" 167 | }, 168 | { 169 | "type": "analysis_multi", 170 | "id": "Dis5", 171 | "name": "Analysis_i" 172 | } 173 | ], 174 | "edges": [ 175 | { 176 | "to": "Dis1", 177 | "from": "_U_", 178 | "name": "x^(0)" 179 | }, 180 | { 181 | "to": "_U_", 182 | "from": "Dis1", 183 | "name": "NoData" 184 | }, 185 | { 186 | "to": "Mda", 187 | "from": "_U_", 188 | "name": "yest^(0)" 189 | }, 190 | { 191 | "to": "Dis5", 192 | "from": "Mda", 193 | "name": "yest_j" 194 | }, 195 | { 196 | "to": "Dis5", 197 | "from": "Mda", 198 | "name": "yest_j" 199 | }, 200 | { 201 | "to": "Dis5", 202 | "from": "Mda", 203 | "name": "yest_j" 204 | }, 205 | { 206 | "to": "Mda", 207 | "from": "Dis5", 208 | "name": "y_i" 209 | }, 210 | { 211 | "to": "Opt", 212 | "from": "_U_", 213 | "name": "x_0^(0)" 214 | }, 215 | { 216 | "to": "Mdo", 217 | "from": "_U_", 218 | "name": "x_i^(0)" 219 | }, 220 | { 221 | "to": "_U_", 222 | "from": "Opt", 223 | "name": "x_0^*" 224 | }, 225 | { 226 | "to": "_U_", 227 | "from": "Mdo", 228 | "name": "x_i^*, y_i^*" 229 | }, 230 | { 231 | "to": "Dis1", 232 | "from": "Opt", 233 | "name": "x_0" 234 | }, 235 | { 236 | "to": "Dis1", 237 | "from": "Opt", 238 | "name": "x_0" 239 | }, 240 | { 241 | "to": "Opt", 242 | "from": "Dis3", 243 | "name": "df_x0, dc_x0" 244 | }, 245 | { 246 | "to": "Dis3", 247 | "from": "Opt", 248 | "name": "x_0" 249 | }, 250 | { 251 | "to": "Mdo", 252 | "from": "Opt", 253 | "name": "x_0" 254 | }, 255 | { 256 | "to": "Opt", 257 | "from": "Mdo", 258 | "name": "f_0, c_0, f_i, c_i" 259 | }, 260 | { 261 | "to": "Opt", 262 | "from": "Dis2", 263 | "name": "f_0, c_0" 264 | }, 265 | { 266 | "to": "Opt", 267 | "from": "Dis4", 268 | "name": "f_i, c_i" 269 | }, 270 | { 271 | "to": "Dis1", 272 | "from": "Mdo", 273 | "name": "x_i" 274 | }, 275 | { 276 | "to": "Dis2", 277 | "from": "Opt", 278 | "name": "x_0" 279 | }, 280 | { 281 | "to": "Dis4", 282 | "from": "Opt", 283 | "name": "x_0" 284 | }, 285 | { 286 | "to": "Mdo", 287 | "from": "Mda", 288 | "name": "yest_j" 289 | } 290 | ], 291 | "workflow": [ 292 | "_U_", 293 | [ 294 | "Dis1", 295 | [ 296 | "Mda", 297 | [ 298 | "Dis5" 299 | ], 300 | "Mdo", 301 | "Opt", 302 | { 303 | "parallel": [ 304 | "Dis3", 305 | "Dis2", 306 | "Dis4" 307 | ] 308 | }, 309 | "Opt" 310 | ] 311 | ] 312 | ], 313 | "optpb": "Minimize (f0*)0 + (df0*/dx0)Dx0 \nwith respect to Dx0\nsubject to (c0*)0 + (dc0*/dx0)Dx0 >= 0\n (ci*)0 + (dci*/dx0)Dx0 >= 0\n Dx0_L <= Dx0 <= Dx0_U" 314 | } 315 | } -------------------------------------------------------------------------------- /examples/dummy.json: -------------------------------------------------------------------------------- 1 | { 2 | "nodes" : [{"id": "Opt", "name":"Optimization", "type":"optimization"}, 3 | {"id": "MDA", "name":"MDA", "type":"mda"}, 4 | {"id": "DA1", "name":"Analysis 1"}, 5 | {"id": "DA2", "name":"Analysis 2"}, 6 | {"id": "DA3", "name":"Analysis 3"}, 7 | {"id": "Func", "name":"Functions"} 8 | ], 9 | "edges" : [{"from":"Opt" ,"to":"DA1", "name":"x_0,x_1"}, 10 | {"from":"DA1" ,"to":"DA3", "name":"x_share"}, 11 | {"from":"DA3" ,"to":"DA1", "name":"y_1^2"}, 12 | {"from":"MDA" ,"to":"DA1", "name":"x_2"}, 13 | {"from":"Func","to":"Opt", "name":"f,c"}, 14 | {"from":"_U_", "to":"DA1", "name":"x_0"}, 15 | {"from":"DA3", "to":"_U_", "name":"y_0"} 16 | ], 17 | "workflow" : ["Opt", ["MDA", "DA1", "DA2", "DA3"], "Func"] 18 | } -------------------------------------------------------------------------------- /examples/idf.json: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": [ 3 | { 4 | "type": "optimization", 5 | "id": "Opt", 6 | "name": "SNOPT" 7 | }, 8 | { 9 | "type": "function", 10 | "id": "Dis1", 11 | "name": "Propulsion" 12 | }, 13 | { 14 | "type": "function", 15 | "id": "Dis2", 16 | "name": "Aerodynamics" 17 | }, 18 | { 19 | "type": "function", 20 | "id": "Dis3", 21 | "name": "Mission" 22 | }, 23 | { 24 | "type": "function", 25 | "id": "Dis4", 26 | "name": "Structure" 27 | } 28 | ], 29 | "edges": [ 30 | { 31 | "to": "Opt", 32 | "from": "_U_", 33 | "name": "x_1^(0), x_2^(0), x_3^(0), x_shared^(0), y_34^(0), y_12^(0), y_14^(0), y_31^(0), y_24^(0), y_32^(0), y_23^(0), y_21^(0)" 34 | }, 35 | { 36 | "to": "_U_", 37 | "from": "Opt", 38 | "name": "y_4^*, y_31_y_32_y_34^*, y_21_y_24_y_23^*, y_12_y_14^*" 39 | }, 40 | { 41 | "to": "Dis1", 42 | "from": "Opt", 43 | "name": "x_shared, y_23, x_3" 44 | }, 45 | { 46 | "to": "Dis2", 47 | "from": "Opt", 48 | "name": "x_2, y_32, x_shared, y_12" 49 | }, 50 | { 51 | "to": "Dis3", 52 | "from": "Opt", 53 | "name": "y_24, x_shared, y_34, y_14" 54 | }, 55 | { 56 | "to": "Opt", 57 | "from": "Dis3", 58 | "name": "y_4" 59 | }, 60 | { 61 | "to": "Dis4", 62 | "from": "Opt", 63 | "name": "y_31, y_21, x_shared, x_1" 64 | }, 65 | { 66 | "to": "_U_", 67 | "from": "Dis1", 68 | "name": "y_31^*, y_32^*, y_3^*, y_34^*, g_3^*" 69 | }, 70 | { 71 | "to": "_U_", 72 | "from": "Dis2", 73 | "name": "y_21^*, y_24^*, y_23^*, y_2^*, g_2^*" 74 | }, 75 | { 76 | "to": "Opt", 77 | "from": "Dis3", 78 | "name": "y_4" 79 | }, 80 | { 81 | "to": "_U_", 82 | "from": "Dis4", 83 | "name": "y_12^*, y_11^*, g_1^*, y_1^*, y_14^*" 84 | } 85 | ], 86 | "workflow": [ 87 | "_U_", 88 | [ 89 | [ 90 | "Opt", 91 | [ 92 | { 93 | "parallel": [ 94 | "Dis1", 95 | "Dis2", 96 | "Dis3", 97 | "Dis4" 98 | ] 99 | } 100 | ] 101 | ] 102 | ] 103 | ] 104 | } -------------------------------------------------------------------------------- /examples/mdf.json: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": [ 3 | { 4 | "type": "optimization", 5 | "id": "Opt", 6 | "name": "SLSQP" 7 | }, 8 | { 9 | "type": "mda", 10 | "id": "Dis1", 11 | "name": "MDAGaussSeidel" 12 | }, 13 | { 14 | "type": "function", 15 | "id": "Dis2", 16 | "name": "Propulsion" 17 | }, 18 | { 19 | "type": "function", 20 | "id": "Dis3", 21 | "name": "Aerodynamics" 22 | }, 23 | { 24 | "type": "function", 25 | "id": "Dis4", 26 | "name": "Mission" 27 | }, 28 | { 29 | "type": "function", 30 | "id": "Dis5", 31 | "name": "Structure" 32 | } 33 | ], 34 | "edges": [ 35 | { 36 | "to": "Opt", 37 | "from": "_U_", 38 | "name": "x_1^(0), x_2^(0), x_3^(0), x_shared^(0)" 39 | }, 40 | { 41 | "to": "_U_", 42 | "from": "Opt", 43 | "name": "y_4^*" 44 | }, 45 | { 46 | "to": "Dis1", 47 | "from": "Opt", 48 | "name": "x_2, x_shared, x_3, x_1" 49 | }, 50 | { 51 | "to": "Opt", 52 | "from": "Dis1", 53 | "name": "y_4" 54 | }, 55 | { 56 | "to": "Dis2", 57 | "from": "Opt", 58 | "name": "x_shared, x_3" 59 | }, 60 | { 61 | "to": "Dis3", 62 | "from": "Opt", 63 | "name": "x_2, x_shared" 64 | }, 65 | { 66 | "to": "Dis4", 67 | "from": "Opt", 68 | "name": "x_shared" 69 | }, 70 | { 71 | "to": "Opt", 72 | "from": "Dis4", 73 | "name": "y_4" 74 | }, 75 | { 76 | "to": "Dis5", 77 | "from": "Opt", 78 | "name": "x_shared, x_1" 79 | }, 80 | { 81 | "to": "_U_", 82 | "from": "Dis2", 83 | "name": "y_31^*, y_32^*, y_3^*, y_34^*, g_3^*" 84 | }, 85 | { 86 | "to": "_U_", 87 | "from": "Dis3", 88 | "name": "y_21^*, y_24^*, y_23^*, y_2^*, g_2^*" 89 | }, 90 | { 91 | "to": "Opt", 92 | "from": "Dis4", 93 | "name": "y_4" 94 | }, 95 | { 96 | "to": "_U_", 97 | "from": "Dis5", 98 | "name": "y_12^*, y_11^*, g_1^*, y_1^*, y_14^*" 99 | }, 100 | { 101 | "to": "Dis2", 102 | "from": "Dis1", 103 | "name": "y_34, g_1, y_4, y_3, y_2, g_3, g_2, y_12, y_11, y_1, y_14, y_31, y_24, y_32, y_23, y_21" 104 | }, 105 | { 106 | "to": "Dis1", 107 | "from": "Dis2", 108 | "name": "y_31, y_32, y_3, y_34, g_3" 109 | }, 110 | { 111 | "to": "Dis3", 112 | "from": "Dis1", 113 | "name": "y_34, g_1, y_4, y_3, y_2, g_3, g_2, y_12, y_11, y_1, y_14, y_31, y_24, y_32, y_23, y_21" 114 | }, 115 | { 116 | "to": "Dis1", 117 | "from": "Dis3", 118 | "name": "y_21, y_24, g_2, y_2, y_23" 119 | }, 120 | { 121 | "to": "Dis4", 122 | "from": "Dis1", 123 | "name": "y_34, g_1, y_4, y_3, y_2, g_3, g_2, y_12, y_11, y_1, y_14, y_31, y_24, y_32, y_23, y_21" 124 | }, 125 | { 126 | "to": "Dis1", 127 | "from": "Dis4", 128 | "name": "y_4" 129 | }, 130 | { 131 | "to": "Dis5", 132 | "from": "Dis1", 133 | "name": "y_34, g_1, y_4, y_3, y_2, g_3, g_2, y_12, y_11, y_1, y_14, y_31, y_24, y_32, y_23, y_21" 134 | }, 135 | { 136 | "to": "Dis1", 137 | "from": "Dis5", 138 | "name": "y_12, y_11, g_1, y_1, y_14" 139 | }, 140 | { 141 | "to": "Dis1", 142 | "from": "Dis2", 143 | "name": "y_31, y_32, y_3, y_34, g_3" 144 | }, 145 | { 146 | "to": "Dis2", 147 | "from": "Dis1", 148 | "name": "y_34, g_1, y_4, y_3, y_2, g_3, g_2, y_12, y_11, y_1, y_14, y_31, y_24, y_32, y_23, y_21" 149 | }, 150 | { 151 | "to": "Dis3", 152 | "from": "Dis2", 153 | "name": "y_31, y_32, y_3, y_34, g_3" 154 | }, 155 | { 156 | "to": "Dis2", 157 | "from": "Dis3", 158 | "name": "y_21, y_24, g_2, y_2, y_23" 159 | }, 160 | { 161 | "to": "Dis4", 162 | "from": "Dis2", 163 | "name": "y_31, y_32, y_3, y_34, g_3" 164 | }, 165 | { 166 | "to": "Dis2", 167 | "from": "Dis4", 168 | "name": "y_4" 169 | }, 170 | { 171 | "to": "Dis5", 172 | "from": "Dis2", 173 | "name": "y_31, y_32, y_3, y_34, g_3" 174 | }, 175 | { 176 | "to": "Dis2", 177 | "from": "Dis5", 178 | "name": "y_12, y_11, g_1, y_1, y_14" 179 | }, 180 | { 181 | "to": "Dis1", 182 | "from": "Dis3", 183 | "name": "y_21, y_24, g_2, y_2, y_23" 184 | }, 185 | { 186 | "to": "Dis3", 187 | "from": "Dis1", 188 | "name": "y_34, g_1, y_4, y_3, y_2, g_3, g_2, y_12, y_11, y_1, y_14, y_31, y_24, y_32, y_23, y_21" 189 | }, 190 | { 191 | "to": "Dis2", 192 | "from": "Dis3", 193 | "name": "y_21, y_24, g_2, y_2, y_23" 194 | }, 195 | { 196 | "to": "Dis3", 197 | "from": "Dis2", 198 | "name": "y_31, y_32, y_3, y_34, g_3" 199 | }, 200 | { 201 | "to": "Dis4", 202 | "from": "Dis3", 203 | "name": "y_21, y_24, g_2, y_2, y_23" 204 | }, 205 | { 206 | "to": "Dis3", 207 | "from": "Dis4", 208 | "name": "y_4" 209 | }, 210 | { 211 | "to": "Dis5", 212 | "from": "Dis3", 213 | "name": "y_21, y_24, g_2, y_2, y_23" 214 | }, 215 | { 216 | "to": "Dis3", 217 | "from": "Dis5", 218 | "name": "y_12, y_11, g_1, y_1, y_14" 219 | }, 220 | { 221 | "to": "Dis1", 222 | "from": "Dis4", 223 | "name": "y_4" 224 | }, 225 | { 226 | "to": "Dis4", 227 | "from": "Dis1", 228 | "name": "y_34, g_1, y_4, y_3, y_2, g_3, g_2, y_12, y_11, y_1, y_14, y_31, y_24, y_32, y_23, y_21" 229 | }, 230 | { 231 | "to": "Dis2", 232 | "from": "Dis4", 233 | "name": "y_4" 234 | }, 235 | { 236 | "to": "Dis4", 237 | "from": "Dis2", 238 | "name": "y_31, y_32, y_3, y_34, g_3" 239 | }, 240 | { 241 | "to": "Dis3", 242 | "from": "Dis4", 243 | "name": "y_4" 244 | }, 245 | { 246 | "to": "Dis4", 247 | "from": "Dis3", 248 | "name": "y_21, y_24, g_2, y_2, y_23" 249 | }, 250 | { 251 | "to": "Dis5", 252 | "from": "Dis4", 253 | "name": "y_4" 254 | }, 255 | { 256 | "to": "Dis4", 257 | "from": "Dis5", 258 | "name": "y_12, y_11, g_1, y_1, y_14" 259 | }, 260 | { 261 | "to": "Dis1", 262 | "from": "Dis5", 263 | "name": "y_12, y_11, g_1, y_1, y_14" 264 | }, 265 | { 266 | "to": "Dis5", 267 | "from": "Dis1", 268 | "name": "y_34, g_1, y_4, y_3, y_2, g_3, g_2, y_12, y_11, y_1, y_14, y_31, y_24, y_32, y_23, y_21" 269 | }, 270 | { 271 | "to": "Dis2", 272 | "from": "Dis5", 273 | "name": "y_12, y_11, g_1, y_1, y_14" 274 | }, 275 | { 276 | "to": "Dis5", 277 | "from": "Dis2", 278 | "name": "y_31, y_32, y_3, y_34, g_3" 279 | }, 280 | { 281 | "to": "Dis3", 282 | "from": "Dis5", 283 | "name": "y_12, y_11, g_1, y_1, y_14" 284 | }, 285 | { 286 | "to": "Dis5", 287 | "from": "Dis3", 288 | "name": "y_21, y_24, g_2, y_2, y_23" 289 | }, 290 | { 291 | "to": "Dis4", 292 | "from": "Dis5", 293 | "name": "y_12, y_11, g_1, y_1, y_14" 294 | }, 295 | { 296 | "to": "Dis5", 297 | "from": "Dis4", 298 | "name": "y_4" 299 | } 300 | ], 301 | "workflow": [ 302 | "_U_", 303 | [ 304 | "Opt", 305 | [ 306 | "Dis1", 307 | [ 308 | "Dis2", 309 | "Dis3", 310 | "Dis4", 311 | "Dis5" 312 | ] 313 | ] 314 | ] 315 | ] 316 | } -------------------------------------------------------------------------------- /examples/nested.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": { 3 | "nodes": [ 4 | { 5 | "id": "233", 6 | "name": "NewInner", 7 | "type": "group", 8 | "subxdsm": "NewInner" 9 | }, 10 | { 11 | "id": "236", 12 | "name": "Disc3", 13 | "type": "function" 14 | } 15 | ], 16 | "edges": [ 17 | { 18 | "from": "_U_", 19 | "to": "233", 20 | "name": "t" 21 | }, 22 | { 23 | "from": "233", 24 | "to": "_U_", 25 | "name": "z" 26 | }, 27 | { 28 | "from": "233", 29 | "to": "236", 30 | "name": "z" 31 | } 32 | ] 33 | }, 34 | "NewInner": { 35 | "nodes": [ 36 | { 37 | "id": "235", 38 | "name": "Disc2", 39 | "type": "function" 40 | } 41 | ], 42 | "edges": [ 43 | { 44 | "from": "_U_", 45 | "to": "235", 46 | "name": "t" 47 | }, 48 | { 49 | "from": "235", 50 | "to": "_U_", 51 | "name": "z" 52 | } 53 | ] 54 | } 55 | } -------------------------------------------------------------------------------- /fontello.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'fontello'; 3 | src: url('data:application/octet-stream;base64,d09GRgABAAAAAAvUAA8AAAAAFLAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAABHU1VCAAABWAAAADsAAABUIIslek9TLzIAAAGUAAAAQwAAAFY+IEiIY21hcAAAAdgAAABiAAABohCG1ONjdnQgAAACPAAAABMAAAAgBtX/BGZwZ20AAAJQAAAFkAAAC3CKkZBZZ2FzcAAAB+AAAAAIAAAACAAAABBnbHlmAAAH6AAAASgAAAGIz1KzKmhlYWQAAAkQAAAAMgAAADYUJNjgaGhlYQAACUQAAAAdAAAAJAc8A1dobXR4AAAJZAAAABQAAAAUDsj//2xvY2EAAAl4AAAADAAAAAwApAEMbWF4cAAACYQAAAAgAAAAIACoC6RuYW1lAAAJpAAAAXcAAALNzJ0fIXBvc3QAAAscAAAAOQAAAE6t8hvUcHJlcAAAC1gAAAB6AAAAhuVBK7x4nGNgZGBg4GIwYLBjYHJx8wlh4MtJLMljkGJgYYAAkDwymzEnMz2RgQPGA8qxgGkOIGaDiAIAJjsFSAB4nGNgZPrKOIGBlYGBqYppDwMDQw+EZnzAYMjIBBRlYGVmwAoC0lxTGBxeMLxgYQ76n8UQxRzEMA0ozAiSAwAOnAwlAHic7ZHBDYAwDAMvbUAIMQoPRmAQXkyfLYrTdgwsXa24Sh8usABVnMLBXozUo9R6Xtl77lyaV7lRwsJbgyBqumS627oX7eRbK7+Oft5z8mxskO3GRI2px0H+RvgA/wBR9BFIAAB4nGNgQAMSEMgc9D8LhAESbAPdAHicrVZpd9NGFB15SZyELCULLWphxMRpsEYmbMGACUGyYyBdnK2VoIsUO+m+8Ynf4F/zZNpz6Dd+Wu8bLySQtOdwmpOjd+fN1czbZRJaktgL65GUmy/F1NYmjew8CemGTctRfCg7eyFlisnfBVEQrZbatx2HREQiULWusEQQ+x5ZmmR86FFGy7akV03KLT3pLlvjQb1V334aOsqxO6GkZjN0aD2yJVUYVaJIpj1S0qZlqPorSSu8v8LMV81QwohOImm8GcbQSN4bZ7TKaDW24yiKbLLcKFIkmuFBFHmU1RLn5IoJDMoHzZDyyqcR5cP8iKzYo5xWsEu20/y+L3mndzk/sV9vUbbkQB/Ijuzg7HQlX4RbW2HctJPtKFQRdtd3QmzZ7FT/Zo/ymkYDtysyvdCMYKl8hRArP6HM/iFZLZxP+ZJHo1qykRNB62VO7Es+gdbjiClxzRhZ0N3RCRHU/ZIzDPaYPh788d4plgsTAngcy3pHJZwIEylhczRJ2jByYCVliyqp9a6YOOV1WsRbwn7t2tGXzmjjUHdiPFsPHVs5UcnxaFKnmUyd2knNoykNopR0JnjMrwMoP6JJXm1jNYmVR9M4ZsaERCICLdxLU0EsO7GkKQTNoxm9uRumuXYtWqTJA/Xco/f05la4udNT2g70s0Z/VqdiOtgL0+lp5C/xadrlIkXp+ukZfkziQdYCMpEtNsOUgwdv/Q7Sy9eWHIXXBtju7fMrqH3WRPCkAfsb0B5P1SkJTIWYVYhWQGKta1mWydWsFqnI1HdDmla+rNMEinIcF8e+jHH9XzMzlpgSvt+J07MjLj1z7UsI0xx8m3U9mtepxXIBcWZ5TqdZlu/rNMfyA53mWZ7X6QhLW6ejLD/UaYHlRzodY3lBC5p038GQizDkAg6QMISlA0NYXoIhLBUMYbkIQ1gWYQjLJRjC8mMYwnIZhrC8rGXV1FNJ49qZWAZsQmBijh65zEXlaiq5VEK7aFRqQ54SbpVUFM+qf2WgXjzyhjmwFkiXyJpfMc6Vj0bl+NYVLW8aO1fAsepvH472OfFS1ouFPwX/1dZUJb1izcOTq/Abhp5sJ6o2qXh0TZfPVT26/l9UVFgL9BtIhVgoyrJscGcihI86nYZqoJVDzGzMPLTrdcuan8P9NzFCFlD9+DcUGgvcg05ZSVnt4KzV19uy3DuDcjgTLEkxN/P6VvgiI7PSfpFZyp6PfB5wBYxKZdhqA60VvNknMQ+Z3iTPBHFbUTZI2tjOBIkNHPOAefOdBCZh6qoN5E7hhg34BWFuwXknXKJ6oyyH7kXs8yik/Fun4kT2qGiMwLPZG2Gv70LKb3EMJDT5pX4MVBWhqRg1FdA0Um6oBl/G2bptQsYO9CMqdsOyrOLDxxb3lZJtGYR8pIjVo6Of1l6iTqrcfmYUl++dvgXBIDUxf3vfdHGQyrtayTJHbQNTtxqVU9eaQ+NVh+rmUfW94+wTOWuabronHnpf06rbwcVcLLD2bQ7SUiYX1PVhhQ2iy8WlUOplNEnvuAcYFhjQ71CKjf+r+th8nitVhdFxJN9O1LfR52AM/A/Yf0f1A9D3Y+hyDS7P95oTn2704WyZrqIX66foNzBrrblZugbc0HQD4iFHrY64yg18pwZxeqS5HOkh4GPdFeIBwCaAxeAT3bWM5lMAo/mMOT7A58xh0GQOgy3mMNhmzhrADnMY7DKHwR5zGHzBnHWAL5nDIGQOg4g5DJ4wJwB4yhwGXzGHwdfMYfANc+4DfMscBjFzGCTMYbCv6dYwzC1e0F2gtkFVoANTT1jcw+JQU2XI/o4Xhv29Qcz+wSCm/qjp9pD6Ey8M9WeDmPqLQUz9VdOdIfU3Xhjq7wYx9Q+DmPpMvxjLZQa/jHyXCgeUXWw+5++J9w/bxUC5AAEAAf//AA94nFWPMU7DQBBFZ3bYtYnjdWycNSGiiJHswqJJsFMEpHSm4QBQo7RpqKCGipMggYS4BfdAdHABYzOOEYjmz2ik+e9/QIDmgXzaAQfUiyXwMNtGleT+URGhCWnra+wGgSveXDyrL62eR4Xu27yFAO3vM12QAx7sLoc2CEAs+YxrAFjlMyFNhn6oDiZxkrLjbDI1dB4Oqk8vRKPJ94x4rz604T1g5S9smuZJHJMH+zBamvFeQOxXtrA1u6/yk9Y0Usm8SxjxWODUWIzJMOazska9AWoH7yWF3qk22CnJ0lFtfLtfX0mJ1y22g+ONktD1eRQL0n/s/4U2bCzMkGEpcqWOmf5kiPxiLmJHa4cBwuhy07JVIYkYXr0ynHOp+va3s67vpIRvUGM51nicY2BkYGAAYjZ5A/94fpuvDNzML4AiDDf6vebD6P///69ifsEsDORyMDCBRAE7TgyOAAB4nGNgZGBgDvqfBSRf/AcC5hcMQBEUwAoAtp8HmAAAAAPoAAADEQAAA1kAAAI7//8COwAAAAAAAAAeAEgAhgDEAAEAAAAFAB4AAQAAAAAAAgAEABQAcwAAACoLcAAAAAB4nHWQy07CQBSG/5GLCokaTdw6KwMxlkviAhISEgxsdEMMW1NKaUtKh0wHEl7Dd/BhfAmfxZ92MAZim+l855szZ04HwDW+IZA/Txw5C5wxyvkEp+hZLtA/Wy6SXyyXUMWb5TL9u+UKHhBYruIGH6wgiueMFvi0LHAlLi2f4ELcWS7QP1ouknuWS7gVr5bL9J7lCiYitVzFvfgaqNVWR0FoZG1Ql+1mqyOnW6moosSNpbs2odKp7Mu5Sowfx8rx1HLPYz9Yx67eh/t54us0UolsOc29GvmJr13jz3bV003QNmYu51ot5dBmyJVWC98zTmjMqtto/D0PAyissIVGxKsKYSBRo61zbqOJFjqkKTMkM/OsCAlcxDQu1twRZisp4z7HnFFC6zMjJjvw+F0e+TEp4P6YVfTR6mE8Ie3OiDIv2ZfD7g6zRqQky3QzO/vtPcWGp7VpDXftutRZVxLDgxqS97FbW9B49E52K4a2iwbff/7vB+x4hFUAeJxjYGKAAC4G7ICVkYmRmZGFkZWRjYG1uCSxqISluCS/gLO4JLVANy+1ogTCKihKLWNgAADPBwuIAAAAeJxj8N7BcCIoYiMjY1/kBsadHAwcDMkFGxlYnTYxMDJogRibuZgYOSAsPgYwi81pF9MBoDQnkM3utIvBAcJmZnDZqMLYERixwaEjYiNzistGNRBvF0cDAyOLQ0dySARISSQQbOZhYuTR2sH4v3UDS+9GJgYXAAx2I/QAAA==') format('woff'); 4 | } 5 | 6 | [class^="icon-"]:before, [class*=" icon-"]:before { 7 | font-family: "fontello"; 8 | font-style: normal; 9 | font-weight: normal; 10 | speak: none; 11 | 12 | display: inline-block; 13 | text-decoration: inherit; 14 | width: 1em; 15 | margin-right: .2em; 16 | text-align: center; 17 | /* opacity: .8; */ 18 | 19 | /* For safety - reset parent styles, that can break glyph codes*/ 20 | font-variant: normal; 21 | text-transform: none; 22 | 23 | /* fix buttons height, for twitter bootstrap */ 24 | line-height: 1em; 25 | 26 | /* Animation center compensation - margins should be symmetric */ 27 | /* remove if not needed */ 28 | margin-left: .2em; 29 | 30 | /* you can be more comfortable with increased icons size */ 31 | font-size: 200%; 32 | 33 | /* Uncomment for 3D effect */ 34 | /* text-shadow: 1px 1px 1px rgba(127, 127, 127, 0.3); */ 35 | } 36 | .icon-start:before { content: '\e800'; } 37 | .icon-stop:before { content: '\e801'; } 38 | .icon-step-next:before { content: '\e803'; } 39 | .icon-step-prev:before { content: '\e804'; } -------------------------------------------------------------------------------- /fontello/LICENSE.txt: -------------------------------------------------------------------------------- 1 | Font license info 2 | 3 | 4 | ## Font Awesome 5 | 6 | Copyright (C) 2016 by Dave Gandy 7 | 8 | Author: Dave Gandy 9 | License: SIL () 10 | Homepage: http://fortawesome.github.com/Font-Awesome/ 11 | 12 | 13 | -------------------------------------------------------------------------------- /fontello/README.txt: -------------------------------------------------------------------------------- 1 | This webfont is generated by http://fontello.com open source project. 2 | 3 | 4 | ================================================================================ 5 | Please, note, that you should obey original font licenses, used to make this 6 | webfont pack. Details available in LICENSE.txt file. 7 | 8 | - Usually, it's enough to publish content of LICENSE.txt file somewhere on your 9 | site in "About" section. 10 | 11 | - If your project is open-source, usually, it will be ok to make LICENSE.txt 12 | file publicly available in your repository. 13 | 14 | - Fonts, used in Fontello, don't require a clickable link on your site. 15 | But any kind of additional authors crediting is welcome. 16 | ================================================================================ 17 | 18 | 19 | Comments on archive content 20 | --------------------------- 21 | 22 | - /font/* - fonts in different formats 23 | 24 | - /css/* - different kinds of css, for all situations. Should be ok with 25 | twitter bootstrap. Also, you can skip style and assign icon classes 26 | directly to text elements, if you don't mind about IE7. 27 | 28 | - demo.html - demo file, to show your webfont content 29 | 30 | - LICENSE.txt - license info about source fonts, used to build your one. 31 | 32 | - config.json - keeps your settings. You can import it back into fontello 33 | anytime, to continue your work 34 | 35 | 36 | Why so many CSS files ? 37 | ----------------------- 38 | 39 | Because we like to fit all your needs :) 40 | 41 | - basic file, .css - is usually enough, it contains @font-face 42 | and character code definitions 43 | 44 | - *-ie7.css - if you need IE7 support, but still don't wish to put char codes 45 | directly into html 46 | 47 | - *-codes.css and *-ie7-codes.css - if you like to use your own @font-face 48 | rules, but still wish to benefit from css generation. That can be very 49 | convenient for automated asset build systems. When you need to update font - 50 | no need to manually edit files, just override old version with archive 51 | content. See fontello source code for examples. 52 | 53 | - *-embedded.css - basic css file, but with embedded WOFF font, to avoid 54 | CORS issues in Firefox and IE9+, when fonts are hosted on the separate domain. 55 | We strongly recommend to resolve this issue by `Access-Control-Allow-Origin` 56 | server headers. But if you ok with dirty hack - this file is for you. Note, 57 | that data url moved to separate @font-face to avoid problems with 2 | 3 | 4 | 278 | 279 | 291 | 292 | 293 |
294 |

fontello font demo

295 | 298 |
299 |
300 |
301 |
icon-start0xe800
302 |
icon-stop0xe801
303 |
icon-step-next0xe803
304 |
icon-step-prev0xe804
305 |
306 |
307 | 308 | 309 | -------------------------------------------------------------------------------- /fontello/font/fontello.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whatsopt/XDSMjs/3cc3372106985de6609e36af56d97eab559dac03/fontello/font/fontello.eot -------------------------------------------------------------------------------- /fontello/font/fontello.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Copyright (C) 2019 by original authors @ fontello.com 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /fontello/font/fontello.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whatsopt/XDSMjs/3cc3372106985de6609e36af56d97eab559dac03/fontello/font/fontello.ttf -------------------------------------------------------------------------------- /fontello/font/fontello.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whatsopt/XDSMjs/3cc3372106985de6609e36af56d97eab559dac03/fontello/font/fontello.woff -------------------------------------------------------------------------------- /fontello/font/fontello.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whatsopt/XDSMjs/3cc3372106985de6609e36af56d97eab559dac03/fontello/font/fontello.woff2 -------------------------------------------------------------------------------- /gallery/xdsm_bliss_anim.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whatsopt/XDSMjs/3cc3372106985de6609e36af56d97eab559dac03/gallery/xdsm_bliss_anim.gif -------------------------------------------------------------------------------- /gallery/xdsm_idf.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whatsopt/XDSMjs/3cc3372106985de6609e36af56d97eab559dac03/gallery/xdsm_idf.png -------------------------------------------------------------------------------- /gallery/xdsm_mdf.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whatsopt/XDSMjs/3cc3372106985de6609e36af56d97eab559dac03/gallery/xdsm_mdf.png -------------------------------------------------------------------------------- /gallery/xdsm_v1_v2.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whatsopt/XDSMjs/3cc3372106985de6609e36af56d97eab559dac03/gallery/xdsm_v1_v2.gif -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "xdsmjs", 3 | "version": "2.0.0", 4 | "description": "XDSM diagram generator", 5 | "main": "src/index.js", 6 | "files": [ 7 | "dist/**/*.js", 8 | "dist/**/*.js.map", 9 | "src/**/*.js", 10 | "*.css" 11 | ], 12 | "directories": { 13 | "example": "examples" 14 | }, 15 | "scripts": { 16 | "lint": "eslint ./src/* || true", 17 | "lintfix": "eslint --fix ./src/* || true", 18 | "build": "webpack --progress --mode=production --config webpack.config.cjs", 19 | "watch": "webpack --progress --watch", 20 | "server": "webpack-dev-server --open", 21 | "test": "npx tape test/xdsmjs-test.mjs | faucet", 22 | "webpack": "webpack" 23 | }, 24 | "repository": "git+https://github.com/OneraHub/XDSMjs.git", 25 | "keywords": [ 26 | "XDSM", 27 | "MDO" 28 | ], 29 | "author": "Remi Lafage", 30 | "license": "Apache-2.0", 31 | "type": "module", 32 | "bugs": { 33 | "url": "https://github.com/OneraHub/XDSMjs/issues" 34 | }, 35 | "homepage": "https://github.com/OneraHub/XDSMjs#readme", 36 | "devDependencies": { 37 | "@babel/core": "^7.0.0", 38 | "@babel/preset-env": "^7.0.0", 39 | "@babel/register": "^7.18.9", 40 | "babel-core": "^7.0.0-bridge", 41 | "babel-loader": "^8.0.0", 42 | "eslint": "^8.33.0", 43 | "eslint-config-airbnb": "^19.0.4", 44 | "eslint-plugin-import": "^2.20.2", 45 | "eslint-plugin-jsx-a11y": "^6.2.3", 46 | "eslint-plugin-react": "^7.19.0", 47 | "eslint-plugin-react-hooks": "^4.2.0", 48 | "faucet": "0.0.4", 49 | "tape": "^5.6.3", 50 | "terser-webpack-plugin": "^5.3.6", 51 | "webpack": "^5.50.0", 52 | "webpack-cli": "5.0.1", 53 | "webpack-dev-server": "^4.11.1" 54 | }, 55 | "dependencies": { 56 | "d3-color": "^3.1.0", 57 | "d3-fetch": "^3.0.1", 58 | "d3-selection": "^3.0.0", 59 | "d3-transition": "^3.0.1" 60 | }, 61 | "eslintConfig": { 62 | "extends": [ 63 | "google" 64 | ], 65 | "settings": { 66 | "react": { 67 | "version": "detect" 68 | } 69 | }, 70 | "parserOptions": { 71 | "ecmaVersion": 6, 72 | "sourceType": "module" 73 | }, 74 | "env": { 75 | "commonjs": true, 76 | "es6": true 77 | }, 78 | "rules": { 79 | "max-len": [ 80 | "error", 81 | 120 82 | ], 83 | "quotes": "off", 84 | "no-var": "off", 85 | "linebreak-style": "off", 86 | "require-jsdoc": "off", 87 | "brace-style": "off" 88 | } 89 | } 90 | } -------------------------------------------------------------------------------- /py_xdsmjs/make_pyxdsmjs.py: -------------------------------------------------------------------------------- 1 | import os 2 | from shutil import copy 3 | import json 4 | 5 | PACKAGE_RELEASE_NUMBER = None 6 | 7 | if not os.path.exists("xdsmjs/dist"): 8 | os.makedirs("xdsmjs/dist") 9 | 10 | # copy assets 11 | copy("../dist/xdsmjs.js", "xdsmjs/dist/") 12 | copy("../fontello.css", "xdsmjs/dist/") 13 | copy("../xdsmjs.css", "xdsmjs/dist/") 14 | 15 | # change version 16 | version = "0.0.0" 17 | with open("../package.json") as pkg: 18 | package = json.load(pkg) 19 | version = package["version"] 20 | 21 | if PACKAGE_RELEASE_NUMBER: 22 | version += ".{}".format(PACKAGE_RELEASE_NUMBER) 23 | 24 | init = [] 25 | with open("xdsmjs/__init__.py") as f: 26 | init = f.readlines() 27 | 28 | init[0] = '__version__ = "{}"\n'.format(version) 29 | 30 | with open("xdsmjs/__init__.py", "w") as f: 31 | f.writelines(init) 32 | -------------------------------------------------------------------------------- /py_xdsmjs/setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from setuptools import setup 4 | from xdsmjs import __version__ 5 | 6 | version = __version__ 7 | PKG_VERSION_NUMBER = None 8 | if PKG_VERSION_NUMBER: 9 | version += ".{}".format(PKG_VERSION_NUMBER) 10 | 11 | CLASSIFIERS = """ 12 | Development Status :: 5 - Production/Stable 13 | Intended Audience :: Science/Research 14 | Intended Audience :: Developers 15 | License :: OSI Approved :: Apache Software License 16 | Programming Language :: Python :: 3 17 | Topic :: Software Development 18 | Topic :: Scientific/Engineering 19 | Operating System :: Microsoft :: Windows 20 | Operating System :: Unix 21 | Operating System :: MacOS 22 | """ 23 | 24 | setup( 25 | name="xdsmjs", 26 | version=version, 27 | description="XDSMjs Python module", 28 | long_description="Python module to distribute [XDSMjs](https://github.com/OneraHub/XDSMjs#xdsmjs) js/css resources", 29 | author="Rémi Lafage", 30 | author_email="remi.lafage@onera.fr", 31 | license="Apache License, Version 2.0", 32 | classifiers=[_f for _f in CLASSIFIERS.split("\n") if _f], 33 | packages=["xdsmjs"], 34 | package_data={"xdsmjs": ["dist/xdsmjs.js", "dist/xdsmjs.css", "dist/fontello.css"]}, 35 | url="https://github.com/OneraHub/XDSMjs", 36 | ) 37 | -------------------------------------------------------------------------------- /py_xdsmjs/xdsmjs/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = "2.0.0" 2 | 3 | import os 4 | 5 | 6 | def read(filename): 7 | 8 | result_string = None 9 | with open(os.path.join(os.path.dirname(__file__), "dist", filename)) as f: 10 | result_string = f.read() 11 | 12 | return result_string 13 | 14 | 15 | def bundlejs(): 16 | return read("xdsmjs.js") 17 | 18 | 19 | def css(): 20 | return "{}\n{}".format(read("fontello.css"), read("xdsmjs.css")) 21 | -------------------------------------------------------------------------------- /py_xdsmjs/xdsmjs/tests/test_xdsmjs.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import xdsmjs 3 | 4 | 5 | class TestXdsmjs(unittest.TestCase): 6 | def test_bundlejs(self): 7 | self.assertTrue(len(xdsmjs.bundlejs()) > 100) 8 | 9 | def test_css(self): 10 | self.assertTrue(len(xdsmjs.css()) > 100) 11 | 12 | 13 | if __name__ == "__main__": 14 | unittest.main() 15 | -------------------------------------------------------------------------------- /src/animation.js: -------------------------------------------------------------------------------- 1 | import { select, selectAll } from 'd3-selection'; 2 | import { rgb } from 'd3-color'; 3 | import Graph from './graph.js'; 4 | 5 | const PULSE_DURATION = 700; 6 | const SUB_ANIM_DELAY = 200; 7 | const RUNNING_COLOR = rgb('seagreen'); 8 | const FAILED_COLOR = rgb('firebrick'); 9 | const PENDING_COLOR = rgb('darkseagreen'); 10 | const DONE_COLOR = rgb('darkcyan'); 11 | 12 | function Animation(xdsms, rootId, delay) { 13 | this.rootId = rootId; 14 | if (typeof (rootId) === 'undefined') { 15 | this.rootId = 'root'; 16 | } 17 | this.root = xdsms[this.rootId]; 18 | this.xdsms = xdsms; 19 | this.duration = PULSE_DURATION; 20 | this.initialDelay = delay || 0; 21 | 22 | this._observers = []; 23 | this.reset(); 24 | } 25 | 26 | Animation.STATUS = { 27 | READY: 'ready', 28 | RUNNING_STEP: 'running_step', 29 | RUNNING_AUTO: 'running_auto', 30 | STOPPED: 'stopped', 31 | DONE: 'done', 32 | DISABLED: 'disabled', 33 | }; 34 | 35 | Animation.prototype.reset = function reset() { 36 | this.curStep = 0; 37 | this.subAnimations = {}; 38 | this._updateStatus(Animation.STATUS.READY); 39 | }; 40 | 41 | Animation.prototype.start = function start() { 42 | this._scheduleAnimation(); 43 | this._updateStatus(Animation.STATUS.RUNNING_AUTO); 44 | }; 45 | 46 | Animation.prototype.stop = function stop() { 47 | this._reset('all'); 48 | this._updateStatus(Animation.STATUS.STOPPED); 49 | }; 50 | 51 | Animation.prototype.stepPrev = function stepPrev() { 52 | this._step('prev'); 53 | }; 54 | 55 | Animation.prototype.stepNext = function stepNext() { 56 | this._step('next'); 57 | }; 58 | 59 | Animation.prototype.setXdsmVersion = function setXdsmVersion(version) { 60 | Object.values(this.xdsms).forEach((xdsm) => { 61 | xdsm.setVersion(version); 62 | xdsm.refresh(); 63 | }, this); 64 | }; 65 | 66 | Animation.prototype._step = function _step(dir) { 67 | const backward = (dir === 'prev'); 68 | const self = this; 69 | const { graph } = self.xdsms[self.rootId]; 70 | const { nodesByStep } = graph; 71 | const incr = backward ? -1 : 1; 72 | 73 | if ((!backward && self.done()) 74 | || (backward && self.ready())) { 75 | return; 76 | } 77 | 78 | if (!self._subAnimationInProgress()) { 79 | self.curStep += incr; 80 | self._reset(); 81 | 82 | const nodesAtStep = nodesByStep[self.curStep]; 83 | nodesAtStep.forEach((nodeId) => { 84 | if (self.running()) { 85 | nodesByStep[self.curStep - incr].forEach((prevNodeId) => { 86 | if (backward) { 87 | self._pulseLink(0, nodeId, prevNodeId); 88 | } else { 89 | self._pulseLink(0, prevNodeId, nodeId); 90 | } 91 | const gnode = `g.id${prevNodeId}`; 92 | self._pulseNode(0, gnode, 'out'); 93 | }); 94 | } 95 | const gnode = `g.id${nodeId}`; 96 | self._pulseNode(0, gnode, 'in'); 97 | }); 98 | } 99 | 100 | if (nodesByStep[self.curStep].some(self._isSubXdsm, this)) { 101 | nodesByStep[self.curStep].forEach((nodeId) => { 102 | if (self._isSubXdsm(nodeId)) { 103 | const xdsmId = graph.getNode(nodeId).getSubXdsmId(); 104 | if (!self.subAnimations[xdsmId]) { 105 | self.subAnimations[xdsmId] = new Animation(self.xdsms, xdsmId); 106 | } 107 | const anim = self.subAnimations[xdsmId]; 108 | anim._step(dir); 109 | } 110 | }, this); 111 | } 112 | if (this.done()) { 113 | this._updateStatus(Animation.STATUS.DONE); 114 | } else if (this.ready()) { 115 | this._updateStatus(Animation.STATUS.READY); 116 | } else { 117 | this._updateStatus(Animation.STATUS.RUNNING_STEP); 118 | } 119 | }; 120 | 121 | Animation.prototype.running = function running() { 122 | return !this.ready() && !this.done(); 123 | }; 124 | Animation.prototype.ready = function ready() { 125 | return this.curStep === 0; 126 | }; 127 | Animation.prototype.done = function done() { 128 | return this.curStep === this.root.graph.nodesByStep.length - 1; 129 | }; 130 | Animation.prototype.isStatus = function isStatus(status) { 131 | return this.status === status; 132 | }; 133 | 134 | Animation.prototype.addObserver = function addObserver(observer) { 135 | if (observer) { 136 | this._observers.push(observer); 137 | } 138 | }; 139 | 140 | Animation.prototype.renderNodeStatuses = function renderNodeStatuses() { 141 | const self = this; 142 | const { graph } = self.xdsms[self.rootId]; 143 | graph.nodes.forEach((node) => { 144 | const gnode = `g.${node.id}`; 145 | switch (node.status) { 146 | case Graph.NODE_STATUS.RUNNING: 147 | self._pulseNode(0, gnode, 'in', RUNNING_COLOR); 148 | break; 149 | case Graph.NODE_STATUS.FAILED: 150 | self._pulseNode(0, gnode, 'in', FAILED_COLOR); 151 | break; 152 | case Graph.NODE_STATUS.PENDING: 153 | self._pulseNode(0, gnode, 'in', PENDING_COLOR); 154 | break; 155 | case Graph.NODE_STATUS.DONE: 156 | self._pulseNode(0, gnode, 'in', DONE_COLOR); 157 | break; 158 | default: 159 | // nothing to do 160 | } 161 | if (self._isSubXdsm(node.id)) { 162 | const xdsmId = graph.getNode(node.id).getSubXdsmId(); 163 | const anim = new Animation(self.xdsms, xdsmId); 164 | 165 | anim.renderNodeStatuses(); 166 | } 167 | }); 168 | }; 169 | 170 | Animation.prototype._updateStatus = function _updateStatus(status) { 171 | this.status = status; 172 | this._notifyObservers(status); 173 | }; 174 | 175 | Animation.prototype._notifyObservers = function _notifyObservers() { 176 | this._observers.map((obs) => obs.update(this.status)); 177 | }; 178 | 179 | Animation.prototype._pulse = function _pulse(delay, toBeSelected, easeInOut, color) { 180 | const colour = color || RUNNING_COLOR; 181 | let sel = select(`svg#${this.rootId}`) 182 | .selectAll(toBeSelected) 183 | .transition().delay(delay); 184 | if (easeInOut !== 'out') { 185 | sel = sel.transition().duration(200) 186 | .style('stroke-width', '8px') 187 | .style('stroke', colour) 188 | .style('fill', (d) => { 189 | if (d.id) { 190 | return colour.brighter(); 191 | } 192 | return null; 193 | }); 194 | } 195 | if (easeInOut !== 'in') { 196 | sel.transition().duration(3 * PULSE_DURATION) 197 | .style('stroke-width', null) 198 | .style('stroke', null) 199 | .style('fill', null); 200 | } 201 | }; 202 | 203 | Animation.prototype._pulseNode = function _pulseNode(delay, gnode, easeInOut, color) { 204 | this._pulse(delay, `${gnode} > rect`, easeInOut, color); 205 | this._pulse(delay, `${gnode} > polygon`, easeInOut, color); 206 | }; 207 | 208 | Animation.prototype._pulseLink = function _pulseLink(delay, fromId, toId) { 209 | const { graph } = this.xdsms[this.rootId]; 210 | const from = graph.idxOf(fromId); 211 | const to = graph.idxOf(toId); 212 | this._pulse(delay, `path.link_${from}_${to}`); 213 | }; 214 | 215 | Animation.prototype._onAnimationStart = function _onAnimationStart(delay) { 216 | const title = select(`svg#${this.rootId}`).select('g.title'); 217 | title.select('text').transition().delay(delay).style('fill', RUNNING_COLOR); 218 | select(`svg#${this.rootId}`).select('rect.border') 219 | .transition().delay(delay) 220 | .style('stroke-width', '5px') 221 | .duration(200) 222 | .transition() 223 | .duration(1000) 224 | .style('stroke', 'black') 225 | .style('stroke-width', '0px'); 226 | }; 227 | 228 | Animation.prototype._onAnimationDone = function _onAnimationDone(delay) { 229 | const self = this; 230 | const title = select(`svg#${this.rootId}`).select('g.title'); 231 | title.select('text').transition() 232 | .delay(delay) 233 | .style('fill', null) 234 | .on('end', () => { 235 | self._updateStatus(Animation.STATUS.DONE); 236 | }); 237 | }; 238 | 239 | Animation.prototype._isSubXdsm = function _isSubXdsm(nodeId) { 240 | const gnode = `g.id${nodeId}`; 241 | const nodeSel = select(`svg#${this.rootId}`).select(gnode); 242 | return nodeSel.classed('mdo') || nodeSel.classed('sub-optimization') 243 | || nodeSel.classed('group') || nodeSel.classed('implicit-group'); 244 | }; 245 | 246 | Animation.prototype._scheduleAnimation = function _scheduleAnimation() { 247 | const self = this; 248 | let delay = this.initialDelay; 249 | const animDelay = SUB_ANIM_DELAY; 250 | const { graph } = self.xdsms[self.rootId]; 251 | 252 | self._onAnimationStart(delay); 253 | 254 | graph.nodesByStep.forEach((nodesAtStep, n, nodesByStep) => { 255 | const offsets = []; 256 | nodesAtStep.forEach((nodeId) => { 257 | const elapsed = delay + n * PULSE_DURATION; 258 | 259 | if (n > 0) { 260 | nodesByStep[n - 1].forEach((prevNodeId) => { // eslint-disable-line space-infix-ops 261 | self._pulseLink(elapsed, prevNodeId, nodeId); 262 | }); 263 | 264 | const gnode = `g.id${nodeId}`; 265 | if (self._isSubXdsm(nodeId)) { 266 | self._pulseNode(elapsed, gnode, 'in'); 267 | const xdsmId = graph.getNode(nodeId).getSubXdsmId(); 268 | const anim = new Animation(self.xdsms, xdsmId, elapsed + animDelay); 269 | const offset = anim._scheduleAnimation(); 270 | offsets.push(offset); 271 | self._pulseNode(offset + elapsed + animDelay, gnode, 'out'); 272 | } else { 273 | self._pulseNode(elapsed, gnode); 274 | } 275 | } 276 | }, this); 277 | 278 | if (offsets.length > 0) { 279 | delay += Math.max.apply(null, offsets); 280 | } 281 | delay += animDelay; 282 | }, this); 283 | 284 | self._onAnimationDone(graph.nodesByStep.length * PULSE_DURATION + delay); 285 | 286 | return graph.nodesByStep.length * PULSE_DURATION; 287 | }; 288 | 289 | Animation.prototype._reset = function _reset(all) { 290 | let svg = select(`svg#${this.rootId}`); 291 | if (all) { 292 | svg = selectAll('svg'); 293 | } 294 | svg.selectAll('rect').transition().duration(0) 295 | .style('stroke-width', null) 296 | .style('stroke', null); 297 | svg.selectAll('polygon').transition().duration(0) 298 | .style('stroke-width', null) 299 | .style('stroke', null); 300 | svg.selectAll('.title > text').transition().duration(0) 301 | .style('fill', null); 302 | 303 | svg.selectAll('.node > rect').transition().duration(0) 304 | .style('stroke-width', null) 305 | .style('stroke', null) 306 | .style('fill', null); 307 | svg.selectAll('.node > polygon').transition().duration(0) 308 | .style('stroke-width', null) 309 | .style('stroke', null) 310 | .style('fill', null); 311 | svg.selectAll('path').transition().duration(0) 312 | .style('stroke-width', null) 313 | .style('stroke', null) 314 | .style('fill', null); 315 | }; 316 | 317 | Animation.prototype._resetPreviousStep = function _resetPreviousStep() { 318 | this.root.graph.nodesByStep[this.curStep - 1].forEach((nodeId) => { 319 | const gnode = `g.id${nodeId}`; 320 | this._pulseNode(0, gnode, 'out'); 321 | }, this); 322 | }; 323 | 324 | Animation.prototype._subAnimationInProgress = function _subAnimationInProgress() { 325 | let running = false; 326 | for (const k in this.subAnimations) { 327 | if (Object.prototype.hasOwnProperty.call(this.subAnimations, k)) { 328 | running = running || this.subAnimations[k].running(); 329 | } 330 | } 331 | return running; 332 | }; 333 | 334 | export default Animation; 335 | -------------------------------------------------------------------------------- /src/controls.js: -------------------------------------------------------------------------------- 1 | import { select } from 'd3-selection'; 2 | import Animation from './animation.js'; 3 | import { VERSION1, VERSION2 } from './xdsm.js'; 4 | 5 | function Controls(animation, defaultVersion) { 6 | this.animation = animation; 7 | this.defaultVersion = defaultVersion || VERSION2; 8 | 9 | const buttonGroup = select('.xdsm-toolbar') 10 | .append('div') 11 | .classed('button_group', true); 12 | buttonGroup.append('button') 13 | .attr('id', 'start') 14 | .append('i').attr('class', 'icon-start'); 15 | buttonGroup.append('button') 16 | .attr('id', 'stop') 17 | .append('i').attr('class', 'icon-stop'); 18 | buttonGroup.append('button') 19 | .attr('id', 'step-prev') 20 | .append('i').attr('class', 'icon-step-prev'); 21 | buttonGroup.append('button') 22 | .attr('id', 'step-next') 23 | .append('i').attr('class', 'icon-step-next'); 24 | buttonGroup.append('label') 25 | .text('XDSM') 26 | .attr('id', 'xdsm-version-label'); 27 | buttonGroup.append('select') 28 | .attr('id', 'xdsm-version-toggle'); 29 | 30 | this.startButton = select('button#start'); 31 | this.stopButton = select('button#stop'); 32 | this.stepPrevButton = select('button#step-prev'); 33 | this.stepNextButton = select('button#step-next'); 34 | this.toggleVersionButton = select('select#xdsm-version-toggle'); 35 | const versions = ['v1', 'v2']; 36 | const versionsMap = { v1: VERSION1, v2: VERSION2 }; 37 | this.toggleVersionButton 38 | .selectAll('versions') 39 | .data(versions) 40 | .enter() 41 | .append('option') 42 | .text((d) => d) 43 | .attr('value', (d) => versionsMap[d]) 44 | .property('selected', (d) => defaultVersion === versionsMap[d]); 45 | 46 | this.startButton.on('click', () => { 47 | this.animation.start(); 48 | }); 49 | this.stopButton.on('click', () => { 50 | this.animation.stop(); 51 | }); 52 | this.stepPrevButton.on('click', () => { 53 | this.animation.stepPrev(); 54 | }); 55 | this.stepNextButton.on('click', () => { 56 | this.animation.stepNext(); 57 | }); 58 | this.toggleVersionButton.on('change', () => { 59 | const selectVersion = select('select#xdsm-version-toggle').property('value'); 60 | let xdsm = select(`.${selectVersion}`); 61 | if (xdsm.empty() && selectVersion === VERSION1) { 62 | xdsm = select(`.${VERSION2}`); 63 | xdsm.classed(VERSION2, false) 64 | .classed(VERSION1, true); 65 | } 66 | if (xdsm.empty() && selectVersion === VERSION2) { 67 | xdsm = select(`.${VERSION1}`); 68 | xdsm.classed(VERSION1, false) 69 | .classed(VERSION2, true); 70 | } 71 | this.animation.setXdsmVersion(selectVersion); 72 | }); 73 | 74 | this.animation.addObserver(this); 75 | this.update(this.animation.status); 76 | } 77 | 78 | Controls.prototype.update = function update(status) { 79 | // console.log("Controls receives: "+status); 80 | switch (status) { 81 | case Animation.STATUS.STOPPED: 82 | case Animation.STATUS.DONE: 83 | this.animation.reset(); // trigger READY status 84 | case Animation.STATUS.READY: // eslint-disable-line no-fallthrough 85 | this._enable(this.startButton); 86 | this._disable(this.stopButton); 87 | this._enable(this.stepNextButton); 88 | this._enable(this.stepPrevButton); 89 | break; 90 | case Animation.STATUS.RUNNING_AUTO: 91 | this._disable(this.startButton); 92 | this._enable(this.stopButton); 93 | this._disable(this.stepNextButton); 94 | this._disable(this.stepPrevButton); 95 | break; 96 | case Animation.STATUS.RUNNING_STEP: 97 | this._disable(this.startButton); 98 | this._enable(this.stopButton); 99 | this._enable(this.stepNextButton); 100 | this._enable(this.stepPrevButton); 101 | break; 102 | default: 103 | console.log(`Unexpected Event: ${status}`); 104 | break; 105 | } 106 | }; 107 | 108 | Controls.prototype._enable = function _enable(button) { 109 | button.attr('disabled', null); 110 | }; 111 | 112 | Controls.prototype._disable = function _disable(button) { 113 | button.attr('disabled', true); 114 | }; 115 | 116 | export default Controls; 117 | -------------------------------------------------------------------------------- /src/graph.js: -------------------------------------------------------------------------------- 1 | const UID = '_U_'; 2 | const UNAME = 'User'; 3 | const MULTI_TYPE = '_multi'; 4 | 5 | const STATUS = { 6 | UNKNOWN: 'UNKNOWN', 7 | PENDING: 'PENDING', 8 | RUNNING: 'RUNNING', 9 | DONE: 'DONE', 10 | FAILED: 'FAILED', 11 | }; 12 | 13 | // *** Node ******************************************************************* 14 | function Node(id, pname, ptype, pstatus, psubxdsm) { 15 | const name = pname || id; 16 | const type = ptype || 'function'; 17 | const status = pstatus || STATUS.UNKNOWN; 18 | if (typeof STATUS[status] === 'undefined') { 19 | throw Error(`Unknown status '${status}' for node ${name}(id=${id})`); 20 | } 21 | this.id = id; 22 | this.name = name; 23 | this.isMulti = (type.search(/_multi$/) >= 0); 24 | this.type = this.isMulti ? type.substr(0, type.length - MULTI_TYPE.length) 25 | : type; 26 | this.status = status; 27 | this.subxdsm = psubxdsm; 28 | } 29 | 30 | Node.prototype.isComposite = function isComposite() { 31 | return this.type === 'mdo' || this.type === 'sub-optimization' 32 | || this.type === 'group' || this.type === 'implicit-group'; 33 | }; 34 | 35 | Node.prototype.getSubXdsmId = function getSubXdsmId() { 36 | if (this.isComposite()) { 37 | // Deprecated 38 | const idxscn = this.name.indexOf('_scn-'); 39 | if (idxscn === -1) { 40 | // console.log(`${'Warning: MDO Scenario not found. ' 41 | // + 'Bad type or name for node: '}${JSON.stringify(this)}`); 42 | } else { 43 | console.log("Use of _scn- pattern in node.name to detect sub scenario 'scn-'" 44 | + ' is deprecated. Use node.subxdsm property instead (i.e. node.subxdsm = )'); 45 | return this.name.substr(idxscn + 1); 46 | } 47 | if (this.subxdsm) { 48 | return this.subxdsm; 49 | } 50 | console.log(`${'Warning: Sub XDSM id not found. ' 51 | + 'Bad type or name for node: '}${JSON.stringify(this)}`); 52 | } 53 | return null; 54 | }; 55 | 56 | // *** Edge ******************************************************************* 57 | function Edge(from, to, nameOrVars, row, col, isMulti) { 58 | this.id = `link_${from}_${to}`; 59 | if (typeof (nameOrVars) === 'string') { 60 | this.name = nameOrVars; 61 | this.vars = {}; 62 | const vars = this.name.split(','); 63 | vars.forEach((n, i) => { this.vars[i] = n.trim(); }); 64 | } else { // vars = {id: name, ...} 65 | this.vars = nameOrVars; 66 | const names = []; 67 | for (const k in this.vars) { 68 | if (Object.prototype.hasOwnProperty.call(this.vars, k)) { 69 | names.push(this.vars[k]); 70 | } 71 | } 72 | this.name = names.join(', '); 73 | } 74 | this.row = row; 75 | this.col = col; 76 | this.iotype = row < col ? 'in' : 'out'; 77 | this.isMulti = isMulti; 78 | } 79 | 80 | Edge.prototype.addVar = function addVar(nameOrVar) { 81 | if (typeof (nameOrVar) === 'string') { 82 | if (this.name === '') { 83 | this.name = nameOrVar; 84 | } else { 85 | this.name += `, ${nameOrVar}`; 86 | } 87 | this.vars[this.vars.length] = nameOrVar; 88 | } else { 89 | for (const k in nameOrVar) { 90 | if (Object.prototype.hasOwnProperty.call(nameOrVar, k)) { 91 | this.vars[k] = nameOrVar[k]; 92 | const names = []; 93 | for (const v in this.vars) { 94 | if (Object.prototype.hasOwnProperty.call(this.vars, v)) { 95 | names.push(this.vars[v]); 96 | } 97 | } 98 | this.name = names.join(', '); 99 | } 100 | } 101 | } 102 | }; 103 | 104 | Edge.prototype.removeVar = function removeVar(nameOrId) { 105 | let found = false; 106 | for (const k in this.vars) { 107 | if (k === nameOrId) { 108 | this.vars.delete(k); 109 | found = true; 110 | } 111 | } 112 | if (!found) { 113 | const newvars = {}; 114 | for (const k in this.vars) { 115 | if (this.vars[k] === nameOrId) { 116 | found = true; 117 | } else { 118 | newvars[k] = this.vars[k]; 119 | } 120 | } 121 | this.vars = newvars; 122 | } 123 | if (found) { 124 | const names = []; 125 | for (const k in this.vars) { 126 | if (Object.prototype.hasOwnProperty.call(this.vars, k)) { 127 | names.push(this.vars[k]); 128 | } 129 | } 130 | this.name = names.join(', '); 131 | } 132 | }; 133 | 134 | // *** Graph ****************************************************************** 135 | function Graph(mdo, withDefaultDriver) { 136 | // withDefaultDriver = true by default 137 | const addDefaultDriver = withDefaultDriver === undefined ? true : withDefaultDriver; 138 | this.nodes = []; 139 | if (addDefaultDriver) { 140 | this.nodes = [new Node(UID, UNAME, 'driver')]; 141 | } 142 | 143 | this.edges = []; 144 | this.chains = []; 145 | 146 | const numbering = Graph.number(mdo.workflow); 147 | const numPrefixes = numbering.toNum; 148 | this.nodesByStep = numbering.toNode; 149 | 150 | mdo.nodes.forEach((item) => { 151 | const num = numPrefixes[item.id]; 152 | this.nodes.push(new Node( 153 | item.id, 154 | num ? `${num}:${item.name}` : item.name, 155 | item.type, 156 | item.status, 157 | item.subxdsm, 158 | )); 159 | }, this); 160 | this.uid = this.nodes[0].id; 161 | 162 | if (mdo.edges) { 163 | mdo.edges.forEach((item) => { 164 | this.addEdge(item.from, item.to, item.vars ? item.vars : item.name); 165 | }, this); 166 | } 167 | 168 | if (mdo.workflow) { 169 | this._makeChaining(mdo.workflow); 170 | } 171 | } 172 | 173 | Graph.NODE_STATUS = STATUS; 174 | 175 | Graph.prototype._makeChaining = function _makeChaining(workflow) { 176 | const echain = Graph.expand(workflow); 177 | echain.forEach((leafChain) => { 178 | if (leafChain.length < 2) { 179 | throw new Error(`Bad process chain (${leafChain.length}elt)`); 180 | } else { 181 | this.chains.push([]); 182 | const ids = this.nodes.map((elt) => elt.id); 183 | leafChain.forEach((item, j) => { 184 | if (j !== 0) { 185 | const idA = ids.indexOf(leafChain[j - 1]); 186 | if (idA < 0) { 187 | throw new Error(`Process chain element (${leafChain[j - 1]}) not found`); 188 | } 189 | const idB = ids.indexOf(leafChain[j]); 190 | if (idB < 0) { 191 | throw new Error(`Process chain element (${leafChain[j]}) not found`); 192 | } 193 | if (idA !== idB) { 194 | this.chains[this.chains.length - 1].push([idA, idB]); 195 | } 196 | } 197 | }, this); 198 | } 199 | }, this); 200 | }; 201 | 202 | Graph.prototype.idxOf = function idxOf(nodeId) { 203 | const idx = this.nodes.map((elt) => elt.id).indexOf(nodeId); 204 | if (idx < 0) { 205 | throw new Error(`Graph.idxOf: ${nodeId} not found in ${JSON.stringify(this.nodes)}`); 206 | } 207 | return idx; 208 | }; 209 | 210 | Graph.prototype.getNode = function getNode(nodeId) { 211 | let theNode; 212 | this.nodes.forEach((node) => { 213 | if (node.id === nodeId) { 214 | theNode = node; 215 | } 216 | }, this); 217 | return theNode; 218 | }; 219 | 220 | Graph.prototype.getNodeFromIndex = function getNodeFromIndex(idx) { 221 | let node; 222 | if (idx >= 0 && idx < this.nodes.length) { 223 | node = this.nodes[idx]; 224 | } else { 225 | throw new Error(`Index out of range : ${idx} not in [0, ${ 226 | this.nodes.length - 1}]`); 227 | } 228 | return node; 229 | }; 230 | 231 | Graph.prototype.addNode = function addNode(node) { 232 | this.nodes.push(new Node(node.id, node.name, node.kind)); 233 | }; 234 | 235 | Graph.prototype.removeNode = function removeNode(index) { 236 | const self = this; 237 | // Update edges 238 | const edges = this.findEdgesOf(index); 239 | edges.toRemove.forEach((edge) => { 240 | const idx = self.edges.indexOf(edge); 241 | if (idx > -1) { 242 | self.edges.splice(idx, 1); 243 | } 244 | }, this); 245 | edges.toShift.forEach((edge) => { 246 | if (edge.row > 1) { 247 | // eslint-disable-next-line no-param-reassign 248 | edge.row -= 1; 249 | } 250 | if (edge.col > 1) { 251 | // eslint-disable-next-line no-param-reassign 252 | edge.col -= 1; 253 | } 254 | }, this); 255 | 256 | // Update nodes 257 | this.nodes.splice(index, 1); 258 | }; 259 | 260 | Graph.prototype.moveLeft = function moveLeft(index) { 261 | if (index > 1) { 262 | const tmp = this.nodes[index - 1]; 263 | this.nodes[index - 1] = this.nodes[index]; 264 | this.nodes[index] = tmp; 265 | } 266 | }; 267 | 268 | Graph.prototype.moveRight = function moveRight(index) { 269 | if (index < this.nodes.length - 1) { 270 | const tmp = this.nodes[index + 1]; 271 | this.nodes[index + 1] = this.nodes[index]; 272 | this.nodes[index] = tmp; 273 | } 274 | }; 275 | 276 | Graph.prototype.addEdge = function addEdge(nodeIdFrom, nodeIdTo, nameOrVar) { 277 | const idA = this.idxOf(nodeIdFrom); 278 | const idB = this.idxOf(nodeIdTo); 279 | const isMulti = this.nodes[idA].isMulti || this.nodes[idB].isMulti; 280 | this.edges 281 | .push(new Edge(nodeIdFrom, nodeIdTo, nameOrVar, idA, idB, isMulti)); 282 | }; 283 | 284 | Graph.prototype.removeEdge = function removeEdge(nodeIdFrom, nodeIdTo) { 285 | const edge = this.findEdge(nodeIdFrom, nodeIdTo); 286 | this.edges.splice(edge.index, 1); 287 | }; 288 | 289 | Graph.prototype.addEdgeVar = function addEdgeVar(nodeIdFrom, nodeIdTo, nameOrVar) { 290 | const edge = this.findEdge(nodeIdFrom, nodeIdTo).element; 291 | if (edge) { 292 | console.log(nameOrVar); 293 | edge.addVar(nameOrVar); 294 | } else { 295 | this.addEdge(nodeIdFrom, nodeIdTo, nameOrVar); 296 | } 297 | }; 298 | 299 | Graph.prototype.removeEdgeVar = function removeEdgeVar(nodeIdFrom, nodeIdTo, nameOrId) { 300 | const ret = this.findEdge(nodeIdFrom, nodeIdTo); 301 | const edge = ret.element; 302 | const { index } = ret; 303 | if (edge) { 304 | edge.removeVar(nameOrId); 305 | } 306 | if (edge.name === '') { 307 | this.edges.splice(index, 1); 308 | } 309 | }; 310 | 311 | Graph.prototype.findEdgesOf = function findEdgesOf(nodeIdx) { 312 | const toRemove = []; 313 | const toShift = []; 314 | this.edges.forEach((edge) => { 315 | if ((edge.row === nodeIdx) || (edge.col === nodeIdx)) { 316 | toRemove.push(edge); 317 | } else if ((edge.row > nodeIdx) || (edge.col > nodeIdx)) { 318 | toShift.push(edge); 319 | } 320 | }, this); 321 | return { 322 | toRemove, 323 | toShift, 324 | }; 325 | }; 326 | 327 | Graph.prototype.findEdge = function findEdge(nodeIdFrom, nodeIdTo) { 328 | let element; 329 | let index = -1; 330 | const idxFrom = this.idxOf(nodeIdFrom); 331 | const idxTo = this.idxOf(nodeIdTo); 332 | this.edges.some((edge, i) => { 333 | if ((edge.row === idxFrom) && (edge.col === idxTo)) { 334 | if (element) { 335 | throw Error(`edge have be uniq between two nodes, but got: ${ 336 | JSON.stringify(element)} and ${JSON.stringify(edge)}`); 337 | } 338 | element = edge; 339 | index = i; 340 | return true; 341 | } 342 | return false; 343 | }, this); 344 | return { element, index }; 345 | }; 346 | 347 | function _expand(workflow) { 348 | let ret = []; 349 | let prev; 350 | workflow.forEach((item) => { 351 | if (item instanceof Array) { 352 | if (Object.prototype.hasOwnProperty.call(item[0], 'parallel')) { 353 | if (prev) { 354 | ret = ret.slice(0, ret.length - 1).concat( 355 | item[0].parallel.map((elt) => [prev].concat(_expand([elt]), prev)), 356 | ); 357 | } else { 358 | throw new Error('Bad workflow structure : ' 359 | + 'cannot parallel loop without previous starting point.'); 360 | } 361 | } else if (prev) { 362 | ret = ret.concat(_expand(item), prev); 363 | } else { 364 | ret = ret.concat(_expand(item)); 365 | } 366 | prev = ret[ret.length - 1]; 367 | } else if (Object.prototype.hasOwnProperty.call(item, 'parallel')) { 368 | if (prev) { 369 | ret = ret.slice(0, ret.length - 1).concat( 370 | item.parallel.map((elt) => [prev].concat(_expand([elt]))), 371 | ); 372 | } else { 373 | ret = ret.concat(item.parallel.map((elt) => _expand([elt]))); 374 | } 375 | prev = undefined; 376 | } else { 377 | let i = ret.length - 1; 378 | let flagParallel = false; 379 | while (i >= 0 && (ret[i] instanceof Array)) { 380 | ret[i] = ret[i].concat(item); 381 | i -= 1; 382 | flagParallel = true; 383 | } 384 | if (!flagParallel) { 385 | ret.push(item); 386 | } 387 | prev = item; 388 | } 389 | }); 390 | return ret; 391 | } 392 | 393 | Graph._isPatchNeeded = function _isPatchNeeded(toBePatched) { 394 | const lastElts = toBePatched.map((arr) => arr[arr.length - 1]); 395 | const lastElt = lastElts[0]; 396 | for (let i = 0; i < lastElts.length; i += 1) { 397 | if (lastElts[i] !== lastElt) { 398 | return true; 399 | } 400 | } 401 | return false; 402 | }; 403 | 404 | Graph._patchParallel = function _patchParallel(expanded) { 405 | const toBePatched = []; 406 | expanded.forEach((elt) => { 407 | if (elt instanceof Array) { 408 | toBePatched.push(elt); 409 | } else if (Graph._isPatchNeeded(toBePatched)) { 410 | toBePatched.forEach((arr) => { 411 | arr.push(elt); 412 | }, this); 413 | } 414 | }, this); 415 | }; 416 | 417 | Graph.expand = function expand(item) { 418 | const expanded = _expand(item); 419 | const result = []; 420 | let current = []; 421 | // first pass to add missing 'end link' in case of parallel branches at the 422 | // end of a loop 423 | // [a, [b, d], [b, c], a] -> [a, [b, d, a], [b, c, a], a] 424 | Graph._patchParallel(expanded); 425 | // [a, aa, [b, c], d] -> [[a, aa, b], [b,c], [c, d]] 426 | expanded.forEach((elt) => { 427 | if (elt instanceof Array) { 428 | if (current.length > 0) { 429 | current.push(elt[0]); 430 | result.push(current); 431 | current = []; 432 | } 433 | result.push(elt); 434 | } else { 435 | if (result.length > 0 && current.length === 0) { 436 | const lastChain = result[result.length - 1]; 437 | const lastElt = lastChain[lastChain.length - 1]; 438 | current.push(lastElt); 439 | } 440 | current.push(elt); 441 | } 442 | }, this); 443 | if (current.length > 0) { 444 | result.push(current); 445 | } 446 | return result; 447 | }; 448 | 449 | Graph.number = function number(workflow, pnum) { 450 | let num = (typeof pnum === 'undefined') ? 0 : pnum; 451 | const toNum = {}; 452 | const toNode = []; 453 | 454 | function setStep(step, nodeId) { 455 | if (step in toNode) { 456 | toNode[step].push(nodeId); 457 | } else { 458 | toNode[step] = [nodeId]; 459 | } 460 | } 461 | 462 | function setNum(nodeId, beg, end) { 463 | if (end === undefined) { 464 | num = String(beg); 465 | setStep(beg, nodeId); 466 | } else { 467 | num = `${end}-${beg}`; 468 | setStep(end, nodeId); 469 | } 470 | if (nodeId in toNum) { 471 | toNum[nodeId] += `,${num}`; 472 | } else { 473 | toNum[nodeId] = num; 474 | } 475 | } 476 | 477 | function _number(wks, nb) { 478 | let ret = 0; 479 | if (wks instanceof Array) { 480 | if (wks.length === 0) { 481 | ret = nb; 482 | } else if (wks.length === 1) { 483 | ret = _number(wks[0], nb); 484 | } else { 485 | const head = wks[0]; 486 | const tail = wks.slice(1); 487 | let beg = _number(head, nb); 488 | if (tail[0] instanceof Array) { 489 | const end = _number(tail[0], beg); 490 | setNum(head, beg, end); 491 | beg = end + 1; 492 | tail.shift(); 493 | } 494 | ret = _number(tail, beg); 495 | } 496 | } else if ((wks instanceof Object) && 'parallel' in wks) { 497 | const nums = wks.parallel.map((branch) => _number(branch, nb)); 498 | ret = Math.max.apply(null, nums); 499 | } else { 500 | setNum(wks, nb); 501 | ret = nb + 1; 502 | } 503 | return ret; 504 | } 505 | 506 | _number(workflow, num); 507 | // console.log('toNodes=', JSON.stringify(toNode)); 508 | // console.log('toNum=',JSON.stringify(toNum)); 509 | return { 510 | toNum, 511 | toNode, 512 | }; 513 | }; 514 | 515 | export default Graph; 516 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import XdsmFactory from './xdsm-factory.js'; 2 | 3 | // eslint-disable-next-line import/prefer-default-export 4 | export function XDSMjs(config) { 5 | return new XdsmFactory(config); 6 | } 7 | 8 | export const { XDSM_V1 } = XdsmFactory; 9 | export const { XDSM_V2 } = XdsmFactory; 10 | -------------------------------------------------------------------------------- /src/labelizer.js: -------------------------------------------------------------------------------- 1 | function Labelizer() { } 2 | 3 | Labelizer.strParse = function strParse(str, subSupScript) { 4 | if (str === '') { 5 | return [{ base: '', sub: undefined, sup: undefined }]; 6 | } 7 | const lstr = str.split(','); 8 | if (subSupScript === false) { 9 | return lstr.map((s) => ({ base: s, sub: undefined, sup: undefined })); 10 | } 11 | 12 | const underscores = /_/g; 13 | const rg = /([0-9-]+:)?([&#;A-Za-z0-9-.]+)(_[&#;A-Za-z0-9-._]+)?(\^.+)?/; 14 | 15 | const res = lstr.map((s) => { 16 | let base; 17 | let sub; 18 | let sup; 19 | 20 | if ((s.match(underscores) || []).length > 1) { 21 | const mu = s.match(/(.+)\^(.+)/); 22 | if (mu) { 23 | return { base: mu[1], sub: undefined, sup: mu[2] }; 24 | } 25 | return { base: s, sub: undefined, sup: undefined }; 26 | } 27 | const m = s.match(rg); 28 | if (m) { 29 | base = (m[1] ? m[1] : '') + m[2]; 30 | if (m[3]) { 31 | sub = m[3].substring(1); 32 | } 33 | if (m[4]) { 34 | sup = m[4].substring(1); 35 | } 36 | } else { 37 | throw new Error(`Labelizer.strParse: Can not parse '${s}'`); 38 | } 39 | return { base, sub, sup }; 40 | }, this); 41 | 42 | return res; 43 | }; 44 | 45 | // eslint-disable-next-line max-len 46 | Labelizer._createVarListLabel = function _createVarListLabel(selection, name, text, ellipsis, subSupScript, subXdsmLink) { 47 | const tokens = Labelizer.strParse(name, subSupScript); 48 | 49 | tokens.every((token, i, ary) => { 50 | let offsetSub = 0; 51 | let offsetSup = 0; 52 | if (ellipsis < 1 || (i < 5 && text.nodes()[0].getBBox().width < 100)) { 53 | text.append('tspan').html(() => { 54 | if (subXdsmLink) { 55 | return `${token.base}`; 56 | } 57 | return token.base; 58 | }); 59 | if (token.sub) { 60 | offsetSub = 10; 61 | text.append('tspan') 62 | .attr('class', 'sub') 63 | .attr('dy', offsetSub) 64 | .html(token.sub); 65 | } 66 | if (token.sup) { 67 | offsetSup = -10; 68 | text.append('tspan') 69 | .attr('class', 'sup') 70 | .attr('dx', -5) 71 | .attr('dy', -offsetSub + offsetSup) 72 | .html(token.sup); 73 | offsetSub = 0; 74 | } 75 | } else { 76 | text.append('tspan') 77 | .attr('dy', -offsetSub - offsetSup) 78 | .html('...'); 79 | selection.classed('ellipsized', true); 80 | return false; 81 | } 82 | if (i < ary.length - 1) { 83 | text.append('tspan') 84 | .attr('dy', -offsetSub - offsetSup) 85 | .html(', '); 86 | } 87 | return true; 88 | }, this); 89 | }; 90 | 91 | Labelizer._createLinkNbLabel = function _createLinkNbLabel(selection, name, text) { 92 | const lstr = name.split(','); 93 | let str = `${lstr.length} var`; 94 | if (lstr.length > 1) { 95 | str += 's'; 96 | } 97 | text.append('tspan').html(str); 98 | selection.classed('ellipsized', true); // activate tooltip 99 | }; 100 | 101 | Labelizer.labelize = function labelize() { 102 | let ellipsis = 0; 103 | let subSupScript = true; 104 | let linkNbOnly = false; 105 | let labelKind = 'node'; 106 | let subXdsmLink = false; 107 | 108 | function createLabel(selection) { 109 | selection.each((d) => { 110 | const text = selection.append('text'); 111 | if (linkNbOnly && labelKind !== 'node') { // show connexion nb 112 | Labelizer._createLinkNbLabel(selection, d.name, text); 113 | } else { 114 | Labelizer._createVarListLabel(selection, d.name, text, ellipsis, subSupScript, subXdsmLink); 115 | } 116 | }); 117 | } 118 | 119 | createLabel.ellipsis = function ellips(value) { 120 | if (!arguments.length) { 121 | return ellipsis; 122 | } 123 | ellipsis = value; 124 | return createLabel; 125 | }; 126 | 127 | createLabel.subSupScript = function subsupscript(value) { 128 | if (!arguments.length) { 129 | return subSupScript; 130 | } 131 | subSupScript = value; 132 | return createLabel; 133 | }; 134 | 135 | createLabel.linkNbOnly = function linknbonly(value) { 136 | if (!arguments.length) { 137 | return linkNbOnly; 138 | } 139 | linkNbOnly = value; 140 | return createLabel; 141 | }; 142 | 143 | createLabel.labelKind = function labelkind(value) { 144 | if (!arguments.length) { 145 | return labelKind; 146 | } 147 | labelKind = value; 148 | return createLabel; 149 | }; 150 | 151 | createLabel.subXdsmLink = function subxdsmlink(value) { 152 | if (!arguments.length) { 153 | return subXdsmLink; 154 | } 155 | subXdsmLink = value; 156 | return createLabel; 157 | }; 158 | 159 | return createLabel; 160 | }; 161 | 162 | Labelizer.tooltipize = function tooltipz() { 163 | let text = ''; 164 | let subSupScript = false; 165 | 166 | function createTooltip(selection) { 167 | let html = []; 168 | if (subSupScript) { 169 | const tokens = Labelizer.strParse(text); 170 | tokens.forEach((token) => { 171 | let item = token.base; 172 | if (token.sub) { 173 | item += `${token.sub}`; 174 | } 175 | if (token.sup) { 176 | item += `${token.sup}`; 177 | } 178 | html.push(item); 179 | }); 180 | } else { 181 | html = text.split(','); 182 | } 183 | selection.html(html.join(', ')); 184 | } 185 | 186 | createTooltip.text = function txt(value) { 187 | if (!arguments.length) { 188 | return text; 189 | } 190 | text = value; 191 | return createTooltip; 192 | }; 193 | 194 | createTooltip.subSupScript = function supsub(value) { 195 | if (!arguments.length) { 196 | return subSupScript; 197 | } 198 | subSupScript = value; 199 | return createTooltip; 200 | }; 201 | 202 | return createTooltip; 203 | }; 204 | 205 | export default Labelizer; 206 | -------------------------------------------------------------------------------- /src/selectable.js: -------------------------------------------------------------------------------- 1 | import { select, selectAll } from 'd3-selection'; 2 | 3 | function Selectable(xdsm, callback) { 4 | this._xdsm = xdsm; 5 | this._selection = null; 6 | this._prevSelection = null; 7 | this._callback = callback; 8 | 9 | this.enable(); 10 | } 11 | 12 | Selectable.prototype.enable = function enable() { 13 | this._addEventHandler('.node'); 14 | this._addEventHandler('.edge'); 15 | }; 16 | 17 | Selectable.prototype._addEventHandler = function _addEventHandler(klass) { 18 | const self = this; 19 | selectAll(klass).on('click', function makeSelection() { 20 | const prevSelection = select('[data-xdsm-selected="true"]'); 21 | self._unselect(prevSelection); 22 | 23 | const selection = select(this); // eslint-disable-line no-invalid-this 24 | if (prevSelection.empty() 25 | || prevSelection.data()[0].id !== selection.data()[0].id) { 26 | self._select(selection); 27 | } 28 | self._callback(self.getFilter()); 29 | }); 30 | }; 31 | 32 | Selectable.prototype._select = function _select(selection) { 33 | selection.attr('data-xdsm-selected', true); 34 | selection.select('.shape') 35 | .transition().duration(100).style('stroke-width', '4px'); 36 | }; 37 | 38 | Selectable.prototype._unselect = function _unselect(selection) { 39 | selection.attr('data-xdsm-selected', null); 40 | selection.select('.shape') 41 | .transition().duration(100).style('stroke-width', null); 42 | }; 43 | 44 | Selectable.prototype.getFilter = function getFilter() { 45 | const filter = { 46 | fr: undefined, 47 | to: undefined, 48 | }; 49 | const selection = select('[data-xdsm-selected="true"]'); 50 | if (!selection.empty()) { 51 | const selected = selection.data()[0]; 52 | if (selected.iotype) { // edge 53 | filter.fr = this._xdsm.graph.getNodeFromIndex(selected.row).id; 54 | filter.to = this._xdsm.graph.getNodeFromIndex(selected.col).id; 55 | } else { // node 56 | filter.fr = selected.id; 57 | filter.to = selected.id; 58 | } 59 | } 60 | return filter; 61 | }; 62 | 63 | Selectable.prototype.setFilter = function setFilter(filter) { 64 | const self = this; 65 | const prevSelection = select('[data-xdsm-selected="true"]'); 66 | let selection; 67 | if (filter.fr === filter.to) { 68 | if (filter.fr !== undefined) { 69 | selection = select(`.id${filter.fr}`); 70 | self._select(selection); 71 | } 72 | } else if (filter.fr !== undefined && filter.to !== undefined) { 73 | selection = select(`.idlink_${filter.fr}_${filter.to}`); 74 | self._select(selection); 75 | } 76 | if (!selection || selection.empty() 77 | || (!prevSelection.empty() && prevSelection.data()[0].id !== selection.data()[0].id)) { 78 | self._unselect(prevSelection); 79 | } 80 | }; 81 | 82 | export default Selectable; 83 | -------------------------------------------------------------------------------- /src/xdsm-factory.js: -------------------------------------------------------------------------------- 1 | /* 2 | * XDSMjs 3 | * Copyright 2016-2020 Rémi Lafage 4 | */ 5 | import { json } from 'd3-fetch'; 6 | import { select } from 'd3-selection'; 7 | import Graph from './graph.js'; 8 | import Xdsm, { VERSION1, VERSION2 } from './xdsm.js'; 9 | 10 | import Selectable from './selectable.js'; 11 | import Animation from './animation.js'; 12 | import Controls from './controls.js'; 13 | 14 | class SelectableXdsm { 15 | constructor(mdo, callback, config) { 16 | const graph = new Graph(mdo, config.withDefaultDriver); 17 | this.xdsm = new Xdsm(graph, 'root', config); 18 | this.xdsm.draw(); 19 | this.selectable = new Selectable(this.xdsm, callback); 20 | this.selectable.enable(); 21 | } 22 | 23 | updateMdo(mdo) { 24 | this.xdsm.updateMdo(mdo); 25 | this.selectable.enable(); 26 | } 27 | 28 | setSelection(filter) { 29 | this.selectable.setFilter(filter); 30 | } 31 | } 32 | 33 | class XdsmFactory { 34 | constructor(config) { 35 | this._version = XdsmFactory._detectVersion() || VERSION2; 36 | this.default_config = { 37 | labelizer: { 38 | ellipsis: 5, 39 | subSupScript: true, 40 | showLinkNbOnly: false, 41 | }, 42 | withDefaultDriver: true, 43 | }; 44 | this._config = { ...this.default_config, ...config }; 45 | this._config.version = this._version; // sure to ignore any version in config 46 | } 47 | 48 | static get XDSM_V1() { 49 | return VERSION1; 50 | } 51 | 52 | static get XDSM_V2() { 53 | return VERSION2; 54 | } 55 | 56 | createXdsm(mdo) { 57 | const version = this._version; 58 | const elt = select(`.${version}`); 59 | if (elt.empty()) { 60 | console.log(`No element of ${version} class. Please add
in your HTML.`); 61 | } else if (mdo) { 62 | this._createXdsm(mdo, version); 63 | } else { 64 | const mdostr = elt.attr('data-mdo'); 65 | if (mdostr) { 66 | this._createXdsm(JSON.parse(mdostr), version); 67 | } else { 68 | const filename = elt.attr('data-mdo-file') || 'xdsm.json'; 69 | json(filename).then((mdoFromFile) => this._createXdsm(mdoFromFile, version)); 70 | } 71 | } 72 | } 73 | 74 | createSelectableXdsm(mdo, callback) { 75 | return new SelectableXdsm(mdo, callback, this._config); 76 | } 77 | 78 | _createXdsm(mdo, version) { 79 | const xdsmNames = XdsmFactory._orderedList(mdo); 80 | 81 | // Optimization problem display setup 82 | select('body').selectAll('optpb').data(xdsmNames).enter() 83 | .append('div') 84 | .filter((d) => mdo[d].optpb) 85 | .attr('class', (d) => `optpb ${d}`) 86 | .style('opacity', 0) 87 | .on('click', function makeTransition() { 88 | select(this).transition().duration(500) // eslint-disable-line 89 | // no-invalid-this 90 | .style('opacity', 0) 91 | .style('pointer-events', 'none'); 92 | }) 93 | .append('pre') 94 | .html((d) => mdo[d].optpb); 95 | 96 | const xdsms = {}; 97 | 98 | if (xdsmNames.indexOf('root') === -1) { 99 | // old format: mono xdsm 100 | const graph = new Graph(mdo, this._config.withDefaultDriver); 101 | xdsms.root = new Xdsm(graph, 'root', this._config); 102 | xdsms.root.draw(); 103 | } else { 104 | // new format managing several XDSM 105 | xdsmNames.forEach((k) => { 106 | if (Object.prototype.hasOwnProperty.call(mdo, k)) { 107 | const graph = new Graph(mdo[k], this._config.withDefaultDriver); 108 | xdsms[k] = new Xdsm(graph, k, this._config); 109 | xdsms[k].draw(); 110 | xdsms[k].svg.select('.optimization').on( 111 | 'click', 112 | (event) => { 113 | const info = select(`.optpb.${k}`); 114 | info.style('opacity', 0.9); 115 | info.style('left', `${event.pageX}px`).style( 116 | 'top', 117 | `${event.pageY - 28}px`, 118 | ); 119 | info.style('pointer-events', 'auto'); 120 | }, 121 | ); 122 | } 123 | }, this); // eslint-disable-line no-invalid-this 124 | } 125 | 126 | const anim = new Animation(xdsms); 127 | if (xdsms.root.hasWorkflow()) { // workflow is optional 128 | const ctrls = new Controls(anim, version); // eslint-disable-line no-unused-vars 129 | } 130 | anim.renderNodeStatuses(); 131 | } 132 | 133 | static _detectVersion() { 134 | if (select(`.${VERSION1}`).empty()) { 135 | return VERSION2; 136 | } 137 | return VERSION1; 138 | } 139 | 140 | static _orderedList(xdsms, root, level) { 141 | const roo = root || 'root'; 142 | const lev = level || 0; 143 | if (xdsms[roo]) { 144 | const subxdsms = xdsms[roo].nodes 145 | .map((n) => n.subxdsm) 146 | .filter((n) => n); 147 | let acc = [roo]; 148 | if (subxdsms.length > 0) { 149 | for (let i = 0; i < subxdsms.length; i += 1) { 150 | acc = acc.concat(XdsmFactory._orderedList(xdsms, subxdsms[i], lev + 1)); 151 | } 152 | } 153 | return acc; 154 | } 155 | if (lev === 0) { 156 | // level 0 no root : return lexicographic order 157 | return Object.keys(xdsms).sort(); 158 | } 159 | throw new Error(`sub-XDSM '${roo}' not found among ${Object.keys(xdsms)}`); 160 | } 161 | } 162 | 163 | export default XdsmFactory; 164 | -------------------------------------------------------------------------------- /src/xdsm.js: -------------------------------------------------------------------------------- 1 | import { select, selectAll } from 'd3-selection'; 2 | import 'd3-transition'; 3 | import Graph from './graph.js'; 4 | import Labelizer from './labelizer.js'; 5 | 6 | export const VERSION1 = 'xdsm'; 7 | export const VERSION2 = 'xdsm2'; 8 | 9 | const WIDTH = 1000; 10 | const HEIGHT = 500; 11 | const X_ORIG = 100; 12 | const Y_ORIG = 20; 13 | const PADDING = 20; 14 | const CELL_W = 250; 15 | const CELL_H = 75; 16 | const MULTI_OFFSET = 3; 17 | const BORDER_PADDING = 4; 18 | const ANIM_DURATION = 0; // ms 19 | const TOOLTIP_WIDTH = 300; 20 | 21 | function Cell(x, y, width, height) { 22 | this.x = x; 23 | this.y = y; 24 | this.width = width; 25 | this.height = height; 26 | } 27 | 28 | function Xdsm(graph, svgid, config) { 29 | this.graph = graph; 30 | this.version = config.version || VERSION2; 31 | 32 | const container = select(`.${this.version}`); 33 | this.svg = container.append('svg') 34 | .attr('width', WIDTH) 35 | .attr('height', HEIGHT) 36 | .attr('viewBox', `0 0 ${WIDTH} ${HEIGHT}`) 37 | .attr('id', svgid); 38 | 39 | this.grid = []; 40 | this.nodes = []; 41 | this.edges = []; 42 | this.svgid = svgid; 43 | 44 | this.default_config = { 45 | labelizer: { 46 | ellipsis: 5, 47 | subSupScript: true, 48 | showLinkNbOnly: false, 49 | }, 50 | layout: { 51 | origin: { x: X_ORIG, y: Y_ORIG }, 52 | cellsize: { w: CELL_W, h: CELL_H }, 53 | padding: PADDING, 54 | }, 55 | withDefaultDriver: true, 56 | withTitleTooltip: true, // allow to use external tooltip 57 | }; 58 | this.config = { ...this.default_config, ...config }; 59 | this.config.labelizer = { ...this.default_config.labelizer, ...config.labelizer }; 60 | this.config.layout = { ...this.default_config.layout, ...config.layout }; 61 | 62 | // Xdsm built-in tooltip for variable connexions 63 | if (this.config.withTitleTooltip) { 64 | this.tooltip = select('body').append('div').attr('class', 'xdsm-tooltip') 65 | .style('opacity', 0); 66 | } 67 | this._initialize(); 68 | } 69 | 70 | Xdsm.prototype.setVersion = function setVersion(version) { 71 | this.version = version; 72 | }; 73 | 74 | Xdsm.prototype.addNode = function addNode(nodeName) { 75 | this.graph.addNode(nodeName); 76 | this.draw(); 77 | }; 78 | 79 | Xdsm.prototype.removeNode = function removeNode(index) { 80 | this.graph.removeNode(index); 81 | this.draw(); 82 | }; 83 | 84 | Xdsm.prototype.hasWorkflow = function hasWorkflow() { 85 | return this.graph.chains.length !== 0; 86 | }; 87 | 88 | Xdsm.prototype._initialize = function _initialize() { 89 | const self = this; 90 | 91 | self._createTitle(); 92 | self.nodeGroup = self.svg.append('g').attr('class', 'nodes'); 93 | self.edgeGroup = self.svg.append('g').attr('class', 'edges'); 94 | }; 95 | 96 | Xdsm.prototype.updateMdo = function updateMda(mdo) { 97 | this.graph = new Graph(mdo, this.config.withDefaultDriver); 98 | this.refresh(); 99 | }; 100 | 101 | Xdsm.prototype.refresh = function refresh() { 102 | const self = this; 103 | self.svg.selectAll('g').remove(); 104 | self.nodeGroup = self.svg.append('g').attr('class', 'nodes'); 105 | self.edgeGroup = self.svg.append('g').attr('class', 'edges'); 106 | self.draw(); 107 | }; 108 | 109 | Xdsm.prototype.draw = function draw() { 110 | const self = this; 111 | 112 | self.nodes = self._createTextGroup('node', self.nodeGroup, self._customRect); 113 | self.edges = self._createTextGroup('edge', self.edgeGroup, self._customTrapz); 114 | 115 | // Workflow 116 | self._createWorkflow(); 117 | 118 | // Dataflow 119 | self._createDataflow(); 120 | 121 | // Border (used by animation) 122 | self._createBorder(); 123 | 124 | // update size 125 | const w = self.config.layout.cellsize.w * (self.graph.nodes.length + 1); 126 | const h = self.config.layout.cellsize.h * (self.graph.nodes.length + 1); 127 | self.svg.attr('width', w).attr('height', h).attr('viewBox', `0 0 ${w} ${h}`); 128 | self.svg.selectAll('.border') 129 | .attr('height', h - BORDER_PADDING) 130 | .attr('width', w - BORDER_PADDING); 131 | }; 132 | 133 | Xdsm.prototype._createTextGroup = function _createTextGroup(kind, group, decorate) { 134 | const self = this; 135 | 136 | const selection = group.selectAll(`.${kind}`) 137 | .data( 138 | this.graph[`${kind}s`], // DATA JOIN 139 | (d) => d.id, 140 | ); 141 | 142 | const labelize = Labelizer.labelize() 143 | .labelKind(kind) 144 | .ellipsis(self.config.labelizer.ellipsis) 145 | .subSupScript(self.config.labelizer.subSupScript) 146 | .linkNbOnly(self.config.labelizer.showLinkNbOnly); 147 | 148 | const textGroups = selection 149 | .enter() // ENTER 150 | .append('g').attr('class', (d) => { 151 | let klass = kind === 'node' ? d.type : 'dataInter'; 152 | if (klass === 'dataInter' && (d.row === 0 || d.col === 0)) { 153 | klass = 'dataIO'; 154 | } 155 | return `id${d.id} ${kind} ${klass}`; 156 | }).each(function makeLabel(d) { 157 | const that = select(this); // eslint-disable-line no-invalid-this 158 | that.call(labelize.subXdsmLink(d.subxdsm)); // eslint-disable-line no-invalid-this 159 | }) 160 | .each(function makeLine(d1, i) { 161 | const { grid } = self; 162 | const item = select(this); // eslint-disable-line no-invalid-this 163 | if (grid[i] === undefined) { 164 | grid[i] = new Array(self.graph.nodes.length); 165 | } 166 | item.select('text').each(function makeCell(d2, j) { 167 | const that = select(this); // eslint-disable-line no-invalid-this 168 | const data = item.data()[0]; 169 | const m = (data.row === undefined) ? i : data.row; 170 | const n = (data.col === undefined) ? i : data.col; 171 | const bbox = that.nodes()[j].getBBox(); 172 | grid[m][n] = new Cell(-bbox.width / 2, 0, bbox.width, bbox.height); 173 | that 174 | .attr('width', () => grid[m][n].width) 175 | .attr('height', () => grid[m][n].height) 176 | .attr('x', () => grid[m][n].x) 177 | .attr('y', () => grid[m][n].y); 178 | }); 179 | }) 180 | .each(function makeDecoration(d, i) { 181 | const that = select(this); // eslint-disable-line no-invalid-this 182 | that.call(decorate.bind(self), d, i, 0); 183 | if (d.isMulti) { 184 | that.call(decorate.bind(self), d, i, 1 * Number(MULTI_OFFSET)); 185 | that.call(decorate.bind(self), d, i, 2 * Number(MULTI_OFFSET)); 186 | } 187 | }) 188 | .merge(selection); // UPDATE + ENTER 189 | 190 | selection.exit().remove(); // EXIT 191 | 192 | if (self.tooltip) { 193 | selectAll('.ellipsized').on('mouseover', (event, d) => { 194 | self.tooltip.transition().duration(200).style('opacity', 0.9); 195 | const tooltipize = Labelizer.tooltipize() 196 | .subSupScript(self.config.labelizer.subSupScript) 197 | .text(d.name); 198 | self.tooltip.call(tooltipize) 199 | .style('width', `${TOOLTIP_WIDTH}px`) 200 | .style('left', `${event.pageX}px`) 201 | .style('top', `${event.pageY - 28}px`); 202 | }).on('mouseout', () => { 203 | self.tooltip.transition().duration(500).style('opacity', 0); 204 | }); 205 | } else { 206 | selectAll('.ellipsized') 207 | .attr('title', (d) => d.name.split(',').join(', ')); 208 | } 209 | self._layoutText(textGroups, decorate, selection.empty() ? 0 : ANIM_DURATION); 210 | }; 211 | 212 | Xdsm.prototype._layoutText = function _layoutText(items, decorate, delay) { 213 | const self = this; 214 | items.transition().duration(delay).attr('transform', (d, i) => { 215 | const m = (d.col === undefined) ? i : d.col; 216 | const n = (d.row === undefined) ? i : d.row; 217 | const w = self.config.layout.cellsize.w * m + self.config.layout.origin.x; 218 | const h = self.config.layout.cellsize.h * n + self.config.layout.origin.y; 219 | return `translate(${self.config.layout.origin.x + w},${self.config.layout.origin.y + h})`; 220 | }); 221 | }; 222 | 223 | Xdsm.prototype._createWorkflow = function _createWorkflow() { 224 | const self = this; 225 | const workflow = this.svg.selectAll('.workflow') 226 | .data([self.graph]) 227 | .enter() 228 | .insert('g', ':first-child') 229 | .attr('class', 'workflow'); 230 | 231 | workflow.selectAll('g') 232 | .data(self.graph.chains) 233 | .enter() 234 | .insert('g') 235 | .attr('class', 'workflow-chain') 236 | .selectAll('path') 237 | .data((d) => d) 238 | .enter() 239 | .append('path') 240 | .attr('class', (d) => `link_${d[0]}_${d[1]}`) 241 | .attr('transform', (d) => { 242 | const max = Math.max(d[0], d[1]); 243 | const min = Math.min(d[0], d[1]); 244 | let w; 245 | let h; 246 | if (d[0] < d[1]) { 247 | w = self.config.layout.cellsize.w * max + self.config.layout.origin.x; 248 | h = self.config.layout.cellsize.h * min + self.config.layout.origin.y; 249 | } else { 250 | w = self.config.layout.cellsize.w * min + self.config.layout.origin.x; 251 | h = self.config.layout.cellsize.h * max + self.config.layout.origin.y; 252 | } 253 | return `translate(${self.config.layout.origin.x + w},${self.config.layout.origin.y + h})`; 254 | }) 255 | .attr('d', (d) => { 256 | const w = self.config.layout.cellsize.w * Math.abs(d[0] - d[1]); 257 | const h = self.config.layout.cellsize.h * Math.abs(d[0] - d[1]); 258 | const points = []; 259 | if (d[0] < d[1]) { 260 | if (d[0] !== 0) { 261 | points.push(`${-w},0`); 262 | } 263 | points.push('0,0'); 264 | if (d[1] !== 0) { 265 | points.push(`0,${h}`); 266 | } 267 | } else { 268 | if (d[0] !== 0) { 269 | points.push(`${w},0`); 270 | } 271 | points.push('0,0'); 272 | if (d[1] !== 0) { 273 | points.push(`0,${-h}`); 274 | } 275 | } 276 | return `M${points.join(' ')}`; 277 | }); 278 | }; 279 | 280 | Xdsm.prototype._createDataflow = function _createDataflow() { 281 | const self = this; 282 | self.svg.selectAll('.dataflow') 283 | .data([self]) 284 | .enter() 285 | .insert('g', ':first-child') 286 | .attr('class', 'dataflow'); 287 | 288 | const selection = self.svg.select('.dataflow').selectAll('path') 289 | .data(self.graph.edges, (d) => d.id); 290 | 291 | selection.enter() 292 | .append('path') 293 | .merge(selection) 294 | .transition() 295 | .duration(selection.empty() ? 0 : ANIM_DURATION) 296 | .attr('transform', (d, i) => { 297 | const m = (d.col === undefined) ? i : d.col; 298 | const n = (d.row === undefined) ? i : d.row; 299 | const w = self.config.layout.cellsize.w * m + self.config.layout.origin.x; 300 | const h = self.config.layout.cellsize.h * n + self.config.layout.origin.y; 301 | return `translate(${self.config.layout.origin.x + w},${self.config.layout.origin.y + h})`; 302 | }) 303 | .attr('d', (d) => { 304 | const w = self.config.layout.cellsize.w * Math.abs(d.col - d.row); 305 | const h = self.config.layout.cellsize.h * Math.abs(d.col - d.row); 306 | const points = []; 307 | if (d.iotype === 'in') { 308 | if (d.row !== 0) { 309 | points.push(`${-w},0`); 310 | } 311 | points.push('0,0'); 312 | if (d.col !== 0) { 313 | points.push(`0,${h}`); 314 | } 315 | } else { 316 | if (d.row !== 0) { 317 | points.push(`${w},0`); 318 | } 319 | points.push('0,0'); 320 | if (d.col !== 0) { 321 | points.push(`0,${-h}`); 322 | } 323 | } 324 | return `M${points.join(' ')}`; 325 | }); 326 | selection.exit().remove(); 327 | }; 328 | 329 | Xdsm.prototype._customRect = function _customRect(node, d, i, offset) { 330 | const self = this; 331 | const { grid } = self; 332 | if (this.version === VERSION2 333 | && (d.type === 'group' 334 | || d.type === 'implicit-group' 335 | || d.type === 'sub-optimization' 336 | || d.type === 'mdo')) { 337 | const x0 = grid[i][i].x + offset - self.config.layout.padding; 338 | const y0 = -grid[i][i].height * (2 / 3) - self.config.layout.padding - offset; 339 | const x1 = grid[i][i].x + offset + self.config.layout.padding + grid[i][i].width; 340 | const y1 = -grid[i][i].height * (2 / 3) + self.config.layout.padding 341 | - offset + grid[i][i].height; 342 | const ch = 10; 343 | const points = `${x0 + ch},${y0} ${x1 - ch},${y0} ${x1},${y0 + ch} ${x1},${y1 - ch} ${x1 - ch},${y1} ${x0 + ch},${y1} ${x0},${y1 - ch} ${x0},${y0 + ch}`; 344 | node.insert('polygon', ':first-child') 345 | .classed('shape', true) 346 | .attr('points', points); 347 | } else { 348 | node.insert('rect', ':first-child') 349 | .classed('shape', true) 350 | .attr('x', () => grid[i][i].x + offset - self.config.layout.padding) 351 | .attr('y', () => -grid[i][i].height * (2 / 3) - self.config.layout.padding - offset) 352 | .attr('width', () => grid[i][i].width + (self.config.layout.padding * 2)) 353 | .attr('height', () => grid[i][i].height + (self.config.layout.padding * 2)) 354 | .attr('rx', () => { 355 | const rounded = d.type === 'driver' 356 | || d.type === 'optimization' 357 | || d.type === 'mda' 358 | || d.type === 'doe'; 359 | return rounded ? (grid[i][i].height + (self.config.layout.padding * 2)) / 2 : 0; 360 | }) 361 | .attr('ry', () => { 362 | const rounded = d.type === 'driver' 363 | || d.type === 'optimization' 364 | || d.type === 'mda' 365 | || d.type === 'doe'; 366 | return rounded ? (grid[i][i].height + (self.config.layout.padding * 2)) / 2 : 0; 367 | }); 368 | } 369 | }; 370 | 371 | Xdsm.prototype._customTrapz = function _customTrapz(edge, dat, i, offset) { 372 | const { grid } = this; 373 | edge.insert('polygon', ':first-child') 374 | .classed('shape', true) 375 | .attr('points', (d) => { 376 | const pad = 5; 377 | const w = grid[d.row][d.col].width; 378 | const h = grid[d.row][d.col].height; 379 | const topleft = `${-pad - w / 2 + offset}, ${-pad - h * (2 / 3) - offset} `; 380 | const topright = `${w / 2 + pad + offset + 5}, ${-pad - h * (2 / 3) - offset} `; 381 | const botright = `${w / 2 + pad + offset - 5 + 5}, ${pad + h / 3 - offset} `; 382 | const botleft = `${-pad - w / 2 + offset - 5}, ${pad + h / 3 - offset} `; 383 | const tpz = [topleft, topright, botright, botleft].join(' '); 384 | return tpz; 385 | }); 386 | }; 387 | 388 | Xdsm.prototype._createTitle = function _createTitle() { 389 | const self = this; 390 | // do not display title if it is 'root' 391 | self.svg.selectAll('.title') 392 | .data([self.svgid]) 393 | .enter() 394 | .append('g') 395 | .classed('title', true) 396 | .append('a') 397 | .attr('id', self.svgid) 398 | .append('text') 399 | .text(self.svgid === 'root' ? '' : self.svgid) 400 | .attr( 401 | 'transform', 402 | `translate(${self.config.layout.origin.x}, ${self.config.layout.origin.y - 5})`, 403 | ); 404 | }; 405 | 406 | Xdsm.prototype._createBorder = function _createBorder() { 407 | const self = this; 408 | const bordercolor = 'black'; 409 | self.svg.selectAll('.border') 410 | .data([self]) 411 | .enter() 412 | .append('rect') 413 | .classed('border', true) 414 | .attr('x', BORDER_PADDING) 415 | .attr('y', BORDER_PADDING) 416 | .style('stroke', bordercolor) 417 | .style('fill', 'none') 418 | .style('stroke-width', 0); 419 | }; 420 | 421 | export default Xdsm; 422 | -------------------------------------------------------------------------------- /test/xdsmjs-test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /test/xdsmjs-test.mjs: -------------------------------------------------------------------------------- 1 | import Labelizer from '../src/labelizer.js'; 2 | import Graph from '../src/graph.js'; 3 | import XdsmFactory from '../src/xdsm-factory.js'; 4 | 5 | import test from 'tape'; 6 | 7 | test("Labelizer.strParse('') returns [{'base':'', 'sub':undefined, 'sup':undefined}]", (t) => { 8 | t.deepEqual(Labelizer.strParse(''), [{ base: '', sub: undefined, sup: undefined }]); 9 | t.end(); 10 | }); 11 | test("Labelizer.strParse('+A') throws an error", (t) => { 12 | t.throws(() => { Labelizer.strParse('+'); }, 'should throw an error'); 13 | t.end(); 14 | }); 15 | test("Labelizer.strParse('ConvCheck') returns [{'base':'ConvCheck', 'sub':undefined, 'sup':undefined}]", (t) => { 16 | t.deepEqual(Labelizer.strParse('ConvCheck'), [{ base: 'ConvCheck', sub: undefined, sup: undefined }]); 17 | t.end(); 18 | }); 19 | test("Labelizer.strParse('x') returns [{'base':'x', 'sub':undefined, 'sup':undefined}]", (t) => { 20 | t.deepEqual(Labelizer.strParse('x'), [{ base: 'x', sub: undefined, sup: undefined }]); 21 | t.end(); 22 | }); 23 | test("Labelizer.strParse('λ') returns [{'base':'λ', 'sub':undefined, 'sup':undefined}]", (t) => { 24 | t.deepEqual(Labelizer.strParse('λ'), [{ base: 'λ', sub: undefined, sup: undefined }]); 25 | t.end(); 26 | }); 27 | test("Labelizer.strParse('λ_λ^λ') " 28 | + "returns [{'base':'λ', 'sub':'λ', 'sup':'λ'}]", (t) => { 29 | t.deepEqual(Labelizer.strParse('λ_λ^λ'), 30 | [{ base: 'λ', sub: 'λ', sup: 'λ' }]); 31 | t.end(); 32 | }); 33 | test("Labelizer.strParse('Optimization') " 34 | + "returns [{'base':'Optimization', 'sub':undefined, 'sup':undefined}]", (t) => { 35 | t.deepEqual(Labelizer.strParse('Optimization'), [{ base: 'Optimization', sub: undefined, sup: undefined }]); 36 | t.end(); 37 | }); 38 | 39 | test("Labelizer.strParse('x_12') returns [{'base':'x', 'sub': '12', 'sup':undefined}]", (t) => { 40 | t.deepEqual(Labelizer.strParse('x_12'), [{ base: 'x', sub: '12', sup: undefined }]); 41 | t.end(); 42 | }); 43 | 44 | test("Labelizer.strParse('x_13^{(0)}') returns [{'base':'x', 'sub': '13', 'sup': '{(0)}'}]", (t) => { 45 | t.deepEqual(Labelizer.strParse('x_13^{(0)}'), [{ base: 'x', sub: '13', sup: '{(0)}' }]); 46 | t.end(); 47 | }); 48 | test("Labelizer.strParse('x_13^0, y_1^{*}') returns [{'base': 'x', 'sub': '13', 'sup': '{*}'}, " 49 | + "{'base':'y', 'sub': '1', 'sup': '*'}]", (t) => { 50 | t.deepEqual(Labelizer.strParse('x_13^{(0)}, y_1^{*}'), [{ base: 'x', sub: '13', sup: '{(0)}' }, 51 | { base: 'y', sub: '1', sup: '{*}' }]); 52 | t.end(); 53 | }); 54 | test("Labelizer.strParse('1:Opt') returns [{'base':'1:Opt', 'sub':undefined, 'sup':undefined}]", (t) => { 55 | t.deepEqual(Labelizer.strParse('1:Opt'), [{ base: '1:Opt', sub: undefined, sup: undefined }]); 56 | t.end(); 57 | }); 58 | test("Labelizer.strParse('1:L-BFGS-B') returns [{'base':'1:L-BFGS-B', 'sub':undefined, 'sup':undefined}]", (t) => { 59 | t.deepEqual(Labelizer.strParse('1:L-BFGS-B'), [{ base: '1:L-BFGS-B', sub: undefined, sup: undefined }]); 60 | t.end(); 61 | }); 62 | test("Labelizer.strParse('y_12_y_34') returns [{'base':'y_12_y_34', 'sub':undefined, 'sup':undefined}]", (t) => { 63 | t.deepEqual(Labelizer.strParse('y_12_y_34'), [{ base: 'y_12_y_34', sub: undefined, sup: undefined }]); 64 | t.end(); 65 | }); 66 | test("Labelizer.strParse('y_12_y_34^*') returns [{'base':'y_12_y_34', 'sub':undefined, 'sup':'*'}]", (t) => { 67 | t.deepEqual(Labelizer.strParse('y_12_y_34^*'), [{ base: 'y_12_y_34', sub: undefined, sup: '*' }]); 68 | t.end(); 69 | }); 70 | test("Graph.expand(['a']) returns [['a']]", (t) => { 71 | t.deepEqual(Graph.expand(['a']), [['a']]); 72 | t.end(); 73 | }); 74 | test("Graph.expand([['a']]) returns [['a']]", (t) => { 75 | t.deepEqual(Graph.expand([['a']]), [['a']]); 76 | t.end(); 77 | }); 78 | test("Graph.expand(['a', 'b']) returns [['a', 'b']]", (t) => { 79 | t.deepEqual(Graph.expand(['a', 'b']), [['a', 'b']]); 80 | t.end(); 81 | }); 82 | test("Graph.expand([['a', 'b']]) returns [['a', 'b']]", (t) => { 83 | t.deepEqual(Graph.expand([['a', 'b']]), [['a', 'b']]); 84 | t.end(); 85 | }); 86 | test("Graph.expand(['a', ['b']]) returns [['a', 'b', 'a']]", (t) => { 87 | t.deepEqual(Graph.expand(['a', ['b']]), [['a', 'b', 'a']]); 88 | t.end(); 89 | }); 90 | test("Graph.expand([['a'], 'b']) returns ['a', 'b']", (t) => { 91 | t.deepEqual(Graph.expand([['a'], 'b']), [['a', 'b']]); 92 | t.end(); 93 | }); 94 | test("Graph.expand([['a'], 'b', 'c']) returns ['a', 'b', 'c']", (t) => { 95 | t.deepEqual(Graph.expand([['a'], 'b', 'c']), [['a', 'b', 'c']]); 96 | t.end(); 97 | }); 98 | test("Graph.expand(['a', ['b'], 'c']) returns [['a', 'b', 'a', 'c']]", (t) => { 99 | t.deepEqual(Graph.expand(['a', ['b'], 'c']), [['a', 'b', 'a', 'c']]); 100 | t.end(); 101 | }); 102 | test("Graph.expand(['a', [['b']], 'c']) returns [['a', 'b', 'a', 'c']]", (t) => { 103 | t.deepEqual(Graph.expand(['a', [['b']], 'c']), [['a', 'b', 'a', 'c']]); 104 | t.end(); 105 | }); 106 | test("Graph.expand(['a', [['b', [d]]], 'c']) returns [['a', 'b', 'd', 'b', 'a', 'c']]", (t) => { 107 | t.deepEqual(Graph.expand(['a', [['b', ['d']]], 'c']), [['a', 'b', 'd', 'b', 'a', 'c']]); 108 | t.end(); 109 | }); 110 | test("Graph.expand(['a', ['b1', 'b2'], 'c']) returns [['a', 'b1', 'b2', 'a', 'c']]", (t) => { 111 | t.deepEqual(Graph.expand(['a', ['b1', 'b2'], 'c']), [['a', 'b1', 'b2', 'a', 'c']]); 112 | t.end(); 113 | }); 114 | test("Graph.expand(['a0', ['b1', 'b2', 'b3'], 'c3']) returns [['a0', 'b1', 'b2', 'b3', 'a0', 'c3']]", (t) => { 115 | t.deepEqual(Graph.expand(['a0', ['b1', 'b2', 'b3'], 'c3']), [['a0', 'b1', 'b2', 'b3', 'a0', 'c3']]); 116 | t.end(); 117 | }); 118 | test("Graph.expand(['opt', ['mda', ['d1', 'd2', 'd3'],'func']]) returns [['opt', 'mda', 'd1', 'd2', 'd3', 'mda','func', 'opt']]", (t) => { 119 | t.deepEqual(Graph.expand(['opt', ['mda', ['d1', 'd2', 'd3'], 'func']]), 120 | [['opt', 'mda', 'd1', 'd2', 'd3', 'mda', 'func', 'opt']]); 121 | t.end(); 122 | }); 123 | test("Graph.expand([{parallel: ['d1', 'd2']}]) returns [[d1], [d2]]", (t) => { 124 | t.deepEqual(Graph.expand([{ parallel: ['d1', 'd2'] }]), 125 | [['d1'], ['d2']]); 126 | t.end(); 127 | }); 128 | test("Graph.expand([{parallel: ['d1', 'd2']}]) returns [[d1], [d2]]", (t) => { 129 | t.deepEqual(Graph.expand([{ parallel: ['d1', 'd2'] }]), 130 | [['d1'], ['d2']]); 131 | t.end(); 132 | }); 133 | test("Graph.expand(['opt', {parallel: ['d1', 'd2', 'd3']}]) returns [['opt', 'd1'], ['opt', 'd2'], ['opt', 'd3']]", (t) => { 134 | t.deepEqual(Graph.expand(['opt', { parallel: ['d1', 'd2', 'd3'] }]), 135 | [['opt', 'd1'], ['opt', 'd2'], ['opt', 'd3']]); 136 | t.end(); 137 | }); 138 | test("Graph.expand(['opt', [{parallel: ['d1', 'd2', 'd3']}]]) returns [['opt', 'd1', 'opt'], ['opt', 'd2', 'opt'], ['opt', 'd3', 'opt']]", (t) => { 139 | t.deepEqual(Graph.expand(['opt', [{ parallel: ['d1', 'd2', 'd3'] }]]), 140 | [['opt', 'd1', 'opt'], ['opt', 'd2', 'opt'], ['opt', 'd3', 'opt']]); 141 | t.end(); 142 | }); 143 | test("Graph.expand(['mda', {parallel: ['d1', 'd2', 'd3']}, 'd4']) returns [['mda', 'd1', 'd4'], ['mda', 'd2', 'd4'], ['mda', 'd3', 'd4']]", (t) => { 144 | t.deepEqual(Graph.expand(['mda', { parallel: ['d1', 'd2', 'd3'] }, 'd4']), 145 | [['mda', 'd1', 'd4'], ['mda', 'd2', 'd4'], ['mda', 'd3', 'd4']]); 146 | t.end(); 147 | }); 148 | test("Graph.expand(['opt', 'mda', {parallel: ['d1', 'd2', 'd3']}, 'd4']]) returns [['opt', 'mda'], ['mda', 'd1', 'd4'], ['mda', 'd2', 'd4'], ['mda', 'd3', 'd4']]", (t) => { 149 | t.deepEqual(Graph.expand(['opt', 'mda', { parallel: ['d1', 'd2', 'd3'] }, 'd4']), 150 | [['opt', 'mda'], ['mda', 'd1', 'd4'], ['mda', 'd2', 'd4'], ['mda', 'd3', 'd4']]); 151 | t.end(); 152 | }); 153 | test("Graph.expand(['opt', ['mda', {parallel: ['d1', 'd2', 'd3']}, 'd4']]) returns [['opt', 'mda'], ['mda', 'd1', 'd4'], ['mda', 'd2', 'd4'], ['mda', 'd3', 'd4'], ['d4', 'opt']]", (t) => { 154 | t.deepEqual(Graph.expand(['opt', ['mda', { parallel: ['d1', 'd2', 'd3'] }, 'd4']]), 155 | [['opt', 'mda'], ['mda', 'd1', 'd4'], ['mda', 'd2', 'd4'], ['mda', 'd3', 'd4'], ['d4', 'opt']]); 156 | t.end(); 157 | }); 158 | test("Graph.expand((['_U_', ['opt', ['mda', {parallel: ['d1', 'd2', 'd3']}, 'd4']]]) returns [['_U_', 'opt', 'mda'], ['mda', 'd1', 'd4'], ['mda', 'd2', 'd4'], ['mda', 'd3', 'd4'], ['d4', 'opt', '_U_']]", (t) => { 159 | t.deepEqual(Graph.expand(['_U_', ['opt', ['mda', { parallel: ['d1', 'd2', 'd3'] }, 'd4']]]), 160 | [['_U_', 'opt', 'mda'], ['mda', 'd1', 'd4'], ['mda', 'd2', 'd4'], ['mda', 'd3', 'd4'], ['d4', 'opt', '_U_']]); 161 | t.end(); 162 | }); 163 | test("Graph.expand((['_U_', ['opt', ['mda', ['d1', 'd2']]]]) returns [['_U_', 'opt', 'mda', 'd1', 'd2', 'mda', 'opt', '_U_']]", (t) => { 164 | t.deepEqual(Graph.expand(['_U_', ['opt', ['mda', ['d1', 'd2']]]]), 165 | [['_U_', 'opt', 'mda', 'd1', 'd2', 'mda', 'opt', '_U_']]); 166 | t.end(); 167 | }); 168 | test("Graph.expand((['_U_', ['opt', ['mda', ['d1', 'd2'], 'mda', ['d1', 'd2']]]]) returns [['_U_', 'opt', 'mda', 'd1', 'd2', 'mda', 'mda', 'd1', 'd2', 'mda', 'opt', '_U_']]", (t) => { 169 | t.deepEqual(Graph.expand(['_U_', ['opt', ['mda', ['d1', 'd2'], 'mda', ['d1', 'd2']]]]), 170 | [['_U_', 'opt', 'mda', 'd1', 'd2', 'mda', 'mda', 'd1', 'd2', 'mda', 'opt', '_U_']]); 171 | t.end(); 172 | }); 173 | test("Graph.expand((['_U_', ['opt', ['mda', ['d1', 'd2'], {parallel: ['sc1', 'sc2']},'mda', ['d1', 'd2']]]]) returns [['_U_', 'opt', 'mda', 'd1', 'd2', 'mda'], ['mda', 'sc1', 'mda'], ['mda', 'sc2', 'mda'], ['mda', 'd1', 'd2', 'mda', 'opt', '_U_']]", (t) => { 174 | t.deepEqual(Graph.expand(['_U_', ['opt', ['mda', ['d1', 'd2'], { parallel: ['sc1', 'sc2'] }, 'mda', ['d1', 'd2']]]]), 175 | [['_U_', 'opt', 'mda', 'd1', 'd2', 'mda'], ['mda', 'sc1', 'mda'], ['mda', 'sc2', 'mda'], ['mda', 'd1', 'd2', 'mda', 'opt', '_U_']]); 176 | t.end(); 177 | }); 178 | test("Graph.expand((['d1', {parallel: ['sc1', 'sc2']}, 'd2']) returns [['d1', 'sc1', 'd2'], ['d1', 'sc2', 'd2']]", (t) => { 179 | t.deepEqual(Graph.expand(['d1', { parallel: ['sc1', 'sc2'] }, 'd2']), [['d1', 'sc1', 'd2'], ['d1', 'sc2', 'd2']]); 180 | t.end(); 181 | }); 182 | test("Graph.expand((['opt', ['d1', {parallel: ['sc1', 'sc2']}]]) returns [['opt', 'd1'] ['d1', 'sc1', 'opt'], ['d1', 'sc2', 'opt']]", (t) => { 183 | t.deepEqual(Graph.expand(['opt', ['d1', { parallel: ['sc1', 'sc2'] }]]), [['opt', 'd1'], ['d1', 'sc1', 'opt'], ['d1', 'sc2', 'opt'], ['opt', 'opt']]); 184 | t.end(); 185 | }); 186 | test('Graph.chains should expand as list of index couples', (t) => { 187 | const g = new Graph({ 188 | nodes: [{ id: 'Opt', name: 'Opt' }, 189 | { id: 'MDA', name: 'MDA' }, 190 | { id: 'DA1', name: 'DA1' }, 191 | { id: 'DA2', name: 'DA2' }, 192 | { id: 'DA3', name: 'DA3' }, 193 | { id: 'Func', name: 'Func' }], 194 | edges: [], 195 | workflow: ['Opt', ['MDA', ['DA1', 'DA2', 'DA3'], 'Func']], 196 | }); 197 | t.deepEqual(g.chains, [[[1, 2], [2, 3], [3, 4], [4, 5], [5, 2], [2, 6], [6, 1]]]); 198 | t.end(); 199 | }); 200 | test('Graph.chains should expand as list of index couples', (t) => { 201 | const g = new Graph({ 202 | nodes: [{ id: 'Opt', name: 'Opt' }, 203 | { id: 'DA1', name: 'DA1' }, 204 | { id: 'DA2', name: 'DA2' }, 205 | { id: 'DA3', name: 'DA3' }, 206 | { id: 'Func', name: 'Func' }], 207 | edges: [], 208 | workflow: [['Opt', ['DA1'], 'Opt', ['DA2'], 'Opt', ['DA3'], 'Func']], 209 | }); 210 | t.deepEqual(g.chains, [[[1, 2], [2, 1], [1, 3], [3, 1], [1, 4], [4, 1], [1, 5]]]); 211 | t.end(); 212 | }); 213 | test("Graph.number(['d1']) returns {'toNum':{d1: '0'}, 'toNodes':[['d1']])", (t) => { 214 | t.deepEqual(Graph.number(['d1']), { 215 | toNum: { d1: '0' }, 216 | toNode: [['d1']], 217 | }); 218 | t.equal(Graph.number(['d1']).toNode.length, 1); 219 | t.end(); 220 | }); 221 | test("Graph.number(['d1', 'd1']) returns {'toNum':{d1: '0,1'}, 'toNodes':[['d1'],['d1']]})", (t) => { 222 | t.deepEqual(Graph.number(['d1', 'd1']), { 223 | toNum: { d1: '0,1' }, 224 | toNode: [['d1'], ['d1']], 225 | }); 226 | t.end(); 227 | }); 228 | test("Graph.number(['mda', 'd1']) returns {'toNum':{mda:'0', d1: '1'}, 'toNode':[['mda'], ['d1']]})", (t) => { 229 | t.deepEqual(Graph.number(['mda', 'd1']), { 230 | toNum: { mda: '0', d1: '1' }, 231 | toNode: [['mda'], ['d1']], 232 | }); 233 | t.end(); 234 | }); 235 | test("Graph.number(['mda', 'd1', 'd2', 'd3']) returns {mda: '0', d1: '1', d2: '2', d3: '3'})", (t) => { 236 | t.deepEqual(Graph.number(['mda', 'd1', 'd2', 'd3']).toNum, { 237 | mda: '0', d1: '1', d2: '2', d3: '3', 238 | }); 239 | t.end(); 240 | }); 241 | test("Graph.number(['mda', ['d1', 'd2', 'd3']]) returns {mda: '0,4-1', d1: '1', d2: '2', d3: '3'} )", (t) => { 242 | t.deepEqual(Graph.number(['mda', ['d1', 'd2', 'd3']]).toNum, { 243 | mda: '0,4-1', d1: '1', d2: '2', d3: '3', 244 | }); 245 | t.end(); 246 | }); 247 | test("Graph.number(['mda', {parallel:['d1', 'd2', 'd3']}]) returns {'mda': '0', 'd1': '1', 'd2': '1', 'd3': '1'})", (t) => { 248 | t.deepEqual(Graph.number(['mda', { parallel: ['d1', 'd2', 'd3'] }]).toNum, { 249 | mda: '0', d1: '1', d2: '1', d3: '1', 250 | }); 251 | t.end(); 252 | }); 253 | test("Graph.number(['mda', [{parallel:['d1', 'd2', 'd3']}]]) returns {'toNum':{'mda': '0,2-1', 'd1': '1', 'd2': '1', 'd3': '1'}, 'toNode':[['mda'], ['d1','d2','d3']]})", (t) => { 254 | t.deepEqual(Graph.number(['mda', [{ parallel: ['d1', 'd2', 'd3'] }]]).toNum, { 255 | mda: '0,2-1', d1: '1', d2: '1', d3: '1', 256 | }); 257 | t.deepEqual(Graph.number(['mda', [{ parallel: ['d1', 'd2', 'd3'] }]]).toNode, [['mda'], ['d1', 'd2', 'd3'], ['mda']]); 258 | t.end(); 259 | }); 260 | test("Graph.number(['opt', 'mda', ['d1', 'd2', 'd3']]) returns {'opt': '0', 'mda': '1,5-2', 'd1': '2', 'd2': '3', 'd3': '4'})", (t) => { 261 | t.deepEqual(Graph.number(['opt', 'mda', ['d1', 'd2', 'd3']]).toNum, { 262 | opt: '0', mda: '1,5-2', d1: '2', d2: '3', d3: '4', 263 | }); 264 | t.end(); 265 | }); 266 | test("Graph.number([['opt', ['mda', ['d1', 'd2', 'd3']]], 'd4']) returns {'opt': '0,6-1', 'mda': '1,5-2', 'd1': '2', 'd2': '3', 'd3': '4', 'd4': '7'})", (t) => { 267 | t.deepEqual(Graph.number([['opt', ['mda', ['d1', 'd2', 'd3']]], 'd4']).toNum, { 268 | opt: '0,6-1', mda: '1,5-2', d1: '2', d2: '3', d3: '4', d4: '7', 269 | }); 270 | t.end(); 271 | }); 272 | test("Graph.number([['Opt', ['mda', ['d1'], 's1']]]) returns {'Opt': '0,5-1', 'mda': '1,3-2', 'd1': '2', 's1': '4'})", (t) => { 273 | t.deepEqual(Graph.number([['Opt', ['mda', ['d1'], 's1']]]).toNum, { 274 | Opt: '0,5-1', mda: '1,3-2', d1: '2', s1: '4', 275 | }); 276 | t.end(); 277 | }); 278 | 279 | function makeGraph() { 280 | const mdo = { 281 | nodes: [{ id: 'A' }, { id: 'B' }, { id: 'C' }, { id: 'D' }, { id: 'E' }], 282 | edges: [{ from: 'A', to: 'B', name: 'a, b' }, 283 | { from: 'C', to: 'A', name: 'CA' }, 284 | { from: 'C', to: 'B', name: 'CB' }, 285 | { from: 'C', to: 'D', name: 'CD' }, 286 | { from: 'E', to: 'A', name: 'EA' }], 287 | workflow: [], 288 | }; 289 | return new Graph(mdo); 290 | } 291 | test('Graph.findEdgesOf(nodeIdx) returns edges to remove and edges to delete in case of node removal', (t) => { 292 | const g = makeGraph(); 293 | // find edges if A removed 294 | t.deepEqual(g.findEdgesOf(1), { toRemove: [g.edges[0], g.edges[1], g.edges[4]], toShift: [g.edges[2], g.edges[3]] }); 295 | // find edges if C removed 296 | t.deepEqual(g.findEdgesOf(3), { toRemove: [g.edges[1], g.edges[2], g.edges[3]], toShift: [g.edges[4]] }); 297 | // find edges if D removed 298 | t.deepEqual(g.findEdgesOf(4), { toRemove: [g.edges[3]], toShift: [g.edges[4]] }); 299 | t.end(); 300 | }); 301 | test('Graph.addNode()', (t) => { 302 | const g = makeGraph(); 303 | t.equal(g.nodes.length, 6); 304 | g.addNode({ id: 'F', name: 'F', kind: 'function' }); 305 | t.equal(g.nodes.length, 7); 306 | t.end(); 307 | }); 308 | test('Graph.removeNode()', (t) => { 309 | const g = makeGraph(); 310 | t.equal(g.nodes.length, 6); 311 | g.removeNode(4); 312 | t.equal(g.nodes.length, 5); 313 | t.end(); 314 | }); 315 | test('Graph.getNode()', (t) => { 316 | const g = makeGraph(); 317 | t.equal(g.getNode('A'), g.nodes[1]); 318 | t.equal(g.getNode('E'), g.nodes[5]); 319 | t.end(); 320 | }); 321 | test('Graph.idxOf()', (t) => { 322 | const g = makeGraph(); 323 | t.equal(g.idxOf('B'), 2); 324 | t.equal(g.idxOf('E'), 5); 325 | t.end(); 326 | }); 327 | 328 | test('Graph constructor should create a graph without edges or workflow input data)', (t) => { 329 | const mdo = { nodes: [{ id: 'A' }, { id: 'B' }] }; 330 | const g = new Graph(mdo); 331 | t.deepEqual(g.edges, []); 332 | t.deepEqual(g.chains, []); 333 | t.end(); 334 | }); 335 | test('Graph nodes have a status UNKNOWN by default', (t) => { 336 | const g = new Graph({ nodes: [{ id: 'A' }, { id: 'B' }] }); 337 | t.deepEqual(g.getNode('A').status, Graph.NODE_STATUS.UNKNOWN); 338 | t.end(); 339 | }); 340 | test('Graph nodes can be to a given status PENDING, RUNNING, DONE or FAILED', (t) => { 341 | const g = new Graph({ 342 | nodes: [{ id: 'A', status: 'PENDING' }, 343 | { id: 'B', status: 'RUNNING' }, 344 | { id: 'C', status: 'DONE' }, 345 | { id: 'D', status: 'FAILED' }, 346 | ], 347 | }); 348 | t.deepEqual(g.getNode('A').status, Graph.NODE_STATUS.PENDING); 349 | t.deepEqual(g.getNode('B').status, Graph.NODE_STATUS.RUNNING); 350 | t.deepEqual(g.getNode('C').status, Graph.NODE_STATUS.DONE); 351 | t.deepEqual(g.getNode('D').status, Graph.NODE_STATUS.FAILED); 352 | t.end(); 353 | }); 354 | test('Graph throws an error if a node status string not known', (t) => { 355 | t.throws(() => { 356 | const g = new Graph({ nodes: [{ id: 'A', status: 'BADSTATUS' }] }); 357 | }, 'should throw an error'); 358 | t.end(); 359 | }); 360 | test('Graph edge can have vars infos id/names from name', (t) => { 361 | const g = makeGraph(); 362 | const actual = g.findEdge('A', 'B'); 363 | t.deepEqual(actual.element.vars, { 0: 'a', 1: 'b' }); 364 | t.end(); 365 | }); 366 | 367 | function makeGraph2() { 368 | const mdo = { 369 | nodes: [{ id: 'A' }, { id: 'B' }, { id: 'C' }, { id: 'D' }, { id: 'E' }], 370 | edges: [{ from: 'A', to: 'B', vars: { 1: 'a', 2: 'b' } }, 371 | { from: 'C', to: 'A', vars: { 1: 'a', 3: 'c' } }, 372 | { from: 'C', to: 'B', vars: { 3: 'c', 2: 'b' } }, 373 | { from: 'C', to: 'D', vars: { 3: 'c', 4: 'd' } }, 374 | { from: 'E', to: 'A', vars: { 5: 'e', 1: 'a' } }], 375 | workflow: [], 376 | }; 377 | return new Graph(mdo); 378 | } 379 | test('Graph edge can have vars infos id/names', (t) => { 380 | const g2 = makeGraph2(); 381 | t.equal(g2.getNode('E'), g2.nodes[5]); 382 | const edgeCD = g2.findEdge('C', 'D').element; 383 | t.equal(edgeCD.vars['3'], 'c'); 384 | t.equal(edgeCD.vars['4'], 'd'); 385 | t.deepEqual(edgeCD.vars, { 3: 'c', 4: 'd' }); 386 | t.equal(edgeCD.name, 'c, d'); 387 | t.end(); 388 | }); 389 | test('Graph add new var between two given nodes not linked', (t) => { 390 | const g2 = makeGraph2(); 391 | g2.addEdgeVar('A', 'D', { 4: 'd' }); 392 | const edgeAD = g2.findEdge('A', 'D').element; 393 | t.equal(edgeAD.vars['4'], 'd'); 394 | t.deepEqual(edgeAD.vars, { 4: 'd' }); 395 | t.equal(edgeAD.name, 'd'); 396 | t.end(); 397 | }); 398 | test('Graph a var should appear once even if added twice', (t) => { 399 | const g2 = makeGraph2(); 400 | g2.addEdgeVar('A', 'D', { 4: 'd' }); 401 | g2.addEdgeVar('A', 'D', { 4: 'd' }); 402 | const edgeAD = g2.findEdge('A', 'D').element; 403 | t.equal(edgeAD.name, 'd'); 404 | g2.removeEdge('A', 'D'); 405 | const { index } = g2.findEdge('A', 'D'); 406 | t.equal(edgeAD.index, undefined); 407 | t.end(); 408 | }); 409 | test('Graph add new var between two given nodes already linked', (t) => { 410 | const g2 = makeGraph2(); 411 | g2.addEdgeVar('A', 'B', { 4: 'd' }); 412 | const edgeAD = g2.findEdge('A', 'B').element; 413 | t.deepEqual(edgeAD.vars, { 1: 'a', 2: 'b', 4: 'd' }); 414 | t.equal(edgeAD.name, 'a, b, d'); 415 | t.end(); 416 | }); 417 | test('Remove var of an edge', (t) => { 418 | const g2 = makeGraph2(); 419 | const edge = g2.findEdge('A', 'B').element; 420 | edge.removeVar('b'); 421 | t.equal(edge.name, 'a'); 422 | t.end(); 423 | }); 424 | test('Remove edge between two given nodes', (t) => { 425 | const g2 = makeGraph2(); 426 | let edge = g2.findEdge('E', 'A').element; 427 | t.notEqual(edge, undefined); 428 | g2.removeEdge('E', 'A'); 429 | edge = g2.findEdge('E', 'A').element; 430 | t.equal(edge, undefined); 431 | t.end(); 432 | }); 433 | test('Remove edge one var between two given nodes', (t) => { 434 | const g2 = makeGraph2(); 435 | let edge = g2.findEdge('E', 'A').element; 436 | t.notEqual(edge, undefined); 437 | g2.removeEdgeVar('E', 'A', 'e'); 438 | edge = g2.findEdge('E', 'A').element; 439 | t.deepEqual(edge.vars, { 1: 'a' }); 440 | t.end(); 441 | }); 442 | test('Remove edge all vars between two given nodes', (t) => { 443 | const g2 = makeGraph2(); 444 | let edge = g2.findEdge('E', 'A').element; 445 | t.notEqual(edge, undefined); 446 | g2.removeEdgeVar('E', 'A', 'e'); 447 | g2.removeEdgeVar('E', 'A', 'a'); 448 | edge = g2.findEdge('E', 'A').element; 449 | t.equal(edge, undefined); 450 | t.end(); 451 | }); 452 | test('find XDSMs order list', (t) => { 453 | t.deepEqual(XdsmFactory._orderedList({ 454 | C: { nodes: [] }, 455 | B: { nodes: [] }, 456 | root: { nodes: [{ name: 'a', subxdsm: 'A' }] }, 457 | A: { nodes: [{ name: 'c', subxdsm: 'C' }, { name: 'b', subxdsm: 'B' }] }, 458 | }, 'root'), ['root', 'A', 'C', 'B']); 459 | t.end(); 460 | }); 461 | test('find XDSMs list of single', (t) => { 462 | t.deepEqual(XdsmFactory._orderedList({ 463 | root: { nodes: [{ name: 'a', subxdsm: 'A' }] }, 464 | A: { nodes: [{ name: 'c' }, { name: 'b' }] }, 465 | }), ['root', 'A']); 466 | t.end(); 467 | }); 468 | test('find XDSMs list of empty xdsms', (t) => { 469 | t.deepEqual(XdsmFactory._orderedList({ 470 | root: { nodes: [] }, 471 | }), ['root']); 472 | t.end(); 473 | }); 474 | -------------------------------------------------------------------------------- /webpack.config.cjs: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const TerserPlugin = require('terser-webpack-plugin'); 3 | 4 | module.exports = { 5 | mode: 'development', 6 | entry: { 7 | xdsmjs: './src/index.js' 8 | }, 9 | output: { 10 | path: path.resolve(__dirname, 'dist'), 11 | filename: '[name].js', 12 | library: 'xdsmjs', 13 | }, 14 | devtool: 'source-map', 15 | module: { 16 | rules: [ 17 | { 18 | test: /\.js$/, 19 | exclude: /node_modules/, 20 | use: { 21 | loader: 'babel-loader', 22 | options: { 23 | presets: ['@babel/preset-env'], 24 | }, 25 | }, 26 | }, 27 | ], 28 | }, 29 | resolve: { 30 | fallback: { 31 | fs: false, 32 | path: false, 33 | stream: false, 34 | }, 35 | }, 36 | optimization: { 37 | minimize: true, 38 | minimizer: [new TerserPlugin({ 39 | extractComments: false, 40 | })], 41 | }, 42 | }; 43 | -------------------------------------------------------------------------------- /xdsm.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 19 | 20 | 21 | 22 |
23 |
24 | 25 | 26 | -------------------------------------------------------------------------------- /xdsm.json: -------------------------------------------------------------------------------- 1 | { 2 | "subopt-i": { 3 | "nodes": [ 4 | { 5 | "type": "optimization", 6 | "id": "Opt", 7 | "name": "DisciplineOptimization_i" 8 | }, 9 | { 10 | "type": "analysis", 11 | "id": "Dis1", 12 | "name": "Analysis_i" 13 | }, 14 | { 15 | "type": "function", 16 | "id": "Dis2", 17 | "name": "SystemFunctions" 18 | }, 19 | { 20 | "type": "function", 21 | "id": "Dis3", 22 | "name": "DisciplineFunctions" 23 | }, 24 | { 25 | "type": "function", 26 | "id": "Dis4", 27 | "name": "DisciplineVarDerivatives_i" 28 | } 29 | ], 30 | "edges": [ 31 | { 32 | "to": "Opt", 33 | "from": "_U_", 34 | "name": "x_i" 35 | }, 36 | { 37 | "to": "_U_", 38 | "from": "Opt", 39 | "name": "x_i^*, y_i^*" 40 | }, 41 | { 42 | "to": "Dis1", 43 | "from": "Opt", 44 | "name": "x_i" 45 | }, 46 | { 47 | "to": "Opt", 48 | "from": "Dis1", 49 | "name": "y_i" 50 | }, 51 | { 52 | "to": "Dis2", 53 | "from": "Dis1", 54 | "name": "x_i, y_i" 55 | }, 56 | { 57 | "to": "Dis3", 58 | "from": "Dis1", 59 | "name": "x_i, y_i" 60 | }, 61 | { 62 | "to": "Dis4", 63 | "from": "Dis1", 64 | "name": "x_i, y_i" 65 | }, 66 | { 67 | "to": "Opt", 68 | "from": "Dis2", 69 | "name": "f_0, c_0" 70 | }, 71 | { 72 | "to": "Opt", 73 | "from": "Dis3", 74 | "name": "f_i, c_i" 75 | }, 76 | { 77 | "to": "Opt", 78 | "from": "Dis4", 79 | "name": "df_xi, dc_xi" 80 | }, 81 | { 82 | "to": "Dis1", 83 | "from": "_U_", 84 | "name": "yest_j" 85 | }, 86 | { 87 | "to": "Dis2", 88 | "from": "_U_", 89 | "name": "yest_j" 90 | }, 91 | { 92 | "to": "Dis3", 93 | "from": "_U_", 94 | "name": "yest_j" 95 | }, 96 | { 97 | "to": "Dis4", 98 | "from": "_U_", 99 | "name": "yest_j" 100 | }, 101 | { 102 | "to": "_U_", 103 | "from": "Dis2", 104 | "name": "f_0, c_0" 105 | }, 106 | { 107 | "to": "_U_", 108 | "from": "Dis3", 109 | "name": "f_i, c_i" 110 | } 111 | ], 112 | "workflow": [ 113 | "_U_", 114 | [ 115 | "Opt", 116 | [ 117 | "Dis1", 118 | { 119 | "parallel": [ 120 | "Dis2", 121 | "Dis3", 122 | "Dis4" 123 | ] 124 | } 125 | ] 126 | ] 127 | ], 128 | "optpb": "Minimize (f0)0 + (df/dxi)Dxi\nwith respect to Dxi\nsubject to (c0)0 + (dc0/dxi)Dxi >= 0\n (ci)0 + (dci/dxi)Dxi >= 0\n Dxi_L <= Dxi <= Dxi_U " 129 | }, 130 | "root": { 131 | "nodes": [ 132 | { 133 | "type": "mda", 134 | "id": "Dis1", 135 | "name": "ConvergenceCheck" 136 | }, 137 | { 138 | "type": "mda", 139 | "id": "Mda", 140 | "name": "MDA" 141 | }, 142 | { 143 | "type": "optimization", 144 | "id": "Opt", 145 | "name": "SystemOptimization" 146 | }, 147 | { 148 | "type": "sub-optimization_multi", 149 | "id": "Mdo", 150 | "name": "DisciplineOptimization_i", 151 | "subxdsm": "subopt-i" 152 | }, 153 | { 154 | "type": "function", 155 | "id": "Dis2", 156 | "name": "SystemFunctions" 157 | }, 158 | { 159 | "type": "function", 160 | "id": "Dis4", 161 | "name": "DisciplineFunctions" 162 | }, 163 | { 164 | "type": "function", 165 | "id": "Dis3", 166 | "name": "SharedVarDerivatives" 167 | }, 168 | { 169 | "type": "analysis_multi", 170 | "id": "Dis5", 171 | "name": "Analysis_i" 172 | } 173 | ], 174 | "edges": [ 175 | { 176 | "to": "Dis1", 177 | "from": "_U_", 178 | "name": "x^(0)" 179 | }, 180 | { 181 | "to": "_U_", 182 | "from": "Dis1", 183 | "name": "NoData" 184 | }, 185 | { 186 | "to": "Mda", 187 | "from": "_U_", 188 | "name": "yest^(0)" 189 | }, 190 | { 191 | "to": "Dis5", 192 | "from": "Mda", 193 | "name": "yest_j" 194 | }, 195 | { 196 | "to": "Dis5", 197 | "from": "Mda", 198 | "name": "yest_j" 199 | }, 200 | { 201 | "to": "Dis5", 202 | "from": "Mda", 203 | "name": "yest_j" 204 | }, 205 | { 206 | "to": "Mda", 207 | "from": "Dis5", 208 | "name": "y_i" 209 | }, 210 | { 211 | "to": "Opt", 212 | "from": "_U_", 213 | "name": "x_0^(0)" 214 | }, 215 | { 216 | "to": "Mdo", 217 | "from": "_U_", 218 | "name": "x_i^(0)" 219 | }, 220 | { 221 | "to": "_U_", 222 | "from": "Opt", 223 | "name": "x_0^*" 224 | }, 225 | { 226 | "to": "_U_", 227 | "from": "Mdo", 228 | "name": "x_i^*, y_i^*" 229 | }, 230 | { 231 | "to": "Dis1", 232 | "from": "Opt", 233 | "name": "x_0" 234 | }, 235 | { 236 | "to": "Dis1", 237 | "from": "Opt", 238 | "name": "x_0" 239 | }, 240 | { 241 | "to": "Opt", 242 | "from": "Dis3", 243 | "name": "df_x0, dc_x0" 244 | }, 245 | { 246 | "to": "Dis3", 247 | "from": "Opt", 248 | "name": "x_0" 249 | }, 250 | { 251 | "to": "Mdo", 252 | "from": "Opt", 253 | "name": "x_0" 254 | }, 255 | { 256 | "to": "Opt", 257 | "from": "Mdo", 258 | "name": "f_0, c_0, f_i, c_i" 259 | }, 260 | { 261 | "to": "Opt", 262 | "from": "Dis2", 263 | "name": "f_0, c_0" 264 | }, 265 | { 266 | "to": "Opt", 267 | "from": "Dis4", 268 | "name": "f_i, c_i" 269 | }, 270 | { 271 | "to": "Dis1", 272 | "from": "Mdo", 273 | "name": "x_i" 274 | }, 275 | { 276 | "to": "Dis2", 277 | "from": "Opt", 278 | "name": "x_0" 279 | }, 280 | { 281 | "to": "Dis4", 282 | "from": "Opt", 283 | "name": "x_0" 284 | }, 285 | { 286 | "to": "Mdo", 287 | "from": "Mda", 288 | "name": "yest_j" 289 | } 290 | ], 291 | "workflow": [ 292 | "_U_", 293 | [ 294 | "Dis1", 295 | [ 296 | "Mda", 297 | [ 298 | "Dis5" 299 | ], 300 | "Mdo", 301 | "Opt", 302 | { 303 | "parallel": [ 304 | "Dis3", 305 | "Dis2", 306 | "Dis4" 307 | ] 308 | }, 309 | "Opt" 310 | ] 311 | ] 312 | ], 313 | "optpb": "Minimize (f0*)0 + (df0*/dx0)Dx0 \nwith respect to Dx0\nsubject to (c0*)0 + (dc0*/dx0)Dx0 >= 0\n (ci*)0 + (dci*/dx0)Dx0 >= 0\n Dx0_L <= Dx0 <= Dx0_U" 314 | } 315 | } -------------------------------------------------------------------------------- /xdsmjs.css: -------------------------------------------------------------------------------- 1 | /* 2 | * XDSMjs 3 | * Copyright 2016-2020 Rémi Lafage 4 | */ 5 | 6 | .node text { 7 | font-size: medium; 8 | } 9 | 10 | .edge text { 11 | font-size: small; 12 | } 13 | 14 | /** XDSM v1 ***************************************************/ 15 | 16 | /* Special hidden first component */ 17 | .xdsm .driver { 18 | visibility: hidden; 19 | } 20 | 21 | .xdsm .node { 22 | stroke: black; 23 | stroke-width: 1px; 24 | } 25 | 26 | /** XDSM v1 */ 27 | .xdsm .optimization { 28 | fill: #ccf; 29 | } 30 | 31 | .xdsm .lp_optimization { 32 | fill: #ccf; 33 | } 34 | 35 | .xdsm .analysis { 36 | fill: #cfc; 37 | } 38 | 39 | /* Forward compatibility */ 40 | .xdsm .implicit_analysis { 41 | fill: #9fccc9; 42 | } 43 | 44 | .xdsm .mdo { 45 | fill: #fcc; 46 | } 47 | 48 | /* Forward compatibility */ 49 | .xdsm .sub-optimization { 50 | fill: #fcc; 51 | } 52 | 53 | .xdsm .function { 54 | fill: #f2ccd9; 55 | } 56 | 57 | /* Forward compatibility */ 58 | .xdsm .implicit-function { 59 | fill: #f2ccd9; 60 | } 61 | 62 | .xdsm .mda { 63 | fill: #ffe5cc; 64 | } 65 | 66 | /* Forward compatibility */ 67 | .xdsm .group { 68 | fill: #ffe5cc; 69 | } 70 | 71 | /* Forward compatibility */ 72 | .xdsm .implicit-group { 73 | fill: #ffe5cc; 74 | } 75 | 76 | .xdsm .metamodel { 77 | fill: #fffccc; 78 | } 79 | 80 | .xdsm .doe { 81 | fill: #fffccc; 82 | } 83 | 84 | /* Text Default */ 85 | .xdsm text { 86 | fill: black; 87 | stroke: none; 88 | font-family: Arial, sans-serif; 89 | } 90 | 91 | /* Title */ 92 | .xdsm g.title text { 93 | display: block; 94 | margin: 0.67em 0; 95 | font-size: 1em; 96 | font-weight: bold; 97 | } 98 | 99 | .xdsm g.title rect { 100 | fill: none; 101 | } 102 | 103 | .xdsm tspan.sub { 104 | font-size: small; 105 | } 106 | 107 | .xdsm tspan.sup { 108 | font-size: small; 109 | } 110 | 111 | /* Data */ 112 | .xdsm .dataInter polygon { 113 | fill: #e5e5e5; 114 | stroke: black; 115 | stroke-width: 1px; 116 | } 117 | 118 | .xdsm .dataIO polygon { 119 | fill: #fff; 120 | stroke: black; 121 | stroke-width: 1px; 122 | } 123 | 124 | /* Dataflow */ 125 | .xdsm g.dataflow { 126 | fill: none; 127 | stroke: grey; 128 | stroke-width: 8px; 129 | } 130 | 131 | /* Workflow */ 132 | .xdsm g.workflow { 133 | fill: none; 134 | stroke: black; 135 | stroke-width: 2px; 136 | } 137 | 138 | /** XDSM v2 ***************************************************/ 139 | 140 | .xdsm2 .driver { 141 | visibility: hidden; 142 | } 143 | 144 | .xdsm2 .node { 145 | stroke: black; 146 | stroke-width: 1px; 147 | } 148 | 149 | .xdsm2 .optimization { 150 | fill: #a0cbe8; 151 | } 152 | 153 | .xdsm2 .sub-optimization { 154 | fill: #a0cbe8; 155 | } 156 | 157 | /* Deprecated: use optimization */ 158 | 159 | .xdsm2 .lp_optimization { 160 | fill: #a0cbe8; 161 | } 162 | 163 | /* Deprecated: use sub-optimization */ 164 | 165 | .xdsm2 .mdo { 166 | fill: #a0cbe8; 167 | } 168 | 169 | .xdsm2 .doe { 170 | fill: #a0cbe8; 171 | } 172 | 173 | .xdsm2 .function { 174 | fill: #8cd17d; 175 | } 176 | 177 | .xdsm2 .implicit-function { 178 | fill: #ff9d9a; 179 | } 180 | 181 | /* Deprecated: use function */ 182 | .xdsm2 .analysis { 183 | fill: #8cd17d; 184 | } 185 | 186 | .xdsm2 .mda { 187 | fill: #ffbe7d; 188 | } 189 | 190 | .xdsm2 .metamodel { 191 | fill: #f1ce63; 192 | } 193 | 194 | .xdsm2 .group { 195 | fill: #8cd17d; 196 | } 197 | 198 | .xdsm2 .implicit-group { 199 | fill: #ff9d9a; 200 | } 201 | 202 | /* Text Default */ 203 | .xdsm2 text { 204 | fill: black; 205 | stroke: none; 206 | font-family: Arial, sans-serif; 207 | } 208 | 209 | /* Title */ 210 | .xdsm2 g.title text { 211 | display: block; 212 | margin: 0.67em 0; 213 | font-size: 1em; 214 | font-weight: bold; 215 | } 216 | 217 | .xdsm2 g.title rect { 218 | fill: none; 219 | } 220 | 221 | .xdsm2 tspan.sub { 222 | font-size: small; 223 | } 224 | 225 | .xdsm2 tspan.sup { 226 | font-size: small; 227 | } 228 | 229 | /* Data */ 230 | .xdsm2 .dataInter polygon { 231 | fill: #e5e5e5; 232 | stroke: black; 233 | stroke-width: 1px; 234 | } 235 | 236 | .xdsm2 .dataIO polygon { 237 | fill: #fff; 238 | stroke: black; 239 | stroke-width: 1px; 240 | } 241 | 242 | /* Dataflow */ 243 | .xdsm2 g.dataflow { 244 | fill: none; 245 | stroke: grey; 246 | stroke-width: 8px; 247 | } 248 | 249 | /* Workflow */ 250 | .xdsm2 g.workflow { 251 | fill: none; 252 | stroke: black; 253 | stroke-width: 2px; 254 | } 255 | 256 | /* Tooltip */ 257 | div.xdsm-tooltip { 258 | position: absolute; 259 | text-align: center; 260 | padding: 10px; 261 | background: lightsteelblue; 262 | border: 0; 263 | border-radius: 8px; 264 | pointer-events: none; 265 | } 266 | 267 | /* Info */ 268 | div.optpb { 269 | position: absolute; 270 | padding: 10px; 271 | background: #ccf; 272 | border: 0; 273 | border-radius: 8px; 274 | font-size: 150%; 275 | } 276 | 277 | sub, 278 | sup { 279 | /* Specified in % so that the sup/sup is the right size relative to the surrounding text */ 280 | font-size: 75%; 281 | 282 | /* Zero out the line-height so that it doesn't interfere with the positioning that follows */ 283 | line-height: 0; 284 | 285 | /* Where the magic happens: makes all browsers position the sup/sup properly, relative to the surrounding text */ 286 | position: relative; 287 | 288 | /* Note that if you're using Eric Meyer's reset.css, this is already set and you can remove this rule */ 289 | vertical-align: baseline; 290 | } 291 | 292 | sup { 293 | /* Move the superscripted text up */ 294 | top: -0.5em; 295 | } 296 | 297 | sub { 298 | /* Move the subscripted text down, but only half as far down as the superscript moved up */ 299 | bottom: -0.5em; 300 | } 301 | 302 | /* Version button */ 303 | label#xdsm-version-label { 304 | margin: 0 5px 0 50px; 305 | font-weight: bolder; 306 | font-size: 24px; 307 | font-family: Arial, sans-serif; 308 | } 309 | 310 | select#xdsm-version-toggle { 311 | display: table-cell; 312 | vertical-align: top; 313 | font-weight: bolder; 314 | font-size: 20px; 315 | font-family: Arial, sans-serif; 316 | } 317 | 318 | .subxdsm-link:hover { 319 | text-decoration: underline; 320 | } 321 | --------------------------------------------------------------------------------