linny-r 1.6.2 → 1.6.3

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.6.2",
3
+ "version": "1.6.3",
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
@@ -1765,6 +1765,8 @@ NOTE: * and ? will be interpreted as wildcards"
1765
1765
  <img id="eq-delete-btn" class="btn disab" src="images/delete.png"
1766
1766
  title="Delete selected equation">
1767
1767
  </div>
1768
+ <img id="equation-outcome" class="not-selected" src="images/outcome.png"
1769
+ title="Click to consider/ignore selected equation as experiment outcome">
1768
1770
  <div id="equation-scroll-area">
1769
1771
  <table id="equation-table">
1770
1772
  </table>
@@ -2011,6 +2013,22 @@ NOTE: * and ? will be interpreted as wildcards"
2011
2013
  </div>
2012
2014
  </div>
2013
2015
 
2016
+ <!-- the ADD WILDCARD VARIABLES dialog allows selecting wildcard
2017
+ variables from a list to be added to the current chart -->
2018
+ <div id="add-wildcard-variables-modal" class="modal">
2019
+ <div id="add-wildcard-variables-dlg" class="inp-dlg">
2020
+ <div class="dlg-title">
2021
+ Add wildcard matches
2022
+ <img class="cancel-btn" src="images/cancel.png">
2023
+ <img class="ok-btn" src="images/ok.png">
2024
+ </div>
2025
+ <div id="add-wildcard-variables-scroll-area">
2026
+ <table id="add-wildcard-variables-table">
2027
+ </table>
2028
+ </div>
2029
+ </div>
2030
+ </div>
2031
+
2014
2032
  <!-- the RENAME CHART dialog prompts for the new title for a chart -->
2015
2033
  <div id="rename-chart-modal" class="modal">
2016
2034
  <div id="rename-chart-dlg" class="inp-dlg">
@@ -2161,7 +2161,8 @@ div.io-box {
2161
2161
  cursor: pointer;
2162
2162
  }
2163
2163
 
2164
- #dataset-outcome.not-selected {
2164
+ #dataset-outcome.not-selected,
2165
+ #equation-outcome.not-selected {
2165
2166
  filter: saturate(0) contrast(50%) brightness(170%);
2166
2167
  }
2167
2168
 
@@ -2273,10 +2274,12 @@ div.modif::before {
2273
2274
  margin-right: 2px;
2274
2275
  }
2275
2276
 
2276
- div.outcome::before {
2277
+ div.outcome::before,
2278
+ span.outcome::before {
2277
2279
  content: ' \25C8';
2278
2280
  color: #b00080;
2279
2281
  margin-right: 1px;
2282
+ font-style: normal;
2280
2283
  }
2281
2284
 
2282
2285
  div.array::before {
@@ -2449,6 +2452,15 @@ td.equation-expression {
2449
2452
  border-top: 1px solid Silver;
2450
2453
  }
2451
2454
 
2455
+ #equation-outcome {
2456
+ position: absolute;
2457
+ top: 25px;
2458
+ right: 4px;
2459
+ width: 16px;
2460
+ height: 16px;
2461
+ cursor: pointer;
2462
+ }
2463
+
2452
2464
  /* NOTE: Rename equation modal must be above Edit variable modal */
2453
2465
  #rename-equation-modal {
2454
2466
  z-index: 110;
@@ -3098,6 +3110,31 @@ img.v-disab {
3098
3110
  width: calc(100% - 6px);
3099
3111
  }
3100
3112
 
3113
+ #add-wildcard-variables-dlg {
3114
+ width: 250px;
3115
+ min-height: 42px;
3116
+ max-height: 50vh;
3117
+ }
3118
+
3119
+ #add-wildcard-variables-scroll-area {
3120
+ position: absolute;
3121
+ top: 22px;
3122
+ left: 2px;
3123
+ width: calc(100% - 4px);
3124
+ height: calc(100% - 26px);
3125
+ overflow-y: auto;
3126
+ border-top: 1px solid Silver;
3127
+ }
3128
+
3129
+ #add-wildcard-variables-table {
3130
+ width: 100%;
3131
+ border-collapse: collapse;
3132
+ line-height: 16px;
3133
+ background-color: white;
3134
+ border: 1px solid Silver;
3135
+ border-top: none;
3136
+ }
3137
+
3101
3138
  /* the SENSITIVITY DIALOG displays the sensitivity analysis */
3102
3139
  #sensitivity-dlg {
3103
3140
  display: none;
@@ -834,12 +834,19 @@ class ChartManager {
834
834
  }
835
835
 
836
836
  resetChartVectors() {
837
- // Reset vectors of all charts
837
+ // Reset vectors of all charts.
838
838
  for(let i = 0; i < MODEL.charts.length; i++) {
839
839
  MODEL.charts[i].resetVectors();
840
840
  }
841
841
  }
842
842
 
843
+ promptForWildcardIndices(chart, dsm) {
844
+ // No GUI dialog, so add *all* vectors for wildcard dataset modifier
845
+ // `dsm` as variables to `chart`.
846
+ const indices = Object.keys(dsm.expression.wildcard_vectors);
847
+ chart.addWildcardVariables(dsm, indices);
848
+ }
849
+
843
850
  setRunsChart(show) {
844
851
  // Indicate whether the chart manager should display a run result chart.
845
852
  this.runs_chart = show;
@@ -131,7 +131,7 @@ class GUIChartManager extends ChartManager {
131
131
  document.getElementById('chart-narrow-btn').addEventListener(
132
132
  'click', () => CHART_MANAGER.stretchChart(-1));
133
133
 
134
- // The Add variable modal
134
+ // The Add variable modal.
135
135
  this.add_variable_modal = new ModalDialog('add-variable');
136
136
  this.add_variable_modal.ok.addEventListener(
137
137
  'click', () => CHART_MANAGER.addVariable());
@@ -143,7 +143,7 @@ class GUIChartManager extends ChartManager {
143
143
  this.add_variable_modal.element('name').addEventListener(
144
144
  'change', () => X_EDIT.updateAttributeSelector('add-'));
145
145
 
146
- // The Edit variable modal
146
+ // The Edit variable modal.
147
147
  this.variable_modal = new ModalDialog('variable');
148
148
  this.variable_modal.ok.addEventListener(
149
149
  'click', () => CHART_MANAGER.modifyVariable());
@@ -160,7 +160,7 @@ class GUIChartManager extends ChartManager {
160
160
  'mouseleave', () => CHART_MANAGER.hidePasteColor());
161
161
  document.getElementById('variable-color').addEventListener(
162
162
  'click', (event) => CHART_MANAGER.copyPasteColor(event));
163
- // NOTE: uses the color picker developed by James Daniel
163
+ // NOTE: Uses the color picker developed by James Daniel.
164
164
  this.color_picker = new iro.ColorPicker("#color-picker", {
165
165
  width: 92,
166
166
  height: 92,
@@ -179,13 +179,20 @@ class GUIChartManager extends ChartManager {
179
179
  CHART_MANAGER.color_picker.color.hexString;
180
180
  });
181
181
 
182
- // The Rename chart modal
182
+ // The Rename chart modal.
183
183
  this.rename_chart_modal = new ModalDialog('rename-chart');
184
184
  this.rename_chart_modal.ok.addEventListener(
185
185
  'click', () => CHART_MANAGER.renameChart());
186
186
  this.rename_chart_modal.cancel.addEventListener(
187
187
  'click', () => CHART_MANAGER.rename_chart_modal.hide());
188
188
 
189
+ // The Add wildcard variables modal.
190
+ this.add_wildcard_modal = new ModalDialog('add-wildcard-variables');
191
+ this.add_wildcard_modal.ok.addEventListener(
192
+ 'click', () => CHART_MANAGER.addSelectedWildcardVariables());
193
+ this.add_wildcard_modal.cancel.addEventListener(
194
+ 'click', () => CHART_MANAGER.add_wildcard_modal.hide());
195
+
189
196
  // Do not display the time step until cursor moves over chart
190
197
  this.time_step.style.display = 'none';
191
198
  document.getElementById('table-only-buttons').style.display = 'none';
@@ -326,16 +333,25 @@ class GUIChartManager extends ChartManager {
326
333
  }
327
334
 
328
335
  updateSelector() {
329
- // Adds one option to the selector for each chart defined for the model
330
- // NOTE: add the "new chart" option if still absent
336
+ // Adds one option to the selector for each chart defined for the model.
337
+ // NOTE: Add the "new chart" option if it is not in the list.
331
338
  MODEL.addChart(this.new_chart_title);
332
339
  if(this.chart_index < 0) this.chart_index = 0;
333
340
  const ol = [];
334
341
  for(let i = 0; i < MODEL.charts.length; i++) {
335
- ol.push('<option value="', i,
336
- (i == this.chart_index ? '"selected="selected' : ''),
337
- '">', MODEL.charts[i].title , '</option>');
342
+ const t = MODEL.charts[i].title;
343
+ ol.push(['<option value="', i,
344
+ (i == this.chart_index ? '" selected="selected' : ''),
345
+ '">', t , '</option>'].join(''));
338
346
  }
347
+ // Sort option list by chart title.
348
+ ol.sort((a, b) => {
349
+ const
350
+ re = /<option value="\d+"( selected="selected")?>(.+)<\/option>/,
351
+ ta = a.replace(re, '$2'),
352
+ tb = b.replace(re, '$2');
353
+ return UI.compareFullNames(ta, tb);
354
+ });
339
355
  this.chart_selector.innerHTML = ol.join('');
340
356
  }
341
357
 
@@ -379,6 +395,11 @@ class GUIChartManager extends ChartManager {
379
395
  } else {
380
396
  this.variable_index = -1;
381
397
  }
398
+ // Just in case variable index has not been adjusted after some
399
+ // variables have been deleted
400
+ if(this.variable_index >= c.variables.length) {
401
+ this.variable_index = -1;
402
+ }
382
403
  // Set the image of the sort type button.
383
404
  if(this.variable_index >= 0) {
384
405
  const
@@ -392,11 +413,6 @@ class GUIChartManager extends ChartManager {
392
413
  u_btn = 'chart-variable-up ',
393
414
  d_btn = 'chart-variable-down ',
394
415
  ed_btns = 'chart-edit-variable chart-sort-variable chart-delete-variable ';
395
- // Just in case variable index has not been adjusted after some
396
- // variables have been deleted
397
- if(this.variable_index >= c.variables.length) {
398
- this.variable_index = -1;
399
- }
400
416
  if(this.variable_index < 0) {
401
417
  UI.disableButtons(ed_btns + u_btn + d_btn);
402
418
  } else {
@@ -713,15 +729,63 @@ class GUIChartManager extends ChartManager {
713
729
  }
714
730
  }
715
731
 
732
+ promptForWildcardIndices(chart, dsm) {
733
+ // Prompt modeler with list of vectors for wildcard dataset modifier
734
+ // `dsm` as variables to `chart`.
735
+ const
736
+ md = this.add_wildcard_modal,
737
+ indices = Object.keys(dsm.expression.wildcard_vectors);
738
+ // First hide the "Add variable" modal.
739
+ this.add_variable_modal.hide();
740
+ // Do not prompt for selection if there is only 1 match.
741
+ if(indices.length < 2) chart.addWildcardVariables(dsm, indices);
742
+ md.chart = chart;
743
+ md.modifier = dsm;
744
+ md.indices = indices;
745
+ const
746
+ tr = [],
747
+ dn = dsm.displayName,
748
+ tbl = md.element('table');
749
+ for(let i = 0; i < indices.length; i++) {
750
+ tr.push('<tr><td class="v-box"><div id="wcv-box-', indices[i],
751
+ '" class="box clear" onclick="UI.toggleBox(event);"></td>',
752
+ '<td class="vname">', dn.replace('??', indices[i]),
753
+ '</td></tr>');
754
+ tbl.innerHTML = tr.join('');
755
+ }
756
+ md.dialog.style.height = (22 + indices.length * 16) + 'px';
757
+ md.show();
758
+ }
759
+
760
+ addSelectedWildcardVariables() {
761
+ // Let the chart add selected wildcard matches (if any) as chart
762
+ // variables.
763
+ const
764
+ md = this.add_wildcard_modal,
765
+ c = md.chart,
766
+ dsm = md.modifier,
767
+ il = md.indices,
768
+ indices = [];
769
+ if(c && dsm && il) {
770
+ for(let i = 0; i < il.length; i++) {
771
+ if(UI.boxChecked('wcv-box-'+ il[i])) indices.push(il[i]);
772
+ }
773
+ }
774
+ if(indices.length) c.addWildcardVariables(dsm, indices);
775
+ // Always hide the dialog.
776
+ md.hide();
777
+ this.updateDialog();
778
+ }
779
+
716
780
  selectVariable(vi) {
717
- // Select variable, and edit it when double-clicked
781
+ // Select variable, and edit it when double-clicked.
718
782
  const
719
783
  now = Date.now(),
720
784
  dt = now - this.last_time_selected;
721
785
  if(vi >= 0 && this.chart_index >= 0) {
722
786
  this.last_time_selected = now;
723
787
  if(vi === this.variable_index) {
724
- // Consider click to be "double" if it occurred less than 300 ms ago
788
+ // Consider click to be "double" if it occurred less than 300 ms ago.
725
789
  if(dt < 300) {
726
790
  this.last_time_selected = 0;
727
791
  this.editVariable();
@@ -109,7 +109,7 @@ class GroupPropertiesDialog extends ModalDialog {
109
109
  // input fields this means `onkeydown` events.
110
110
  const fnc = (event) => {
111
111
  const id = event.target.id.split('-').shift();
112
- // NOTE: add a short delay to permit checkboxes to update their
112
+ // NOTE: Add a short delay to permit checkboxes to update their
113
113
  // status first, before checking for change.
114
114
  setTimeout(() => UI.modals[id].highlightModifiedFields(), 100);
115
115
  };
@@ -819,8 +819,8 @@ class GUIController extends Controller {
819
819
 
820
820
  // Add all draggable stay-on-top dialogs as controller properties.
821
821
 
822
- // Make checkboxes respond to click
823
- // NOTE: checkbox-specific events must be bound AFTER this general setting
822
+ // Make checkboxes respond to click.
823
+ // NOTE: Checkbox-specific events must be bound AFTER this general setting.
824
824
  const
825
825
  cbs = document.getElementsByClassName('box'),
826
826
  cbf = (event) => UI.toggleBox(event);
@@ -54,6 +54,9 @@ class EquationManager {
54
54
  'click', () => EQUATION_MANAGER.editEquation());
55
55
  document.getElementById('eq-delete-btn').addEventListener(
56
56
  'click', () => EQUATION_MANAGER.deleteEquation());
57
+ this.outcome_btn = document.getElementById('equation-outcome');
58
+ this.outcome_btn.addEventListener(
59
+ 'click', () => EQUATION_MANAGER.toggleOutcome());
57
60
 
58
61
  // Create modal dialogs
59
62
  this.new_modal = new ModalDialog('new-equation');
@@ -134,6 +137,11 @@ class EquationManager {
134
137
  ml = [],
135
138
  msl = ed.selectorList,
136
139
  sm = this.selected_modifier;
140
+ if(sm && sm.outcome_equation) {
141
+ this.outcome_btn.classList.remove('not-selected');
142
+ } else {
143
+ this.outcome_btn.classList.add('not-selected');
144
+ }
137
145
  let smid = 'eqmtr';
138
146
  for(let i = 0; i < msl.length; i++) {
139
147
  const
@@ -155,6 +163,7 @@ class EquationManager {
155
163
  (m.expression.noMethodObject ? ' no-object' : ''),
156
164
  (m.expression.isStatic ? '' : ' it'), issue,
157
165
  (wild ? ' wildcard' : ''), clk, ', false);"', mover, '>',
166
+ (m.outcome_equation ? '<span class="outcome"></span>' : ''),
158
167
  (wild ? wildcardFormat(m.selector) : m.selector),
159
168
  '</td><td class="equation-expression', issue,
160
169
  (issue ? '"title="' +
@@ -201,6 +210,16 @@ class EquationManager {
201
210
  this.updateDialog();
202
211
  }
203
212
 
213
+ toggleOutcome() {
214
+ const m = this.selected_modifier;
215
+ // NOTE: Methods cannot be outcomes.
216
+ if(m && !m.selector.startsWith(':')) {
217
+ m.outcome_equation = !m.outcome_equation;
218
+ this.updateDialog();
219
+ if(!UI.hidden('experiment-dlg')) EXPERIMENT_MANAGER.updateDialog();
220
+ }
221
+ }
222
+
204
223
  promptForEquation(add=false) {
205
224
  this.add_to_chart = add;
206
225
  this.new_modal.element('name').value = '';
@@ -377,7 +377,7 @@ NOTE: Grouping groups results in a single group, e.g., (1;2);(3;4;5) evaluates a
377
377
  }
378
378
  va.innerHTML = options.join('');
379
379
  // NOTE: Chart Manager variable dialog is 60px wider
380
- va.style.width = (prefix ? 'calc(100% - 82px)' : 'calc(100% - 142px)');
380
+ va.style.width = (prefix ? 'calc(100% - 84px)' : 'calc(100% - 142px)');
381
381
  return;
382
382
  }
383
383
  // Add "empty" as first and initial option, as it denotes "use default"
@@ -280,6 +280,7 @@ class GUIMonitor {
280
280
 
281
281
  hideCallStack() {
282
282
  document.getElementById('call-stack-modal').style.display = 'none';
283
+ this.call_stack_shown = false;
283
284
  }
284
285
 
285
286
  logMessage(block, msg) {
@@ -170,11 +170,11 @@ class LinnyRModel {
170
170
  olist.push(ds.displayName);
171
171
  }
172
172
  }
173
- // ALL equation results are stored, so add all equation selectors...
173
+ // Also add all outcome equation selectors.
174
174
  const dsm = this.equations_dataset.modifiers;
175
- // ... except selectors starting with a colon (methods).
175
+ // Exclude selectors starting with a colon (methods) -- just in case.
176
176
  for(let k in dsm) if(dsm.hasOwnProperty(k) && !k.startsWith(':')) {
177
- olist.push(dsm[k].selector);
177
+ if(dsm[k].outcome_equation) olist.push(dsm[k].selector);
178
178
  }
179
179
  return olist;
180
180
  }
@@ -1593,8 +1593,8 @@ class LinnyRModel {
1593
1593
  nbp = note.nearby_pos;
1594
1594
  if(nbp) {
1595
1595
  // Adjust (x, y) so as to retain the relative position.
1596
- note.x += nbp.node.x - npb.oldx;
1597
- note.y += nbp.node.y - npb.oldy;
1596
+ note.x += nbp.node.x - nbp.oldx;
1597
+ note.y += nbp.node.y - nbp.oldy;
1598
1598
  note.nearby_pos = null;
1599
1599
  }
1600
1600
  }
@@ -3155,8 +3155,8 @@ class LinnyRModel {
3155
3155
  const
3156
3156
  dm = ds.modifiers[ms],
3157
3157
  n = dm.displayName;
3158
- // Do not add if already in the list.
3159
- if(names.indexOf(n) < 0) {
3158
+ // Do not add if already in the list, or equation is not outcome.
3159
+ if(names.indexOf(n) < 0 && (!eq || dm.outcome_equation)) {
3160
3160
  // Here, too, NULL can be used as "owner chart".
3161
3161
  const cv = new ChartVariable(null);
3162
3162
  // NOTE: For equations, the object is the dataset modifier.
@@ -3169,10 +3169,17 @@ class LinnyRModel {
3169
3169
  // Sort variables by their name.
3170
3170
  vbls.sort((a, b) => UI.compareFullNames(a.displayName, b.displayName));
3171
3171
  // Create a new chart as dummy, so without adding it to this model.
3172
- const c = new Chart();
3172
+ const
3173
+ c = new Chart(),
3174
+ wcdm = [];
3173
3175
  for(let i = 0; i < vbls.length; i++) {
3174
3176
  const v = vbls[i];
3175
- c.addVariable(v.object.displayName, v.attribute);
3177
+ // NOTE: Prevent adding wildcard dataset modifiers more than once.
3178
+ if(wcdm.indexOf(v.object) < 0) {
3179
+ if(v.object instanceof DatasetModifier &&
3180
+ v.object.selector.indexOf('??') >= 0) wcdm.push(v.object);
3181
+ c.addVariable(v.object.displayName, v.attribute);
3182
+ }
3176
3183
  }
3177
3184
  // NOTE: Call `draw` with FALSE to prevent display in the chart manager.
3178
3185
  c.draw(false);
@@ -8477,7 +8484,7 @@ class DatasetModifier {
8477
8484
  this.dataset = dataset;
8478
8485
  this.selector = selector;
8479
8486
  this.expression = new Expression(dataset, selector, '');
8480
- this.expression_cache = {};
8487
+ this.outcome_equation = false;
8481
8488
  }
8482
8489
 
8483
8490
  get type() {
@@ -8507,12 +8514,21 @@ class DatasetModifier {
8507
8514
  // NOTE: For some reason, selector may become empty string, so prevent
8508
8515
  // saving such unidentified modifiers.
8509
8516
  if(this.selector.trim().length === 0) return '';
8510
- return ['<modifier><selector>', xmlEncoded(this.selector),
8517
+ const oe = (this.outcome_equation ? ' outcome="1"' : '');
8518
+ return ['<modifier', oe, '><selector>', xmlEncoded(this.selector),
8511
8519
  '</selector><expression>', xmlEncoded(this.expression.text),
8512
8520
  '</expression></modifier>'].join('');
8513
8521
  }
8514
8522
 
8515
8523
  initFromXML(node) {
8524
+ // NOTE: Up to version 1.6.2, all equations were considered as
8525
+ // outcomes. To maintain upward compatibility, check for the model
8526
+ // version number.
8527
+ if(earlierVersion(MODEL.version, '1.6.3')) {
8528
+ this.outcome_equation = true;
8529
+ } else {
8530
+ this.outcome_equation = nodeParameterValue(node, 'outcome') === '1';
8531
+ }
8516
8532
  this.expression.text = xmlDecoded(nodeContentByTag(node, 'expression'));
8517
8533
  if(IO_CONTEXT) {
8518
8534
  // Contextualize the included expression.
@@ -8824,12 +8840,19 @@ class Dataset {
8824
8840
  this.max = Math.max(this.max, this.data[i]);
8825
8841
  sum += this.data[i];
8826
8842
  }
8827
- this.mean = sum / this.data.length;
8828
- let sumsq = 0;
8829
- for(let i = 0; i < this.data.length; i++) {
8830
- sumsq += Math.pow(this.data[i] - this.mean, 2);
8843
+ // NOTE: Avoid small differences due to numerical imprecision.
8844
+ if(this.max - this.min < VM.NEAR_ZERO) {
8845
+ this.max = this.min;
8846
+ this.mean = this.min;
8847
+ this.standard_deviation = 0;
8848
+ } else {
8849
+ this.mean = sum / this.data.length;
8850
+ let sumsq = 0;
8851
+ for(let i = 0; i < this.data.length; i++) {
8852
+ sumsq += Math.pow(this.data[i] - this.mean, 2);
8853
+ }
8854
+ this.standard_deviation = Math.sqrt(sumsq / this.data.length);
8831
8855
  }
8832
- this.standard_deviation = Math.sqrt(sumsq / this.data.length);
8833
8856
  }
8834
8857
 
8835
8858
  get statisticsAsString() {
@@ -9081,7 +9104,6 @@ class Dataset {
9081
9104
  for(let m in this.modifiers) if(this.modifiers.hasOwnProperty(m)) {
9082
9105
  // NOTE: "empty" expressions for modifiers default to dataset default.
9083
9106
  this.modifiers[m].expression.reset(this.defaultValue);
9084
- this.modifiers[m].expression_cache = {};
9085
9107
  }
9086
9108
  }
9087
9109
 
@@ -9141,6 +9163,8 @@ class ChartVariable {
9141
9163
  this.non_zero_tally = 0;
9142
9164
  this.exceptions = 0;
9143
9165
  this.bin_tallies = [];
9166
+ // The actual wildcard index is set for each variable that is added
9167
+ // during this "expansion".
9144
9168
  this.wildcard_index = false;
9145
9169
  }
9146
9170
 
@@ -9210,6 +9234,8 @@ class ChartVariable {
9210
9234
  }
9211
9235
  const xml = ['<chart-variable', (this.stacked ? ' stacked="1"' : ''),
9212
9236
  (this.visible ? ' visible="1"' : ''),
9237
+ (this.wildcard_index !== false ?
9238
+ ` wildcard-index="${this.wildcard_index}"` : ''),
9213
9239
  ` sorted="${this.sorted}"`,
9214
9240
  '><object-id>', xmlEncoded(id),
9215
9241
  '</object-id><attribute>', this.attribute,
@@ -9255,6 +9281,9 @@ class ChartVariable {
9255
9281
  UI.warn(`No chart variable entity with ID "${id}"`);
9256
9282
  return false;
9257
9283
  }
9284
+ // For wildcard variables, a subset of wildcard indices may be specified.
9285
+ const wci = nodeParameterValue(node, 'wildcard-index');
9286
+ this.wildcard_index = (wci ? parseInt(wci) : false);
9258
9287
  this.setProperties(
9259
9288
  obj,
9260
9289
  nodeContentByTag(node, 'attribute'),
@@ -9327,7 +9356,7 @@ class ChartVariable {
9327
9356
  for(let t = 0; t <= t_end; t++) {
9328
9357
  // Get the result, store it, and incorporate it in statistics.
9329
9358
  if(!av) {
9330
- // Undefined attribute => zero (no error)
9359
+ // Undefined attribute => zero (no error).
9331
9360
  v = 0;
9332
9361
  } else if(Array.isArray(av)) {
9333
9362
  // Attribute value is a vector.
@@ -9339,19 +9368,19 @@ class ChartVariable {
9339
9368
  // this index as context number.
9340
9369
  v = av.result(t, this.wildcard_index);
9341
9370
  } else {
9342
- // Attribute value must be a number
9371
+ // Attribute value must be a number.
9343
9372
  v = av;
9344
9373
  }
9345
- // Map undefined values and all errors to 0
9374
+ // Map undefined values and all errors to 0.
9346
9375
  if(v < VM.MINUS_INFINITY || v > VM.PLUS_INFINITY) {
9347
- // Do not include values for t = 0 in statistics
9376
+ // Do not include values for t = 0 in statistics.
9348
9377
  if(t > 0) this.exceptions++;
9349
9378
  v = 0;
9350
9379
  }
9351
- // Scale the value unless run result (these are already scaled!)
9380
+ // Scale the value unless run result (these are already scaled!).
9352
9381
  if(!rr) v *= this.scale_factor;
9353
9382
  this.vector.push(v);
9354
- // Do not include values for t = 0 in statistics
9383
+ // Do not include values for t = 0 in statistics.
9355
9384
  if(t > 0) {
9356
9385
  if(Math.abs(v) > VM.NEAR_ZERO) {
9357
9386
  this.sum += v;
@@ -9361,17 +9390,24 @@ class ChartVariable {
9361
9390
  this.maximum = Math.max(this.maximum, v);
9362
9391
  }
9363
9392
  }
9364
- // Compute the mean
9365
- this.mean = this.sum / t_end;
9366
- // Compute the variance for t=1, ..., N
9367
- let sumsq = 0;
9368
- for(let t = 1; t <= t_end; t++) {
9369
- v = this.vector[t];
9370
- // Here, too, ignore exceptional values, and use 0 instead
9371
- if(v < VM.MINUS_INFINITY || v > VM.PLUS_INFINITY) v = 0;
9372
- sumsq += Math.pow(v - this.mean, 2);
9393
+ if(this.maximum - this.minimum < VM.NEAR_ZERO) {
9394
+ // Ignore minute differences.
9395
+ this.maximum = this.minimum;
9396
+ this.mean = this.minimum;
9397
+ this.variance = 0;
9398
+ } else {
9399
+ // Compute the mean.
9400
+ this.mean = this.sum / t_end;
9401
+ // Compute the variance for t=1, ..., N.
9402
+ let sumsq = 0;
9403
+ for(let t = 1; t <= t_end; t++) {
9404
+ v = this.vector[t];
9405
+ // Here, too, ignore exceptional values, and use 0 instead.
9406
+ if(v < VM.MINUS_INFINITY || v > VM.PLUS_INFINITY) v = 0;
9407
+ sumsq += Math.pow(v - this.mean, 2);
9408
+ }
9409
+ this.variance = sumsq / t_end;
9373
9410
  }
9374
- this.variance = sumsq / t_end;
9375
9411
  }
9376
9412
 
9377
9413
  tallyVector() {
@@ -9540,16 +9576,16 @@ class Chart {
9540
9576
  }
9541
9577
 
9542
9578
  addVariable(n, a) {
9543
- // Adds variable [entity name `n`|attribute `a`] to the chart unless it
9544
- // is already in the variable list.
9579
+ // Add variable [entity name `n`|attribute `a`] to the chart unless
9580
+ // it is already in the variable list.
9545
9581
  let dn = n + UI.OA_SEPARATOR + a;
9546
- // Adapt display name for special cases
9582
+ // Adapt display name for special cases.
9547
9583
  if(n === UI.EQUATIONS_DATASET_NAME) {
9548
- // For equations only the attribute (modifier selector)
9584
+ // For equations only the attribute (modifier selector).
9549
9585
  dn = a;
9550
9586
  n = a;
9551
9587
  } else if(!a) {
9552
- // If no attribute specified (=> dataset) only the entity name
9588
+ // If no attribute specified (=> dataset) only the entity name.
9553
9589
  dn = n;
9554
9590
  }
9555
9591
  let vi = this.variableIndexByName(dn);
@@ -9564,17 +9600,14 @@ class Chart {
9564
9600
  // No equation and no attribute specified? Then assume default.
9565
9601
  if(!eq && a === '') a = obj.defaultAttribute;
9566
9602
  if(eq && (n.indexOf('??') >= 0 || obj.expression.isMethod)) {
9567
- // Special case: for wildcard equations and methods, add dummy
9568
- // variables for each vector in the wildcard vector set of the
9569
- // expression.
9570
- const
9571
- clr = this.nextAvailableDefaultColor,
9572
- indices = Object.keys(obj.expression.wildcard_vectors);
9573
- for(let i = 0; i < indices.length; i++) {
9574
- const v = new ChartVariable(this);
9575
- v.setProperties(obj, dn, false, clr);
9576
- v.wildcard_index = parseInt(indices[i]);
9577
- this.variables.push(v);
9603
+ // Special case: for wildcard equations and methods, prompt the
9604
+ // modeler which wildcard possibilities to add UNLESS this is an
9605
+ // untitled "dummy" chart used to report outcomes.
9606
+ if(this.title) {
9607
+ CHART_MANAGER.promptForWildcardIndices(this, obj);
9608
+ } else {
9609
+ this.addWildcardVariables(obj,
9610
+ Object.keys(obj.expression.wildcard_vectors));
9578
9611
  }
9579
9612
  } else {
9580
9613
  const v = new ChartVariable(this);
@@ -9583,6 +9616,19 @@ class Chart {
9583
9616
  }
9584
9617
  return this.variables.length - 1;
9585
9618
  }
9619
+
9620
+ addWildcardVariables(dsm, indices) {
9621
+ // For dataset modifier `dsm`, add dummy variables for those vectors
9622
+ // in the wildcard vector set of the expression that are indicated
9623
+ // by the list `indices`.
9624
+ const dn = dsm.displayName;
9625
+ for(let i = 0; i < indices.length; i++) {
9626
+ const v = new ChartVariable(this);
9627
+ v.setProperties(dsm, dn, false, this.nextAvailableDefaultColor);
9628
+ v.wildcard_index = parseInt(indices[i]);
9629
+ this.variables.push(v);
9630
+ }
9631
+ }
9586
9632
 
9587
9633
  addSVG(lines) {
9588
9634
  // Appends a string or an array of strings to the SVG
@@ -10401,7 +10447,7 @@ class Chart {
10401
10447
  if(CHART_MANAGER.drawing_chart) {
10402
10448
  return '(chart statistics not calculated yet)';
10403
10449
  }
10404
- // NOTE: unlike statistics, series data is output in columns
10450
+ // NOTE: Unlike statistics, series data is output in columns.
10405
10451
  const data = [], vbl = [], line = ['t'];
10406
10452
  // First line: column labels (variable names, but time step in first column)
10407
10453
  for(let i = 0; i < this.variables.length; i++) {
@@ -394,11 +394,12 @@ class Expression {
394
394
  vmi[0](this, vmi[1]);
395
395
  this.program_counter++;
396
396
  }
397
- // Stack should now have length 1.
397
+ // Stack should now have length 1. If not, report error unless the
398
+ // length is due to some other error.
398
399
  if(this.stack.length > 1) {
399
- v[t] = VM.OVERFLOW;
400
+ if(v[t] > VM.ERROR) v[t] = VM.OVERFLOW;
400
401
  } else if(this.stack.length < 1) {
401
- v[t] = VM.UNDERFLOW;
402
+ if(v[t] > VM.ERROR) v[t] = VM.UNDERFLOW;
402
403
  } else {
403
404
  v[t] = this.stack.pop();
404
405
  }
@@ -2476,7 +2477,7 @@ class VirtualMachine {
2476
2477
  // Return number `n` formatted so as to show 2-3 significant digits
2477
2478
  // NOTE: as `n` should be a number, a warning sign will typically
2478
2479
  // indicate a bug in the software.
2479
- if(n === undefined) return '\u26A0'; // Warning sign
2480
+ if(n === undefined || isNaN(n)) return '\u26A0'; // Warning sign
2480
2481
  const sv = this.specialValue(n);
2481
2482
  // If `n` has a special value, return its representation.
2482
2483
  if(sv[0]) return sv[1];
@@ -2972,10 +2973,10 @@ class VirtualMachine {
2972
2973
  if(vcnt == 0) return '(no variables)';
2973
2974
  let l = '';
2974
2975
  for(let i = 0; i < vcnt; i++) {
2975
- const obj = this.variables[i][1];
2976
- let v = 'X' + (i+1).toString().padStart(z, '0');
2977
- v += ' '.slice(v.length) + obj.displayName;
2978
- const p = (obj instanceof Process && obj.pace > 1 ? ' 1/' + obj.pace : '');
2976
+ const
2977
+ obj = this.variables[i][1],
2978
+ v = 'X' + (i+1).toString().padStart(z, '0') + ' ' + obj.displayName,
2979
+ p = (obj instanceof Process && obj.pace > 1 ? ' 1/' + obj.pace : '');
2979
2980
  l += v + ' [' + this.variables[i][0] + p + ']\n';
2980
2981
  }
2981
2982
  if(this.chunk_variables.length > 0) {