linny-r 1.4.3 → 1.4.5

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 +450 -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 +34 -15
  47. package/static/scripts/linny-r-utils.js +11 -1
  48. package/static/scripts/linny-r-vm.js +21 -12
  49. package/static/scripts/linny-r-gui.js +0 -16908
@@ -0,0 +1,1944 @@
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-expmgr.js) provides the GUI functionality
9
+ for the Linny-R Experiment Manager 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 GUIExperimentManager provides the experiment dialog functionality
36
+ class GUIExperimentManager extends ExperimentManager {
37
+ constructor() {
38
+ super();
39
+ this.dialog = UI.draggableDialog('experiment');
40
+ UI.resizableDialog('experiment', 'EXPERIMENT_MANAGER');
41
+ this.new_btn = document.getElementById('xp-new-btn');
42
+ this.new_btn.addEventListener(
43
+ 'click', () => EXPERIMENT_MANAGER.promptForExperiment());
44
+ document.getElementById('xp-rename-btn').addEventListener(
45
+ 'click', () => EXPERIMENT_MANAGER.promptForName());
46
+ this.view_btn = document.getElementById('xp-view-btn');
47
+ this.view_btn.addEventListener(
48
+ 'click', () => EXPERIMENT_MANAGER.viewerMode());
49
+ this.reset_btn = document.getElementById('xp-reset-btn');
50
+ this.reset_btn.addEventListener(
51
+ 'click', () => EXPERIMENT_MANAGER.clearRunResults());
52
+ document.getElementById('xp-delete-btn').addEventListener(
53
+ 'click', () => EXPERIMENT_MANAGER.deleteExperiment());
54
+ this.default_message = document.getElementById('experiment-default-message');
55
+
56
+ this.design = document.getElementById('experiment-design');
57
+ this.experiment_table = document.getElementById('experiment-table');
58
+ this.params_div = document.getElementById('experiment-params-div');
59
+ this.dimension_table = document.getElementById('experiment-dim-table');
60
+ this.chart_table = document.getElementById('experiment-chart-table');
61
+ // NOTE: the Exclude input field responds to several events
62
+ this.exclude = document.getElementById('experiment-exclude');
63
+ this.exclude.addEventListener(
64
+ 'focus', () => EXPERIMENT_MANAGER.editExclusions());
65
+ this.exclude.addEventListener(
66
+ 'keyup', (event) => { if(event.key === 'Enter') event.target.blur(); });
67
+ this.exclude.addEventListener(
68
+ 'blur', () => EXPERIMENT_MANAGER.setExclusions());
69
+
70
+ // Viewer pane controls
71
+ this.viewer = document.getElementById('experiment-viewer');
72
+ this.viewer.addEventListener(
73
+ 'mousemove', (event) => EXPERIMENT_MANAGER.showInfo(-1, event.shiftKey));
74
+ this.viewer_progress = document.getElementById('viewer-progress');
75
+ this.start_btn = document.getElementById('xv-start-btn');
76
+ this.start_btn.addEventListener(
77
+ 'click', () => EXPERIMENT_MANAGER.startExperiment());
78
+ this.pause_btn = document.getElementById('xv-pause-btn');
79
+ this.pause_btn.addEventListener(
80
+ 'click', () => EXPERIMENT_MANAGER.pauseExperiment());
81
+ this.stop_btn = document.getElementById('xv-stop-btn');
82
+ this.stop_btn.addEventListener(
83
+ 'click', () => EXPERIMENT_MANAGER.stopExperiment());
84
+
85
+ // Make other dialog buttons responsive
86
+ document.getElementById('experiment-close-btn').addEventListener(
87
+ 'click', (event) => UI.toggleDialog(event));
88
+ document.getElementById('xp-d-add-btn').addEventListener(
89
+ 'click', () => EXPERIMENT_MANAGER.promptForParameter('dimension'));
90
+ document.getElementById('xp-d-up-btn').addEventListener(
91
+ 'click', () => EXPERIMENT_MANAGER.moveDimension(-1));
92
+ document.getElementById('xp-d-down-btn').addEventListener(
93
+ 'click', () => EXPERIMENT_MANAGER.moveDimension(1));
94
+ document.getElementById('xp-d-settings-btn').addEventListener(
95
+ 'click', () => EXPERIMENT_MANAGER.editSettingsDimensions());
96
+ document.getElementById('xp-d-iterator-btn').addEventListener(
97
+ 'click', () => EXPERIMENT_MANAGER.editIteratorRanges());
98
+ document.getElementById('xp-d-combination-btn').addEventListener(
99
+ 'click', () => EXPERIMENT_MANAGER.editCombinationDimensions());
100
+ document.getElementById('xp-d-actor-btn').addEventListener(
101
+ 'click', () => EXPERIMENT_MANAGER.editActorDimension());
102
+ document.getElementById('xp-d-delete-btn').addEventListener(
103
+ 'click', () => EXPERIMENT_MANAGER.deleteParameter());
104
+ document.getElementById('xp-c-add-btn').addEventListener(
105
+ 'click', () => EXPERIMENT_MANAGER.promptForParameter('chart'));
106
+ document.getElementById('xp-c-delete-btn').addEventListener(
107
+ 'click', () => EXPERIMENT_MANAGER.deleteParameter());
108
+ document.getElementById('xp-ignore-btn').addEventListener(
109
+ 'click', () => EXPERIMENT_MANAGER.showClustersToIgnore());
110
+ document.getElementById('xv-back-btn').addEventListener(
111
+ 'click', () => EXPERIMENT_MANAGER.designMode());
112
+ document.getElementById('xv-copy-btn').addEventListener(
113
+ 'click', () => EXPERIMENT_MANAGER.copyTableToClipboard());
114
+ document.getElementById('xv-download-btn').addEventListener(
115
+ 'click', () => EXPERIMENT_MANAGER.promptForDownload());
116
+ // The viewer's drop-down selectors
117
+ document.getElementById('viewer-variable').addEventListener(
118
+ 'change', () => EXPERIMENT_MANAGER.setVariable());
119
+ document.getElementById('viewer-statistic').addEventListener(
120
+ 'change', () => EXPERIMENT_MANAGER.setStatistic());
121
+ document.getElementById('viewer-scale').addEventListener(
122
+ 'change', () => EXPERIMENT_MANAGER.setScale());
123
+ // The spin buttons
124
+ document.getElementById('xp-cd-minus').addEventListener(
125
+ 'click', () => EXPERIMENT_MANAGER.updateSpinner('c', -1));
126
+ document.getElementById('xp-cd-plus').addEventListener(
127
+ 'click', () => EXPERIMENT_MANAGER.updateSpinner('c', 1));
128
+ document.getElementById('xp-sd-minus').addEventListener(
129
+ 'click', () => EXPERIMENT_MANAGER.updateSpinner('s', -1));
130
+ document.getElementById('xp-sd-plus').addEventListener(
131
+ 'click', () => EXPERIMENT_MANAGER.updateSpinner('s', 1));
132
+ // The color scale buttons have ID `xv-NN-scale` where NN defines the scale
133
+ const csf = (event) =>
134
+ EXPERIMENT_MANAGER.setColorScale(event.target.id.split('-')[1]);
135
+ document.getElementById('xv-rb-scale').addEventListener('click', csf);
136
+ document.getElementById('xv-br-scale').addEventListener('click', csf);
137
+ document.getElementById('xv-rg-scale').addEventListener('click', csf);
138
+ document.getElementById('xv-gr-scale').addEventListener('click', csf);
139
+ document.getElementById('xv-no-scale').addEventListener('click', csf);
140
+
141
+ // Create modal dialogs for the Experiment Manager
142
+ this.new_modal = new ModalDialog('xp-new');
143
+ this.new_modal.ok.addEventListener(
144
+ 'click', () => EXPERIMENT_MANAGER.newExperiment());
145
+ this.new_modal.cancel.addEventListener(
146
+ 'click', () => EXPERIMENT_MANAGER.new_modal.hide());
147
+
148
+ this.rename_modal = new ModalDialog('xp-rename');
149
+ this.rename_modal.ok.addEventListener(
150
+ 'click', () => EXPERIMENT_MANAGER.renameExperiment());
151
+ this.rename_modal.cancel.addEventListener(
152
+ 'click', () => EXPERIMENT_MANAGER.rename_modal.hide());
153
+
154
+ this.parameter_modal = new ModalDialog('xp-parameter');
155
+ this.parameter_modal.ok.addEventListener(
156
+ 'click', () => EXPERIMENT_MANAGER.addParameter());
157
+ this.parameter_modal.cancel.addEventListener(
158
+ 'click', () => EXPERIMENT_MANAGER.parameter_modal.hide());
159
+
160
+ this.iterator_modal = new ModalDialog('xp-iterator');
161
+ this.iterator_modal.ok.addEventListener(
162
+ 'click', () => EXPERIMENT_MANAGER.modifyIteratorRanges());
163
+ this.iterator_modal.cancel.addEventListener(
164
+ 'click', () => EXPERIMENT_MANAGER.iterator_modal.hide());
165
+
166
+ this.settings_modal = new ModalDialog('xp-settings');
167
+ this.settings_modal.close.addEventListener(
168
+ 'click', () => EXPERIMENT_MANAGER.closeSettingsDimensions());
169
+ this.settings_modal.element('s-add-btn').addEventListener(
170
+ 'click', () => EXPERIMENT_MANAGER.editSettingsSelector(-1));
171
+ this.settings_modal.element('d-add-btn').addEventListener(
172
+ 'click', () => EXPERIMENT_MANAGER.editSettingsDimension(-1));
173
+
174
+ this.settings_selector_modal = new ModalDialog('xp-settings-selector');
175
+ this.settings_selector_modal.ok.addEventListener(
176
+ 'click', () => EXPERIMENT_MANAGER.modifySettingsSelector());
177
+ this.settings_selector_modal.cancel.addEventListener(
178
+ 'click', () => EXPERIMENT_MANAGER.settings_selector_modal.hide());
179
+
180
+ this.settings_dimension_modal = new ModalDialog('xp-settings-dimension');
181
+ this.settings_dimension_modal.ok.addEventListener(
182
+ 'click', () => EXPERIMENT_MANAGER.modifySettingsDimension());
183
+ this.settings_dimension_modal.cancel.addEventListener(
184
+ 'click', () => EXPERIMENT_MANAGER.settings_dimension_modal.hide());
185
+
186
+ this.combination_modal = new ModalDialog('xp-combination');
187
+ this.combination_modal.close.addEventListener(
188
+ 'click', () => EXPERIMENT_MANAGER.closeCombinationDimensions());
189
+ this.combination_modal.element('s-add-btn').addEventListener(
190
+ 'click', () => EXPERIMENT_MANAGER.editCombinationSelector(-1));
191
+ this.combination_modal.element('d-add-btn').addEventListener(
192
+ 'click', () => EXPERIMENT_MANAGER.editCombinationDimension(-1));
193
+
194
+ this.combination_selector_modal = new ModalDialog('xp-combination-selector');
195
+ this.combination_selector_modal.ok.addEventListener(
196
+ 'click', () => EXPERIMENT_MANAGER.modifyCombinationSelector());
197
+ this.combination_selector_modal.cancel.addEventListener(
198
+ 'click', () => EXPERIMENT_MANAGER.combination_selector_modal.hide());
199
+
200
+ this.combination_dimension_modal = new ModalDialog('xp-combination-dimension');
201
+ this.combination_dimension_modal.ok.addEventListener(
202
+ 'click', () => EXPERIMENT_MANAGER.modifyCombinationDimension());
203
+ this.combination_dimension_modal.cancel.addEventListener(
204
+ 'click', () => EXPERIMENT_MANAGER.combination_dimension_modal.hide());
205
+
206
+ this.actor_dimension_modal = new ModalDialog('xp-actor-dimension');
207
+ this.actor_dimension_modal.close.addEventListener(
208
+ 'click', () => EXPERIMENT_MANAGER.closeActorDimension());
209
+ this.actor_dimension_modal.element('add-btn').addEventListener(
210
+ 'click', () => EXPERIMENT_MANAGER.editActorSelector(-1));
211
+
212
+ this.actor_selector_modal = new ModalDialog('xp-actor-selector');
213
+ this.actor_selector_modal.ok.addEventListener(
214
+ 'click', () => EXPERIMENT_MANAGER.modifyActorSelector());
215
+ this.actor_selector_modal.cancel.addEventListener(
216
+ 'click', () => EXPERIMENT_MANAGER.actor_selector_modal.hide());
217
+
218
+ this.clusters_modal = new ModalDialog('xp-clusters');
219
+ this.clusters_modal.ok.addEventListener(
220
+ 'click', () => EXPERIMENT_MANAGER.modifyClustersToIgnore());
221
+ this.clusters_modal.cancel.addEventListener(
222
+ 'click', () => EXPERIMENT_MANAGER.clusters_modal.hide());
223
+ this.clusters_modal.element('add-btn').addEventListener(
224
+ 'click', () => EXPERIMENT_MANAGER.addClusterToIgnoreList());
225
+ const sinp = this.clusters_modal.element('selectors');
226
+ sinp.addEventListener(
227
+ 'focus', () => EXPERIMENT_MANAGER.editIgnoreSelectors());
228
+ sinp.addEventListener(
229
+ 'keyup', (event) => {
230
+ if (event.key === 'Enter') {
231
+ event.stopPropagation();
232
+ event.target.blur();
233
+ }
234
+ });
235
+ sinp.addEventListener(
236
+ 'blur', () => EXPERIMENT_MANAGER.setIgnoreSelectors());
237
+ this.clusters_modal.element('delete-btn').addEventListener(
238
+ 'click', () => EXPERIMENT_MANAGER.deleteClusterFromIgnoreList());
239
+
240
+ this.download_modal = new ModalDialog('xp-download');
241
+ this.download_modal.ok.addEventListener(
242
+ 'click', () => EXPERIMENT_MANAGER.downloadDataAsCSV());
243
+ this.download_modal.cancel.addEventListener(
244
+ 'click', () => EXPERIMENT_MANAGER.download_modal.hide());
245
+
246
+ // Initialize properties
247
+ this.reset();
248
+ }
249
+
250
+ reset() {
251
+ super.reset();
252
+ this.selected_parameter = '';
253
+ this.edited_selector_index = -1;
254
+ this.edited_dimension_index = -1;
255
+ this.edited_combi_selector_index = -1;
256
+ this.color_scale = new ColorScale('no');
257
+ this.focal_table = null;
258
+ this.designMode();
259
+ }
260
+
261
+ upDownKey(dir) {
262
+ // Select row above or below the selected one (if possible)
263
+ const srl = this.focal_table.getElementsByClassName('sel-set');
264
+ if(srl.length > 0) {
265
+ const r = this.focal_table.rows[srl[0].rowIndex + dir];
266
+ if(r) {
267
+ UI.scrollIntoView(r);
268
+ r.dispatchEvent(new Event('click'));
269
+ }
270
+ }
271
+ }
272
+
273
+ updateDialog() {
274
+ this.updateChartList();
275
+ // Warn modeler if no meaningful experiments can be defined
276
+ if(MODEL.outcomeNames.length === 0 && this.suitable_charts.length === 0) {
277
+ this.default_message.style.display = 'block';
278
+ this.params_div.style.display = 'none';
279
+ this.selected_experiment = null;
280
+ // Disable experiment dialog menu buttons
281
+ UI.disableButtons('xp-new xp-rename xp-view xp-delete xp-ignore');
282
+ } else {
283
+ this.default_message.style.display = 'none';
284
+ UI.enableButtons('xp-new');
285
+ if(MODEL.experiments.length === 0) this.selected_experiment = null;
286
+ }
287
+ const
288
+ xl = [],
289
+ xtl = [],
290
+ sx = this.selected_experiment;
291
+ for(let i = 0; i < MODEL.experiments.length; i++) {
292
+ xtl.push(MODEL.experiments[i].title);
293
+ }
294
+ xtl.sort(ciCompare);
295
+ for(let i = 0; i < xtl.length; i++) {
296
+ const
297
+ xi = MODEL.indexOfExperiment(xtl[i]),
298
+ x = (xi < 0 ? null : MODEL.experiments[xi]);
299
+ xl.push(['<tr class="experiment',
300
+ (x == sx ? ' sel-set' : ''),
301
+ '" onclick="EXPERIMENT_MANAGER.selectExperiment(\'',
302
+ escapedSingleQuotes(xtl[i]),
303
+ '\');" onmouseover="EXPERIMENT_MANAGER.showInfo(', xi,
304
+ ', event.shiftKey);"><td>', x.title, '</td></tr>'].join(''));
305
+ }
306
+ this.experiment_table.innerHTML = xl.join('');
307
+ const
308
+ btns = 'xp-rename xp-view xp-delete xp-ignore',
309
+ icnt = document.getElementById('xp-ignore-count');
310
+ icnt.innerHTML = '';
311
+ icnt.title = '';
312
+ if(sx) {
313
+ UI.enableButtons(btns);
314
+ const nc = sx.clusters_to_ignore.length;
315
+ if(Object.keys(MODEL.clusters).length <= 1) {
316
+ // Disable ignore button if model comprises only the top cluster
317
+ UI.disableButtons('xp-ignore');
318
+ } else if(nc > 0) {
319
+ icnt.innerHTML = nc;
320
+ icnt.title = pluralS(nc, 'cluster') + ' set to be ignored';
321
+ }
322
+ } else {
323
+ UI.disableButtons(btns);
324
+ }
325
+ // Show the "clear results" button only when selected experiment has run
326
+ if(sx && sx.runs.length > 0) {
327
+ document.getElementById('xp-reset-btn').classList.remove('off');
328
+ } else {
329
+ document.getElementById('xp-reset-btn').classList.add('off');
330
+ }
331
+ this.updateParameters();
332
+ }
333
+
334
+ updateParameters() {
335
+ MODEL.inferDimensions();
336
+ let canview = true;
337
+ const
338
+ dim_count = document.getElementById('experiment-dim-count'),
339
+ combi_count = document.getElementById('experiment-combi-count'),
340
+ header = document.getElementById('experiment-params-header'),
341
+ x = this.selected_experiment;
342
+ if(!x) {
343
+ dim_count.innerHTML = pluralS(
344
+ MODEL.dimensions.length, ' data dimension') + ' in model';
345
+ combi_count.innerHTML = '';
346
+ header.innerHTML = '(no experiment selected)';
347
+ this.params_div.style.display = 'none';
348
+ return;
349
+ }
350
+ x.updateActorDimension();
351
+ x.updateIteratorDimensions();
352
+ x.inferAvailableDimensions();
353
+ dim_count.innerHTML = pluralS(x.available_dimensions.length,
354
+ 'more dimension');
355
+ x.inferActualDimensions();
356
+ x.inferCombinations();
357
+ combi_count.innerHTML = pluralS(x.combinations.length, 'combination');
358
+ if(x.combinations.length === 0) canview = false;
359
+ header.innerHTML = x.title;
360
+ this.params_div.style.display = 'block';
361
+ const tr = [];
362
+ for(let i = 0; i < x.dimensions.length; i++) {
363
+ tr.push(['<tr class="dataset',
364
+ (this.selected_parameter == 'd'+i ? ' sel-set' : ''),
365
+ '" onclick="EXPERIMENT_MANAGER.selectParameter(\'d',
366
+ i, '\');"><td>',
367
+ setString(x.dimensions[i]),
368
+ '</td></tr>'].join(''));
369
+ }
370
+ this.dimension_table.innerHTML = tr.join('');
371
+ // Add button must be enabled only if there still are unused dimensions
372
+ if(x.available_dimensions.length > 0) {
373
+ document.getElementById('xp-d-add-btn').classList.remove('v-disab');
374
+ } else {
375
+ document.getElementById('xp-d-add-btn').classList.add('v-disab');
376
+ }
377
+ this.updateUpDownButtons();
378
+ tr.length = 0;
379
+ for(let i = 0; i < x.charts.length; i++) {
380
+ tr.push(['<tr class="dataset',
381
+ (this.selected_parameter == 'c'+i ? ' sel-set' : ''),
382
+ '" onclick="EXPERIMENT_MANAGER.selectParameter(\'c',
383
+ i, '\');"><td>',
384
+ x.charts[i].title, '</td></tr>'].join(''));
385
+ }
386
+ this.chart_table.innerHTML = tr.join('');
387
+ // Do not show viewer unless at least 1 dependent variable has been defined
388
+ if(x.charts.length === 0 && MODEL.outcomeNames.length === 0) canview = false;
389
+ if(tr.length >= this.suitable_charts.length) {
390
+ document.getElementById('xp-c-add-btn').classList.add('v-disab');
391
+ } else {
392
+ document.getElementById('xp-c-add-btn').classList.remove('v-disab');
393
+ }
394
+ this.exclude.value = x.excluded_selectors;
395
+ const
396
+ dbtn = document.getElementById('xp-d-delete-btn'),
397
+ cbtn = document.getElementById('xp-c-delete-btn');
398
+ if(this.selected_parameter.startsWith('d')) {
399
+ dbtn.classList.remove('v-disab');
400
+ cbtn.classList.add('v-disab');
401
+ } else if(this.selected_parameter.startsWith('c')) {
402
+ dbtn.classList.add('v-disab');
403
+ cbtn.classList.remove('v-disab');
404
+ } else {
405
+ dbtn.classList.add('v-disab');
406
+ cbtn.classList.add('v-disab');
407
+ }
408
+ // Enable viewing only if > 1 dimensions and > 1 outcome variables
409
+ if(canview) {
410
+ UI.enableButtons('xp-view');
411
+ } else {
412
+ UI.disableButtons('xp-view');
413
+ }
414
+ }
415
+
416
+ promptForExperiment() {
417
+ if(this.new_btn.classList.contains('enab')) {
418
+ this.new_modal.element('name').value = '';
419
+ this.new_modal.show('name');
420
+ }
421
+ }
422
+
423
+ newExperiment() {
424
+ const n = this.new_modal.element('name').value.trim();
425
+ const x = MODEL.addExperiment(n);
426
+ if(x) {
427
+ this.new_modal.hide();
428
+ this.selected_experiment = x;
429
+ this.updateDialog();
430
+ }
431
+ }
432
+
433
+ promptForName() {
434
+ if(this.selected_experiment) {
435
+ this.rename_modal.element('former-name').innerHTML =
436
+ this.selected_experiment.title;
437
+ this.rename_modal.element('name').value = '';
438
+ this.rename_modal.show('name');
439
+ }
440
+ }
441
+
442
+ renameExperiment() {
443
+ if(this.selected_experiment) {
444
+ const
445
+ nel = this.rename_modal.element('name'),
446
+ n = UI.cleanName(nel.value);
447
+ // Show modeler the "cleaned" new name
448
+ nel.value = n;
449
+ // Keep prompt open if title is empty string
450
+ if(n) {
451
+ // Warn modeler if name already in use for some experiment
452
+ if(MODEL.indexOfExperiment(n) >= 0) {
453
+ UI.warn(`An experiment with title "${n}" already exists`);
454
+ } else {
455
+ this.selected_experiment.title = n;
456
+ this.rename_modal.hide();
457
+ this.updateDialog();
458
+ }
459
+ }
460
+ }
461
+ }
462
+
463
+ designMode() {
464
+ // Switch to default view
465
+ this.viewer.style.display = 'none';
466
+ this.design.style.display = 'block';
467
+ }
468
+
469
+ viewerMode() {
470
+ // Switch to table view
471
+ // NOTE: check if button is disabled, as it then still responds to click
472
+ if(this.view_btn.classList.contains('disab')) return;
473
+ const x = this.selected_experiment;
474
+ if(x) {
475
+ this.design.style.display = 'none';
476
+ document.getElementById('viewer-title').innerHTML = x.title;
477
+ document.getElementById('viewer-statistic').value = x.selected_statistic;
478
+ this.updateViewerVariable();
479
+ // NOTE: calling updateSpinner with dir=0 will update without changes
480
+ this.updateSpinner('c', 0);
481
+ this.drawTable();
482
+ document.getElementById('viewer-scale').value = x.selected_scale;
483
+ this.setColorScale(x.selected_color_scale);
484
+ this.viewer.style.display = 'block';
485
+ }
486
+ }
487
+
488
+ updateViewerVariable() {
489
+ // Update the variable drop-down selector of the viewer
490
+ const x = this.selected_experiment;
491
+ if(x) {
492
+ x.inferVariables();
493
+ const
494
+ ol = [],
495
+ vl = MODEL.outcomeNames;
496
+ for(let i = 0; i < x.variables.length; i++) {
497
+ addDistinct(x.variables[i].displayName, vl);
498
+ }
499
+ vl.sort((a, b) => UI.compareFullNames(a, b));
500
+ for(let i = 0; i < vl.length; i++) {
501
+ ol.push(['<option value="', vl[i], '"',
502
+ (vl[i] == x.selected_variable ? ' selected="selected"' : ''),
503
+ '>', vl[i], '</option>'].join(''));
504
+ }
505
+ document.getElementById('viewer-variable').innerHTML = ol.join('');
506
+ if(x.selected_variable === '') {
507
+ x.selected_variable = vl[0];
508
+ }
509
+ }
510
+ }
511
+
512
+ drawTable() {
513
+ // Draw experimental design as table
514
+ const x = this.selected_experiment;
515
+ if(x) {
516
+ this.clean_columns = [];
517
+ this.clean_rows = [];
518
+ // Calculate the actual number of columns and rows of the table
519
+ const
520
+ coldims = x.configuration_dims + x.column_scenario_dims,
521
+ rowdims = x.actual_dimensions.length - coldims,
522
+ excsel = x.excluded_selectors.split(' ');
523
+ let nc = 1,
524
+ nr = 1;
525
+ for(let i = 0; i < coldims; i++) {
526
+ const d = complement(x.actual_dimensions[i], excsel);
527
+ if(d.length > 0) {
528
+ nc *= d.length;
529
+ this.clean_columns.push(d);
530
+ }
531
+ }
532
+ for(let i = coldims; i < x.actual_dimensions.length; i++) {
533
+ const d = complement(x.actual_dimensions[i], excsel);
534
+ if(d.length > 0) {
535
+ nr *= d.length;
536
+ this.clean_rows.push(d);
537
+ }
538
+ }
539
+ const
540
+ tr = [],
541
+ trl = [],
542
+ cfgd = x.configuration_dims,
543
+ // Opacity decrement to "bleach" yellow shades
544
+ ystep = (cfgd > 1 ? 0.8 / (cfgd - 1) : 0),
545
+ // NOTE: # blue shades needed is *lowest* of # column scenario
546
+ // dimensions and # row dimensions
547
+ scnd = Math.max(coldims - cfgd, rowdims),
548
+ // Opacity decrement to "bleach" blue shades
549
+ bstep = (scnd > 1 ? 0.8 / (scnd - 1) : 0);
550
+ let
551
+ // Index for leaf configuration numbering
552
+ cfgi = 0,
553
+ // Blank leading cell to fill the spcace left of configuration labels
554
+ ltd = rowdims > 0 ? `<td colspan="${rowdims + 1}"></td>` : '';
555
+ // Add the configurations label if there are any ...
556
+ if(cfgd > 0) {
557
+ trl.push('<tr>', ltd, '<th class="conf-ttl" colspan="',
558
+ nc, '">Configurations</th></tr>');
559
+ } else if(coldims > 0) {
560
+ // ... otherwise add the scenarios label if there are any
561
+ trl.push('<tr>', ltd, '<th class="scen-h-ttl" colspan="',
562
+ nc, '">Scenario space</th></tr>');
563
+ }
564
+ // Add the column label rows
565
+ let n = 1,
566
+ c = nc,
567
+ style,
568
+ cfgclass,
569
+ selclass,
570
+ onclick;
571
+ for(let i = 0; i < coldims; i++) {
572
+ const scnt = this.clean_columns[i].length;
573
+ tr.length = 0;
574
+ tr.push('<tr>', ltd);
575
+ c = c / scnt;
576
+ const csp = (c > 1 ? ` colspan="${c}"` : '');
577
+ cfgclass = '';
578
+ if(i < cfgd) {
579
+ const perc = 1 - i * ystep;
580
+ style = `background-color: rgba(250, 250, 0, ${perc});` +
581
+ `filter: hue-rotate(-${25 * perc}deg)`;
582
+ if(i === cfgd - 1) cfgclass = ' leaf-conf';
583
+ } else {
584
+ style = 'background-color: rgba(100, 170, 255, ' +
585
+ (1 - (i - cfgd) * bstep) + ')';
586
+ if(i == coldims - 1) style += '; border-bottom: 1.5px silver inset';
587
+ }
588
+ for(let j = 0; j < n; j++) {
589
+ for(let k = 0; k < scnt; k++) {
590
+ if(i == cfgd - 1) {
591
+ onclick = ` onclick="EXPERIMENT_MANAGER.setReference(${cfgi});"`;
592
+ selclass = (cfgi == x.reference_configuration ? ' sel-leaf' : '');
593
+ cfgi++;
594
+ } else {
595
+ onclick = '';
596
+ selclass = '';
597
+ }
598
+ tr.push(['<th', csp, ' class="conf-hdr', cfgclass, selclass,
599
+ '" style="', style, '"', onclick, '>', this.clean_columns[i][k],
600
+ '</th>'].join(''));
601
+ }
602
+ }
603
+ tr.push('</tr>');
604
+ trl.push(tr.join(''));
605
+ n *= scnt;
606
+ }
607
+ // Retain the number of configurations, as it is used in data display
608
+ this.nr_of_configurations = cfgi;
609
+ // Add the row scenarios
610
+ const
611
+ srows = [],
612
+ rowsperdim = [1];
613
+ // Calculate for each dimension how many rows it takes per selector
614
+ for(let i = 1; i < rowdims; i++) {
615
+ for(let j = 0; j < i; j++) {
616
+ rowsperdim[j] *= this.clean_rows[i].length;
617
+ }
618
+ rowsperdim.push(1);
619
+ }
620
+ for(let i = 0; i < nr; i++) {
621
+ srows.push('<tr>');
622
+ // Add scenario title row if there are still row dimensions
623
+ if(i == 0 && coldims < x.actual_dimensions.length) {
624
+ srows[i] += '<th class="scen-v-ttl" rowspan="' + nr +
625
+ '"><div class="v-rot">Scenario space</div></th>';
626
+ }
627
+ // Only add the scenario dimension header cell when appropriate,
628
+ // and then give then the correct "rowspan"
629
+ let lth = '', rsp;
630
+ for(let j = 0; j < rowdims; j++) {
631
+ // If no remainder of division, add the selector
632
+ if(i % rowsperdim[j] === 0) {
633
+ if(rowsperdim[j] > 1) {
634
+ rsp = ` rowspan="${rowsperdim[j]}"`;
635
+ } else {
636
+ rsp = '';
637
+ }
638
+ // Calculate the dimension selector index
639
+ const dsi = Math.floor(
640
+ i / rowsperdim[j]) % this.clean_rows[j].length;
641
+ lth += ['<th', rsp, ' class="scen-hdr" style="background-color: ',
642
+ 'rgba(100, 170, 255, ', 1 - j * bstep, ')">',
643
+ this.clean_rows[j][dsi], '</th>'].join('');
644
+ }
645
+ }
646
+ srows[i] += lth;
647
+ for(let j = 0; j < nc; j++) {
648
+ const run = i + j*nr;
649
+ srows[i] += ['<td id="xr', run, '" class="data-cell not-run"',
650
+ ' onclick="EXPERIMENT_MANAGER.toggleChartCombi(', run,
651
+ ', event.shiftKey, event.altKey);" ',
652
+ 'onmouseover="EXPERIMENT_MANAGER.showRunInfo(',
653
+ run, ', event.shiftKey);">', run, '</td>'].join('');
654
+ }
655
+ srows[i] += '</tr>';
656
+ }
657
+ trl.push(srows.join(''));
658
+ document.getElementById('viewer-table').innerHTML = trl.join('');
659
+ // NOTE: grid cells are identifiable by their ID => are updated separately
660
+ this.updateData();
661
+ }
662
+ }
663
+
664
+ toggleChartRow(r, n, shift) {
665
+ // Toggle `n` consecutive rows, starting at row `r` (0 = top), to be
666
+ // (no longer) part of the chart combination set
667
+ const
668
+ x = this.selected_experiment,
669
+ // Let `n` be the number of the first run on row `r`
670
+ nconf = r * this.nr_of_configurations;
671
+ if(x && r < x.combinations.length / this.nr_of_configurations) {
672
+ // NOTE: first cell of row determines ADD or REMOVE
673
+ const add = x.chart_combinations.indexOf(n) < 0;
674
+ for(let i = 0; i < this.nr_of_configurations; i++) {
675
+ const ic = x.chart_combinations.indexOf(i);
676
+ if(add) {
677
+ if(ic < 0) x.chart_combinations.push(nconf + i);
678
+ } else {
679
+ if(!add) x.chart_combinations.splice(nconf + i, 1);
680
+ }
681
+ }
682
+ this.updateData();
683
+ }
684
+ }
685
+
686
+ toggleChartColumn(c, shift) {
687
+ // Toggle column `c` (0 = leftmost) to be part of the chart combination set
688
+ }
689
+
690
+ toggleChartCombi(n, shift, alt) {
691
+ // Set `n` to be the chart combination, or toggle if Shift-key is pressed,
692
+ // or execute single run if Alt-key is pressed
693
+ const x = this.selected_experiment;
694
+ if(x && alt && n >= 0) {
695
+ this.startExperiment(n);
696
+ return;
697
+ }
698
+ if(x && n < x.combinations.length) {
699
+ // Clear current selection unless Shift-key is pressed
700
+ if(!shift) x.chart_combinations.length = 0;
701
+ // Toggle => add if not in selection, otherwise remove
702
+ const ci = x.chart_combinations.indexOf(n);
703
+ if(ci < 0) {
704
+ x.chart_combinations.push(n);
705
+ } else {
706
+ x.chart_combinations.splice(ci, 1);
707
+ }
708
+ }
709
+ this.updateData();
710
+ if(MODEL.running_experiment) {
711
+ // NOTE: do NOT do this while VM is solving, as this would interfer!
712
+ UI.notify('Selected run cannot be viewed while running an experiment');
713
+ } else {
714
+ // Show the messages for this run in the monitor
715
+ VM.setRunMessages(n);
716
+ // Update the chart
717
+ CHART_MANAGER.resetChartVectors();
718
+ CHART_MANAGER.updateDialog();
719
+ }
720
+ }
721
+
722
+ runInfo(n) {
723
+ // Return information on the n-th combination as object {title, html}
724
+ const
725
+ x = this.selected_experiment,
726
+ info = {};
727
+ if(x && n < x.combinations.length) {
728
+ const combi = x.combinations[n];
729
+ info.title = `Combination: <tt>${tupelString(combi)}</tt>`;
730
+ const html = [], list = [];
731
+ for(let i = 0; i < combi.length; i++) {
732
+ const sel = combi[i];
733
+ html.push('<h3>Selector <tt>', sel, '</tt></h3>');
734
+ // List associated model settings (if any)
735
+ list.length = 0;
736
+ for(let j = 0; j < x.settings_selectors.length; j++) {
737
+ const ss = x.settings_selectors[j].split('|');
738
+ if(sel === ss[0]) list.push(ss[1]);
739
+ }
740
+ if(list.length > 0) {
741
+ html.push('<p><em>Model settings:</em> <tt>', list.join(';'),
742
+ '</tt></p>');
743
+ }
744
+ // List associated actor settings (if any)
745
+ list.length = 0;
746
+ for(let j = 0; j < x.actor_selectors.length; j++) {
747
+ const as = x.actor_selectors[j];
748
+ if(sel === as.selector) {
749
+ list.push(as.round_sequence);
750
+ }
751
+ }
752
+ if(list.length > 0) {
753
+ html.push('<p><em>Actor settings:</em> <tt>', list.join(';'),
754
+ '</tt></p>');
755
+ }
756
+ // List associated datasets (if any)
757
+ list.length = 0;
758
+ for(let id in MODEL.datasets) if(MODEL.datasets.hasOwnProperty(id)) {
759
+ const ds = MODEL.datasets[id];
760
+ for(let k in ds.modifiers) if(ds.modifiers.hasOwnProperty(k)) {
761
+ if(ds.modifiers[k].match(sel)) {
762
+ list.push('<li>', ds.displayName, '<span class="dsx">',
763
+ ds.modifiers[k].expression.text,'</span></li>');
764
+ }
765
+ }
766
+ }
767
+ if(list.length > 0) {
768
+ html.push('<em>Datasets:</em> <ul>', list.join(''), '</ul>');
769
+ }
770
+ }
771
+ info.html = html.join('');
772
+ return info;
773
+ }
774
+ // Fall-through (should not occur)
775
+ return null;
776
+ }
777
+
778
+ showInfo(n, shift) {
779
+ // Display documentation for the n-th experiment defined in the model
780
+ // NOTE: skip when viewer is showing!
781
+ if(!UI.hidden('experiment-viewer')) return;
782
+ if(n < MODEL.experiments.length) {
783
+ // NOTE: mouse move over title in viewer passes n = -1
784
+ const x = (n < 0 ? this.selected_experiment : MODEL.experiments[n]);
785
+ DOCUMENTATION_MANAGER.update(x, shift);
786
+ }
787
+ }
788
+
789
+ showRunInfo(n, shift) {
790
+ // Display information on the n-th combination if docu-viewer is visible
791
+ // and cursor is moved over run cell while Shift button is held down
792
+ if(shift && DOCUMENTATION_MANAGER.visible) {
793
+ const info = this.runInfo(n);
794
+ if(info) {
795
+ // Display information as read-only HTML
796
+ DOCUMENTATION_MANAGER.title.innerHTML = info.title;
797
+ DOCUMENTATION_MANAGER.viewer.innerHTML = info.html;
798
+ DOCUMENTATION_MANAGER.edit_btn.classList.remove('enab');
799
+ DOCUMENTATION_MANAGER.edit_btn.classList.add('disab');
800
+ }
801
+ }
802
+ }
803
+
804
+ updateData() {
805
+ // Fill table cells with their data value or status
806
+ const x = this.selected_experiment;
807
+ if(x) {
808
+ if(x.completed) {
809
+ const ts = msecToTime(x.time_stopped - x.time_started);
810
+ this.viewer_progress.innerHTML =
811
+ `<span class="x-checked" title="${ts}">&#10004;</span>`;
812
+ }
813
+ const rri = x.resultIndex(x.selected_variable);
814
+ if(rri < 0) {
815
+ // @@@ For debugging purposes
816
+ console.log('Variable not found', x.selected_variable);
817
+ return;
818
+ }
819
+ // Get the selected statistic for each run so as to get an array of numbers
820
+ const data = [];
821
+ for(let i = 0; i < x.runs.length; i++) {
822
+ const
823
+ r = x.runs[i],
824
+ rr = r.results[rri];
825
+ if(!rr) {
826
+ data.push(VM.UNDEFINED);
827
+ } else if(x.selected_scale === 'sec') {
828
+ data.push(r.solver_seconds);
829
+ } else if(x.selected_statistic === 'N') {
830
+ data.push(rr.N);
831
+ } else if(x.selected_statistic === 'sum') {
832
+ data.push(rr.sum);
833
+ } else if(x.selected_statistic === 'mean') {
834
+ data.push(rr.mean);
835
+ } else if(x.selected_statistic === 'sd') {
836
+ data.push(Math.sqrt(rr.variance));
837
+ } else if(x.selected_statistic === 'min') {
838
+ data.push(rr.minimum);
839
+ } else if(x.selected_statistic === 'max') {
840
+ data.push(rr.maximum);
841
+ } else if(x.selected_statistic === 'nz') {
842
+ data.push(rr.non_zero_tally);
843
+ } else if(x.selected_statistic === 'except') {
844
+ data.push(rr.exceptions);
845
+ } else if(x.selected_statistic === 'last') {
846
+ data.push(rr.last);
847
+ }
848
+ }
849
+ // Scale data as selected
850
+ const scaled = data.slice();
851
+ // NOTE: scale only after the experiment has been completed AND
852
+ // configurations have been defined (otherwise comparison is pointless)
853
+ if(x.completed && this.nr_of_configurations > 0) {
854
+ const n = scaled.length / this.nr_of_configurations;
855
+ if(x.selected_scale === 'dif') {
856
+ // Compute difference: current configuration - reference configuration
857
+ const rc = x.reference_configuration;
858
+ for(let i = 0; i < this.nr_of_configurations; i++) {
859
+ if(i != rc) {
860
+ for(let j = 0; j < n; j++) {
861
+ scaled[i * n + j] = scaled[i * n + j] - scaled[rc * n + j];
862
+ }
863
+ }
864
+ }
865
+ // Set difference for reference configuration itself to 0
866
+ for(let i = 0; i < n; i++) {
867
+ scaled[rc * n + i] = 0;
868
+ }
869
+ } else if(x.selected_scale === 'reg') {
870
+ // Compute regret: current config - high value config in same scenario
871
+ for(let i = 0; i < n; i++) {
872
+ // Get high value
873
+ let high = VM.MINUS_INFINITY;
874
+ for(let j = 0; j < this.nr_of_configurations; j++) {
875
+ high = Math.max(high, scaled[j * n + i]);
876
+ }
877
+ // Scale (so high config always has value 0)
878
+ for(let j = 0; j < this.nr_of_configurations; j++) {
879
+ scaled[j * n + i] -= high;
880
+ }
881
+ }
882
+ }
883
+ }
884
+ // For color scales, compute normalized scores
885
+ let normalized = scaled.slice(),
886
+ high = VM.MINUS_INFINITY,
887
+ low = VM.PLUS_INFINITY;
888
+ for(let i = 0; i < normalized.length; i++) {
889
+ high = Math.max(high, normalized[i]);
890
+ low = Math.min(low, normalized[i]);
891
+ }
892
+ // Avoid too small value ranges
893
+ const range = (high - low < VM.NEAR_ZERO ? 0 : high - low);
894
+ if(range > 0) {
895
+ for(let i = 0; i < normalized.length; i++) {
896
+ normalized[i] = (normalized[i] - low) / range;
897
+ }
898
+ }
899
+ // Format data such that they all have same number of decimals
900
+ let formatted = [];
901
+ for(let i = 0; i < scaled.length; i++) {
902
+ formatted.push(VM.sig4Dig(scaled[i]));
903
+ }
904
+ uniformDecimals(formatted);
905
+ // Display formatted data in cells
906
+ for(let i = 0; i < x.combinations.length; i++) {
907
+ const cell = document.getElementById('xr' + i);
908
+ if(i < x.runs.length) {
909
+ cell.innerHTML = formatted[i];
910
+ cell.classList.remove('not-run');
911
+ cell.style.backgroundColor = this.color_scale.rgb(normalized[i]);
912
+ const
913
+ r = x.runs[i],
914
+ rr = r.results[rri],
915
+ rdt = (r.time_recorded - r.time_started) * 0.001,
916
+ rdts = VM.sig2Dig(rdt),
917
+ ss = VM.sig2Dig(r.solver_seconds),
918
+ ssp = (rdt < VM.NEAR_ZERO ? '' :
919
+ ' (' + Math.round(r.solver_seconds * 100 / rdt) + '%)'),
920
+ w = (r.warning_count > 0 ?
921
+ ' ' + pluralS(r.warning_count, 'warning') + '. ' : '');
922
+ cell.title = ['Run #', i, ' (', r.time_steps, ' time steps of ',
923
+ r.time_step_duration, ' h) took ', rdts, ' s. Solver used ', ss, ' s',
924
+ ssp, '.', w, (rr ? `
925
+ N = ${rr.N}, vector length = ${rr.vector.length}` : '')].join('');
926
+ if(r.warning_count > 0) cell.classList.add('warnings');
927
+ }
928
+ if(x.chart_combinations.indexOf(i) < 0) {
929
+ cell.classList.remove('in-chart');
930
+ } else {
931
+ cell.classList.add('in-chart');
932
+ }
933
+ }
934
+ }
935
+ }
936
+
937
+ setVariable() {
938
+ // Update view for selected variable
939
+ const x = this.selected_experiment;
940
+ if(x) {
941
+ x.selected_variable = document.getElementById('viewer-variable').value;
942
+ this.updateData();
943
+ }
944
+ }
945
+
946
+ setStatistic() {
947
+ // Update view for selected variable
948
+ const x = this.selected_experiment;
949
+ if(x) {
950
+ x.selected_statistic = document.getElementById('viewer-statistic').value;
951
+ this.updateData();
952
+ }
953
+ }
954
+
955
+ setReference(cfg) {
956
+ // Set reference configuration
957
+ const x = this.selected_experiment;
958
+ if(x) {
959
+ x.reference_configuration = cfg;
960
+ this.drawTable();
961
+ }
962
+ }
963
+
964
+ updateSpinner(type, dir) {
965
+ // Increase or decrease spinner value (within constraints)
966
+ const x = this.selected_experiment,
967
+ xdims = x.actual_dimensions.length;
968
+ if(x) {
969
+ if(type === 'c') {
970
+ // NOTE: check for actual change, as then reference config must be reset
971
+ const cd = Math.max(0, Math.min(
972
+ xdims - x.column_scenario_dims, x.configuration_dims + dir));
973
+ if(cd != x.configuration_dims) {
974
+ x.configuration_dims = cd;
975
+ x.reference_configuration = 0;
976
+ }
977
+ document.getElementById('xp-cd-value').innerHTML = x.configuration_dims;
978
+ } else if(type === 's') {
979
+ x.column_scenario_dims = Math.max(0, Math.min(
980
+ xdims - x.configuration_dims, x.column_scenario_dims + dir));
981
+ document.getElementById('xp-sd-value').innerHTML = x.column_scenario_dims;
982
+ }
983
+ // Disable "minus" when already at 0
984
+ if(x.configuration_dims > 0) {
985
+ document.getElementById('xp-cd-minus').classList.remove('no-spin');
986
+ } else {
987
+ document.getElementById('xp-cd-minus').classList.add('no-spin');
988
+ }
989
+ if(x.column_scenario_dims > 0) {
990
+ document.getElementById('xp-sd-minus').classList.remove('no-spin');
991
+ } else {
992
+ document.getElementById('xp-sd-minus').classList.add('no-spin');
993
+ }
994
+ // Ensure that # configurations + # column scenarios <= # dimensions
995
+ const
996
+ spl = this.viewer.getElementsByClassName('spin-plus'),
997
+ rem = (x.configuration_dims + x.column_scenario_dims < xdims);
998
+ for(let i = 0; i < spl.length; i++) {
999
+ if(rem) {
1000
+ spl.item(i).classList.remove('no-spin');
1001
+ } else {
1002
+ spl.item(i).classList.add('no-spin');
1003
+ }
1004
+ }
1005
+ if(dir != 0 ) this.drawTable();
1006
+ }
1007
+ }
1008
+
1009
+ setScale() {
1010
+ // Update view for selected scale
1011
+ const x = this.selected_experiment;
1012
+ if(x) {
1013
+ x.selected_scale = document.getElementById('viewer-scale').value;
1014
+ this.updateData();
1015
+ }
1016
+ }
1017
+
1018
+ setColorScale(cs) {
1019
+ // Update view for selected color scale (values: rb, br, rg, gr or no)
1020
+ const x = this.selected_experiment;
1021
+ if(x) {
1022
+ if(cs) {
1023
+ x.selected_color_scale = cs;
1024
+ this.color_scale.set(cs);
1025
+ const csl = this.viewer.getElementsByClassName('color-scale');
1026
+ for(let i = 0; i < csl.length; i++) {
1027
+ csl.item(i).classList.remove('sel-cs');
1028
+ }
1029
+ document.getElementById(`xv-${cs}-scale`).classList.add('sel-cs');
1030
+ }
1031
+ this.updateData();
1032
+ }
1033
+ }
1034
+
1035
+ deleteExperiment() {
1036
+ const x = this.selected_experiment;
1037
+ if(x) {
1038
+ const xi = MODEL.indexOfExperiment(x.title);
1039
+ if(xi >= 0) MODEL.experiments.splice(xi, 1);
1040
+ this.selected_experiment = null;
1041
+ this.updateDialog();
1042
+ }
1043
+ }
1044
+
1045
+ selectParameter(p) {
1046
+ this.selected_parameter = p;
1047
+ this.focal_table = (p.startsWith('d') ? this.dimension_table :
1048
+ this.chart_table);
1049
+ this.updateDialog();
1050
+ }
1051
+
1052
+ updateUpDownButtons() {
1053
+ // Show position and (de)activate up and down buttons as appropriate
1054
+ let mvup = false, mvdown = false;
1055
+ const x = this.selected_experiment, sp = this.selected_parameter;
1056
+ if(x && sp) {
1057
+ const type = sp.charAt(0),
1058
+ index = parseInt(sp.slice(1));
1059
+ if(type == 'd') {
1060
+ mvup = index > 0;
1061
+ mvdown = index < x.dimensions.length - 1;
1062
+ }
1063
+ }
1064
+ const
1065
+ ub = document.getElementById('xp-d-up-btn'),
1066
+ db = document.getElementById('xp-d-down-btn');
1067
+ if(mvup) {
1068
+ ub.classList.remove('v-disab');
1069
+ } else {
1070
+ ub.classList.add('v-disab');
1071
+ }
1072
+ if(mvdown) {
1073
+ db.classList.remove('v-disab');
1074
+ } else {
1075
+ db.classList.add('v-disab');
1076
+ }
1077
+ }
1078
+
1079
+ moveDimension(dir) {
1080
+ // Move dimension one position up (-1) or down (+1)
1081
+ const x = this.selected_experiment, sp = this.selected_parameter;
1082
+ if(x && sp) {
1083
+ const type = sp.charAt(0),
1084
+ index = parseInt(sp.slice(1));
1085
+ if(type == 'd') {
1086
+ if(dir > 0 && index < x.dimensions.length - 1 ||
1087
+ dir < 0 && index > 0) {
1088
+ const
1089
+ d = x.dimensions.splice(index, 1),
1090
+ ndi = index + dir;
1091
+ x.dimensions.splice(ndi, 0, d[0]);
1092
+ this.selected_parameter = 'd' + ndi;
1093
+ }
1094
+ this.updateParameters();
1095
+ }
1096
+ }
1097
+ }
1098
+
1099
+ editIteratorRanges() {
1100
+ // Open dialog for editing iterator ranges
1101
+ const
1102
+ x = this.selected_experiment,
1103
+ md = this.iterator_modal,
1104
+ il = ['i', 'j', 'k'];
1105
+ if(x) {
1106
+ // NOTE: there are always 3 iterators (i, j k) so these have fixed
1107
+ // FROM and TO input fields in the dialog
1108
+ for(let i = 0; i < 3; i++) {
1109
+ const k = il[i];
1110
+ md.element(k + '-from').value = x.iterator_ranges[i][0];
1111
+ md.element(k + '-to').value = x.iterator_ranges[i][1];
1112
+ }
1113
+ this.iterator_modal.show();
1114
+ }
1115
+ }
1116
+
1117
+ modifyIteratorRanges() {
1118
+ const
1119
+ x = this.selected_experiment,
1120
+ md = this.iterator_modal;
1121
+ if(x) {
1122
+ // First validate all input fields (must be integer values)
1123
+ // NOTE: test using a copy so as not to overwrite values until OK
1124
+ const
1125
+ il = ['i', 'j', 'k'],
1126
+ ir = [[0, 0], [0, 0], [0, 0]],
1127
+ re = /^[\+\-]?[0-9]+$/;
1128
+ let el, f, t;
1129
+ for(let i = 0; i < 3; i++) {
1130
+ const k = il[i];
1131
+ el = md.element(k + '-from');
1132
+ f = el.value.trim() || '0';
1133
+ if(f === '' || re.test(f)) {
1134
+ el = md.element(k + '-to');
1135
+ t = el.value.trim() || '0';
1136
+ if(t === '' || re.test(t)) el = null;
1137
+ }
1138
+ // NULL value signals that field inputs are valid
1139
+ if(el === null) {
1140
+ ir[i] = [f, t];
1141
+ } else {
1142
+ el.focus();
1143
+ UI.warn('Iterator range limits must be integers (or default to 0)');
1144
+ return;
1145
+ }
1146
+ }
1147
+ // Input validated, so modify the iterator dimensions
1148
+ x.iterator_ranges = ir;
1149
+ this.updateDialog();
1150
+ }
1151
+ md.hide();
1152
+ }
1153
+
1154
+ editSettingsDimensions() {
1155
+ // Open dialog for editing model settings dimensions
1156
+ const x = this.selected_experiment, rows = [];
1157
+ if(x) {
1158
+ // Initialize selector list
1159
+ for(let i = 0; i < x.settings_selectors.length; i++) {
1160
+ const sel = x.settings_selectors[i].split('|');
1161
+ rows.push('<tr onclick="EXPERIMENT_MANAGER.editSettingsSelector(', i,
1162
+ ');"><td width="25%">', sel[0], '</td><td>', sel[1], '</td></tr>');
1163
+ }
1164
+ this.settings_modal.element('s-table').innerHTML = rows.join('');
1165
+ // Initialize combination list
1166
+ rows.length = 0;
1167
+ for(let i = 0; i < x.settings_dimensions.length; i++) {
1168
+ const dim = x.settings_dimensions[i];
1169
+ rows.push('<tr onclick="EXPERIMENT_MANAGER.editSettingsDimension(', i,
1170
+ ');"><td>', setString(dim), '</td></tr>');
1171
+ }
1172
+ this.settings_modal.element('d-table').innerHTML = rows.join('');
1173
+ this.settings_modal.show();
1174
+ // NOTE: clear infoline because dialog can generate warnings that would
1175
+ // otherwise remain visible while no longer relevant
1176
+ UI.setMessage('');
1177
+ }
1178
+ }
1179
+
1180
+ closeSettingsDimensions() {
1181
+ // Hide editor, and then update the experiment manager to reflect changes
1182
+ this.settings_modal.hide();
1183
+ this.updateDialog();
1184
+ }
1185
+
1186
+ editSettingsSelector(selnr) {
1187
+ const x = this.selected_experiment;
1188
+ if(!x) return;
1189
+ let action = 'Add',
1190
+ clear = '',
1191
+ sel = ['', ''];
1192
+ this.edited_selector_index = selnr;
1193
+ if(selnr >= 0) {
1194
+ action = 'Edit';
1195
+ clear = '(clear to remove)';
1196
+ sel = x.settings_selectors[selnr].split('|');
1197
+ }
1198
+ const md = this.settings_selector_modal;
1199
+ md.element('action').innerHTML = action;
1200
+ md.element('clear').innerHTML = clear;
1201
+ md.element('code').value = sel[0];
1202
+ md.element('string').value = sel[1];
1203
+ md.show(sel[0] ? 'string' : 'code');
1204
+ }
1205
+
1206
+ modifySettingsSelector() {
1207
+ // Accepts valid selectors and settings, tolerating a decimal comma
1208
+ let x = this.selected_experiment;
1209
+ if(x) {
1210
+ const
1211
+ md = this.settings_selector_modal,
1212
+ sc = md.element('code'),
1213
+ ss = md.element('string'),
1214
+ code = sc.value.replace(/[^\w\+\-\%]/g, ''),
1215
+ value = ss.value.trim().replace(',', '.'),
1216
+ add = this.edited_selector_index < 0;
1217
+ // Remove selector if either field has been cleared
1218
+ if(code.length === 0 || value.length === 0) {
1219
+ if(!add) {
1220
+ x.settings_selectors.splice(this.edited_selector_index, 1);
1221
+ }
1222
+ } else {
1223
+ // Check for uniqueness of code
1224
+ for(let i = 0; i < x.settings_selectors.length; i++) {
1225
+ // NOTE: ignore selector being edited, as this selector can be renamed
1226
+ if(i != this.edited_selector_index &&
1227
+ x.settings_selectors[i].split('|')[0] === code) {
1228
+ UI.warn(`Settings selector "${code}"already defined`);
1229
+ sc.focus();
1230
+ return;
1231
+ }
1232
+ }
1233
+ // Check for valid syntax -- canonical example: s=0.25h t=1-100 b=12 l=6
1234
+ const re = /^(s\=\d+(\.?\d+)?(yr?|wk?|d|h|m|min|s)\s+)?(t\=\d+(\-\d+)?\s+)?(b\=\d+\s+)?(l=\d+\s+)?$/i;
1235
+ if(!re.test(value + ' ')) {
1236
+ UI.warn(`Invalid settings "${value}"`);
1237
+ ss.focus();
1238
+ return;
1239
+ }
1240
+ // Parse settings with testing = TRUE to avoid start time > end time,
1241
+ // or block length = 0, as regex test does not prevent this
1242
+ if(!MODEL.parseSettings(value, true)) {
1243
+ ss.focus();
1244
+ return;
1245
+ }
1246
+ // Selector has format code|settings
1247
+ const sel = code + '|' + value;
1248
+ if(add) {
1249
+ x.settings_selectors.push(sel);
1250
+ } else {
1251
+ // NOTE: rename occurrence of code in dimension (should at most be 1)
1252
+ const oc = x.settings_selectors[this.edited_selector_index].split('|')[0];
1253
+ x.settings_selectors[this.edited_selector_index] = sel;
1254
+ x.renameSelectorInDimensions(oc, code);
1255
+ }
1256
+ }
1257
+ md.hide();
1258
+ }
1259
+ // Update settings dimensions dialog
1260
+ this.editSettingsDimensions();
1261
+ }
1262
+
1263
+ editSettingsDimension(dimnr) {
1264
+ const x = this.selected_experiment;
1265
+ if(!x) return;
1266
+ let action = 'Add',
1267
+ clear = '',
1268
+ value = '';
1269
+ this.edited_dimension_index = dimnr;
1270
+ if(dimnr >= 0) {
1271
+ action = 'Edit';
1272
+ clear = '(clear to remove)';
1273
+ // NOTE: present to modeler as space-separated string
1274
+ value = x.settings_dimensions[dimnr].join(' ');
1275
+ }
1276
+ const md = this.settings_dimension_modal;
1277
+ md.element('action').innerHTML = action;
1278
+ md.element('clear').innerHTML = clear;
1279
+ md.element('string').value = value;
1280
+ md.show('string');
1281
+ }
1282
+
1283
+ modifySettingsDimension() {
1284
+ let x = this.selected_experiment;
1285
+ if(x) {
1286
+ const
1287
+ add = this.edited_dimension_index < 0,
1288
+ // Trim whitespace and reduce inner spacing to a single space
1289
+ dimstr = this.settings_dimension_modal.element('string').value.trim();
1290
+ // Remove dimension if field has been cleared
1291
+ if(dimstr.length === 0) {
1292
+ if(!add) {
1293
+ x.settings_dimensions.splice(this.edited_dimension_index, 1);
1294
+ }
1295
+ } else {
1296
+ // Check for valid selector list
1297
+ const
1298
+ dim = dimstr.split(/\s+/g),
1299
+ ssl = [];
1300
+ // Get this experiment's settings selector list
1301
+ for(let i = 0; i < x.settings_selectors.length; i++) {
1302
+ ssl.push(x.settings_selectors[i].split('|')[0]);
1303
+ }
1304
+ // All selectors in string should have been defined
1305
+ let c = complement(dim, ssl);
1306
+ if(c.length > 0) {
1307
+ UI.warn('Settings dimension contains ' +
1308
+ pluralS(c.length, 'unknown selector') + ': ' + c.join(' '));
1309
+ return;
1310
+ }
1311
+ // No selectors in string may occur in another dimension
1312
+ for(let i = 0; i < x.settings_dimensions.length; i++) {
1313
+ c = intersection(dim, x.settings_dimensions[i]);
1314
+ if(c.length > 0 && i != this.edited_dimension_index) {
1315
+ UI.warn(pluralS(c.length, 'selector') + ' already in use: ' +
1316
+ c.join(' '));
1317
+ return;
1318
+ }
1319
+ }
1320
+ // OK? Then add or modify
1321
+ if(add) {
1322
+ x.settings_dimensions.push(dim);
1323
+ } else {
1324
+ x.settings_dimensions[this.edited_dimension_index] = dim;
1325
+ }
1326
+ }
1327
+ }
1328
+ this.settings_dimension_modal.hide();
1329
+ // Update settings dimensions dialog
1330
+ this.editSettingsDimensions();
1331
+ }
1332
+
1333
+ editCombinationDimensions() {
1334
+ // Open dialog for editing combination dimensions
1335
+ const
1336
+ x = this.selected_experiment,
1337
+ rows = [];
1338
+ if(x) {
1339
+ // Initialize selector list
1340
+ for(let i = 0; i < x.combination_selectors.length; i++) {
1341
+ const sel = x.combination_selectors[i].split('|');
1342
+ rows.push('<tr onclick="EXPERIMENT_MANAGER.editCombinationSelector(', i,
1343
+ ');"><td width="25%">', sel[0], '</td><td>', sel[1], '</td></tr>');
1344
+ }
1345
+ this.combination_modal.element('s-table').innerHTML = rows.join('');
1346
+ // Initialize combination list
1347
+ rows.length = 0;
1348
+ for(let i = 0; i < x.combination_dimensions.length; i++) {
1349
+ const dim = x.combination_dimensions[i];
1350
+ rows.push('<tr onclick="EXPERIMENT_MANAGER.editCombinationDimension(', i,
1351
+ ');"><td>', setString(dim), '</td></tr>');
1352
+ }
1353
+ this.combination_modal.element('d-table').innerHTML = rows.join('');
1354
+ this.combination_modal.show();
1355
+ // NOTE: clear infoline because dialog can generate warnings that would
1356
+ // otherwise remain visible while no longer relevant
1357
+ UI.setMessage('');
1358
+ }
1359
+ }
1360
+
1361
+ closeCombinationDimensions() {
1362
+ // Hide editor, and then update the experiment manager to reflect changes
1363
+ this.combination_modal.hide();
1364
+ this.updateDialog();
1365
+ }
1366
+
1367
+ editCombinationSelector(selnr) {
1368
+ const x = this.selected_experiment;
1369
+ if(!x) return;
1370
+ let action = 'Add',
1371
+ clear = '',
1372
+ sel = ['', ''];
1373
+ this.edited_combi_selector_index = selnr;
1374
+ if(selnr >= 0) {
1375
+ action = 'Edit';
1376
+ clear = '(clear to remove)';
1377
+ sel = x.combination_selectors[selnr].split('|');
1378
+ }
1379
+ const md = this.combination_selector_modal;
1380
+ md.element('action').innerHTML = action;
1381
+ md.element('clear').innerHTML = clear;
1382
+ md.element('code').value = sel[0];
1383
+ md.element('string').value = sel[1];
1384
+ md.show(sel[0] ? 'string' : 'code');
1385
+ }
1386
+
1387
+ modifyCombinationSelector() {
1388
+ // Accepts an "orthogonal" set of selectors
1389
+ let x = this.selected_experiment;
1390
+ if(x) {
1391
+ const
1392
+ md = this.combination_selector_modal,
1393
+ sc = md.element('code'),
1394
+ ss = md.element('string'),
1395
+ // Ignore invalid characters in the combination selector
1396
+ code = sc.value.replace(/[^\w\+\-\%]/g, ''),
1397
+ // Reduce comma's, semicolons and multiple spaces in the
1398
+ // combination string to a single space
1399
+ value = ss.value.trim().replace(/[\,\;\s]+/g, ' '),
1400
+ add = this.edited_combi_selector_index < 0;
1401
+ // Remove selector if either field has been cleared
1402
+ if(code.length === 0 || value.length === 0) {
1403
+ if(!add) {
1404
+ x.combination_selectors.splice(this.edited_combi_selector_index, 1);
1405
+ }
1406
+ } else {
1407
+ let ok = x.allDimensionSelectors.indexOf(code) < 0;
1408
+ if(ok) {
1409
+ // Check for uniqueness of code
1410
+ for(let i = 0; i < x.combination_selectors.length; i++) {
1411
+ // NOTE: ignore selector being edited, as this selector can be renamed
1412
+ if(i != this.edited_combi_selector_index &&
1413
+ x.combination_selectors[i].startsWith(code + '|')) ok = false;
1414
+ }
1415
+ }
1416
+ if(!ok) {
1417
+ UI.warn(`Combination selector "${code}" already defined`);
1418
+ sc.focus();
1419
+ return;
1420
+ }
1421
+ // Test for orthogonality (and existence!) of the selectors
1422
+ if(!x.orthogonalSelectors(value.split(' '))) {
1423
+ ss.focus();
1424
+ return;
1425
+ }
1426
+ // Combination selector has format code|space-separated selectors
1427
+ const sel = code + '|' + value;
1428
+ if(add) {
1429
+ x.combination_selectors.push(sel);
1430
+ } else {
1431
+ // NOTE: rename occurrence of code in dimension (should at most be 1)
1432
+ const oc = x.combination_selectors[this.edited_combi_selector_index].split('|')[0];
1433
+ x.combination_selectors[this.edited_combi_selector_index] = sel;
1434
+ for(let i = 0; i < x.combination_dimensions.length; i++) {
1435
+ const si = x.combination_dimensions[i].indexOf(oc);
1436
+ if(si >= 0) x.combination_dimensions[i][si] = code;
1437
+ }
1438
+ }
1439
+ }
1440
+ md.hide();
1441
+ }
1442
+ // Update combination dimensions dialog
1443
+ this.editCombinationDimensions();
1444
+ }
1445
+
1446
+ editCombinationDimension(dimnr) {
1447
+ const x = this.selected_experiment;
1448
+ if(!x) return;
1449
+ let action = 'Add',
1450
+ clear = '',
1451
+ value = '';
1452
+ this.edited_combi_dimension_index = dimnr;
1453
+ if(dimnr >= 0) {
1454
+ action = 'Edit';
1455
+ clear = '(clear to remove)';
1456
+ // NOTE: present to modeler as space-separated string
1457
+ value = x.combination_dimensions[dimnr].join(' ');
1458
+ }
1459
+ const md = this.combination_dimension_modal;
1460
+ md.element('action').innerHTML = action;
1461
+ md.element('clear').innerHTML = clear;
1462
+ md.element('string').value = value;
1463
+ md.show('string');
1464
+ }
1465
+
1466
+ modifyCombinationDimension() {
1467
+ let x = this.selected_experiment;
1468
+ if(x) {
1469
+ const
1470
+ add = this.edited_combi_dimension_index < 0,
1471
+ // Trim whitespace and reduce inner spacing to a single space
1472
+ dimstr = this.combination_dimension_modal.element('string').value.trim();
1473
+ // Remove dimension if field has been cleared
1474
+ if(dimstr.length === 0) {
1475
+ if(!add) {
1476
+ x.combination_dimensions.splice(this.edited_combi_dimension_index, 1);
1477
+ }
1478
+ } else {
1479
+ // Check for valid selector list
1480
+ const
1481
+ dim = dimstr.split(/\s+/g),
1482
+ ssl = [];
1483
+ // Get this experiment's combination selector list
1484
+ for(let i = 0; i < x.combination_selectors.length; i++) {
1485
+ ssl.push(x.combination_selectors[i].split('|')[0]);
1486
+ }
1487
+ // All selectors in string should have been defined
1488
+ let c = complement(dim, ssl);
1489
+ if(c.length > 0) {
1490
+ UI.warn('Combination dimension contains ' +
1491
+ pluralS(c.length, 'unknown selector') + ': ' + c.join(' '));
1492
+ return;
1493
+ }
1494
+ // All selectors should expand to non-overlapping selector sets
1495
+ if(!x.orthogonalCombinationDimensions(dim)) return;
1496
+ // Do not add when a (setwise) identical combination dimension exists
1497
+ for(let i = 0; i < x.combination_dimensions.length; i++) {
1498
+ const cd = x.combination_dimensions[i];
1499
+ if(intersection(dim, cd).length === dim.length) {
1500
+ UI.notify('Combination already defined: ' + setString(cd));
1501
+ return;
1502
+ }
1503
+ }
1504
+ // OK? Then add or modify
1505
+ if(add) {
1506
+ x.combination_dimensions.push(dim);
1507
+ } else {
1508
+ x.combination_dimensions[this.edited_combi_dimension_index] = dim;
1509
+ }
1510
+ }
1511
+ }
1512
+ this.combination_dimension_modal.hide();
1513
+ // Update combination dimensions dialog
1514
+ this.editCombinationDimensions();
1515
+ }
1516
+
1517
+ editActorDimension() {
1518
+ // Open dialog for editing the actor dimension
1519
+ const x = this.selected_experiment, rows = [];
1520
+ if(x) {
1521
+ // Initialize selector list
1522
+ for(let i = 0; i < x.actor_selectors.length; i++) {
1523
+ rows.push('<tr onclick="EXPERIMENT_MANAGER.editActorSelector(', i,
1524
+ ');"><td>', x.actor_selectors[i].selector,
1525
+ '</td><td style="font-family: monospace">',
1526
+ x.actor_selectors[i].round_sequence, '</td></tr>');
1527
+ }
1528
+ this.actor_dimension_modal.element('table').innerHTML = rows.join('');
1529
+ this.actor_dimension_modal.show();
1530
+ // NOTE: clear infoline because dialog can generate warnings that would
1531
+ // otherwise remain visible while no longer relevant
1532
+ UI.setMessage('');
1533
+ }
1534
+ }
1535
+
1536
+ closeActorDimension() {
1537
+ // Hide editor, and then update the experiment manager to reflect changes
1538
+ this.actor_dimension_modal.hide();
1539
+ this.updateDialog();
1540
+ }
1541
+
1542
+ editActorSelector(selnr) {
1543
+ let x = this.selected_experiment;
1544
+ if(!x) return;
1545
+ let action = 'Add',
1546
+ clear = '', asel;
1547
+ this.edited_selector_index = selnr;
1548
+ if(selnr >= 0) {
1549
+ action = 'Edit';
1550
+ clear = '(clear to remove)';
1551
+ asel = x.actor_selectors[selnr];
1552
+ } else {
1553
+ asel = new ActorSelector();
1554
+ }
1555
+ const md = this.actor_selector_modal;
1556
+ md.element('action').innerHTML = action;
1557
+ md.element('code').value = asel.selector;
1558
+ md.element('rounds').value = asel.round_sequence;
1559
+ md.element('clear').innerHTML = clear;
1560
+ md.show('code');
1561
+ }
1562
+
1563
+ modifyActorSelector() {
1564
+ let x = this.selected_experiment;
1565
+ if(x) {
1566
+ const
1567
+ easc = this.actor_selector_modal.element('code'),
1568
+ code = easc.value.replace(/[^\w\+\-\%]/g, ''),
1569
+ add = this.edited_selector_index < 0;
1570
+ // Remove selector if code has been cleared
1571
+ if(code.length === 0) {
1572
+ if(!add) {
1573
+ x.actor_selectors.splice(this.edited_selector_index, 1);
1574
+ }
1575
+ } else {
1576
+ // Check for uniqueness of code
1577
+ for(let i = 0; i < x.actor_selectors.length; i++) {
1578
+ // NOTE: ignore selector being edited, as this selector can be renamed
1579
+ if(i != this.edited_selector_index &&
1580
+ x.actor_selectors[i].selector == code) {
1581
+ UI.warn(`Actor selector "${code}"already defined`);
1582
+ easc.focus();
1583
+ return;
1584
+ }
1585
+ }
1586
+ const
1587
+ rs = this.actor_selector_modal.element('rounds'),
1588
+ rss = rs.value.replace(/[^a-zA-E]/g, ''),
1589
+ rsa = ACTOR_MANAGER.checkRoundSequence(rss);
1590
+ if(!rsa) {
1591
+ // NOTE: warning is already displayed by parser
1592
+ rs.focus();
1593
+ return;
1594
+ }
1595
+ const
1596
+ asel = (add ? new ActorSelector() :
1597
+ x.actor_selectors[this.edited_selector_index]);
1598
+ asel.selector = code;
1599
+ asel.round_sequence = rsa;
1600
+ rs.value = rss;
1601
+ if(add) x.actor_selectors.push(asel);
1602
+ }
1603
+ }
1604
+ this.actor_selector_modal.hide();
1605
+ // Update actor dimensions dialog
1606
+ this.editActorDimension();
1607
+ }
1608
+
1609
+ showClustersToIgnore() {
1610
+ // Opens the "clusters to ignore" dialog
1611
+ const x = this.selected_experiment;
1612
+ if(!x) return;
1613
+ const
1614
+ md = this.clusters_modal,
1615
+ clist = [],
1616
+ csel = md.element('select'),
1617
+ sinp = md.element('selectors');
1618
+ // NOTE: copy experiment property to modal dialog property, so that changes
1619
+ // are made only when OK is clicked
1620
+ md.clusters = [];
1621
+ for(let i = 0; i < x.clusters_to_ignore.length; i++) {
1622
+ const cs = x.clusters_to_ignore[i];
1623
+ md.clusters.push({cluster: cs.cluster, selectors: cs. selectors});
1624
+ }
1625
+ md.cluster_index = -1;
1626
+ for(let k in MODEL.clusters) if(MODEL.clusters.hasOwnProperty(k)) {
1627
+ const c = MODEL.clusters[k];
1628
+ // Do not add top cluster, nor clusters already on the list
1629
+ if(c !== MODEL.top_cluster && !c.ignore && !x.mayBeIgnored(c)) {
1630
+ clist.push(`<option value="${k}">${c.displayName}</option>`);
1631
+ }
1632
+ }
1633
+ csel.innerHTML = clist.join('');
1634
+ sinp.style.backgroundColor = 'inherit';
1635
+ this.updateClusterList();
1636
+ md.show();
1637
+ }
1638
+
1639
+ updateClusterList() {
1640
+ const
1641
+ md = this.clusters_modal,
1642
+ clst = md.element('list'),
1643
+ nlst = md.element('no-list'),
1644
+ ctbl = md.element('table'),
1645
+ sinp = md.element('selectors'),
1646
+ sdiv = md.element('selectors-div'),
1647
+ cl = md.clusters.length;
1648
+ if(cl > 0) {
1649
+ // Show cluster+selectors list
1650
+ const ol = [];
1651
+ for(let i = 0; i < cl; i++) {
1652
+ const cti = md.clusters[i];
1653
+ ol.push('<tr class="variable',
1654
+ (i === md.cluster_index ? ' sel-set' : ''),
1655
+ '" onclick="EXPERIMENT_MANAGER.selectCluster(', i, ');"><td>',
1656
+ cti.cluster.displayName, '</td><td>', cti.selectors, '</td></tr>');
1657
+ }
1658
+ ctbl.innerHTML = ol.join('');
1659
+ clst.style.display = 'block';
1660
+ nlst.style.display = 'none';
1661
+ } else {
1662
+ // Hide list and show "no clusters set to be ignored"
1663
+ clst.style.display = 'none';
1664
+ nlst.style.display = 'block';
1665
+ }
1666
+ if(md.cluster_index < 0) {
1667
+ // Hide selectors and delete button
1668
+ sdiv.style.display = 'none';
1669
+ } else {
1670
+ // Show selectors and enable input and delete button
1671
+ sinp.value = md.clusters[md.cluster_index].selectors;
1672
+ sdiv.style.display = 'block';
1673
+ }
1674
+ sinp.style.backgroundColor = 'inherit';
1675
+ }
1676
+
1677
+ selectCluster(n) {
1678
+ // Set selected cluster index to `n`
1679
+ this.clusters_modal.cluster_index = n;
1680
+ this.updateClusterList();
1681
+ }
1682
+
1683
+ addClusterToIgnoreList() {
1684
+ const
1685
+ md = this.clusters_modal,
1686
+ sel = md.element('select'),
1687
+ c = MODEL.objectByID(sel.value);
1688
+ if(c) {
1689
+ md.clusters.push({cluster: c, selectors: ''});
1690
+ md.cluster_index = md.clusters.length - 1;
1691
+ // Remove cluster from select so it cannot be added again
1692
+ sel.remove(sel.selectedIndex);
1693
+ this.updateClusterList();
1694
+ }
1695
+ }
1696
+
1697
+ editIgnoreSelectors() {
1698
+ this.clusters_modal.element('selectors').style.backgroundColor = 'white';
1699
+ }
1700
+
1701
+ setIgnoreSelectors() {
1702
+ const
1703
+ md = this.clusters_modal,
1704
+ sinp = md.element('selectors'),
1705
+ s = sinp.value.replace(/[\;\,]/g, ' ').trim().replace(
1706
+ /[^a-zA-Z0-9\+\-\%\_\s]/g, '').split(/\s+/).join(' ');
1707
+ if(md.cluster_index >= 0) {
1708
+ md.clusters[md.cluster_index].selectors = s;
1709
+ }
1710
+ this.updateClusterList();
1711
+ }
1712
+
1713
+ deleteClusterFromIgnoreList() {
1714
+ // Delete selected cluster+selectors from list
1715
+ const md = this.clusters_modal;
1716
+ if(md.cluster_index >= 0) {
1717
+ md.clusters.splice(md.cluster_index, 1);
1718
+ md.cluster_index = -1;
1719
+ this.updateClusterList();
1720
+ }
1721
+ }
1722
+
1723
+ modifyClustersToIgnore() {
1724
+ // Replace current list by cluster+selectors list of modal dialog
1725
+ const
1726
+ md = this.clusters_modal,
1727
+ x = this.selected_experiment;
1728
+ if(x) x.clusters_to_ignore = md.clusters;
1729
+ md.hide();
1730
+ this.updateDialog();
1731
+ }
1732
+
1733
+ promptForParameter(type) {
1734
+ // Open dialog for adding new dimension or chart
1735
+ const x = this.selected_experiment;
1736
+ if(x) {
1737
+ const ol = [];
1738
+ this.parameter_modal.element('type').innerHTML = type;
1739
+ if(type === 'dimension') {
1740
+ x.inferAvailableDimensions();
1741
+ for(let i = 0; i < x.available_dimensions.length; i++) {
1742
+ const ds = setString(x.available_dimensions[i]);
1743
+ ol.push(`<option value="${ds}">${ds}</option>`);
1744
+ }
1745
+ } else {
1746
+ for(let i = 0; i < this.suitable_charts.length; i++) {
1747
+ const c = this.suitable_charts[i];
1748
+ // NOTE: exclude charts already in the selected experiment
1749
+ if (x.charts.indexOf(c) < 0) {
1750
+ ol.push(`<option value="${c.title}">${c.title}</option>`);
1751
+ }
1752
+ }
1753
+ }
1754
+ this.parameter_modal.element('select').innerHTML = ol.join('');
1755
+ this.parameter_modal.show('select');
1756
+ }
1757
+ }
1758
+
1759
+ addParameter() {
1760
+ // Add parameter (dimension or chart) to experiment
1761
+ const
1762
+ x = this.selected_experiment,
1763
+ name = this.parameter_modal.element('select').value;
1764
+ if(x && name) {
1765
+ if(this.parameter_modal.element('type').innerHTML === 'chart') {
1766
+ const ci = MODEL.indexOfChart(name);
1767
+ if(ci >= 0 && x.charts.indexOf(MODEL.charts[ci]) < 0) {
1768
+ x.charts.push(MODEL.charts[ci]);
1769
+ }
1770
+ } else {
1771
+ // Convert set notation to selector list
1772
+ const d = name.replace(/[\{\}]/g, '').split(', ');
1773
+ // Append it to the list
1774
+ x.dimensions.push(d);
1775
+ }
1776
+ this.updateParameters();
1777
+ this.parameter_modal.hide();
1778
+ }
1779
+ }
1780
+
1781
+ deleteParameter() {
1782
+ // Remove selected dimension or chart from selected experiment
1783
+ const
1784
+ x = this.selected_experiment,
1785
+ sp = this.selected_parameter;
1786
+ if(x && sp) {
1787
+ const type = sp.charAt(0), index = sp.slice(1);
1788
+ if(type === 'd') {
1789
+ x.dimensions.splice(index, 1);
1790
+ } else {
1791
+ x.charts.splice(index, 1);
1792
+ }
1793
+ this.selected_parameter = '';
1794
+ this.updateParameters();
1795
+ }
1796
+ }
1797
+
1798
+ editExclusions() {
1799
+ // Give visual feedback by setting background color to white
1800
+ this.exclude.style.backgroundColor = 'white';
1801
+ }
1802
+
1803
+ setExclusions() {
1804
+ // Sanitize string before accepting it as space-separated selector list
1805
+ const
1806
+ x = this.selected_experiment;
1807
+ if(x) {
1808
+ x.excluded_selectors = this.exclude.value.replace(
1809
+ /[\;\,]/g, ' ').trim().replace(
1810
+ /[^a-zA-Z0-9\+\-\=\%\_\s]/g, '').split(/\s+/).join(' ');
1811
+ this.exclude.value = x.excluded_selectors;
1812
+ this.updateParameters();
1813
+ }
1814
+ this.exclude.style.backgroundColor = 'inherit';
1815
+ }
1816
+
1817
+ readyButtons() {
1818
+ // Set experiment run control buttons in "ready" state
1819
+ this.pause_btn.classList.add('off');
1820
+ this.stop_btn.classList.add('off');
1821
+ this.start_btn.classList.remove('off', 'blink');
1822
+ }
1823
+
1824
+ pausedButtons(aci) {
1825
+ // Set experiment run control buttons in "paused" state
1826
+ this.pause_btn.classList.remove('blink');
1827
+ this.pause_btn.classList.add('off');
1828
+ this.start_btn.classList.remove('off');
1829
+ // Blinking start button indicates: paused -- click to resume
1830
+ this.start_btn.classList.add('blink');
1831
+ this.viewer_progress.innerHTML = `Run ${aci} PAUSED`;
1832
+ }
1833
+
1834
+ resumeButtons() {
1835
+ // Changes buttons to "running" state, and return TRUE if state was "paused"
1836
+ const paused = this.start_btn.classList.contains('blink');
1837
+ this.start_btn.classList.remove('blink');
1838
+ this.start_btn.classList.add('off');
1839
+ this.pause_btn.classList.remove('off');
1840
+ this.stop_btn.classList.add('off');
1841
+ return paused;
1842
+ }
1843
+
1844
+ pauseExperiment() {
1845
+ // Interrupt solver but retain data on server and allow resume
1846
+ UI.notify('Run sequence will be suspended after the current run');
1847
+ this.pause_btn.classList.add('blink');
1848
+ this.stop_btn.classList.remove('off');
1849
+ this.must_pause = true;
1850
+ }
1851
+
1852
+ stopExperiment() {
1853
+ // Interrupt solver but retain data on server (and no resume)
1854
+ VM.halt();
1855
+ MODEL.running_experiment = null;
1856
+ UI.notify('Experiment has been stopped');
1857
+ this.viewer_progress.innerHTML = '';
1858
+ this.readyButtons();
1859
+ }
1860
+
1861
+ showProgress(ci, p, n) {
1862
+ // Show progress in the viewer
1863
+ this.viewer_progress.innerHTML = `Run ${ci} (${p}% of ${n})`;
1864
+ }
1865
+
1866
+ copyTableToClipboard() {
1867
+ UI.copyHtmlToClipboard(
1868
+ document.getElementById('viewer-scroll-area').innerHTML);
1869
+ UI.notify('Table copied to clipboard (as HTML)');
1870
+ }
1871
+
1872
+ promptForDownload() {
1873
+ // Show the download modal
1874
+ const x = this.selected_experiment;
1875
+ if(!x) return;
1876
+ const
1877
+ md = this.download_modal,
1878
+ ds = x.download_settings,
1879
+ runs = x.runs.length,
1880
+ sruns = x.chart_combinations.length;
1881
+ if(!runs) {
1882
+ UI.notify('No experiment results');
1883
+ return;
1884
+ }
1885
+ md.element(ds.variables + '-v').checked = true;
1886
+ // Disable "selected runs" button when no runs have been selected
1887
+ if(sruns) {
1888
+ md.element('selected-r').disabled = false;
1889
+ md.element(ds.runs + '-r').checked = true;
1890
+ } else {
1891
+ md.element('selected-r').disabled = true;
1892
+ // Check "all runs" but do not change download setting
1893
+ md.element('all-r').checked = true;
1894
+ }
1895
+ this.download_modal.show();
1896
+ md.element('statistics').checked = ds.statistics;
1897
+ md.element('series').checked = ds.series;
1898
+ md.element('solver').checked = ds.solver;
1899
+ md.element('separator').value = ds.separator;
1900
+ md.element('quotes').value = ds.quotes;
1901
+ md.element('precision').value = ds.precision;
1902
+ md.element('var-count').innerText = x.runs[0].results.length;
1903
+ md.element('run-count').innerText = runs;
1904
+ md.element('run-s').innerText = (sruns === 1 ? '' : 's');
1905
+ }
1906
+
1907
+ downloadDataAsCSV() {
1908
+ // Push results to browser
1909
+ if(this.selected_experiment) {
1910
+ const md = this.download_modal;
1911
+ this.selected_experiment.download_settings = {
1912
+ variables: md.element('all-v').checked ? 'all' : 'selected',
1913
+ runs: md.element('all-r').checked ? 'all' : 'selected',
1914
+ statistics: md.element('statistics').checked,
1915
+ series: md.element('series').checked,
1916
+ solver: md.element('solver').checked,
1917
+ separator: md.element('separator').value,
1918
+ quotes: md.element('quotes').value,
1919
+ precision: safeStrToInt(md.element('precision').value,
1920
+ CONFIGURATION.results_precision),
1921
+ };
1922
+ md.hide();
1923
+ const data = this.selected_experiment.resultsAsCSV;
1924
+ if(data) {
1925
+ UI.setMessage('CSV file size: ' + UI.sizeInBytes(data.length));
1926
+ const el = document.getElementById('xml-saver');
1927
+ el.href = 'data:attachment/text,' + encodeURI(data);
1928
+ console.log('Encoded CSV file size:', el.href.length);
1929
+ el.download = 'results.csv';
1930
+ if(el.href.length > 25*1024*1024 &&
1931
+ navigator.userAgent.search('Chrome') <= 0) {
1932
+ UI.notify('CSV file size exceeds 25 MB. ' +
1933
+ 'If it does not download, select fewer runs');
1934
+ }
1935
+ el.click();
1936
+ UI.normalCursor();
1937
+ } else {
1938
+ UI.notify('No data');
1939
+ }
1940
+ }
1941
+ }
1942
+
1943
+ } // END of class GUIExperimentManager
1944
+