linny-r 1.6.2 → 1.6.4

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.4",
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
@@ -1300,6 +1300,23 @@ NOTE: Unit symbols are case-sensitive, so BTU ≠ Btu">
1300
1300
  </div>
1301
1301
  </div>
1302
1302
 
1303
+ <!-- BOUNDLINE POINT modal -->
1304
+ <div id="boundline-point-modal" class="modal">
1305
+ <div id="boundline-point-dlg" class="inp-dlg">
1306
+ <div class="dlg-title">
1307
+ Precise point coordinates
1308
+ <img class="cancel-btn" src="images/cancel.png">
1309
+ <img class="ok-btn" src="images/ok.png">
1310
+ </div>
1311
+ <div id="boundline-point-x-lbl">X =</div>
1312
+ <input id="boundline-point-x" type="text" autocomplete="off">
1313
+ <div id="boundline-point-x-percent">%</div>
1314
+ <div id="boundline-point-y-lbl">Y =</div>
1315
+ <input id="boundline-point-y" type="text" autocomplete="off">
1316
+ <div id="boundline-point-y-percent">%</div>
1317
+ </div>
1318
+ </div>
1319
+
1303
1320
  <!-- PROCESS properties dialog -->
1304
1321
  <div id="process-modal" class="modal">
1305
1322
  <div id="process-dlg" class="inp-dlg">
@@ -1765,6 +1782,8 @@ NOTE: * and ? will be interpreted as wildcards"
1765
1782
  <img id="eq-delete-btn" class="btn disab" src="images/delete.png"
1766
1783
  title="Delete selected equation">
1767
1784
  </div>
1785
+ <img id="equation-outcome" class="not-selected" src="images/outcome.png"
1786
+ title="Click to consider/ignore selected equation as experiment outcome">
1768
1787
  <div id="equation-scroll-area">
1769
1788
  <table id="equation-table">
1770
1789
  </table>
@@ -2011,6 +2030,22 @@ NOTE: * and ? will be interpreted as wildcards"
2011
2030
  </div>
2012
2031
  </div>
2013
2032
 
2033
+ <!-- the ADD WILDCARD VARIABLES dialog allows selecting wildcard
2034
+ variables from a list to be added to the current chart -->
2035
+ <div id="add-wildcard-variables-modal" class="modal">
2036
+ <div id="add-wildcard-variables-dlg" class="inp-dlg">
2037
+ <div class="dlg-title">
2038
+ Add wildcard matches
2039
+ <img class="cancel-btn" src="images/cancel.png">
2040
+ <img class="ok-btn" src="images/ok.png">
2041
+ </div>
2042
+ <div id="add-wildcard-variables-scroll-area">
2043
+ <table id="add-wildcard-variables-table">
2044
+ </table>
2045
+ </div>
2046
+ </div>
2047
+ </div>
2048
+
2014
2049
  <!-- the RENAME CHART dialog prompts for the new title for a chart -->
2015
2050
  <div id="rename-chart-modal" class="modal">
2016
2051
  <div id="rename-chart-dlg" class="inp-dlg">
@@ -1922,8 +1922,8 @@ div.menu-item:hover {
1922
1922
  #constraint-point {
1923
1923
  position: absolute;
1924
1924
  top: 393px;
1925
- left: 160px;
1926
- width: 75px;
1925
+ left: 100px;
1926
+ width: 175px;
1927
1927
  text-align: center;
1928
1928
  }
1929
1929
 
@@ -2009,6 +2009,50 @@ div.menu-item:hover {
2009
2009
  right: 0px;
2010
2010
  }
2011
2011
 
2012
+ /* the BOUNDLINE POINT DIALOG allows entering precise coordinates */
2013
+ #boundline-point-dlg {
2014
+ width: 200px;
2015
+ height: 67px;
2016
+ }
2017
+
2018
+ #boundline-point-x-lbl {
2019
+ position: absolute;
2020
+ top: 25px;
2021
+ left: 2px;
2022
+ }
2023
+
2024
+ #boundline-point-x {
2025
+ position: absolute;
2026
+ top: 23px;
2027
+ left: 24px;
2028
+ width: 160px;
2029
+ }
2030
+
2031
+ #boundline-point-x-percent {
2032
+ position: absolute;
2033
+ top: 25px;
2034
+ left: 188px;
2035
+ }
2036
+
2037
+ #boundline-point-y-lbl {
2038
+ position: absolute;
2039
+ top: 47px;
2040
+ left: 2px;
2041
+ }
2042
+
2043
+ #boundline-point-y {
2044
+ position: absolute;
2045
+ top: 45px;
2046
+ left: 24px;
2047
+ width: 160px;
2048
+ }
2049
+
2050
+ #boundline-point-y-percent {
2051
+ position: absolute;
2052
+ top: 47px;
2053
+ left: 188px;
2054
+ }
2055
+
2012
2056
  /* NOTE: The following dialogs are not modal, but "stay-on-top",
2013
2057
  i.e., windows that remain open (until dismissed) and above
2014
2058
  the main window, and they are draggable and resizable.
@@ -2161,7 +2205,8 @@ div.io-box {
2161
2205
  cursor: pointer;
2162
2206
  }
2163
2207
 
2164
- #dataset-outcome.not-selected {
2208
+ #dataset-outcome.not-selected,
2209
+ #equation-outcome.not-selected {
2165
2210
  filter: saturate(0) contrast(50%) brightness(170%);
2166
2211
  }
2167
2212
 
@@ -2273,10 +2318,12 @@ div.modif::before {
2273
2318
  margin-right: 2px;
2274
2319
  }
2275
2320
 
2276
- div.outcome::before {
2321
+ div.outcome::before,
2322
+ span.outcome::before {
2277
2323
  content: ' \25C8';
2278
2324
  color: #b00080;
2279
2325
  margin-right: 1px;
2326
+ font-style: normal;
2280
2327
  }
2281
2328
 
2282
2329
  div.array::before {
@@ -2449,6 +2496,15 @@ td.equation-expression {
2449
2496
  border-top: 1px solid Silver;
2450
2497
  }
2451
2498
 
2499
+ #equation-outcome {
2500
+ position: absolute;
2501
+ top: 25px;
2502
+ right: 4px;
2503
+ width: 16px;
2504
+ height: 16px;
2505
+ cursor: pointer;
2506
+ }
2507
+
2452
2508
  /* NOTE: Rename equation modal must be above Edit variable modal */
2453
2509
  #rename-equation-modal {
2454
2510
  z-index: 110;
@@ -3098,6 +3154,31 @@ img.v-disab {
3098
3154
  width: calc(100% - 6px);
3099
3155
  }
3100
3156
 
3157
+ #add-wildcard-variables-dlg {
3158
+ width: 250px;
3159
+ min-height: 42px;
3160
+ max-height: 50vh;
3161
+ }
3162
+
3163
+ #add-wildcard-variables-scroll-area {
3164
+ position: absolute;
3165
+ top: 22px;
3166
+ left: 2px;
3167
+ width: calc(100% - 4px);
3168
+ height: calc(100% - 26px);
3169
+ overflow-y: auto;
3170
+ border-top: 1px solid Silver;
3171
+ }
3172
+
3173
+ #add-wildcard-variables-table {
3174
+ width: 100%;
3175
+ border-collapse: collapse;
3176
+ line-height: 16px;
3177
+ background-color: white;
3178
+ border: 1px solid Silver;
3179
+ border-top: none;
3180
+ }
3181
+
3101
3182
  /* the SENSITIVITY DIALOG displays the sensitivity analysis */
3102
3183
  #sensitivity-dlg {
3103
3184
  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();
@@ -56,7 +56,7 @@ class ConstraintEditor {
56
56
  this.container.addEventListener('mousemove',
57
57
  (event) => CONSTRAINT_EDITOR.mouseMove(event));
58
58
  this.container.addEventListener('mousedown',
59
- () => CONSTRAINT_EDITOR.mouseDown());
59
+ () => CONSTRAINT_EDITOR.mouseDown(event));
60
60
  this.container.addEventListener('mouseup',
61
61
  () => CONSTRAINT_EDITOR.mouseUp());
62
62
  // NOTE: interpret leaving the area as a mouse-up so that dragging ceases
@@ -84,6 +84,11 @@ class ConstraintEditor {
84
84
  this.delete_bl_btn = document.getElementById('del-bl-btn');
85
85
  this.delete_bl_btn.addEventListener('click',
86
86
  () => CONSTRAINT_EDITOR.deleteBoundLine());
87
+ this.point_modal = new ModalDialog('boundline-point');
88
+ this.point_modal.ok.addEventListener(
89
+ 'click', () => CONSTRAINT_EDITOR.setPointPosition());
90
+ this.point_modal.cancel.addEventListener(
91
+ 'click', () => CONSTRAINT_EDITOR.point_modal.hide());
87
92
  // The chart is stored as an SVG string
88
93
  this.svg = '';
89
94
  // Scale, origin X and Y assume a 300x300 px square chart area
@@ -93,7 +98,7 @@ class ConstraintEditor {
93
98
  // 0 => silver, LE => orange/red, GE => cyan/blue, EQ => purple
94
99
  this.line_color = ['#a0a0a0', '#c04000', '#0040c0', '#9000a0'];
95
100
  // Use brighter shades if selected (darker for gray)
96
- this.selected_color = ['#808080', '#ff8040', '#00ffff', '#a800ff'];
101
+ this.selected_color = ['#808080', '#ff8040', '#00b0d0', '#a800ff'];
97
102
  // The selected bound line object (NULL => no line selected)
98
103
  this.selected = null;
99
104
  // Cursor position in chart coordinates (100 x 100 grid)
@@ -105,6 +110,7 @@ class ConstraintEditor {
105
110
  this.on_point = -1;
106
111
  this.dragged_point = -1;
107
112
  this.selected_point = -1;
113
+ this.last_time_clicked = 0;
108
114
  this.cursor = 'default';
109
115
  // Properties for tracking which constraint is being edited.
110
116
  this.edited_constraint = null;
@@ -118,6 +124,21 @@ class ConstraintEditor {
118
124
  // NOTE: All edits will be ignored unless the modeler clicks OK.
119
125
  }
120
126
 
127
+ get doubleClicked() {
128
+ const
129
+ now = Date.now(),
130
+ dt = now - this.last_time_clicked;
131
+ this.last_time_clicked = now;
132
+ if(this.on_point === this.selected_point) {
133
+ // Consider click to be "double" if it occurred less than 300 ms ago
134
+ if(dt < 300) {
135
+ this.last_time_clicked = 0;
136
+ return true;
137
+ }
138
+ }
139
+ return false;
140
+ }
141
+
121
142
  mouseMove(e) {
122
143
  // The onMouseMove response of the constraint editor's graph area
123
144
  // Calculate cursor point without restricting it to 100x100 grid
@@ -132,16 +153,18 @@ class ConstraintEditor {
132
153
  this.pos_y = Math.min(100, Math.max(0, y));
133
154
  this.updateStatus();
134
155
  if(this.dragged_point >= 0) {
135
- this.movePoint();
156
+ this.movePoint(this.pos_x, this.pos_y);
136
157
  } else {
137
158
  this.checkLines();
138
159
  }
139
160
  }
140
161
 
141
- mouseDown() {
142
- // The onMouseDown response of the constraint editor's graph area
162
+ mouseDown(e) {
163
+ // The onMouseDown response of the constraint editor's graph area.
143
164
  if(this.adding_point) {
144
165
  this.doAddPointToLine();
166
+ } else if((e.altKey || this.doubleClicked) && this.on_point >= 0) {
167
+ this.positionPoint();
145
168
  } else if(this.on_line) {
146
169
  this.selectBoundLine(this.on_line);
147
170
  this.dragged_point = this.on_point;
@@ -155,7 +178,7 @@ class ConstraintEditor {
155
178
  }
156
179
 
157
180
  mouseUp() {
158
- // The onMouseUp response of the constraint editor's graph area
181
+ // The onMouseUp response of the constraint editor's graph area.
159
182
  this.dragged_point = -1;
160
183
  this.container.style.cursor = this.cursor;
161
184
  this.updateStatus();
@@ -179,36 +202,85 @@ class ConstraintEditor {
179
202
  this.container.style.cursor = this.cursor;
180
203
  }
181
204
 
182
- arrowKey(k) {
205
+ arrowKey(e) {
206
+ // Move point by 1 grid unit (1/3 pixel), or more precisely when
207
+ // Shift, Ctrl and/or Alt are pressed. Shift resolution is 1/10,
208
+ // Ctrl resolution = 1/100, combined => 1/1000. Just Alt resolution
209
+ // is 1/10000, and with Shift + Ctrl becomes 1e-7.
183
210
  if(this.selected && this.selected_point >= 0) {
211
+ const custom = e.shiftKey || e.ctrlKey || e.altKey;
212
+ let divisor = 3;
213
+ if(e.shiftKey) {
214
+ divisor = 10;
215
+ if(e.ctrlKey) divisor = 1000;
216
+ } else if(e.ctrlKey) {
217
+ divisor = 100;
218
+ }
219
+ if(e.altKey) {
220
+ if(divisor === 3) {
221
+ divisor = 10000;
222
+ } else {
223
+ divisor *= 10000;
224
+ }
225
+ }
184
226
  const
227
+ k = e.keyCode,
185
228
  i = this.selected_point,
186
229
  pts = this.selected.points,
187
230
  li = pts.length - 1,
188
231
  p = pts[this.selected_point],
189
232
  minx = (i === 0 ? 0 : (i === li ? 100 : pts[i - 1][0])),
190
233
  maxx = (i === 0 ? 0 : (i === li ? 100 : pts[i + 1][0]));
234
+ let cx = false,
235
+ cy = false;
191
236
  if(k === 37) {
192
- p[0] = Math.max(minx, p[0] - 1/3);
193
- } else if (k === 38 && p[1] <= 299/3) {
194
- p[1] += 1/3;
237
+ p[0] = Math.max(minx, p[0] - 1 / divisor);
238
+ cx = true;
239
+ } else if (k === 38 && p[1] <= 100 - 1 / divisor) {
240
+ p[1] += 1 / divisor;
241
+ cy = true;
195
242
  } else if (k === 39) {
196
- p[0] = Math.min(maxx, p[0] + 1/3);
197
- } else if (k === 40 && p[1] >= 1/3) {
198
- p[1] -= 1/3;
243
+ p[0] = Math.min(maxx, p[0] + 1 / divisor);
244
+ cx = true;
245
+ } else if (k === 40 && p[1] >= 1 / divisor) {
246
+ p[1] -= 1 / divisor;
247
+ cy = true;
248
+ }
249
+ // NOTE: Compensate for small numerical errors
250
+ const cp = this.customPoint(p[0], p[1]);
251
+ if(cx) {
252
+ if(cp & 1 && custom) {
253
+ p[0] = Math.round(divisor * p[0]) / divisor;
254
+ } else {
255
+ p[0] = Math.round(3 * p[0]) / 3;
256
+ }
257
+ }
258
+ if(cy) {
259
+ if(cp & 2 && custom) {
260
+ p[1] = Math.round(divisor * p[1]) / divisor;
261
+ } else {
262
+ p[1] = Math.round(3 * p[1]) / 3;
263
+ }
199
264
  }
200
- // NOTE: compensate for small numerical errors
201
- p[0] = Math.round(3 * p[0]) / 3;
202
- p[1] = Math.round(3 * p[1]) / 3;
203
265
  this.draw();
204
266
  this.updateEquation();
205
267
  }
206
268
  }
207
269
 
270
+ customPoint(x, y) {
271
+ // Return 0 if `x` and `y` both are "regular" points on the pixel
272
+ // grid. For these regular points, X and Y are multiples of 1/3,
273
+ // so 3*X and 3*Y should both be integer (apart from numerical
274
+ // imprecision). If only X is custom, return 1, if only Y is custom,
275
+ // return 2, and if both are custom, return 3.
276
+ return (Math.abs(3*x - Math.round(3*x)) > VM.NEAR_ZERO ? 1 : 0) +
277
+ (Math.abs(3*y - Math.round(3*y)) > VM.NEAR_ZERO ? 2 : 0);
278
+ }
279
+
208
280
  point(x, y) {
209
- // Returns a string denoting the point (x, y) in SVG notation, assuming
281
+ // Return a string denoting the point (x, y) in SVG notation, assuming
210
282
  // that x and y are mathematical coordinates (y-axis pointing UP) and
211
- // scaled to the constraint editor chart area, cf. global constants
283
+ // scaled to the constraint editor chart area, cf. global constants.
212
284
  // defined for the constraint editor.
213
285
  return (this.oX + x * this.scale) + ',' + (this.oY - y * this.scale);
214
286
  }
@@ -396,10 +468,52 @@ class ConstraintEditor {
396
468
  }
397
469
  this.equation_div.innerHTML = segeq;
398
470
  }
471
+
472
+ positionPoint() {
473
+ // Prompt modeler for precise point coordinates.
474
+ if(this.selected_point < 0) return;
475
+ const
476
+ md = this.point_modal,
477
+ pc = this.point_div.innerHTML.split(', ');
478
+ md.element('x').value = pc[0].substring(1);
479
+ md.element('y').value = pc[1].substring(0, pc[1].length - 1);
480
+ md.show('x');
481
+ }
482
+
483
+ validPointCoordinate(c) {
484
+ // Return floating point for coordinate `c` (= 'x' or 'y') or FALSE
485
+ // if input is invalid.
486
+ const
487
+ md = this.point_modal,
488
+ e = md.element(c),
489
+ v = safeStrToFloat(e.value, false);
490
+ if(v === false || v < 0 || v > 100) {
491
+ UI.warn('Invalid boundline point coordinate');
492
+ e.select();
493
+ e.focus();
494
+ return false;
495
+ }
496
+ return v;
497
+ }
498
+
499
+ setPointPosition() {
500
+ // Check coordinates, and if valid update those of the selected point.
501
+ // Otherwise, warn user and select offending input field.
502
+ if(this.selected_point < 0) return;
503
+ const
504
+ x = this.validPointCoordinate('x'),
505
+ y = this.validPointCoordinate('y');
506
+ if(x !== false && y !== false) {
507
+ this.dragged_point = this.selected_point;
508
+ this.movePoint(x, y);
509
+ this.dragged_point = -1;
510
+ this.point_modal.hide();
511
+ }
512
+ }
399
513
 
400
- movePoint() {
401
- // Moves the dragged point of the selected bound line
402
- // Use l as shorthand for the selected line
514
+ movePoint(x, y) {
515
+ // Move the dragged point of the selected bound line.
516
+ // Use `l` as shorthand for the selected line.
403
517
  const
404
518
  l = this.selected,
405
519
  pi = this.dragged_point,
@@ -411,8 +525,8 @@ class ConstraintEditor {
411
525
  py = p[1],
412
526
  minx = (pi === 0 ? 0 : (pi === lpi ? 100 : l.points[pi - 1][0])),
413
527
  maxx = (pi === 0 ? 0 : (pi === lpi ? 100 : l.points[pi + 1][0])),
414
- newx = Math.min(maxx, Math.max(minx, this.pos_x)),
415
- newy = Math.min(100, Math.max(0, this.pos_y));
528
+ newx = Math.min(maxx, Math.max(minx, x)),
529
+ newy = Math.min(100, Math.max(0, y));
416
530
  // No action needed unless point has been moved
417
531
  if(newx !== px || newy !== py) {
418
532
  p[0] = newx;
@@ -424,17 +538,22 @@ class ConstraintEditor {
424
538
 
425
539
  updateStatus() {
426
540
  // Displays cursor position as X and Y (in chart coordinates), and updates
427
- // controls
541
+ // controls.
428
542
  this.pos_x_div.innerHTML = 'X = ' + this.pos_x.toPrecision(3);
429
543
  this.pos_y_div.innerHTML = 'Y = ' + this.pos_y.toPrecision(3);
544
+ this.point_div.innerHTML = '';
430
545
  const blbtns = 'add-point del-bl';
431
546
  if(this.selected) {
432
547
  if(this.selected_point >= 0) {
433
548
  const p = this.selected.points[this.selected_point];
434
- this.point_div.innerHTML =
435
- `(${p[0].toPrecision(3)}, ${p[1].toPrecision(3)})`;
436
- } else {
437
- this.point_div.innerHTML = '';
549
+ let px = p[0].toPrecision(3),
550
+ py = p[1].toPrecision(3),
551
+ cp = this.customPoint(p[0], p[1]);
552
+ if(cp & 1) px = p[0].toPrecision(10).replace(/[0]+$/, '')
553
+ .replace(/\.$/, '');
554
+ if(cp & 2) py = p[1].toPrecision(10).replace(/[0]+$/, '')
555
+ .replace(/\.$/, '');
556
+ this.point_div.innerHTML = `(${px}, ${py})`;
438
557
  }
439
558
  // Check whether selected point is an end point
440
559
  const ep = this.selected_point === 0 ||
@@ -591,13 +710,19 @@ class ConstraintEditor {
591
710
  width = 1.5;
592
711
  color = this.line_color[l.type];
593
712
  }
594
- const cfs = `fill="${color}" stroke="${color}" stroke-width="${width}"`;
713
+ const
714
+ cfs = `fill="${color}" stroke="${color}" stroke-width="${width}"`,
715
+ icfs = 'fill="white" stroke="white" stroke-width="1"';
595
716
  for(let i = 0; i < l.points.length; i++) {
596
717
  const
597
718
  px = l.points[i][0],
598
719
  py = l.points[i][1];
599
720
  pp.push(this.point(px, py));
600
721
  dots += `<circle ${this.circleCenter(px, py)} r="3" ${cfs}></circle>`;
722
+ // Draw "custom points" with a white inner circle.
723
+ if(this.customPoint(px, py)) {
724
+ dots += `<circle ${this.circleCenter(px, py)} r="1.5" ${icfs}></circle>`;
725
+ }
601
726
  }
602
727
  const cp = 'M' + pp.join('L');
603
728
  // For EQ bound lines, the line path is the contour; this will be
@@ -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);
@@ -2154,7 +2154,7 @@ class GUIController extends Controller {
2154
2154
  if(topmod && topmod.id === 'constraint-modal') {
2155
2155
  if([37, 38, 39, 40].indexOf(e.keyCode) >= 0) {
2156
2156
  e.preventDefault();
2157
- CONSTRAINT_EDITOR.arrowKey(e.keyCode);
2157
+ CONSTRAINT_EDITOR.arrowKey(e);
2158
2158
  return;
2159
2159
  }
2160
2160
  }
@@ -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) {
@@ -6953,7 +6954,7 @@ function VMI_log(x) {
6953
6954
  if(d !== false) {
6954
6955
  if(DEBUGGING) console.log('LOG (' + d.join(', ') + ')');
6955
6956
  try {
6956
- d = Math.exp(Math.log(d[1]) / Math.log(d[0]));
6957
+ d = Math.log(d[1]) / Math.log(d[0]);
6957
6958
  } catch(err) {
6958
6959
  d = VM.BAD_CALC;
6959
6960
  }
@@ -8092,9 +8093,9 @@ const
8092
8093
  '%', '^', 'log', '|'],
8093
8094
  DYADIC_CODES = [
8094
8095
  VMI_concat, VMI_if_then, VMI_if_else, VMI_or, VMI_and,
8095
- VMI_eq, VMI_ne, VMI_ne,
8096
- VMI_gt, VMI_lt, VMI_ge, VMI_le, VMI_add, VMI_sub, VMI_mul, VMI_div,
8097
- VMI_mod, VMI_power, VMI_log, VMI_replace_undefined],
8096
+ VMI_eq, VMI_ne, VMI_ne, VMI_gt, VMI_lt, VMI_ge, VMI_le,
8097
+ VMI_add, VMI_sub, VMI_mul, VMI_div, VMI_mod,
8098
+ VMI_power, VMI_log, VMI_replace_undefined],
8098
8099
 
8099
8100
  // Compiler checks for random codes as they make an expression dynamic
8100
8101
  RANDOM_CODES = [VMI_binomial, VMI_exponential, VMI_normal, VMI_poisson,