linny-r 1.5.7 → 1.6.0

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "linny-r",
3
- "version": "1.5.7",
3
+ "version": "1.6.0",
4
4
  "description": "Executable graphical language with WYSIWYG editor for MILP models",
5
5
  "main": "server.js",
6
6
  "scripts": {
package/static/index.html CHANGED
@@ -304,8 +304,12 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
304
304
  </svg>
305
305
  </div>
306
306
  </main>
307
- <!-- the footer displays the status bar with X/Y coordinates, zoom in/out buttons, buttons for moving forward/backward
308
- in time, and the status line that displays info, notifications, warnings, and error messages
307
+ <!-- The footer displays the status bar with X/Y coordinates, zoom
308
+ in/out buttons, buttons for moving forward/backward in time, and
309
+ the status line that displays info, notifications, warnings, and
310
+ error messages. The set-up progress bar shows a growing colored
311
+ needle indicating progress of making Simplex tableau and input
312
+ file for the solver.
309
313
  -->
310
314
  <footer>
311
315
  <div id="set-up-progress">
@@ -2238,7 +2242,7 @@ NOTE: * and ? will be interpreted as wildcards"
2238
2242
  </div>
2239
2243
  <div id="viewer-selectors">
2240
2244
  <label>Variable:</label>
2241
- <select id="viewer-variable" style="margin-right: 10px">
2245
+ <select id="viewer-variable">
2242
2246
  </select>
2243
2247
  <label>Statistic:</label>
2244
2248
  <select id="viewer-statistic">
@@ -2366,6 +2366,24 @@ td.equation-selector {
2366
2366
  text-overflow: ellipsis;
2367
2367
  }
2368
2368
 
2369
+ td.method {
2370
+ color: #1080e0;
2371
+ font-weight: bold;
2372
+ }
2373
+
2374
+ td.no-object {
2375
+ color: #80a0c0;
2376
+ font-weight: normal;
2377
+ }
2378
+
2379
+ td.compile-issue {
2380
+ color: #e00000;
2381
+ }
2382
+
2383
+ td.compute-issue {
2384
+ color: #f08000;
2385
+ }
2386
+
2369
2387
  td.wildcard {
2370
2388
  color: #400080;
2371
2389
  }
@@ -3558,6 +3576,7 @@ td.sa-not-run {
3558
3576
  #viewer-variable {
3559
3577
  font-size: 10px;
3560
3578
  max-width: calc(100% - 160px);
3579
+ margin-right: 10px;
3561
3580
  }
3562
3581
 
3563
3582
  #sensitivity-statistic,
@@ -304,15 +304,15 @@ class ActorManager {
304
304
  if(a.displayName != ali[1]) a.rename(ali[1]);
305
305
  // Set its round flags
306
306
  a.round_flags = ali[2];
307
- // Double-check: parse expression if weight has been changed
307
+ // Double-check: parse expression if weight has been changed.
308
308
  if(a.weight.text != ali[3]) {
309
- xp.expr = ali[3];
309
+ xp.expr = monoSpacedVariables(ali[3]);
310
310
  xp.compile();
311
311
  if(xp.error) {
312
312
  UI.warningInvalidWeightExpression(a, xp.error);
313
313
  ok = false;
314
314
  } else {
315
- a.weight.update(xp);
315
+ a.weight.update(ali[3]);
316
316
  }
317
317
  }
318
318
  // Update import/export status
@@ -477,9 +477,9 @@ class GUIChartManager extends ChartManager {
477
477
  y = e.pageY -
478
478
  this.svg_container.getBoundingClientRect().top + window.scrollY,
479
479
  yfract = (c.plot_oy - y / scale) / c.plot_height,
480
- yres = Math.round(
481
- Math.max(Math.abs(c.plot_min_y), Math.abs(c.plot_max_y)) / 500),
482
480
  yval = c.plot_min_y + yfract * (c.plot_max_y - c.plot_min_y),
481
+ yhigh = Math.max(Math.abs(c.plot_min_y), Math.abs(c.plot_max_y)),
482
+ yres = (yhigh > 1000 ? Math.round(yhigh / 500) : 1),
483
483
  ytrunc = Math.round(yval / yres) * yres,
484
484
  ylbl = (yfract < 0 || yfract > 1 || c.plot_min_y >= c.plot_max_y ?
485
485
  '' : 'y = ' + VM.sig2Dig(parseFloat(ytrunc.toPrecision(2))));
@@ -574,7 +574,7 @@ class GUIChartManager extends ChartManager {
574
574
  }
575
575
 
576
576
  cloneChart() {
577
- // Creates a new chart that is identical to the current one
577
+ // Create a new chart that is identical to the current one.
578
578
  if(this.chart_index >= 0) {
579
579
  let c = MODEL.charts[this.chart_index],
580
580
  nt = c.title + '-copy';
@@ -582,7 +582,7 @@ class GUIChartManager extends ChartManager {
582
582
  nt += '-copy';
583
583
  }
584
584
  const nc = MODEL.addChart(nt);
585
- // Copy properties of c to nc
585
+ // Copy properties of `c` to `nc`;
586
586
  nc.histogram = c.histogram;
587
587
  nc.bins = c.bins;
588
588
  nc.show_title = c.show_title;
@@ -609,7 +609,7 @@ class GUIChartManager extends ChartManager {
609
609
  }
610
610
 
611
611
  toggleRunStat() {
612
- // Toggles the Boolean property that signals charts that they must
612
+ // Toggle the Boolean property that signals charts that they must
613
613
  // plot the selected statistic for the selected runs if they are
614
614
  // part of the selected experiment chart set.
615
615
  this.setRunsStat(!this.runs_stat);
@@ -618,16 +618,17 @@ class GUIChartManager extends ChartManager {
618
618
  }
619
619
 
620
620
  deleteChart() {
621
- // Deletes the shown chart (if any)
621
+ // Delete the shown chart (if any).
622
622
  if(this.chart_index >= 0) {
623
- // NOTE: do not delete the default chart, but clear it
623
+ // NOTE: Do not delete the default chart, but clear it instead.
624
624
  if(MODEL.charts[this.chart_index].title === this.new_chart_title) {
625
625
  MODEL.charts[this.chart_index].reset();
626
626
  } else {
627
627
  MODEL.charts.splice(this.chart_index, 1);
628
628
  this.chart_index = -1;
629
629
  }
630
- // Also update the experiment viewer (charts define the output variables)
630
+ // Also update the experiment viewer, because this chart may be
631
+ // one of the output charts of the selected experiment.
631
632
  UI.updateControllerDialogs('CFX');
632
633
  }
633
634
  }
@@ -689,8 +690,8 @@ class GUIChartManager extends ChartManager {
689
690
  }
690
691
 
691
692
  addVariable(eq='') {
692
- // Adds the variable specified by the add-variable-dialog to the chart
693
- // NOTE: when defined, `eq` is the selector of the equation to be added
693
+ // Add the variable specified by the add-variable-dialog to the chart.
694
+ // NOTE: When defined, `eq` is the selector of the equation to be added.
694
695
  if(this.chart_index >= 0) {
695
696
  let o = '',
696
697
  a = eq;
@@ -698,7 +699,7 @@ class GUIChartManager extends ChartManager {
698
699
  o = this.add_variable_modal.selectedOption('name').text;
699
700
  a = this.add_variable_modal.selectedOption('attr').text;
700
701
  }
701
- // NOTE: when equation is added, object specifier is empty string
702
+ // NOTE: When equation is added, object specifier is empty string.
702
703
  if(!o && a) o = UI.EQUATIONS_DATASET_NAME;
703
704
  this.variable_index = MODEL.charts[this.chart_index].addVariable(o, a);
704
705
  if(this.variable_index >= 0) {
@@ -2608,10 +2608,11 @@ class GUIController extends Controller {
2608
2608
  document.body.className = '';
2609
2609
  }
2610
2610
 
2611
- setProgressNeedle(fraction) {
2611
+ setProgressNeedle(fraction, color='#500080') {
2612
2612
  // Shows a thin purple line just above the status line to indicate progress
2613
2613
  const el = document.getElementById('set-up-progress-bar');
2614
2614
  el.style.width = Math.round(Math.max(0, Math.min(1, fraction)) * 100) + '%';
2615
+ el.style.backgroundColor = color;
2615
2616
  }
2616
2617
 
2617
2618
  hideStayOnTopDialogs() {
@@ -486,12 +486,14 @@ class GUIDatasetManager extends DatasetManager {
486
486
  m = sd.modifiers[UI.nameToID(msl[i])],
487
487
  wild = m.hasWildcards,
488
488
  defsel = (m.selector === sd.default_selector),
489
+ issue = (m.expression.compile_issue ? ' compile-issue' :
490
+ (m.expression.compute_issue ? ' compute-issue' : '')),
489
491
  clk = '" onclick="DATASET_MANAGER.selectModifier(event, \'' +
490
492
  m.selector + '\'';
491
493
  if(m === sm) smid += i;
492
494
  ml.push(['<tr id="dsmtr', i, '" class="dataset-modif',
493
495
  (m === sm ? ' sel-set' : ''),
494
- '"><td class="dataset-selector',
496
+ '"><td class="dataset-selector', issue,
495
497
  (wild ? ' wildcard' : ''),
496
498
  '" title="Shift-click to ', (defsel ? 'clear' : 'set as'),
497
499
  ' default modifier',
@@ -499,7 +501,10 @@ class GUIDatasetManager extends DatasetManager {
499
501
  (defsel ? '<img src="images/solve.png" style="height: 14px;' +
500
502
  ' width: 14px; margin: 0 1px -3px -1px;">' : ''),
501
503
  (wild ? wildcardFormat(m.selector, true) : m.selector),
502
- '</td><td class="dataset-expression',
504
+ '</td><td class="dataset-expression', issue,
505
+ (issue ? '"title="' +
506
+ safeDoubleQuotes(m.expression.compile_issue ||
507
+ m.expression.compute_issue) : ''),
503
508
  clk, ');">', m.expression.text, '</td></tr>'].join(''));
504
509
  }
505
510
  this.modifier_table.innerHTML = ml.join('');
@@ -654,14 +659,14 @@ class GUIDatasetManager extends DatasetManager {
654
659
  }
655
660
 
656
661
  renameDataset() {
657
- // Changes the name of the selected dataset
662
+ // Change the name of the selected dataset.
658
663
  if(this.selected_dataset) {
659
664
  const
660
665
  inp = this.rename_modal.element('name'),
661
666
  n = UI.cleanName(inp.value);
662
- // Show modeler the "cleaned" new name
667
+ // Show modeler the "cleaned" new name.
663
668
  inp.value = n;
664
- // Then try to rename -- this may generate a warning
669
+ // Then try to rename -- this may generate a warning.
665
670
  if(this.selected_dataset.rename(n)) {
666
671
  this.rename_modal.hide();
667
672
  if(EXPERIMENT_MANAGER.selected_experiment) {
@@ -670,59 +675,22 @@ class GUIDatasetManager extends DatasetManager {
670
675
  UI.updateControllerDialogs('CDEFJX');
671
676
  }
672
677
  } else if(this.selected_prefix_row) {
673
- // Create a list of datasets to be renamed
678
+ // Create a list of datasets to be renamed.
674
679
  let e = this.rename_modal.element('name'),
675
680
  prefix = e.value.trim();
676
681
  e.focus();
677
- // Trim trailing colon if user entered it
682
+ // Trim trailing colon if user added it.
678
683
  while(prefix.endsWith(':')) prefix = prefix.slice(0, -1);
679
- // NOTE: prefix may be empty string, but otherwise should be a valid name
684
+ // NOTE: Prefix may be empty string, but otherwise should be a
685
+ // valid name.
680
686
  if(prefix && !UI.validName(prefix)) {
681
687
  UI.warn('Invalid prefix');
682
688
  return;
683
689
  }
684
- // Now add the colon-plus-space prefix separator
690
+ // Now add the colon-plus-space prefix separator.
685
691
  prefix += UI.PREFIXER;
686
- const
687
- oldpref = this.selectedPrefix,
688
- key = oldpref.toLowerCase().split(UI.PREFIXER).join(':_'),
689
- newkey = prefix.toLowerCase().split(UI.PREFIXER).join(':_'),
690
- dsl = [];
691
- // No change if new prefix is identical to old prefix
692
- if(oldpref !== prefix) {
693
- for(let k in MODEL.datasets) if(MODEL.datasets.hasOwnProperty(k)) {
694
- if(k.startsWith(key)) dsl.push(k);
695
- }
696
- // NOTE: no check needed for mere upper/lower case changes
697
- if(newkey !== key) {
698
- let nc = 0;
699
- for(let i = 0; i < dsl.length; i++) {
700
- let nk = newkey + dsl[i].substring(key.length);
701
- if(MODEL.datasets[nk]) nc++;
702
- }
703
- if(nc) {
704
- UI.warn('Renaming ' + pluralS(dsl.length, 'dataset').toLowerCase() +
705
- ' would cause ' + pluralS(nc, 'name conflict'));
706
- return;
707
- }
708
- }
709
- // Reset counts of effects of a rename operation
710
- this.entity_count = 0;
711
- this.expression_count = 0;
712
- // Rename datasets one by one, suppressing notifications
713
- for(let i = 0; i < dsl.length; i++) {
714
- const d = MODEL.datasets[dsl[i]];
715
- d.rename(d.displayName.replace(oldpref, prefix), false);
716
- }
717
- let msg = 'Renamed ' + pluralS(dsl.length, 'dataset').toLowerCase();
718
- if(MODEL.variable_count) msg += ', and updated ' +
719
- pluralS(MODEL.variable_count, 'variable') + ' in ' +
720
- pluralS(MODEL.expression_count, 'expression');
721
- UI.notify(msg);
722
- if(EXPERIMENT_MANAGER.selected_experiment) {
723
- EXPERIMENT_MANAGER.selected_experiment.inferVariables();
724
- }
725
- UI.updateControllerDialogs('CDEFJX');
692
+ // Perform the renaming operation.
693
+ if(MODEL.renamePrefixedDatasets(this.selectedPrefix, prefix)) {
726
694
  this.selectPrefixRow(prefix);
727
695
  }
728
696
  }
@@ -309,30 +309,50 @@ class DocumentationManager {
309
309
  }
310
310
 
311
311
  update(e, shift) {
312
- // Display name of entity under cursor on the infoline, and details in
313
- // the documentation dialog
312
+ // Display name of entity under cursor on the infoline, and details
313
+ // in the documentation dialog.
314
314
  if(!e) return;
315
- const
316
- et = e.type,
315
+ let et = e.type,
317
316
  edn = e.displayName;
318
- // TO DO: when debugging, display additional data for nodes on the infoline
317
+ if(et === 'Equation' && e.selector.startsWith(':')) et = 'Method';
318
+ // TO DO: when debugging, display additional data for nodes on the
319
+ // infoline.
319
320
  UI.setMessage(
320
321
  e instanceof NodeBox ? e.infoLineName : `<em>${et}:</em> ${edn}`);
321
- // NOTE: update the dialog ONLY when shift is pressed (this permits modelers
322
- // to rapidly browse comments without having to click on entities, and then
323
- // release the shift key to move to the documentation dialog to edit)
324
- // Moreover, the documentation dialog must be visible, and the entity must
325
- // have the `comments` property
326
- if(!this.editing && shift && this.visible && e.hasOwnProperty('comments')) {
327
- this.title.innerHTML = `<em>${et}:</em>&nbsp;${edn}`;
328
- this.entity = e;
329
- this.markup = (e.comments ? e.comments : '');
330
- this.editor.value = this.markup;
331
- this.viewer.innerHTML = this.markdown;
332
- this.edit_btn.classList.remove('disab');
333
- this.edit_btn.classList.add('enab');
334
- // NOTE: permit documentation of the model by raising the dialog
335
- if(this.entity === MODEL) this.dialog.style.zIndex = 101;
322
+ // NOTE: Update the dialog ONLY when shift is pressed. This permits
323
+ // modelers to rapidly browse comments without having to click on
324
+ // entities, and then release the shift key to move to the documentation
325
+ // dialog to edit. Moreover, the documentation dialog must be visible,
326
+ // and the entity must have the `comments` property.
327
+ // NOTE: Equations constitute an exception, as DatasetModifiers do
328
+ // not have the `comments` property. Now that methods can be defined
329
+ // (since version 1.6.0), the documentation window displays the eligible
330
+ // prefixes when the cursor is Shift-moved over the name of a method
331
+ // (in the Equation Manager).
332
+ if(!this.editing && shift && this.visible) {
333
+ if(e.hasOwnProperty('comments')) {
334
+ this.title.innerHTML = `<em>${et}:</em>&nbsp;${edn}`;
335
+ this.entity = e;
336
+ this.markup = (e.comments ? e.comments : '');
337
+ this.editor.value = this.markup;
338
+ this.viewer.innerHTML = this.markdown;
339
+ this.edit_btn.classList.remove('disab');
340
+ this.edit_btn.classList.add('enab');
341
+ // NOTE: permit documentation of the model by raising the dialog
342
+ if(this.entity === MODEL) this.dialog.style.zIndex = 101;
343
+ } else if(e instanceof DatasetModifier) {
344
+ this.title.innerHTML = e.selector;
345
+ this.viewer.innerHTML = 'Method <tt>' + e.selector +
346
+ '</tt> does not apply to any entity';
347
+ if(e.expression.eligible_prefixes) {
348
+ const el = Object.keys(e.expression.eligible_prefixes)
349
+ .sort(compareSelectors);
350
+ if(el.length > 0) this.viewer.innerHTML = 'Method <tt>' +
351
+ e.selector + '</tt> applies to ' +
352
+ pluralS(el.length, 'prefixed entity group').toLowerCase() +
353
+ ':<ul><li>' + el.join('</li><li>') + '</li></ul>';
354
+ }
355
+ }
336
356
  }
337
357
  }
338
358
 
@@ -139,16 +139,27 @@ class EquationManager {
139
139
  const
140
140
  m = ed.modifiers[UI.nameToID(msl[i])],
141
141
  wild = (m.selector.indexOf('??') >= 0),
142
+ method = m.selector.startsWith(':'),
143
+ issue = (m.expression.compile_issue ? ' compile-issue' :
144
+ (m.expression.compute_issue ? ' compute-issue' : '')),
142
145
  clk = '" onclick="EQUATION_MANAGER.selectModifier(event, \'' +
143
- m.selector + '\'';
146
+ m.selector + '\'',
147
+ mover = (method ? ' onmouseover="EQUATION_MANAGER.showInfo(\'' +
148
+ m.identifier + '\', event.shiftKey);"' : '');
144
149
  if(m === sm) smid += i;
145
150
  ml.push(['<tr id="eqmtr', i, '" class="dataset-modif',
146
151
  (m === sm ? ' sel-set' : ''),
147
152
  '"><td class="equation-selector',
148
- (m.expression.isStatic ? '' : ' it'),
149
- (wild ? ' wildcard' : ''), clk, ', false);">',
153
+ (method ? ' method' : ''),
154
+ // Display in gray when method cannot be applied
155
+ (m.expression.noMethodObject ? ' no-object' : ''),
156
+ (m.expression.isStatic ? '' : ' it'), issue,
157
+ (wild ? ' wildcard' : ''), clk, ', false);"', mover, '>',
150
158
  (wild ? wildcardFormat(m.selector) : m.selector),
151
- '</td><td class="equation-expression',
159
+ '</td><td class="equation-expression', issue,
160
+ (issue ? '"title="' +
161
+ safeDoubleQuotes(m.expression.compile_issue ||
162
+ m.expression.compute_issue) : ''),
152
163
  clk, ');">', m.expression.text, '</td></tr>'].join(''));
153
164
  }
154
165
  this.table.innerHTML = ml.join('');
@@ -164,6 +175,8 @@ class EquationManager {
164
175
 
165
176
  showInfo(id, shift) {
166
177
  // @@TO DO: Display documentation for the equation => extra comments field?
178
+ const d = MODEL.equations_dataset.modifiers[id];
179
+ if(d) DOCUMENTATION_MANAGER.update(d, shift);
167
180
  }
168
181
 
169
182
  selectModifier(event, id, x=true) {
@@ -114,11 +114,14 @@ class GUIExperimentManager extends ExperimentManager {
114
114
  document.getElementById('xv-download-btn').addEventListener(
115
115
  'click', () => EXPERIMENT_MANAGER.promptForDownload());
116
116
  // The viewer's drop-down selectors
117
- document.getElementById('viewer-variable').addEventListener(
117
+ this.viewer_variable = document.getElementById('viewer-variable');
118
+ this.viewer_variable.addEventListener(
118
119
  'change', () => EXPERIMENT_MANAGER.setVariable());
119
- document.getElementById('viewer-statistic').addEventListener(
120
+ this.viewer_statistic = document.getElementById('viewer-statistic');
121
+ this.viewer_statistic.addEventListener(
120
122
  'change', () => EXPERIMENT_MANAGER.setStatistic());
121
- document.getElementById('viewer-scale').addEventListener(
123
+ this.viewer_scale = document.getElementById('viewer-scale');
124
+ this.viewer_scale.addEventListener(
122
125
  'change', () => EXPERIMENT_MANAGER.setScale());
123
126
  // The spin buttons
124
127
  document.getElementById('xp-cd-minus').addEventListener(
@@ -272,12 +275,12 @@ class GUIExperimentManager extends ExperimentManager {
272
275
 
273
276
  updateDialog() {
274
277
  this.updateChartList();
275
- // Warn modeler if no meaningful experiments can be defined
278
+ // Warn modeler if no meaningful experiments can be defined.
276
279
  if(MODEL.outcomeNames.length === 0 && this.suitable_charts.length === 0) {
277
280
  this.default_message.style.display = 'block';
278
281
  this.params_div.style.display = 'none';
279
282
  this.selected_experiment = null;
280
- // Disable experiment dialog menu buttons
283
+ // Disable experiment dialog menu buttons.
281
284
  UI.disableButtons('xp-new xp-rename xp-view xp-delete xp-ignore');
282
285
  } else {
283
286
  this.default_message.style.display = 'none';
@@ -388,7 +391,7 @@ class GUIExperimentManager extends ExperimentManager {
388
391
  x.charts[i].title, '</td></tr>'].join(''));
389
392
  }
390
393
  this.chart_table.innerHTML = tr.join('');
391
- // Do not show viewer unless at least 1 dependent variable has been defined
394
+ // Do not show viewer unless at least 1 dependent variable has been defined.
392
395
  if(x.charts.length === 0 && MODEL.outcomeNames.length === 0) canview = false;
393
396
  if(tr.length >= this.suitable_charts.length) {
394
397
  document.getElementById('xp-c-add-btn').classList.add('v-disab');
@@ -409,7 +412,7 @@ class GUIExperimentManager extends ExperimentManager {
409
412
  dbtn.classList.add('v-disab');
410
413
  cbtn.classList.add('v-disab');
411
414
  }
412
- // Enable viewing only if > 1 dimensions and > 1 outcome variables
415
+ // Enable viewing only if > 1 dimensions and > 1 outcome variables.
413
416
  if(canview) {
414
417
  UI.enableButtons('xp-view');
415
418
  } else {
@@ -478,38 +481,51 @@ class GUIExperimentManager extends ExperimentManager {
478
481
  if(x) {
479
482
  this.design.style.display = 'none';
480
483
  document.getElementById('viewer-title').innerHTML = x.title;
481
- document.getElementById('viewer-statistic').value = x.selected_statistic;
484
+ this.viewer_statistic.value = x.selected_statistic;
482
485
  this.updateViewerVariable();
483
486
  // NOTE: calling updateSpinner with dir=0 will update without changes
484
487
  this.updateSpinner('c', 0);
485
488
  this.drawTable();
486
- document.getElementById('viewer-scale').value = x.selected_scale;
489
+ this.viewer_scale.value = x.selected_scale;
487
490
  this.setColorScale(x.selected_color_scale);
488
491
  this.viewer.style.display = 'block';
489
492
  }
490
493
  }
491
494
 
492
495
  updateViewerVariable() {
493
- // Update the variable drop-down selector of the viewer
496
+ // Update the variable drop-down selector of the viewer.
494
497
  const x = this.selected_experiment;
495
498
  if(x) {
496
499
  x.inferVariables();
497
500
  const
498
501
  ol = [],
499
- vl = MODEL.outcomeNames;
502
+ ov = MODEL.outcomeNames,
503
+ vl = [...ov];
500
504
  for(let i = 0; i < x.variables.length; i++) {
501
- addDistinct(x.variables[i].displayName, vl);
505
+ const
506
+ vn = x.variables[i].displayName,
507
+ oi = ov.indexOf(vn);
508
+ // If an outcome dataset or equation is plotted in an experiment
509
+ // chart, remove its name from the outcome variable list.
510
+ if(oi >= 0) ov.splice(oi, 1);
511
+ addDistinct(vn, vl);
502
512
  }
503
513
  vl.sort((a, b) => UI.compareFullNames(a, b));
504
514
  for(let i = 0; i < vl.length; i++) {
505
- ol.push(['<option value="', vl[i], '"',
506
- (vl[i] == x.selected_variable ? ' selected="selected"' : ''),
507
- '>', vl[i], '</option>'].join(''));
508
- }
509
- document.getElementById('viewer-variable').innerHTML = ol.join('');
510
- if(x.selected_variable === '') {
511
- x.selected_variable = vl[0];
515
+ const vn = vl[i];
516
+ // NOTE: FireFox selector dropdown areas have a pale gray
517
+ // background that darkens when color is set, so always set it
518
+ // to white (like Chrome). Then set color of outcome variables
519
+ // to fuchsia to differentiate from variables for which time
520
+ // series are stored as experiment run results.
521
+ ol.push(['<option value="', vn, '" style="background-color: white',
522
+ (ov.indexOf(vn) >= 0 ? '; color: #b00080"' : '"'),
523
+ (vn == x.selected_variable ? ' selected="selected"' : ''),
524
+ '>', vn, '</option>'].join(''));
512
525
  }
526
+ this.viewer_variable.innerHTML = ol.join('');
527
+ // Initially, select the first variable on the list.
528
+ if(x.selected_variable === '') x.selected_variable = vl[0];
513
529
  }
514
530
  }
515
531
 
@@ -684,8 +700,10 @@ class GUIExperimentManager extends ExperimentManager {
684
700
  for(let j = 0; j < n; j++) {
685
701
  const
686
702
  c = r + j + i * nrows,
703
+ run = x.runs[c],
687
704
  ic = x.chart_combinations.indexOf(c);
688
- if(add) {
705
+ // NOTE: Only add if run has been executed and stored.
706
+ if(add && run) {
689
707
  if(ic < 0) x.chart_combinations.push(c);
690
708
  } else {
691
709
  if(ic >= 0) x.chart_combinations.splice(ic, 1);
@@ -952,7 +970,7 @@ N = ${rr.N}, vector length = ${rr.vector.length}` : '')].join('');
952
970
  // Update view for selected variable
953
971
  const x = this.selected_experiment;
954
972
  if(x) {
955
- x.selected_variable = document.getElementById('viewer-variable').value;
973
+ x.selected_variable = this.viewer_variable.value;
956
974
  this.updateData();
957
975
  }
958
976
  }
@@ -961,7 +979,7 @@ N = ${rr.N}, vector length = ${rr.vector.length}` : '')].join('');
961
979
  // Update view for selected variable.
962
980
  const x = this.selected_experiment;
963
981
  if(x) {
964
- x.selected_statistic = document.getElementById('viewer-statistic').value;
982
+ x.selected_statistic = this.viewer_statistic.value;
965
983
  this.updateData();
966
984
  // NOTE: Update of Chart Manager is needed only when it is showing
967
985
  // run statistics.
@@ -1027,7 +1045,7 @@ N = ${rr.N}, vector length = ${rr.vector.length}` : '')].join('');
1027
1045
  // Update view for selected scale
1028
1046
  const x = this.selected_experiment;
1029
1047
  if(x) {
1030
- x.selected_scale = document.getElementById('viewer-scale').value;
1048
+ x.selected_scale = this.viewer_scale.value;
1031
1049
  this.updateData();
1032
1050
  // NOTE: Update of Chart Manager is needed when it is showing
1033
1051
  // run statistics because solver times may be plotted.