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.
- 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 +450 -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 +34 -15
- package/static/scripts/linny-r-utils.js +11 -1
- package/static/scripts/linny-r-vm.js +21 -12
- 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}">✔</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
|
+
|