├── .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
element in the html file.
75 |
76 |
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 | 
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 |
299 |
300 |
301 |
icon-start 0xe800
302 |
icon-stop 0xe801
303 |
icon-step-next 0xe803
304 |
icon-step-prev 0xe804
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 |
--------------------------------------------------------------------------------