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,944 @@
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-chartmgr.js) provides the GUI functionality
9
+ for the Linny-R Chart 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 GUIChartManager
36
+ class GUIChartManager extends ChartManager {
37
+ constructor() {
38
+ super();
39
+ this.dialog = UI.draggableDialog('chart');
40
+ UI.resizableDialog('chart', 'CHART_MANAGER');
41
+ this.dialog.addEventListener('mousemove',
42
+ (event) => CHART_MANAGER.showInfo(event.shiftKey));
43
+ this.dialog.addEventListener('dragover',
44
+ (event) => CHART_MANAGER.dragOver(event));
45
+ this.dialog.addEventListener('drop',
46
+ (event) => CHART_MANAGER.handleDrop(event));
47
+ // Toolbar buttons
48
+ document.getElementById('chart-close-btn').addEventListener(
49
+ 'click', (event) => UI.toggleDialog(event));
50
+ document.getElementById('chart-rename-btn').addEventListener(
51
+ 'click', () => CHART_MANAGER.promptForTitle());
52
+ document.getElementById('chart-clone-btn').addEventListener(
53
+ 'click', () => CHART_MANAGER.cloneChart());
54
+ this.results_btn = document.getElementById('chart-results-btn');
55
+ this.results_btn.addEventListener(
56
+ 'click', () => CHART_MANAGER.toggleRunResults());
57
+ document.getElementById('chart-delete-btn').addEventListener(
58
+ 'click', () => CHART_MANAGER.deleteChart());
59
+ this.control_panel = document.getElementById('chart-control-panel');
60
+ this.chart_selector = document.getElementById('chart-selector');
61
+ this.chart_selector.addEventListener(
62
+ 'change', () => CHART_MANAGER.selectChart());
63
+ document.getElementById('chart-histogram').addEventListener(
64
+ 'click', () => CHART_MANAGER.toggleHistogram());
65
+ this.histogram_options = document.getElementById('chart-histogram-options');
66
+ this.bins_selector = document.getElementById('histogram-bins');
67
+ this.bins_selector.addEventListener(
68
+ 'change', () => CHART_MANAGER.changeBins());
69
+ document.getElementById('chart-title').addEventListener(
70
+ 'click', () => CHART_MANAGER.toggleTitle());
71
+ this.legend_selector = document.getElementById('chart-legend');
72
+ this.legend_selector.addEventListener(
73
+ 'change', () => CHART_MANAGER.changeLegend());
74
+ document.getElementById('chart-add-variable-btn').addEventListener(
75
+ 'click', (event) => CHART_MANAGER.promptForVariable(event.shiftKey));
76
+ document.getElementById('chart-variable-up-btn').addEventListener(
77
+ 'click', () => CHART_MANAGER.moveVariable(-1));
78
+ document.getElementById('chart-variable-down-btn').addEventListener(
79
+ 'click', () => CHART_MANAGER.moveVariable(1));
80
+ document.getElementById('chart-edit-variable-btn').addEventListener(
81
+ 'click', () => CHART_MANAGER.editVariable());
82
+ document.getElementById('chart-delete-variable-btn').addEventListener(
83
+ 'click', () => CHART_MANAGER.deleteVariable());
84
+ this.variables_table = document.getElementById('chart-variables-table');
85
+ this.display_panel = document.getElementById('chart-display-panel');
86
+ this.toggle_chevron = document.getElementById('chart-toggle-chevron');
87
+ this.table_panel = document.getElementById('chart-table-panel');
88
+ this.statistics_table = document.getElementById('chart-table');
89
+ this.svg_container = document.getElementById('chart-svg-container');
90
+ this.svg_container.addEventListener(
91
+ 'mousemove', (event) => CHART_MANAGER.updateTimeStep(event, true));
92
+ this.svg_container.addEventListener(
93
+ 'mouseleave', (event) => CHART_MANAGER.updateTimeStep(event, false));
94
+ this.time_step = document.getElementById('chart-time-step');
95
+ document.getElementById('chart-toggle-chevron').addEventListener(
96
+ 'click', () => CHART_MANAGER.toggleControlPanel());
97
+ document.getElementById('chart-stats-btn').addEventListener(
98
+ 'click', () => CHART_MANAGER.toggleStatistics());
99
+ document.getElementById('chart-copy-stats-btn').addEventListener(
100
+ 'click', () => CHART_MANAGER.copyStatistics());
101
+ document.getElementById('chart-copy-data-btn').addEventListener(
102
+ 'click', () => CHART_MANAGER.copyData());
103
+ document.getElementById('chart-copy-table-btn').addEventListener(
104
+ 'click', () => CHART_MANAGER.copyTable());
105
+ document.getElementById('chart-save-btn').addEventListener(
106
+ 'click', () => CHART_MANAGER.downloadChart());
107
+ document.getElementById('chart-render-btn').addEventListener(
108
+ 'click', () => CHART_MANAGER.renderChartAsPNG());
109
+ document.getElementById('chart-widen-btn').addEventListener(
110
+ 'click', () => CHART_MANAGER.stretchChart(1));
111
+ document.getElementById('chart-narrow-btn').addEventListener(
112
+ 'click', () => CHART_MANAGER.stretchChart(-1));
113
+
114
+ // The Add variable modal
115
+ this.add_variable_modal = new ModalDialog('add-variable');
116
+ this.add_variable_modal.ok.addEventListener(
117
+ 'click', () => CHART_MANAGER.addVariable());
118
+ this.add_variable_modal.cancel.addEventListener(
119
+ 'click', () => CHART_MANAGER.add_variable_modal.hide());
120
+ // NOTE: uses methods of the Expression Editor
121
+ this.add_variable_modal.element('obj').addEventListener(
122
+ 'change', () => X_EDIT.updateVariableBar('add-'));
123
+ this.add_variable_modal.element('name').addEventListener(
124
+ 'change', () => X_EDIT.updateAttributeSelector('add-'));
125
+
126
+ // The Edit variable modal
127
+ this.variable_modal = new ModalDialog('variable');
128
+ this.variable_modal.ok.addEventListener(
129
+ 'click', () => CHART_MANAGER.modifyVariable());
130
+ this.variable_modal.cancel.addEventListener(
131
+ 'click', () => CHART_MANAGER.variable_modal.hide());
132
+ this.change_equation_btns = document.getElementById('change-equation-btns');
133
+ document.getElementById('chart-rename-equation-btn').addEventListener(
134
+ 'click', () => CHART_MANAGER.renameEquation());
135
+ document.getElementById('chart-edit-equation-btn').addEventListener(
136
+ 'click', () => CHART_MANAGER.editEquation());
137
+ document.getElementById('variable-color').addEventListener(
138
+ 'mouseenter', () => CHART_MANAGER.showPasteColor());
139
+ document.getElementById('variable-color').addEventListener(
140
+ 'mouseleave', () => CHART_MANAGER.hidePasteColor());
141
+ document.getElementById('variable-color').addEventListener(
142
+ 'click', (event) => CHART_MANAGER.copyPasteColor(event));
143
+ // NOTE: uses the color picker developed by James Daniel
144
+ this.color_picker = new iro.ColorPicker("#color-picker", {
145
+ width: 92,
146
+ height: 92,
147
+ color: '#a00',
148
+ markerRadius: 10,
149
+ padding: 1,
150
+ sliderMargin: 6,
151
+ sliderHeight: 10,
152
+ borderWidth: 1,
153
+ borderColor: '#fff',
154
+ anticlockwise: true
155
+ });
156
+ this.color_picker.on('input:end',
157
+ () => {
158
+ document.getElementById('variable-color').style.backgroundColor =
159
+ CHART_MANAGER.color_picker.color.hexString;
160
+ });
161
+
162
+ // The Rename chart modal
163
+ this.rename_chart_modal = new ModalDialog('rename-chart');
164
+ this.rename_chart_modal.ok.addEventListener(
165
+ 'click', () => CHART_MANAGER.renameChart());
166
+ this.rename_chart_modal.cancel.addEventListener(
167
+ 'click', () => CHART_MANAGER.rename_chart_modal.hide());
168
+
169
+ // Do not display the time step until cursor moves over chart
170
+ this.time_step.style.display = 'none';
171
+ document.getElementById('table-only-buttons').style.display = 'none';
172
+ // Initialize properties
173
+ this.reset();
174
+ }
175
+
176
+ reset() {
177
+ // Basic reset (same as console-only class)
178
+ this.visible = false;
179
+ this.chart_index = -1;
180
+ this.variable_index = -1;
181
+ this.stretch_factor = 1;
182
+ this.drawing_graph = false;
183
+ this.runs_chart = false;
184
+ // Clear the model-related DOM elements
185
+ this.chart_selector.innerHTML = '';
186
+ this.variables_table.innerHTML = '';
187
+ this.options_shown = true;
188
+ this.setRunsChart(false);
189
+ this.last_time_selected = 0;
190
+ this.paste_color = '';
191
+ }
192
+
193
+ enterKey() {
194
+ // Open "edit" dialog for the selected chart variable
195
+ const srl = this.variables_table.getElementsByClassName('sel-set');
196
+ if(srl.length > 0) {
197
+ const r = this.variables_table.rows[srl[0].rowIndex];
198
+ if(r) {
199
+ // Emulate a double-click to edit the variable properties
200
+ this.last_time_selected = Date.now();
201
+ r.dispatchEvent(new Event('click'));
202
+ }
203
+ }
204
+ }
205
+
206
+ upDownKey(dir) {
207
+ // Select row above or below the selected one (if possible)
208
+ const srl = this.variables_table.getElementsByClassName('sel-set');
209
+ if(srl.length > 0) {
210
+ const r = this.variables_table.rows[srl[0].rowIndex + dir];
211
+ if(r) {
212
+ UI.scrollIntoView(r);
213
+ r.dispatchEvent(new Event('click'));
214
+ }
215
+ }
216
+ }
217
+
218
+ setRunsChart(show) {
219
+ // Indicates whether the chart manager should display a run result chart
220
+ this.runs_chart = show;
221
+ if(show) {
222
+ this.results_btn.classList.add('stay-activ');
223
+ } else {
224
+ this.results_btn.classList.remove('stay-activ');
225
+ }
226
+ }
227
+
228
+ showInfo(shift) {
229
+ if(this.chart_index >= 0) {
230
+ DOCUMENTATION_MANAGER.update(MODEL.charts[this.chart_index], shift);
231
+ }
232
+ }
233
+
234
+ dragOver(ev) {
235
+ const
236
+ n = ev.dataTransfer.getData('text'),
237
+ obj = MODEL.objectByID(n);
238
+ if(obj) ev.preventDefault();
239
+ }
240
+
241
+ handleDrop(ev) {
242
+ const
243
+ n = ev.dataTransfer.getData('text'),
244
+ obj = MODEL.objectByID(n);
245
+ ev.preventDefault();
246
+ if(!obj) {
247
+ UI.alert(`Unknown entity ID "${n}"`);
248
+ } else if(this.chart_index >= 0) {
249
+ if(obj instanceof DatasetModifier) {
250
+ // Equations can be added directly as chart variable
251
+ this.addVariable(obj.selector);
252
+ return;
253
+ }
254
+ // For other entities, the attribute must be specified
255
+ this.add_variable_modal.show();
256
+ const
257
+ tn = VM.object_types.indexOf(obj.type),
258
+ dn = obj.displayName;
259
+ this.add_variable_modal.element('obj').value = tn;
260
+ X_EDIT.updateVariableBar('add-');
261
+ const s = this.add_variable_modal.element('name');
262
+ let i = 0;
263
+ for(let k in s.options) if(s.options.hasOwnProperty(k)) {
264
+ if(s[k].text === dn) {
265
+ i = s[k].value;
266
+ break;
267
+ }
268
+ }
269
+ s.value = i;
270
+ X_EDIT.updateAttributeSelector('add-');
271
+ }
272
+ }
273
+
274
+ toggleControlPanel() {
275
+ if(this.options_shown) {
276
+ this.control_panel.style.display = 'none';
277
+ this.display_panel.style.left = '1px';
278
+ this.display_panel.style.width = 'calc(100% - 8px)';
279
+ this.toggle_chevron.innerHTML = '»';
280
+ this.toggle_chevron.title = 'Show control panel';
281
+ this.options_shown = false;
282
+ } else {
283
+ this.control_panel.style.display = 'block';
284
+ this.display_panel.style.left = '205px';
285
+ this.display_panel.style.width = 'calc(100% - 212px)';
286
+ this.toggle_chevron.innerHTML = '«';
287
+ this.toggle_chevron.title = 'Hide control panel';
288
+ this.options_shown = true;
289
+ }
290
+ this.stretchChart(0);
291
+ }
292
+
293
+ updateSelector() {
294
+ // Adds one option to the selector for each chart defined for the model
295
+ // NOTE: add the "new chart" option if still absent
296
+ MODEL.addChart(this.new_chart_title);
297
+ if(this.chart_index < 0) this.chart_index = 0;
298
+ const ol = [];
299
+ for(let i = 0; i < MODEL.charts.length; i++) {
300
+ ol.push('<option value="', i,
301
+ (i == this.chart_index ? '"selected="selected' : ''),
302
+ '">', MODEL.charts[i].title , '</option>');
303
+ }
304
+ this.chart_selector.innerHTML = ol.join('');
305
+ }
306
+
307
+ updateDialog() {
308
+ // Refreshes all dialog fields to display actual MODEL chart properties
309
+ this.updateSelector();
310
+ let c = null;
311
+ if(this.chart_index >= 0) {
312
+ c = MODEL.charts[this.chart_index];
313
+ UI.setBox('chart-histogram', c.histogram);
314
+ this.bins_selector.value = c.bins;
315
+ if(c.histogram) {
316
+ this.histogram_options.style.display = 'block';
317
+ } else {
318
+ this.histogram_options.style.display = 'none';
319
+ }
320
+ UI.setBox('chart-title', c.show_title);
321
+ const ol = [];
322
+ for(let i = 0; i < this.legend_options.length; i++) {
323
+ const opt = this.legend_options[i], val = opt.toLowerCase();
324
+ ol.push(['<option value="', val,
325
+ (c.legend_position === val ? '" selected="selected' : ''),
326
+ '">', opt, '</option>'].join(''));
327
+ }
328
+ this.legend_selector.innerHTML = ol.join('');
329
+ ol.length = 0;
330
+ for(let i = 0; i < c.variables.length; i++) {
331
+ const cv = c.variables[i];
332
+ ol.push(['<tr class="variable',
333
+ (i === this.variable_index ? ' sel-set' : ''),
334
+ '" title="', cv.displayName,
335
+ '" onclick="CHART_MANAGER.selectVariable(', i, ');">',
336
+ '<td class="v-box"><div id="v-box-', i, '" class="vbox',
337
+ (cv.visible ? ' checked' : ' clear'),
338
+ '" onclick="CHART_MANAGER.toggleVariable(', i,
339
+ ');"></div></td><td class="v-name">', cv.displayName,
340
+ '</td></tr>'].join(''));
341
+ }
342
+ this.variables_table.innerHTML = ol.join('');
343
+ } else {
344
+ this.variable_index = -1;
345
+ }
346
+ const
347
+ u_btn = 'chart-variable-up ',
348
+ d_btn = 'chart-variable-down ',
349
+ ed_btns = 'chart-edit-variable chart-delete-variable ';
350
+ // Just in case variable index has not been adjusted after some
351
+ // variables have been deleted
352
+ if(this.variable_index >= c.variables.length) {
353
+ this.variable_index = -1;
354
+ }
355
+ if(this.variable_index < 0) {
356
+ UI.disableButtons(ed_btns + u_btn + d_btn);
357
+ } else {
358
+ UI.enableButtons(ed_btns);
359
+ if(this.variable_index > 0) {
360
+ UI.enableButtons(u_btn);
361
+ } else {
362
+ UI.disableButtons(u_btn);
363
+ }
364
+ if(c && this.variable_index < c.variables.length - 1) {
365
+ UI.enableButtons(d_btn);
366
+ } else {
367
+ UI.disableButtons(d_btn);
368
+ }
369
+ // If the Edit variable dialog is showing, update its header
370
+ if(this.variable_index >= 0 && !UI.hidden('variable-dlg')) {
371
+ document.getElementById('variable-dlg-name').innerHTML =
372
+ c.variables[this.variable_index].displayName;
373
+ }
374
+ }
375
+ this.add_variable_modal.element('obj').value = 0;
376
+ // Update variable dropdown list of the "add variable" modal
377
+ X_EDIT.updateVariableBar('add-');
378
+ this.stretchChart(0);
379
+ }
380
+
381
+ updateExperimentInfo() {
382
+ // Display selected experiment title in dialog header if run data are used
383
+ const
384
+ selx = EXPERIMENT_MANAGER.selected_experiment,
385
+ el = document.getElementById('chart-experiment-info');
386
+ if(selx && this.runs_chart) {
387
+ el.innerHTML = '<em>Experiment:</em> ' + selx.title;
388
+ } else {
389
+ el.innerHTML = '';
390
+ }
391
+ }
392
+
393
+ updateTimeStep(e, show) {
394
+ // Shows the time step corresponding to the horizontal cursor position,
395
+ // or hides it if the cursor is not over the chart area
396
+ const c = (this.chart_index >= 0 ? MODEL.charts[this.chart_index] : null);
397
+ if(show && c) {
398
+ const
399
+ scale = this.container_height / this.svg_height,
400
+ r = c.chart_area_rect,
401
+ ox = r.left * scale,
402
+ w = r.width * scale,
403
+ x = e.pageX -
404
+ this.svg_container.getBoundingClientRect().left + window.scrollX;
405
+ let n = '';
406
+ if(c.histogram) {
407
+ let vv = [];
408
+ for(let i = 0; i < c.variables.length; i++) {
409
+ if(c.variables[i].visible) vv.push(c.variables[i]);
410
+ }
411
+ const
412
+ l = vv.length,
413
+ bars = c.bins * l,
414
+ b = Math.max(0, Math.min(bars, Math.floor(bars * (x - ox) / w))),
415
+ v = vv[b % l],
416
+ t = Math.floor(b / l);
417
+ if(x > ox && b < bars) n = 'N = ' + v.bin_tallies[t];
418
+ } else {
419
+ const
420
+ runs = EXPERIMENT_MANAGER.selectedRuns(c),
421
+ p = c.total_time_steps,
422
+ first = (runs.length > 0 ? 1 : MODEL.start_period),
423
+ last = (runs.length > 0 ? p : MODEL.end_period),
424
+ t = Math.round(first - 0.5 + p * (x - ox) / w);
425
+ n = 't = ' + Math.max(0, Math.min(t, last));
426
+ }
427
+ this.time_step.innerHTML = n;
428
+ this.time_step.style.display = 'block';
429
+ } else {
430
+ this.time_step.style.display = 'none';
431
+ }
432
+ }
433
+
434
+ selectChart() {
435
+ // Sets the selected chart to be the "active" chart
436
+ const ci = parseInt(this.chart_selector.value);
437
+ // Deselect variable only if different chart is selected
438
+ if(ci !== this.chart_index) this.variable_index = -1;
439
+ this.chart_index = ci;
440
+ this.updateDialog();
441
+ }
442
+
443
+ promptForTitle() {
444
+ // Prompts modeler for a new title for the current chart
445
+ if(this.chart_index >= 0) {
446
+ this.rename_chart_modal.show();
447
+ const nct = document.getElementById('new-chart-title');
448
+ nct.value = MODEL.charts[this.chart_index].displayName;
449
+ nct.focus();
450
+ }
451
+ }
452
+
453
+ renameChart() {
454
+ // Renames the current chart
455
+ if(this.chart_index >= 0) {
456
+ const t = document.getElementById('new-chart-title').value.trim();
457
+ // Check if a chart with this title already exists
458
+ const ci = MODEL.indexOfChart(t);
459
+ if(ci >= 0 && ci != this.chart_index) {
460
+ UI.warn(`A chart with title "${t}" already exists`);
461
+ } else {
462
+ const c = MODEL.charts[this.chart_index];
463
+ // Remember the old title of the chart-to-be-renamed
464
+ const ot = c.title;
465
+ c.title = t;
466
+ // If the default '(new chart)' has been renamed, create a new one
467
+ if(ot === this.new_chart_title) {
468
+ MODEL.addChart(ot);
469
+ }
470
+ // Update the chart index so that it points to the renamed chart
471
+ this.chart_index = MODEL.indexOfChart(t);
472
+ this.updateSelector();
473
+ // Redraw the chart if title is shown
474
+ if(c.show_title) this.drawChart();
475
+ }
476
+ // Update experiment viewer in case its current experiment uses this chart
477
+ UI.updateControllerDialogs('CFX');
478
+ }
479
+ this.rename_chart_modal.hide();
480
+ }
481
+
482
+ cloneChart() {
483
+ // Creates a new chart that is identical to the current one
484
+ if(this.chart_index >= 0) {
485
+ let c = MODEL.charts[this.chart_index],
486
+ nt = c.title + '-copy';
487
+ while(MODEL.indexOfChart(nt) >= 0) {
488
+ nt += '-copy';
489
+ }
490
+ const nc = MODEL.addChart(nt);
491
+ // Copy properties of c to nc
492
+ nc.histogram = c.histogram;
493
+ nc.bins = c.bins;
494
+ nc.show_title = c.show_title;
495
+ nc.legend_position = c.legend_position;
496
+ for(let i = 0; i < c.variables.length; i++) {
497
+ const
498
+ cv = c.variables[i],
499
+ nv = new ChartVariable(nc);
500
+ nv.setProperties(cv.object, cv.attribute, cv.stacked,
501
+ cv.color, cv.scale_factor, cv.line_width);
502
+ nc.variables.push(nv);
503
+ }
504
+ this.chart_index = MODEL.indexOfChart(nc.title);
505
+ this.updateDialog();
506
+ }
507
+ }
508
+
509
+ toggleRunResults() {
510
+ // Toggles the Boolean property that signals charts that they must plot
511
+ // run results if they are part of the selected experiment chart set
512
+ this.setRunsChart(!this.runs_chart);
513
+ this.resetChartVectors();
514
+ this.updateDialog();
515
+ }
516
+
517
+ deleteChart() {
518
+ // Deletes the shown chart (if any)
519
+ if(this.chart_index >= 0) {
520
+ // NOTE: do not delete the default chart, but clear it
521
+ if(MODEL.charts[this.chart_index].title === this.new_chart_title) {
522
+ MODEL.charts[this.chart_index].reset();
523
+ } else {
524
+ MODEL.charts.splice(this.chart_index, 1);
525
+ this.chart_index = -1;
526
+ }
527
+ // Also update the experiment viewer (charts define the output variables)
528
+ UI.updateControllerDialogs('CFX');
529
+ }
530
+ }
531
+
532
+ changeBins() {
533
+ if(this.chart_index >= 0) {
534
+ const
535
+ c = MODEL.charts[this.chart_index],
536
+ b = parseInt(this.bins_selector.value);
537
+ if(b !== c.bins) {
538
+ c.bins = b;
539
+ this.drawChart();
540
+ }
541
+ }
542
+ }
543
+
544
+ toggleHistogram() {
545
+ if(this.chart_index >= 0) {
546
+ const c = MODEL.charts[this.chart_index];
547
+ c.histogram = !c.histogram;
548
+ if(c.histogram) {
549
+ this.histogram_options.style.display = 'block';
550
+ } else {
551
+ this.histogram_options.style.display = 'none';
552
+ }
553
+ this.drawChart();
554
+ }
555
+ }
556
+
557
+ toggleTitle() {
558
+ // window.event.stopPropagation();
559
+ if(this.chart_index >= 0) {
560
+ const c = MODEL.charts[this.chart_index];
561
+ c.show_title = !c.show_title;
562
+ this.drawChart();
563
+ }
564
+ }
565
+
566
+ changeLegend() {
567
+ if(this.chart_index >= 0) {
568
+ const c = MODEL.charts[this.chart_index];
569
+ c.legend_position = document.getElementById('chart-legend').value;
570
+ this.drawChart();
571
+ }
572
+ }
573
+
574
+ promptForVariable(shift) {
575
+ // Prompts for variable to add to chart
576
+ // NOTE: shortcut (Shift-click) to add a new equation to the chart
577
+ if(shift) {
578
+ if(UI.hidden('equation-dlg')) {
579
+ UI.buttons.equation.dispatchEvent(new Event('click'));
580
+ }
581
+ // NOTE: TRUE signals equation manager to add new equation to the chart
582
+ EQUATION_MANAGER.promptForEquation(true);
583
+ } else {
584
+ this.add_variable_modal.show();
585
+ }
586
+ }
587
+
588
+ addVariable(eq='') {
589
+ // Adds the variable specified by the add-variable-dialog to the chart
590
+ // NOTE: when defined, `eq` is the selector of the equation to be added
591
+ if(this.chart_index >= 0) {
592
+ let o = '',
593
+ a = eq;
594
+ if(!eq) {
595
+ o = this.add_variable_modal.selectedOption('name').text;
596
+ a = this.add_variable_modal.selectedOption('attr').text;
597
+ }
598
+ // NOTE: when equation is added, object specifier is empty string
599
+ if(!o && a) o = UI.EQUATIONS_DATASET_NAME;
600
+ this.variable_index = MODEL.charts[this.chart_index].addVariable(o, a);
601
+ if(this.variable_index >= 0) {
602
+ this.add_variable_modal.hide();
603
+ // Also update the experiment viewer (charts define the output variables)
604
+ if(EXPERIMENT_MANAGER.selected_experiment) {
605
+ EXPERIMENT_MANAGER.selected_experiment.inferVariables();
606
+ }
607
+ UI.updateControllerDialogs('CFX');
608
+ }
609
+ }
610
+ }
611
+
612
+ selectVariable(vi) {
613
+ // Select variable, and edit it when double-clicked
614
+ const
615
+ now = Date.now(),
616
+ dt = now - this.last_time_selected;
617
+ if(vi >= 0 && this.chart_index >= 0) {
618
+ this.last_time_selected = now;
619
+ if(vi === this.variable_index) {
620
+ // Consider click to be "double" if it occurred less than 300 ms ago
621
+ if(dt < 300) {
622
+ this.last_time_selected = 0;
623
+ this.editVariable();
624
+ return;
625
+ }
626
+ }
627
+ }
628
+ this.variable_index = vi;
629
+ this.updateDialog();
630
+ }
631
+
632
+ setColorPicker(color) {
633
+ // Robust way to set iro color picker color
634
+ try {
635
+ this.color_picker.color.hexString = color;
636
+ } catch(e) {
637
+ this.color_picker.color.rgbString = color;
638
+ }
639
+ }
640
+
641
+ editVariable() {
642
+ // Shows the edit (or rather: format) variable dialog
643
+ if(this.chart_index >= 0 && this.variable_index >= 0) {
644
+ const cv = MODEL.charts[this.chart_index].variables[this.variable_index];
645
+ document.getElementById('variable-dlg-name').innerHTML = cv.displayName;
646
+ UI.setBox('variable-stacked', cv.stacked);
647
+ this.variable_modal.element('scale').value = VM.sig4Dig(cv.scale_factor);
648
+ this.variable_modal.element('width').value = VM.sig4Dig(cv.line_width);
649
+ this.variable_modal.element('color').style.backgroundColor = cv.color;
650
+ this.setColorPicker(cv.color);
651
+ // Show change equation buttons only for equation variables
652
+ if(cv.object === MODEL.equations_dataset) {
653
+ this.change_equation_btns.style.display = 'block';
654
+ } else {
655
+ this.change_equation_btns.style.display = 'none';
656
+ }
657
+ this.variable_modal.show();
658
+ }
659
+ }
660
+
661
+ showPasteColor() {
662
+ // Show last copied color (if any) as smaller square next to color box
663
+ if(this.paste_color) {
664
+ const pc = this.variable_modal.element('paste-color');
665
+ pc.style.backgroundColor = this.paste_color;
666
+ pc.style.display = 'inline-block';
667
+ }
668
+ }
669
+
670
+ hidePasteColor() {
671
+ // Hide paste color box
672
+ this.variable_modal.element('paste-color').style.display = 'none';
673
+ }
674
+
675
+ copyPasteColor(event) {
676
+ // Store the current color as past color, or set it to the current
677
+ // paste color if this is defined and the Shift key was pressed
678
+ event.stopPropagation();
679
+ const cbox = this.variable_modal.element('color');
680
+ if(event.shiftKey && this.paste_color) {
681
+ cbox.style.backgroundColor = this.paste_color;
682
+ this.setColorPicker(this.paste_color);
683
+ } else {
684
+ this.paste_color = cbox.style.backgroundColor;
685
+ this.showPasteColor();
686
+ }
687
+ }
688
+
689
+ toggleVariable(vi) {
690
+ window.event.stopPropagation();
691
+ if(vi >= 0 && this.chart_index >= 0) {
692
+ const cv = MODEL.charts[this.chart_index].variables[vi];
693
+ // toggle visibility of the selected variable
694
+ cv.visible = !cv.visible;
695
+ // update the check box
696
+ UI.setBox('v-box-' + vi, cv.visible);
697
+ // redraw chart and table (with one variable more or less)
698
+ this.drawChart();
699
+ // Also update the experiment viewer (charts define the output variables)
700
+ if(EXPERIMENT_MANAGER.selected_experiment) {
701
+ EXPERIMENT_MANAGER.updateDialog();
702
+ }
703
+ }
704
+ }
705
+
706
+ moveVariable(dir) {
707
+ if(this.chart_index >= 0 && this.variable_index >= 0) {
708
+ const c = MODEL.charts[this.chart_index];
709
+ let vi = this.variable_index;
710
+ if((dir > 0 && vi < c.variables.length - 1) || (dir < 0 && vi > 0)) {
711
+ vi += dir;
712
+ const v = c.variables.splice(this.variable_index, 1)[0];
713
+ c.variables.splice(vi, 0, v);
714
+ this.variable_index = vi;
715
+ }
716
+ this.updateDialog();
717
+ }
718
+ }
719
+
720
+ modifyVariable() {
721
+ if(this.variable_index >= 0) {
722
+ const s = UI.validNumericInput('variable-scale', 'scale factor');
723
+ if(!s) return;
724
+ const w = UI.validNumericInput('variable-width', 'line width');
725
+ if(!w) return;
726
+ const
727
+ c = MODEL.charts[this.chart_index],
728
+ cv = c.variables[this.variable_index];
729
+ cv.stacked = UI.boxChecked('variable-stacked');
730
+ cv.scale_factor = s;
731
+ cv.line_width = w;
732
+ cv.color = this.color_picker.color.hexString;
733
+ // NOTE: clear the vector so it will be recalculated
734
+ cv.vector.length = 0;
735
+ }
736
+ this.variable_modal.hide();
737
+ this.updateDialog();
738
+ }
739
+
740
+ renameEquation() {
741
+ // Renames the selected variable (if it is an equation)
742
+ if(this.chart_index >= 0 && this.variable_index >= 0) {
743
+ const v = MODEL.charts[this.chart_index].variables[this.variable_index];
744
+ if(v.object === MODEL.equations_dataset) {
745
+ const m = MODEL.equations_dataset.modifiers[UI.nameToID(v.attribute)];
746
+ if(m instanceof DatasetModifier) {
747
+ EQUATION_MANAGER.selected_modifier = m;
748
+ EQUATION_MANAGER.promptForName();
749
+ }
750
+ }
751
+ }
752
+ }
753
+
754
+ editEquation() {
755
+ // Opens the expression editor for the selected variable (if equation)
756
+ if(this.chart_index >= 0 && this.variable_index >= 0) {
757
+ const v = MODEL.charts[this.chart_index].variables[this.variable_index];
758
+ if(v.object === MODEL.equations_dataset) {
759
+ const m = MODEL.equations_dataset.modifiers[UI.nameToID(v.attribute)];
760
+ if(m instanceof DatasetModifier) {
761
+ EQUATION_MANAGER.selected_modifier = m;
762
+ EQUATION_MANAGER.editEquation();
763
+ }
764
+ }
765
+ }
766
+ }
767
+
768
+ deleteVariable() {
769
+ // Deletes the selected variable from the chart
770
+ if(this.variable_index >= 0) {
771
+ MODEL.charts[this.chart_index].variables.splice(this.variable_index, 1);
772
+ this.variable_index = -1;
773
+ this.updateDialog();
774
+ // Also update the experiment viewer (charts define the output variables)
775
+ // and finder dialog
776
+ if(EXPERIMENT_MANAGER.selected_experiment) UI.updateControllerDialogs('FX');
777
+ }
778
+ this.variable_modal.hide();
779
+ }
780
+
781
+ showChartImage(c) {
782
+ // Displays the SVG image for chart `c` (computed by this Chart object)
783
+ if(c) document.getElementById('chart-svg').innerHTML = c.svg;
784
+ }
785
+
786
+ drawTable() {
787
+ // Shows the statistics on the chart variables
788
+ const html = [];
789
+ let vbl = [];
790
+ if(this.chart_index >= 0) vbl = MODEL.charts[this.chart_index].variables;
791
+ // First get the (potentially floating point) numbers so that their format
792
+ // can be made uniform per column
793
+ const data = [];
794
+ let nr = 0;
795
+ for(let i = 0; i < vbl.length; i++) {
796
+ const v = vbl[i];
797
+ if(v.visible) {
798
+ data.push([VM.sig4Dig(v.minimum), VM.sig4Dig(v.maximum),
799
+ VM.sig4Dig(v.mean), VM.sig4Dig(Math.sqrt(v.variance)),
800
+ VM.sig4Dig(v.sum)]);
801
+ nr++;
802
+ }
803
+ }
804
+ if(nr == 0 || this.drawing_chart) {
805
+ this.table_panel.html = '<div id="no-chart-data">No data</div>';
806
+ return;
807
+ }
808
+ // Process each of 5 columns separately
809
+ for(let c = 0; c < 5; c++) {
810
+ const col = [];
811
+ for(let r = 0; r < data.length; r++) {
812
+ col.push(data[r][c]);
813
+ }
814
+ uniformDecimals(col);
815
+ for(let r = 0; r < data.length; r++) {
816
+ data[r][c] = col[r];
817
+ }
818
+ }
819
+ html.push('<table id="chart-table">',
820
+ '<tr><th style="text-align: left">Variable</th>',
821
+ '<th>N</th><th style="font-size: 11px">MIN</th>',
822
+ '<th style="font-size: 11px">MAX</th>',
823
+ '<th>&mu;</th><th>&sigma;</th><th>&Sigma;</th>',
824
+ '<th>&ne;0</th><th>&#x26A0;</th></tr>');
825
+ nr = 0;
826
+ for(let i = 0; i < vbl.length; i++) {
827
+ const v = vbl[i];
828
+ if(v.visible) {
829
+ // NOTE: while still solving, display t-1 as N
830
+ const n = Math.max(0, v.N);
831
+ html.push('<tr><td class="v-name">',
832
+ [v.displayName, n, data[nr][0], data[nr][1], data[nr][2],
833
+ data[nr][3], data[nr][4],
834
+ v.non_zero_tally, v.exceptions].join('</td><td>'),
835
+ '</td></tr>');
836
+ nr++;
837
+ }
838
+ }
839
+ html.push('</table>');
840
+ this.table_panel.innerHTML = html.join('');
841
+ }
842
+
843
+ toggleStatistics() {
844
+ const btn = document.getElementById('chart-stats-btn');
845
+ let hs = 'Show';
846
+ if(btn.classList.contains('stay-activ')) {
847
+ btn.classList.remove('stay-activ');
848
+ } else {
849
+ btn.classList.add('stay-activ');
850
+ hs = 'Hide';
851
+ }
852
+ btn.title = hs + ' descriptive statistics';
853
+ UI.toggle('chart-only-buttons', 'inline-block');
854
+ UI.toggle('table-only-buttons', 'inline-block');
855
+ UI.toggle('chart-table-panel');
856
+ UI.toggle('chart-svg-scroller');
857
+ this.stretchChart(0);
858
+ }
859
+
860
+ stretchChart(delta) {
861
+ this.stretch_factor = Math.max(1, Math.min(10, this.stretch_factor + delta));
862
+ // NOTE: do not use 'auto', as this produces poor results
863
+ document.getElementById('chart-svg-scroller').style.overflowX =
864
+ (this.stretch_factor === 1 ? 'hidden' : 'scroll');
865
+ const csc = document.getElementById('chart-svg-container');
866
+ csc.style.width = (this.stretch_factor * 100 + '%');
867
+ // Size the chart proportional to its the display area
868
+ const style = window.getComputedStyle(csc);
869
+ this.container_width = parseFloat(style.width);
870
+ // If stretch factor > 1, the horizontal scroll bar takes up space,
871
+ // but this is accounted for by the container style!
872
+ this.container_height = parseFloat(style.height);
873
+ this.drawChart();
874
+ const
875
+ nbtn = document.getElementById('chart-narrow-btn'),
876
+ wbtn = document.getElementById('chart-widen-btn');
877
+ if(this.stretch_factor === 1) {
878
+ nbtn.classList.remove('enab');
879
+ nbtn.classList.add('disab');
880
+ } else if(this.stretch_factor === 2) {
881
+ nbtn.classList.remove('disab');
882
+ nbtn.classList.add('enab');
883
+ } else if(this.stretch_factor === 9) {
884
+ wbtn.classList.remove('disab');
885
+ wbtn.classList.add('enab');
886
+ } else if(this.stretch_factor === 10) {
887
+ wbtn.classList.remove('enab');
888
+ wbtn.classList.add('disab');
889
+ }
890
+ }
891
+
892
+ copyTable() {
893
+ UI.copyHtmlToClipboard(this.table_panel.innerHTML);
894
+ UI.notify('Table copied to clipboard (as HTML)');
895
+ }
896
+
897
+ copyStatistics() {
898
+ if(this.chart_index >= 0) {
899
+ UI.copyStringToClipboard(
900
+ MODEL.charts[this.chart_index].statisticsAsString);
901
+ }
902
+ }
903
+
904
+ copyData() {
905
+ if(this.chart_index >= 0) {
906
+ UI.copyStringToClipboard(
907
+ MODEL.charts[this.chart_index].dataAsString);
908
+ }
909
+ }
910
+
911
+ downloadChart() {
912
+ // Pushes the SVG of the selected chart as file to the browser
913
+ if(this.chart_index >= 0) {
914
+ FILE_MANAGER.pushOutSVG(MODEL.charts[this.chart_index].svg);
915
+ }
916
+ }
917
+
918
+ renderChartAsPNG() {
919
+ window.localStorage.removeItem('png-url');
920
+ FILE_MANAGER.renderSVGAsPNG(MODEL.charts[this.chart_index].svg);
921
+ }
922
+
923
+ drawChart() {
924
+ // Displays the selected chart unless an experiment is running, or already
925
+ // busy with an earlier drawChart call
926
+ if(MODEL.running_experiment) {
927
+ UI.notify(UI.NOTICE.NO_CHARTS);
928
+ } else if(this.chart_index >= 0 && !this.drawing_chart) {
929
+ this.drawing_chart = true;
930
+ CHART_MANAGER.actuallyDrawChart();
931
+ } else {
932
+ console.log(`Skipped drawing chart "${MODEL.charts[this.chart_index]}"`);
933
+ }
934
+ }
935
+
936
+ actuallyDrawChart() {
937
+ // Draws the chart, and resets the cursor when done
938
+ MODEL.charts[this.chart_index].draw();
939
+ this.drawing_chart = false;
940
+ this.drawTable();
941
+ }
942
+
943
+ } // END of class ChartManager
944
+