linny-r 1.4.3 → 1.4.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/README.md +102 -48
  2. package/package.json +1 -1
  3. package/server.js +31 -6
  4. package/static/images/check-off-not-same-changed.png +0 -0
  5. package/static/images/check-off-not-same-not-changed.png +0 -0
  6. package/static/images/check-off-same-changed.png +0 -0
  7. package/static/images/check-off-same-not-changed.png +0 -0
  8. package/static/images/check-on-not-same-changed.png +0 -0
  9. package/static/images/check-on-not-same-not-changed.png +0 -0
  10. package/static/images/check-on-same-changed.png +0 -0
  11. package/static/images/check-on-same-not-changed.png +0 -0
  12. package/static/images/eq-not-same-changed.png +0 -0
  13. package/static/images/eq-not-same-not-changed.png +0 -0
  14. package/static/images/eq-same-changed.png +0 -0
  15. package/static/images/eq-same-not-changed.png +0 -0
  16. package/static/images/ne-not-same-changed.png +0 -0
  17. package/static/images/ne-not-same-not-changed.png +0 -0
  18. package/static/images/ne-same-changed.png +0 -0
  19. package/static/images/ne-same-not-changed.png +0 -0
  20. package/static/images/sort-asc-lead.png +0 -0
  21. package/static/images/sort-asc.png +0 -0
  22. package/static/images/sort-desc-lead.png +0 -0
  23. package/static/images/sort-desc.png +0 -0
  24. package/static/images/sort-not.png +0 -0
  25. package/static/index.html +51 -35
  26. package/static/linny-r.css +167 -53
  27. package/static/scripts/linny-r-gui-actor-manager.js +340 -0
  28. package/static/scripts/linny-r-gui-chart-manager.js +944 -0
  29. package/static/scripts/linny-r-gui-constraint-editor.js +681 -0
  30. package/static/scripts/linny-r-gui-controller.js +4005 -0
  31. package/static/scripts/linny-r-gui-dataset-manager.js +1176 -0
  32. package/static/scripts/linny-r-gui-documentation-manager.js +739 -0
  33. package/static/scripts/linny-r-gui-equation-manager.js +307 -0
  34. package/static/scripts/linny-r-gui-experiment-manager.js +1944 -0
  35. package/static/scripts/linny-r-gui-expression-editor.js +449 -0
  36. package/static/scripts/linny-r-gui-file-manager.js +392 -0
  37. package/static/scripts/linny-r-gui-finder.js +727 -0
  38. package/static/scripts/linny-r-gui-model-autosaver.js +230 -0
  39. package/static/scripts/linny-r-gui-monitor.js +448 -0
  40. package/static/scripts/linny-r-gui-paper.js +2789 -0
  41. package/static/scripts/linny-r-gui-receiver.js +323 -0
  42. package/static/scripts/linny-r-gui-repository-browser.js +819 -0
  43. package/static/scripts/linny-r-gui-scale-unit-manager.js +244 -0
  44. package/static/scripts/linny-r-gui-sensitivity-analysis.js +778 -0
  45. package/static/scripts/linny-r-gui-undo-redo.js +560 -0
  46. package/static/scripts/linny-r-model.js +24 -11
  47. package/static/scripts/linny-r-utils.js +10 -0
  48. package/static/scripts/linny-r-vm.js +21 -12
  49. package/static/scripts/linny-r-gui.js +0 -16908
@@ -0,0 +1,778 @@
1
+ /*
2
+ Linny-R is an executable graphical specification language for (mixed integer)
3
+ linear programming (MILP) problems, especially unit commitment problems (UCP).
4
+ The Linny-R language and tool have been developed by Pieter Bots at Delft
5
+ University of Technology, starting in 2009. The project to develop a browser-
6
+ based version started in 2017. See https://linny-r.org for more information.
7
+
8
+ This JavaScript file (linny-r-gui-sensitivity.js) provides the functionality
9
+ for the Linny-R Sensitivity Analysis dialog.
10
+
11
+ */
12
+
13
+ /*
14
+ Copyright (c) 2017-2023 Delft University of Technology
15
+
16
+ Permission is hereby granted, free of charge, to any person obtaining a copy
17
+ of this software and associated documentation files (the "Software"), to deal
18
+ in the Software without restriction, including without limitation the rights to
19
+ use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
20
+ of the Software, and to permit persons to whom the Software is furnished to do
21
+ so, subject to the following conditions:
22
+
23
+ The above copyright notice and this permission notice shall be included in
24
+ all copies or substantial portions of the Software.
25
+
26
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
27
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
28
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
29
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
30
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
31
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
32
+ SOFTWARE.
33
+ */
34
+
35
+ // CLASS GUISensitivityAnalysis implements the GUI for the parent class
36
+ // SensitivityAnalysis defined in file `linny-r-ctrl.js`
37
+ class GUISensitivityAnalysis extends SensitivityAnalysis {
38
+ constructor() {
39
+ super();
40
+ this.dialog = UI.draggableDialog('sensitivity');
41
+ UI.resizableDialog('sensitivity', 'SENSITIVITY_ANALYSIS');
42
+ this.close_btn = document.getElementById('sensitivity-close-btn');
43
+ this.close_btn.addEventListener('click', (e) => UI.toggleDialog(e));
44
+ // Control panel accepts drag/drop of entities
45
+ this.control_panel = document.getElementById('sensitivity-control-panel');
46
+ this.control_panel.addEventListener(
47
+ 'dragover', (event) => SENSITIVITY_ANALYSIS.dragOver(event));
48
+ this.control_panel.addEventListener(
49
+ 'drop', (event) => SENSITIVITY_ANALYSIS.handleDrop(event));
50
+ this.base_selectors = document.getElementById('sa-base-selectors');
51
+ this.base_selectors.addEventListener('mouseover',
52
+ (event) => SENSITIVITY_ANALYSIS.showSelectorInfo(event.shiftKey));
53
+ this.base_selectors.addEventListener(
54
+ 'focus', () => SENSITIVITY_ANALYSIS.editBaseSelectors());
55
+ this.base_selectors.addEventListener(
56
+ 'blur', () => SENSITIVITY_ANALYSIS.setBaseSelectors());
57
+
58
+ this.delta = document.getElementById('sensitivity-delta');
59
+ this.delta.addEventListener(
60
+ 'focus', () => SENSITIVITY_ANALYSIS.editDelta());
61
+ this.delta.addEventListener(
62
+ 'blur', () => SENSITIVITY_ANALYSIS.setDelta());
63
+
64
+ // NOTE: both the base selectors and the delta input blur on Enter
65
+ const blurf = (event) => { if(event.key === 'Enter') event.target.blur(); };
66
+ this.base_selectors.addEventListener('keyup', blurf);
67
+ this.delta.addEventListener('keyup', blurf);
68
+
69
+ // Make parameter buttons responsive
70
+ document.getElementById('sa-p-add-btn').addEventListener(
71
+ 'click', () => SENSITIVITY_ANALYSIS.promptForParameter());
72
+ document.getElementById('sa-p-up-btn').addEventListener(
73
+ 'click', () => SENSITIVITY_ANALYSIS.moveParameter(-1));
74
+ document.getElementById('sa-p-down-btn').addEventListener(
75
+ 'click', () => SENSITIVITY_ANALYSIS.moveParameter(1));
76
+ document.getElementById('sa-p-delete-btn').addEventListener(
77
+ 'click', () => SENSITIVITY_ANALYSIS.deleteParameter());
78
+ // Make outcome buttons responsive
79
+ document.getElementById('sa-o-add-btn').addEventListener(
80
+ 'click', () => SENSITIVITY_ANALYSIS.promptForOutcome());
81
+ document.getElementById('sa-o-up-btn').addEventListener(
82
+ 'click', () => SENSITIVITY_ANALYSIS.moveOutcome(-1));
83
+ document.getElementById('sa-o-down-btn').addEventListener(
84
+ 'click', () => SENSITIVITY_ANALYSIS.moveOutcome(1));
85
+ document.getElementById('sa-o-delete-btn').addEventListener(
86
+ 'click', () => SENSITIVITY_ANALYSIS.deleteOutcome());
87
+ // The toggle button to hide/show the control panel
88
+ this.toggle_chevron = document.getElementById('sa-toggle-chevron');
89
+ this.toggle_chevron.addEventListener(
90
+ 'click', () => SENSITIVITY_ANALYSIS.toggleControlPanel());
91
+
92
+ // The display panel and its buttons
93
+ this.display_panel = document.getElementById('sensitivity-display-panel');
94
+ this.start_btn = document.getElementById('sa-start-btn');
95
+ this.start_btn.addEventListener(
96
+ 'click', () => SENSITIVITY_ANALYSIS.start());
97
+ this.pause_btn = document.getElementById('sa-pause-btn');
98
+ this.pause_btn.addEventListener(
99
+ 'click', () => SENSITIVITY_ANALYSIS.pause());
100
+ this.stop_btn = document.getElementById('sa-stop-btn');
101
+ this.stop_btn.addEventListener(
102
+ 'click', () => SENSITIVITY_ANALYSIS.stop());
103
+ this.reset_btn = document.getElementById('sa-reset-btn');
104
+ this.reset_btn.addEventListener(
105
+ 'click', () => SENSITIVITY_ANALYSIS.clearResults());
106
+ this.progress = document.getElementById('sa-progress');
107
+ this.statistic = document.getElementById('sensitivity-statistic');
108
+ this.statistic.addEventListener(
109
+ 'change', () => SENSITIVITY_ANALYSIS.setStatistic());
110
+ // Scroll area for the outcomes table
111
+ this.scroll_area = document.getElementById('sa-scroll-area');
112
+ this.scroll_area.addEventListener(
113
+ 'mouseover', (event) => SENSITIVITY_ANALYSIS.showOutcome(event, ''));
114
+ this.table = document.getElementById('sa-table');
115
+ // Buttons at panel bottom
116
+ this.abs_rel_btn = document.getElementById('sa-abs-rel');
117
+ this.abs_rel_btn.addEventListener(
118
+ 'click', () => SENSITIVITY_ANALYSIS.toggleAbsoluteRelative());
119
+ this.color_scales = {
120
+ rb: document.getElementById('sa-rb-scale'),
121
+ no: document.getElementById('sa-no-scale')
122
+ };
123
+ const csf = (event) => SENSITIVITY_ANALYSIS.setColorScale(event);
124
+ this.color_scales.rb.addEventListener('click', csf);
125
+ this.color_scales.no.addEventListener('click', csf);
126
+ document.getElementById('sa-copy-btn').addEventListener(
127
+ 'click', () => SENSITIVITY_ANALYSIS.copyTableToClipboard());
128
+ document.getElementById('sa-copy-data-btn').addEventListener(
129
+ 'click', () => SENSITIVITY_ANALYSIS.copyDataToClipboard());
130
+ this.outcome_name = document.getElementById('sa-outcome-name');
131
+
132
+ // The add variable modal
133
+ this.variable_modal = new ModalDialog('add-sa-variable');
134
+ this.variable_modal.ok.addEventListener(
135
+ 'click', () => SENSITIVITY_ANALYSIS.addVariable());
136
+ this.variable_modal.cancel.addEventListener(
137
+ 'click', () => SENSITIVITY_ANALYSIS.variable_modal.hide());
138
+ // NOTE: the modal calls methods of the Expression Editor dialog
139
+ this.variable_modal.element('obj').addEventListener(
140
+ 'change', () => X_EDIT.updateVariableBar('add-sa-'));
141
+ this.variable_modal.element('name').addEventListener(
142
+ 'change', () => X_EDIT.updateAttributeSelector('add-sa-'));
143
+
144
+ // Initialize main dialog properties
145
+ this.reset();
146
+ }
147
+
148
+ updateDialog() {
149
+ this.updateControlPanel();
150
+ this.drawTable();
151
+ this.color_scales[this.color_scale.range].classList.add('sel-cs');
152
+ }
153
+
154
+ updateControlPanel() {
155
+ // Shows the control panel, or when the analysis is running the
156
+ // legend to the outcomes (also to prevent changes to parameters)
157
+ this.base_selectors.value = MODEL.base_case_selectors;
158
+ this.delta.value = VM.sig4Dig(MODEL.sensitivity_delta);
159
+ const tr = [];
160
+ for(let i = 0; i < MODEL.sensitivity_parameters.length; i++) {
161
+ const p = MODEL.sensitivity_parameters[i];
162
+ tr.push('<tr class="dataset',
163
+ (this.selected_parameter === i ? ' sel-set' : ''),
164
+ '" onclick="SENSITIVITY_ANALYSIS.selectParameter(', i, ');">',
165
+ '<td class="v-box"><div id="sap-box-', i, '" class="vbox',
166
+ (this.checked_parameters[p] ? ' crossed' : ' clear'),
167
+ '" onclick="SENSITIVITY_ANALYSIS.toggleParameter(', i,
168
+ ');"></div></td><td>', p, '</td></tr>');
169
+ }
170
+ document.getElementById('sa-p-table').innerHTML = tr.join('');
171
+ tr.length = 0;
172
+ for(let i = 0; i < MODEL.sensitivity_outcomes.length; i++) {
173
+ const o = MODEL.sensitivity_outcomes[i];
174
+ tr.push('<tr class="dataset',
175
+ (this.selected_outcome === i ? ' sel-set' : ''),
176
+ '" onclick="SENSITIVITY_ANALYSIS.selectOutcome(', i, ');">',
177
+ '<td class="v-box"><div id="sao-box-', i, '" class="vbox',
178
+ (this.checked_outcomes[o] ? ' crossed' : ' clear'),
179
+ '" onclick="SENSITIVITY_ANALYSIS.toggleOutcome(', i,
180
+ ');"></div></td><td>', o, '</td></tr>');
181
+ }
182
+ document.getElementById('sa-o-table').innerHTML = tr.join('');
183
+ this.updateControlButtons('p');
184
+ this.updateControlButtons('o');
185
+ // NOTE: allow run without parameters, but not without outcomes
186
+ if(MODEL.sensitivity_outcomes.length > 0) {
187
+ this.start_btn.classList.remove('disab');
188
+ this.start_btn.classList.add('enab');
189
+ } else {
190
+ this.start_btn.classList.remove('enab');
191
+ this.start_btn.classList.add('disab');
192
+ }
193
+ // Show the "clear results" button only when selected experiment has run
194
+ if(MODEL.sensitivity_runs.length > 0) {
195
+ this.reset_btn.classList.remove('off');
196
+ } else {
197
+ this.reset_btn.classList.add('off');
198
+ this.progress.innerHTML = '';
199
+ }
200
+ this.variable_modal.element('obj').value = 0;
201
+ // Update variable dropdown list of the "add SA variable" modal using
202
+ // a method of the Expression Editor dialog
203
+ X_EDIT.updateVariableBar('add-sa-');
204
+ }
205
+
206
+ updateControlButtons(b) {
207
+ const
208
+ up = document.getElementById(`sa-${b}-up-btn`),
209
+ down = document.getElementById(`sa-${b}-down-btn`),
210
+ del = document.getElementById(`sa-${b}-delete-btn`);
211
+ let index, last;
212
+ if(b === 'p') {
213
+ index = this.selected_parameter;
214
+ last = MODEL.sensitivity_parameters.length - 1;
215
+ } else {
216
+ index = this.selected_outcome;
217
+ last = MODEL.sensitivity_outcomes.length - 1;
218
+ }
219
+ up.classList.add('v-disab');
220
+ down.classList.add('v-disab');
221
+ del.classList.add('v-disab');
222
+ if(index >= 0) {
223
+ del.classList.remove('v-disab');
224
+ if(index > 0) up.classList.remove('v-disab');
225
+ if(index < last) down.classList.remove('v-disab');
226
+ }
227
+ }
228
+
229
+ toggleControlPanel() {
230
+ if(this.options_shown) {
231
+ this.control_panel.style.display = 'none';
232
+ this.display_panel.style.left = '1px';
233
+ this.display_panel.style.width = 'calc(100% - 8px)';
234
+ this.toggle_chevron.innerHTML = '&raquo;';
235
+ this.toggle_chevron.title = 'Show control panel';
236
+ this.options_shown = false;
237
+ } else {
238
+ this.control_panel.style.display = 'block';
239
+ this.display_panel.style.left = 'calc(40% + 2px)';
240
+ this.display_panel.style.width = 'calc(60% - 5px)';
241
+ this.toggle_chevron.innerHTML = '&laquo;';
242
+ this.toggle_chevron.title = 'Hide control panel';
243
+ this.options_shown = true;
244
+ }
245
+ }
246
+
247
+ showSelectorInfo(shift) {
248
+ // Called when cursor is moved over the base selectors input field
249
+ if(shift && MODEL.base_case_selectors.length > 0) {
250
+ // When selector(s) are specified and shift is pressed, show info on
251
+ // what the selectors constitute as base scenario
252
+ this.showBaseCaseInfo();
253
+ return;
254
+ }
255
+ // Otherwise, display list of all dataset selectors in docu-viewer
256
+ if(DOCUMENTATION_MANAGER.visible) {
257
+ const
258
+ ds_dict = MODEL.listOfAllSelectors,
259
+ html = [],
260
+ sl = Object.keys(ds_dict).sort((a, b) => UI.compareFullNames(a, b, true));
261
+ for(let i = 0; i < sl.length; i++) {
262
+ const
263
+ s = sl[i],
264
+ dl = ds_dict[s],
265
+ dnl = [],
266
+ bs = (dl.length > 1 ?
267
+ ' style="border: 0.5px solid #a080c0; border-right: none"' : '');
268
+ for(let j = 0; j < dl.length; j++) {
269
+ dnl.push(dl[j].displayName);
270
+ }
271
+ html.push('<tr><td class="sa-ds-sel" ',
272
+ 'onclick="SENSITIVITY_ANALYSIS.toggleSelector(this);">',
273
+ s, '</td><td', bs, '>', dnl.join('<br>'), '</td></tr>');
274
+ }
275
+ if(html.length > 0) {
276
+ // Display information as read-only HTML
277
+ DOCUMENTATION_MANAGER.title.innerText = 'Dataset selectors';
278
+ DOCUMENTATION_MANAGER.viewer.innerHTML =
279
+ '<table><tr><td><strong>Selector</strong></td>' +
280
+ '<td><strong>Dataset(s)</strong><td></tr>' + html.join('') +
281
+ '</table>';
282
+ DOCUMENTATION_MANAGER.edit_btn.classList.remove('enab');
283
+ DOCUMENTATION_MANAGER.edit_btn.classList.add('disab');
284
+ }
285
+ }
286
+ }
287
+
288
+ showBaseCaseInfo() {
289
+ // Display information on the base case selectors combination if docu-viewer
290
+ // is visible and cursor is moved over base case input field
291
+ const combi = MODEL.base_case_selectors.split(' ');
292
+ if(combi.length > 0 && DOCUMENTATION_MANAGER.visible) {
293
+ const
294
+ info = {},
295
+ html = [],
296
+ list = [];
297
+ info.title = `Base scenario: <tt>${tupelString(combi)}</tt>`;
298
+ for(let i = 0; i < combi.length; i++) {
299
+ const sel = combi[i];
300
+ html.push('<h3>Selector <tt>', sel, '</tt></h3>');
301
+ // List associated datasets (if any)
302
+ list.length = 0;
303
+ for(let id in MODEL.datasets) if(MODEL.datasets.hasOwnProperty(id)) {
304
+ const ds = MODEL.datasets[id];
305
+ for(let k in ds.modifiers) if(ds.modifiers.hasOwnProperty(k)) {
306
+ if(ds.modifiers[k].match(sel)) {
307
+ list.push('<li>', MODEL.datasets[id].displayName,
308
+ '<span class="dsx">',
309
+ MODEL.datasets[id].modifiers[UI.nameToID(sel)].expression.text,
310
+ '</span></li>');
311
+ }
312
+ }
313
+ }
314
+ if(list.length > 0) {
315
+ html.push('<em>Datasets:</em> <ul>', list.join(''), '</ul>');
316
+ }
317
+ info.html = html.join('');
318
+ // Display information as read-only HTML
319
+ DOCUMENTATION_MANAGER.title.innerHTML = info.title;
320
+ DOCUMENTATION_MANAGER.viewer.innerHTML = info.html;
321
+ DOCUMENTATION_MANAGER.edit_btn.classList.remove('enab');
322
+ DOCUMENTATION_MANAGER.edit_btn.classList.add('disab');
323
+ }
324
+ }
325
+ }
326
+
327
+ toggleSelector(obj) {
328
+ const
329
+ sel = obj.textContent,
330
+ bsl = MODEL.base_case_selectors.split(' '),
331
+ index = bsl.indexOf(sel);
332
+ if(index >= 0) {
333
+ bsl.splice(index, 1);
334
+ } else {
335
+ bsl.push(sel);
336
+ }
337
+ MODEL.base_case_selectors = bsl.join(' ');
338
+ this.base_selectors.value = MODEL.base_case_selectors;
339
+ }
340
+
341
+ editBaseSelectors() {
342
+ // Give visual feedback by setting background color to white
343
+ this.base_selectors.style.backgroundColor = 'white';
344
+ }
345
+
346
+ setBaseSelectors() {
347
+ // Sanitize string before accepting it as space-separated selector list
348
+ const
349
+ sl = this.base_selectors.value.replace(/[\;\,]/g, ' ').trim().replace(
350
+ /[^a-zA-Z0-9\+\-\%\_\s]/g, '').split(/\s+/),
351
+ bs = sl.join(' '),
352
+ sd = MODEL.listOfAllSelectors,
353
+ us = [];
354
+ for(let i = 0; i < sl.length; i++) {
355
+ if(sl[i].length > 0 && !(sl[i] in sd)) us.push(sl[i]);
356
+ }
357
+ if(us.length > 0) {
358
+ UI.warn('Base contains ' + pluralS(us.length, 'unknown selector') +
359
+ ': "' + us.join('", "') + '"');
360
+ } else if(MODEL.base_case_selectors !== bs &&
361
+ MODEL.sensitivity_runs.length > 0) {
362
+ UI.notify('Change may have invalidated the analysis results');
363
+ }
364
+ MODEL.base_case_selectors = bs;
365
+ this.base_selectors.value = bs;
366
+ this.base_selectors.style.backgroundColor = 'inherit';
367
+ }
368
+
369
+ editDelta() {
370
+ // Give visual feedback by setting background color to white
371
+ this.delta.backgroundColor = 'white';
372
+ }
373
+
374
+ setDelta() {
375
+ const
376
+ did = 'sensitivity-delta',
377
+ d = UI.validNumericInput(did, 'Delta');
378
+ if(d !== false) {
379
+ if(MODEL.sensitivity_delta !== d && MODEL.sensitivity_runs.length > 0) {
380
+ UI.notify('Change may have invalidated the analysis results');
381
+ }
382
+ MODEL.sensitivity_delta = d;
383
+ document.getElementById(did).style.backgroundColor = 'inherit';
384
+ }
385
+ this.updateDialog();
386
+ }
387
+
388
+ selectParameter(p) {
389
+ this.selected_parameter = p;
390
+ this.updateControlPanel();
391
+ }
392
+
393
+ selectOutcome(o) {
394
+ this.selected_outcome = o;
395
+ this.updateControlPanel();
396
+ }
397
+
398
+ toggleParameter(n) {
399
+ const p = MODEL.sensitivity_parameters[n];
400
+ let c = false;
401
+ if(p in this.checked_parameters) c = this.checked_parameters[p];
402
+ this.checked_parameters[p] = !c;
403
+ this.drawTable();
404
+ }
405
+
406
+ toggleOutcome(n) {
407
+ const o = MODEL.sensitivity_outcomes[n];
408
+ let c = false;
409
+ if(o in this.checked_outcomes) c = this.checked_outcomes[o];
410
+ this.checked_outcomes[o] = !c;
411
+ this.drawTable();
412
+ }
413
+
414
+ moveParameter(dir) {
415
+ let n = this.selected_parameter;
416
+ if(n < 0) return;
417
+ if(dir > 0 && n < MODEL.sensitivity_parameters.length - 1 ||
418
+ dir < 0 && n > 0) {
419
+ n += dir;
420
+ const v = MODEL.sensitivity_parameters.splice(this.selected_parameter, 1)[0];
421
+ MODEL.sensitivity_parameters.splice(n, 0, v);
422
+ this.selected_parameter = n;
423
+ }
424
+ this.updateDialog();
425
+ }
426
+
427
+ moveOutcome(dir) {
428
+ let n = this.selected_outcome;
429
+ if(n < 0) return;
430
+ if(dir > 0 && n < MODEL.sensitivity_outcomes.length - 1 ||
431
+ dir < 0 && n > 0) {
432
+ n += dir;
433
+ const v = MODEL.sensitivity_outcomes.splice(this.selected_outcome, 1)[0];
434
+ MODEL.sensitivity_outcomes.splice(n, 0, v);
435
+ this.selected_outcome = n;
436
+ }
437
+ this.updateDialog();
438
+ }
439
+
440
+ promptForParameter() {
441
+ // Open dialog for adding new parameter
442
+ const md = this.variable_modal;
443
+ md.element('type').innerText = 'parameter';
444
+ // NOTE: clusters have no suitable attributes, and equations are endogenous
445
+ md.element('cluster').style.display = 'none';
446
+ md.element('equation').style.display = 'none';
447
+ // NOTE: update to ensure that valid attributes are selectable
448
+ X_EDIT.updateVariableBar('add-sa-');
449
+ md.show();
450
+ }
451
+
452
+ promptForOutcome() {
453
+ // Open dialog for adding new outcome
454
+ const md = this.variable_modal;
455
+ md.element('type').innerText = 'outcome';
456
+ md.element('cluster').style.display = 'block';
457
+ md.element('equation').style.display = 'block';
458
+ // NOTE: update to ensure that valid attributes are selectable
459
+ X_EDIT.updateVariableBar('add-sa-');
460
+ md.show();
461
+ }
462
+
463
+ dragOver(ev) {
464
+ const
465
+ tid = ev.target.id,
466
+ ok = (tid.startsWith('sa-p-') || tid.startsWith('sa-o-')),
467
+ n = ev.dataTransfer.getData('text'),
468
+ obj = MODEL.objectByID(n);
469
+ if(ok && obj) ev.preventDefault();
470
+ }
471
+
472
+ handleDrop(ev) {
473
+ // Prompt for attribute if dropped object is a suitable entity
474
+ ev.preventDefault();
475
+ const
476
+ tid = ev.target.id,
477
+ param = tid.startsWith('sa-p-'),
478
+ n = ev.dataTransfer.getData('text'),
479
+ obj = MODEL.objectByID(n);
480
+ if(!obj) {
481
+ UI.alert(`Unknown entity ID "${n}"`);
482
+ } else if(param && obj instanceof Cluster) {
483
+ UI.warn('Clusters do not have exogenous attributes');
484
+ } else if(obj instanceof DatasetModifier) {
485
+ if(param) {
486
+ UI.warn('Equations can only be outcomes');
487
+ } else {
488
+ MODEL.sensitivity_outcomes.push(obj.displayName);
489
+ this.updateDialog();
490
+ }
491
+ } else {
492
+ const vt = this.variable_modal.element('type');
493
+ if(param) {
494
+ vt.innerText = 'parameter';
495
+ } else {
496
+ vt.innerText = 'outcome';
497
+ }
498
+ this.variable_modal.show();
499
+ const
500
+ tn = VM.object_types.indexOf(obj.type),
501
+ dn = obj.displayName;
502
+ this.variable_modal.element('obj').value = tn;
503
+ X_EDIT.updateVariableBar('add-sa-');
504
+ const s = this.variable_modal.element('name');
505
+ let i = 0;
506
+ for(let k in s.options) if(s.options.hasOwnProperty(k)) {
507
+ if(s[k].text === dn) {
508
+ i = s[k].value;
509
+ break;
510
+ }
511
+ }
512
+ s.value = i;
513
+ // NOTE: use method of the Expression Editor, specifying the SA prefix
514
+ X_EDIT.updateAttributeSelector('add-sa-');
515
+ }
516
+ }
517
+
518
+ addVariable() {
519
+ // Add parameter or outcome to the respective list
520
+ const
521
+ md = this.variable_modal,
522
+ t = md.element('type').innerText,
523
+ e = md.selectedOption('obj').text,
524
+ o = md.selectedOption('name').text,
525
+ a = md.selectedOption('attr').text;
526
+ let n = '';
527
+ if(e === 'Equation' && a) {
528
+ // For equations, the attribute denotes the name
529
+ n = a;
530
+ } else if(o && a) {
531
+ // Most variables are defined by name + attribute ...
532
+ n = o + UI.OA_SEPARATOR + a;
533
+ } else if(e === 'Dataset' && o) {
534
+ // ... but for datasets the selector is optional
535
+ n = o;
536
+ }
537
+ if(n) {
538
+ if(t === 'parameter' && MODEL.sensitivity_parameters.indexOf(n) < 0) {
539
+ MODEL.sensitivity_parameters.push(n);
540
+ } else if(t === 'outcome' && MODEL.sensitivity_outcomes.indexOf(n) < 0) {
541
+ MODEL.sensitivity_outcomes.push(n);
542
+ }
543
+ this.updateDialog();
544
+ }
545
+ md.hide();
546
+ }
547
+
548
+ deleteParameter() {
549
+ // Remove selected parameter from the analysis
550
+ MODEL.sensitivity_parameters.splice(this.selected_parameter, 1);
551
+ this.selected_parameter = -1;
552
+ this.updateDialog();
553
+ }
554
+
555
+ deleteOutcome() {
556
+ // Remove selected outcome from the analysis
557
+ MODEL.sensitivity_outcomes.splice(this.selected_outcome, 1);
558
+ this.selected_outcome = -1;
559
+ this.updateDialog();
560
+ }
561
+
562
+ pause() {
563
+ // Interrupt solver but retain data on server and allow resume
564
+ UI.notify('Run sequence will be suspended after the current run');
565
+ this.pause_btn.classList.add('blink');
566
+ this.stop_btn.classList.remove('off');
567
+ this.must_pause = true;
568
+ }
569
+
570
+ resumeButtons() {
571
+ // Changes buttons to "running" state, and return TRUE if state was "paused"
572
+ const paused = this.start_btn.classList.contains('blink');
573
+ this.start_btn.classList.remove('blink');
574
+ this.start_btn.classList.add('off');
575
+ this.pause_btn.classList.remove('off');
576
+ this.stop_btn.classList.add('off');
577
+ this.must_pause = false;
578
+ return paused;
579
+ }
580
+
581
+ readyButtons() {
582
+ // Sets experiment run control buttons in "ready" state
583
+ this.pause_btn.classList.add('off');
584
+ this.stop_btn.classList.add('off');
585
+ this.start_btn.classList.remove('off', 'blink');
586
+ this.must_pause = false;
587
+ }
588
+
589
+ pausedButtons(aci) {
590
+ // Sets experiment run control buttons in "paused" state
591
+ this.pause_btn.classList.remove('blink');
592
+ this.pause_btn.classList.add('off');
593
+ this.start_btn.classList.remove('off');
594
+ // Blinking start button indicates: paused -- click to resume
595
+ this.start_btn.classList.add('blink');
596
+ this.progress.innerHTML = `Run ${aci} PAUSED`;
597
+ }
598
+
599
+ clearResults() {
600
+ // Clears results, and resets control buttons
601
+ MODEL.sensitivity_runs.length = 0;
602
+ this.readyButtons();
603
+ this.reset_btn.classList.add('off');
604
+ this.selected_run = -1;
605
+ this.must_pause = false;
606
+ this.progress.innerHTML = '';
607
+ this.updateDialog();
608
+ }
609
+
610
+ setProgress(msg) {
611
+ // Shows `msg` in the progress field of the dialog
612
+ this.progress.innerHTML = msg;
613
+ }
614
+
615
+ showCheckmark(t) {
616
+ // Shows green checkmark (with elapsed time `t` as title) in progress field
617
+ this.progress.innerHTML =
618
+ `<span class="x-checked" title="${t}">&#10004;</span>`;
619
+ }
620
+
621
+ drawTable() {
622
+ // Draws sensitivity analysis as table
623
+ const
624
+ html = [],
625
+ pl = MODEL.sensitivity_parameters.length,
626
+ ol = MODEL.sensitivity_outcomes.length;
627
+ if(ol === 0) {
628
+ this.table.innerHTML = '';
629
+ return;
630
+ }
631
+ html.push('<tr><td colspan="2"></td>');
632
+ for(let i = 0; i < ol; i++) {
633
+ const o = MODEL.sensitivity_outcomes[i];
634
+ if(!this.checked_outcomes[o]) {
635
+ html.push('<td class="sa-col-hdr" ',
636
+ 'onmouseover="SENSITIVITY_ANALYSIS.showOutcome(event, \'', o, '\');">',
637
+ i+1, '</td>');
638
+ }
639
+ }
640
+ html.push('</tr><tr class="sa-p-row" ',
641
+ 'onclick="SENSITIVITY_ANALYSIS.selectRun(0);">',
642
+ '<td colspan="2" class="sa-row-hdr"><em>Base scenario</em></td>');
643
+ for(let i = 0; i < ol; i++) {
644
+ const o = MODEL.sensitivity_outcomes[i];
645
+ if(!this.checked_outcomes[o]) {
646
+ html.push('<td id="sa-r0c', i,
647
+ '" onmouseover="SENSITIVITY_ANALYSIS.showOutcome(event, \'',
648
+ o, '\');"></td>');
649
+ }
650
+ }
651
+ html.push('</tr>');
652
+ const
653
+ sdelta = (MODEL.sensitivity_delta >= 0 ? '+' : '') +
654
+ VM.sig4Dig(MODEL.sensitivity_delta) + '%',
655
+ dc = sdelta.startsWith('+') ? 'sa-plus' : 'sa-minus';
656
+ for(let i = 0; i < pl; i++) {
657
+ const p = MODEL.sensitivity_parameters[i];
658
+ if(!this.checked_parameters[p]) {
659
+ html.push('<tr class="sa-p-row" ',
660
+ 'onclick="SENSITIVITY_ANALYSIS.selectRun(', i+1, ');">',
661
+ '<td class="sa-row-hdr" title="', p, '">', p,
662
+ '</td><td class="', dc, '">', sdelta, '</td>');
663
+ for(let j = 0; j < MODEL.sensitivity_outcomes.length; j++) {
664
+ const o = MODEL.sensitivity_outcomes[j];
665
+ if(!this.checked_outcomes[o]) {
666
+ html.push('<td id="sa-r', i+1, 'c', j,
667
+ '" onmouseover="SENSITIVITY_ANALYSIS.showOutcome(event, \'',
668
+ o, '\');"></td>');
669
+ }
670
+ }
671
+ }
672
+ html.push('</tr>');
673
+ }
674
+ this.table.innerHTML = html.join('');
675
+ if(this.selected_run >= 0) document.getElementById(
676
+ `sa-r${this.selected_run}c0`).parentNode.classList.add('sa-p-sel');
677
+ this.updateData();
678
+ }
679
+
680
+ updateData() {
681
+ // Fills table cells with their data value or status
682
+ const
683
+ pl = MODEL.sensitivity_parameters.length,
684
+ ol = MODEL.sensitivity_outcomes.length,
685
+ rl = MODEL.sensitivity_runs.length;
686
+ if(ol === 0) return;
687
+ // NOTE: computeData is a parent class method
688
+ this.computeData(this.selected_statistic);
689
+ // Draw per row (i) where i=0 is the base case
690
+ for(let i = 0; i <= pl; i ++) {
691
+ if(i < 1 || !this.checked_parameters[MODEL.sensitivity_parameters[i-1]]) {
692
+ for(let j = 0; j < ol; j++) {
693
+ if(!this.checked_outcomes[MODEL.sensitivity_outcomes[j]]) {
694
+ const c = document.getElementById(`sa-r${i}c${j}`);
695
+ c.classList.add('sa-data');
696
+ if(i >= rl) {
697
+ c.classList.add('sa-not-run');
698
+ } else {
699
+ if(i < 1) {
700
+ c.classList.add('sa-brd');
701
+ } else if(this.color_scale.range === 'no') {
702
+ c.style.backgroundColor = 'white';
703
+ } else {
704
+ c.style.backgroundColor =
705
+ this.color_scale.rgb(this.shade[j][i - 1]);
706
+ }
707
+ if(i > 0 && this.relative_scale) {
708
+ let p = this.perc[j][i - 1];
709
+ // Replace warning sign by dash
710
+ if(p === '\u26A0') p = '-';
711
+ c.innerText = p + (p !== '-' ? '%' : '');
712
+ } else {
713
+ c.innerText = this.data[j][i];
714
+ }
715
+ }
716
+ }
717
+ }
718
+ }
719
+ }
720
+ }
721
+
722
+ showOutcome(event, o) {
723
+ // Displays outcome `o` (the name of the variable) below the table
724
+ event.stopPropagation();
725
+ this.outcome_name.innerHTML = o;
726
+ }
727
+
728
+ selectRun(n) {
729
+ // Selects run `n`, or toggles if already selected
730
+ const rows = this.scroll_area.getElementsByClassName('sa-p-sel');
731
+ for(let i = 0; i < rows.length; i++) {
732
+ rows.item(i).classList.remove('sa-p-sel');
733
+ }
734
+ if(n === this.selected_run) {
735
+ this.selected_run = -1;
736
+ } else if(n < MODEL.sensitivity_runs.length) {
737
+ this.selected_run = n;
738
+ if(n >= 0) document.getElementById(
739
+ `sa-r${n}c0`).parentNode.classList.add('sa-p-sel');
740
+ }
741
+ VM.setRunMessages(this.selected_run);
742
+ }
743
+
744
+ setStatistic() {
745
+ // Update view for selected variable
746
+ this.selected_statistic = this.statistic.value;
747
+ this.updateData();
748
+ }
749
+
750
+ toggleAbsoluteRelative() {
751
+ // Toggles between # (absolute) and % (relative) display of outcome values
752
+ this.relative_scale = !this.relative_scale;
753
+ this.abs_rel_btn.innerText = (this.relative_scale ? '%' : '#');
754
+ this.updateData();
755
+ }
756
+
757
+ setColorScale(event) {
758
+ // Infers clicked color scale button from event, and selects it
759
+ if(event) {
760
+ const cs = event.target.id.split('-')[1];
761
+ this.color_scale.set(cs);
762
+ this.color_scales.rb.classList.remove('sel-cs');
763
+ this.color_scales.no.classList.remove('sel-cs');
764
+ this.color_scales[cs].classList.add('sel-cs');
765
+ }
766
+ this.updateData();
767
+ }
768
+
769
+ copyTableToClipboard() {
770
+ UI.copyHtmlToClipboard(this.scroll_area.innerHTML);
771
+ UI.notify('Table copied to clipboard (as HTML)');
772
+ }
773
+
774
+ copyDataToClipboard() {
775
+ UI.notify(UI.NOTICE.WORK_IN_PROGRESS);
776
+ }
777
+
778
+ } // END of class SensitivityAnalysis