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.
- package/README.md +102 -48
- package/package.json +1 -1
- package/server.js +31 -6
- package/static/images/check-off-not-same-changed.png +0 -0
- package/static/images/check-off-not-same-not-changed.png +0 -0
- package/static/images/check-off-same-changed.png +0 -0
- package/static/images/check-off-same-not-changed.png +0 -0
- package/static/images/check-on-not-same-changed.png +0 -0
- package/static/images/check-on-not-same-not-changed.png +0 -0
- package/static/images/check-on-same-changed.png +0 -0
- package/static/images/check-on-same-not-changed.png +0 -0
- package/static/images/eq-not-same-changed.png +0 -0
- package/static/images/eq-not-same-not-changed.png +0 -0
- package/static/images/eq-same-changed.png +0 -0
- package/static/images/eq-same-not-changed.png +0 -0
- package/static/images/ne-not-same-changed.png +0 -0
- package/static/images/ne-not-same-not-changed.png +0 -0
- package/static/images/ne-same-changed.png +0 -0
- package/static/images/ne-same-not-changed.png +0 -0
- package/static/images/sort-asc-lead.png +0 -0
- package/static/images/sort-asc.png +0 -0
- package/static/images/sort-desc-lead.png +0 -0
- package/static/images/sort-desc.png +0 -0
- package/static/images/sort-not.png +0 -0
- package/static/index.html +51 -35
- package/static/linny-r.css +167 -53
- package/static/scripts/linny-r-gui-actor-manager.js +340 -0
- package/static/scripts/linny-r-gui-chart-manager.js +944 -0
- package/static/scripts/linny-r-gui-constraint-editor.js +681 -0
- package/static/scripts/linny-r-gui-controller.js +4005 -0
- package/static/scripts/linny-r-gui-dataset-manager.js +1176 -0
- package/static/scripts/linny-r-gui-documentation-manager.js +739 -0
- package/static/scripts/linny-r-gui-equation-manager.js +307 -0
- package/static/scripts/linny-r-gui-experiment-manager.js +1944 -0
- package/static/scripts/linny-r-gui-expression-editor.js +449 -0
- package/static/scripts/linny-r-gui-file-manager.js +392 -0
- package/static/scripts/linny-r-gui-finder.js +727 -0
- package/static/scripts/linny-r-gui-model-autosaver.js +230 -0
- package/static/scripts/linny-r-gui-monitor.js +448 -0
- package/static/scripts/linny-r-gui-paper.js +2789 -0
- package/static/scripts/linny-r-gui-receiver.js +323 -0
- package/static/scripts/linny-r-gui-repository-browser.js +819 -0
- package/static/scripts/linny-r-gui-scale-unit-manager.js +244 -0
- package/static/scripts/linny-r-gui-sensitivity-analysis.js +778 -0
- package/static/scripts/linny-r-gui-undo-redo.js +560 -0
- package/static/scripts/linny-r-model.js +24 -11
- package/static/scripts/linny-r-utils.js +10 -0
- package/static/scripts/linny-r-vm.js +21 -12
- 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 = '»';
|
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 = '«';
|
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}">✔</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
|