linny-r 2.1.0 → 2.1.2

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": "2.1.0",
3
+ "version": "2.1.2",
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
@@ -2352,10 +2352,13 @@ NOTE: * and ? will be interpreted as wildcards"
2352
2352
  title="Download chart as bitmap image (PNG)
2353
2353
  ✎ Shift-click to download chart as Scalable Vector Graphics (SVG)">
2354
2354
  <img id="chart-widen-btn" class="btn enab" src="images/stretch.png"
2355
- title="Widen chart" style="margin-left: 24px">
2355
+ title="Widen chart" style="margin-left: 12px">
2356
2356
  <img id="chart-narrow-btn" class="btn enab" src="images/compress.png"
2357
2357
  title="Narrow chart">
2358
2358
  </div>
2359
+ <div id="chart-prefix-div">
2360
+ <select id="chart-prefix"></select>
2361
+ </div>
2359
2362
  </div>
2360
2363
  <div id="chart-time-div">
2361
2364
  <div id="chart-time-step"></div>
@@ -3029,6 +3029,14 @@ td.equation-expression-multi {
3029
3029
  cursor: pointer;
3030
3030
  }
3031
3031
 
3032
+ #equation-info {
3033
+ position: absolute;
3034
+ bottom: 2px;
3035
+ left: 2px;
3036
+ color: Gray;
3037
+ font-style: italic;
3038
+ }
3039
+
3032
3040
  /* NOTE: Rename equation modal must be above Edit variable modal */
3033
3041
  #rename-equation-modal {
3034
3042
  z-index: 110;
@@ -3559,6 +3567,16 @@ img.v-disab {
3559
3567
  margin-left: 4px;
3560
3568
  }
3561
3569
 
3570
+ #chart-prefix-div {
3571
+ display: inline-block;
3572
+ margin-left: 12px;
3573
+ }
3574
+
3575
+ #chart-prefix {
3576
+ font-size: 9px !important;
3577
+ max-width: 80px;
3578
+ }
3579
+
3562
3580
  #chart-time-div {
3563
3581
  position: absolute;
3564
3582
  bottom: 0px;
@@ -112,6 +112,10 @@ class GUIChartManager extends ChartManager {
112
112
  this.svg_container.addEventListener(
113
113
  'mouseleave', (event) => CHART_MANAGER.updateTimeStep(event, false));
114
114
  this.time_step = document.getElementById('chart-time-step');
115
+ this.prefix_div = document.getElementById('chart-prefix-div');
116
+ this.prefix_selector = document.getElementById('chart-prefix');
117
+ this.prefix_selector.addEventListener(
118
+ 'change', () => CHART_MANAGER.selectPrefix());
115
119
  document.getElementById('chart-toggle-chevron').addEventListener(
116
120
  'click', () => CHART_MANAGER.toggleControlPanel());
117
121
  document.getElementById('chart-stats-btn').addEventListener(
@@ -354,8 +358,9 @@ class GUIChartManager extends ChartManager {
354
358
  }
355
359
 
356
360
  updateDialog() {
357
- // Refreshe all dialog fields to display actual MODEL chart properties.
361
+ // Refresh all dialog fields to display actual MODEL chart properties.
358
362
  this.updateSelector();
363
+ this.prefix_div.style.display = 'none';
359
364
  let c = null;
360
365
  if(this.chart_index >= 0) {
361
366
  c = MODEL.charts[this.chart_index];
@@ -390,6 +395,19 @@ class GUIChartManager extends ChartManager {
390
395
  '</td></tr>'].join(''));
391
396
  }
392
397
  this.variables_table.innerHTML = ol.join('');
398
+ const
399
+ cp = c.prefix,
400
+ pp = c.possiblePrefixes,
401
+ html = [];
402
+ if(pp.length) {
403
+ for(const p of pp) {
404
+ const cap = capitalized(p);
405
+ html.push('<option value="', cap, '"', (cap === cp ? ' selected' : ''), '>',
406
+ cap, '</option>');
407
+ }
408
+ this.prefix_div.style.display = 'inline-block';
409
+ }
410
+ this.prefix_selector.innerHTML = html.join('');
393
411
  } else {
394
412
  this.variable_index = -1;
395
413
  }
@@ -439,6 +457,15 @@ class GUIChartManager extends ChartManager {
439
457
  this.stretchChart(0);
440
458
  }
441
459
 
460
+ selectPrefix() {
461
+ // Set the preferred prefix for this chart. This will override the
462
+ // title prefix (if any).
463
+ if(this.chart_index >= 0) {
464
+ MODEL.charts[this.chart_index].preferred_prefix = this.prefix_selector.value;
465
+ }
466
+ this.updateDialog();
467
+ }
468
+
442
469
  showSortingMenu() {
443
470
  // Show the pane with sort type buttons only if variable is selected.
444
471
  this.sorting_menu.style.display =
@@ -594,27 +621,31 @@ class GUIChartManager extends ChartManager {
594
621
 
595
622
  cloneChart() {
596
623
  // Create a new chart that is identical to the current one.
597
- if(this.chart_index >= 0) {
598
- let c = MODEL.charts[this.chart_index],
599
- nt = c.title + '-copy';
600
- while(MODEL.indexOfChart(nt) >= 0) {
601
- nt += '-copy';
602
- }
603
- const nc = MODEL.addChart(nt);
604
- // Copy properties of `c` to `nc`;
605
- nc.histogram = c.histogram;
606
- nc.bins = c.bins;
607
- nc.show_title = c.show_title;
608
- nc.legend_position = c.legend_position;
609
- for(const cv of c.variables) {
610
- const nv = new ChartVariable(nc);
611
- nv.setProperties(cv.object, cv.attribute, cv.stacked,
612
- cv.color, cv.scale_factor, cv.line_width, cv.sorted);
613
- nc.variables.push(nv);
614
- }
615
- this.chart_index = MODEL.indexOfChart(nc.title);
616
- this.updateDialog();
617
- }
624
+ if(this.chart_index < 0) return;
625
+ const
626
+ c = MODEL.charts[this.chart_index],
627
+ pp = c.possiblePrefixes;
628
+ let nt = c.title;
629
+ if(pp) {
630
+ // Remove title prefix (if any), and add selected one.
631
+ nt = c.prefix + UI.PREFIXER + nt.split(UI.PREFIXER).pop();
632
+ }
633
+ // If title is not new, keep adding a suffix until it is new.
634
+ while(MODEL.indexOfChart(nt) >= 0) nt += '-copy';
635
+ const nc = MODEL.addChart(nt);
636
+ // Copy properties of `c` to `nc`;
637
+ nc.histogram = c.histogram;
638
+ nc.bins = c.bins;
639
+ nc.show_title = c.show_title;
640
+ nc.legend_position = c.legend_position;
641
+ for(const cv of c.variables) {
642
+ const nv = new ChartVariable(nc);
643
+ nv.setProperties(cv.object, cv.attribute, cv.stacked,
644
+ cv.color, cv.scale_factor, cv.line_width, cv.sorted);
645
+ nc.variables.push(nv);
646
+ }
647
+ this.chart_index = MODEL.indexOfChart(nc.title);
648
+ this.updateDialog();
618
649
  }
619
650
 
620
651
  toggleRunResults() {
@@ -736,9 +767,6 @@ class GUIChartManager extends ChartManager {
736
767
  if(indices.length < 2) {
737
768
  if(indices.length) {
738
769
  chart.addWildcardVariables(dsm, indices);
739
- } else if(dsm.selector.startsWith(':')) {
740
- UI.notify('Plotting methods is work-in-progress!');
741
- console.log('HERE dsm', dsm, 'expr', dsm.expression.text, 'indices', indices);
742
770
  } else {
743
771
  UI.notify(`Variable "${dsm.displayName}" cannot be plotted`);
744
772
  }
@@ -726,7 +726,7 @@ class GUIController extends Controller {
726
726
  this.dbl_clicked_node = null;
727
727
  this.target_cluster = null;
728
728
  this.constraint_under_cursor = null;
729
- this.last_up_down_without_move = Date.now();
729
+ this.last_up_down_without_move = {up: 0, down: 0};
730
730
  // Keyboard shortcuts: Ctrl-x associates with menu button ID.
731
731
  this.shortcuts = {
732
732
  'A': 'actors',
@@ -1672,15 +1672,15 @@ class GUIController extends Controller {
1672
1672
  }
1673
1673
  }
1674
1674
 
1675
- get doubleClicked() {
1676
- // Return TRUE when a "double-click" occurred
1675
+ doubleClicked(ud) {
1676
+ // Return TRUE when a "double-click" occurred.
1677
1677
  const
1678
1678
  now = Date.now(),
1679
- dt = now - this.last_up_down_without_move;
1680
- this.last_up_down_without_move = now;
1679
+ dt = now - this.last_up_down_without_move[ud];
1680
+ this.last_up_down_without_move[ud] = now;
1681
1681
  // Consider click to be "double" if it occurred less than 300 ms ago
1682
1682
  if(dt < 300) {
1683
- this.last_up_down_without_move = 0;
1683
+ this.last_up_down_without_move[ud] = 0;
1684
1684
  return true;
1685
1685
  }
1686
1686
  return false;
@@ -2230,7 +2230,9 @@ class GUIController extends Controller {
2230
2230
  }
2231
2231
  // When dragging a selection over a cluster, change cursor to "cell" to
2232
2232
  // indicate that selected process(es) will be moved into the cluster.
2233
- if(this.dragged_node) {
2233
+ // NOTE: Do not do this when the dragged selection is just a single note!
2234
+ if(this.dragged_node &&
2235
+ !(this.dragged_node instanceof Note && MODEL.selection.length < 2)) {
2234
2236
  // NOTE: Cursor will always be over the dragged node, so do not indicate
2235
2237
  // "drop here?" unless dragged over a different cluster.
2236
2238
  if(this.on_cluster && this.on_cluster !== this.dragged_node) {
@@ -2312,14 +2314,8 @@ class GUIController extends Controller {
2312
2314
  return;
2313
2315
  } // END IF Ctrl
2314
2316
 
2315
- // Clear selection unless SHIFT pressed or mouseDown while hovering
2316
- // over a SELECTED node or link.
2317
- if(!(e.shiftKey ||
2318
- (this.on_node && MODEL.selection.indexOf(this.on_node) >= 0) ||
2319
- (this.on_cluster && MODEL.selection.indexOf(this.on_cluster) >= 0) ||
2320
- (this.on_note && MODEL.selection.indexOf(this.on_note) >= 0) ||
2321
- (this.on_link && MODEL.selection.indexOf(this.on_link) >= 0) ||
2322
- (this.on_constraint && MODEL.selection.indexOf(this.on_constraint) >= 0))) {
2317
+ // Clear selection unless SHIFT pressed or double-clicking.
2318
+ if(!(this.doubleClicked('down') || e.shiftKey)) {
2323
2319
  MODEL.clearSelection();
2324
2320
  UI.drawDiagram(MODEL);
2325
2321
  }
@@ -2538,7 +2534,7 @@ class GUIController extends Controller {
2538
2534
  absdx = Math.abs(this.net_move_x),
2539
2535
  absdy = Math.abs(this.net_move_y),
2540
2536
  sigmv = (MODEL.align_to_grid ? MODEL.grid_pixels / 4 : 2.5);
2541
- if(this.doubleClicked) {
2537
+ if(this.doubleClicked('up')) {
2542
2538
  // Ignore insignificant move.
2543
2539
  if(absdx < sigmv && absdy < sigmv) {
2544
2540
  // Undo the move and remove the action from the UNDO-stack.
@@ -2584,13 +2580,15 @@ class GUIController extends Controller {
2584
2580
  this.paper.container.style.cursor = 'pointer';
2585
2581
  // NOTE: Cursor will always be over the selected cluster (while dragging).
2586
2582
  if(this.on_cluster && !this.on_cluster.selected) {
2587
- UNDO_STACK.push('drop', this.on_cluster);
2588
- MODEL.dropSelectionIntoCluster(this.on_cluster);
2589
- this.on_node = null;
2590
- this.on_note = null;
2591
- this.target_cluster = null;
2592
- // Redraw cluster to erase its orange "target corona".
2593
- UI.paper.drawCluster(this.on_cluster);
2583
+ if(!(this.dragged_node instanceof Note && MODEL.selection.length < 2)) {
2584
+ UNDO_STACK.push('drop', this.on_cluster);
2585
+ MODEL.dropSelectionIntoCluster(this.on_cluster);
2586
+ // Redraw cluster to erase its orange "target corona".
2587
+ UI.paper.drawCluster(this.on_cluster);
2588
+ this.on_node = null;
2589
+ this.on_note = null;
2590
+ this.target_cluster = null;
2591
+ }
2594
2592
  }
2595
2593
  // Only now align to grid.
2596
2594
  MODEL.alignToGrid();
@@ -2599,11 +2597,11 @@ class GUIController extends Controller {
2599
2597
 
2600
2598
  // Then check whether the user is clicking on a link.
2601
2599
  } else if(this.on_link) {
2602
- if(this.doubleClicked) {
2600
+ if(this.doubleClicked('up')) {
2603
2601
  this.showLinkPropertiesDialog(this.on_link);
2604
2602
  }
2605
2603
  } else if(this.on_constraint) {
2606
- if(this.doubleClicked) {
2604
+ if(this.doubleClicked('up')) {
2607
2605
  this.showConstraintPropertiesDialog(this.on_constraint);
2608
2606
  }
2609
2607
  }
@@ -3713,6 +3711,14 @@ class GUIController extends Controller {
3713
3711
 
3714
3712
  // AUXILIARY FUNCTIONS
3715
3713
 
3714
+ function namedObjects() {
3715
+ // Return TRUE iff XML contains named objects.
3716
+ for(const cn of entities_node.childNodes) {
3717
+ if(cn.nodeName !== 'note') return true;
3718
+ }
3719
+ return false;
3720
+ }
3721
+
3716
3722
  function fullName(node) {
3717
3723
  // Return full entity name inferred from XML node data.
3718
3724
  if(node.nodeName === 'from-to' || node.nodeName === 'selc') {
@@ -3848,7 +3854,18 @@ class GUIController extends Controller {
3848
3854
  fn = fullName(node),
3849
3855
  mn = mappedName(fn);
3850
3856
  let obj;
3851
- if(et === 'process' && !MODEL.processByID(UI.nameToID(mn))) {
3857
+ if(et === 'note') {
3858
+ // Ensure that copy had new time stamp.
3859
+ let cn = childNodeByTag(node, 'timestamp').firstChild;
3860
+ cn.nodeValue = new Date().getTime().toString();
3861
+ cn = childNodeByTag(node, 'x-coord').firstChild;
3862
+ // Move note a bit right and down.
3863
+ cn.nodeValue = (safeStrToInt(cn.nodeValue, 0) + 12).toString();
3864
+ cn = childNodeByTag(node, 'y-coord').firstChild;
3865
+ cn.nodeValue = (safeStrToInt(cn.nodeValue, 0) + 12).toString();
3866
+ obj = MODEL.addNote(node);
3867
+ if(obj) new_entities.push(obj);
3868
+ } else if(et === 'process' && !MODEL.processByID(UI.nameToID(mn))) {
3852
3869
  const
3853
3870
  na = nameAndActor(mn),
3854
3871
  new_actor = !MODEL.actorByID(UI.nameToID(na[1]));
@@ -3913,7 +3930,8 @@ class GUIController extends Controller {
3913
3930
  // Prompt for mapping when pasting to the same model and cluster.
3914
3931
  if(parseInt(mts) === MODEL.time_created.getTime() &&
3915
3932
  ca === fca && mapping.from_prefix === mapping.to_prefix &&
3916
- !(mapping.prefix || mapping.actor || mapping.increment)) {
3933
+ !(mapping.prefix || mapping.actor || mapping.increment) &&
3934
+ namedObjects()) {
3917
3935
  // Prompt for names of selected cluster nodes.
3918
3936
  if(selc_node.childNodes.length && !mapping.prefix) {
3919
3937
  mapping.top_clusters = {};
@@ -209,8 +209,9 @@ class GUIMonitor {
209
209
  `ERROR at t=${t}: ` + VM.errorMessage(err);
210
210
  for(const x of VM.call_stack) {
211
211
  // For equations, only show the attribute.
212
- const ons = (x.object === MODEL.equations_dataset ? '' :
213
- x.object.displayName + '|');
212
+ const ons = (x.object === MODEL.equations_dataset ?
213
+ (x.attribute.startsWith(':') ? x.method_object_prefix : '') :
214
+ x.object.displayName + '|');
214
215
  vlist.push(ons + x.attribute);
215
216
  // Trim spaces around all object-attribute separators in the expression.
216
217
  xlist.push(x.text.replace(/\s*\|\s*/g, '|'));
@@ -2161,7 +2161,9 @@ class LinnyRModel {
2161
2161
  // Move all selected nodes to cluster `c`.
2162
2162
  // NOTE: The relative position of the selected notes is presserved,
2163
2163
  // but the nodes are positioned to the right of the diagram of `c`
2164
- // with a margin of 50 pixels.
2164
+ // with a margin of 50 pixels.
2165
+ // NOTE: No dropping if the selection contains one note.
2166
+ if(this.selection.length === 1 && this.selection[0] instanceof Note) return;
2165
2167
  const
2166
2168
  space = 50,
2167
2169
  rmx = c.rightMarginX + space,
@@ -5130,38 +5132,41 @@ class ObjectWithXYWH {
5130
5132
 
5131
5133
  // CLASS NoteField: numeric value of "field" [[variable]] in note text
5132
5134
  class NoteField {
5133
- constructor(n, f, o, u='1', m=1, w=false) {
5135
+ constructor(n, f, o, u='1', m=1, w=false, p='') {
5134
5136
  // `n` is the note that "owns" this note field
5135
5137
  // `f` holds the unmodified tag string [[dataset]] to be replaced by
5136
5138
  // the value of vector or expression `o` for the current time step;
5137
5139
  // if specified, `u` is the unit of the value to be displayed,
5138
- // `m` is the multiplier for the value to be displayed, and `w` is
5139
- // the wildcard number to use in a wildcard equation
5140
+ // `m` is the multiplier for the value to be displayed, `w` is
5141
+ // the wildcard number to use in a wildcard equation, and `p` is
5142
+ // the prefix to use for a method equation.
5140
5143
  this.note = n;
5141
5144
  this.field = f;
5142
5145
  this.object = o;
5143
5146
  this.unit = u;
5144
5147
  this.multiplier = m;
5145
5148
  this.wildcard_number = (w ? parseInt(w) : false);
5149
+ this.prefix = p;
5146
5150
  }
5147
5151
 
5148
5152
  get value() {
5149
- // Returns the numeric value of this note field as a numeric string
5150
- // followed by its unit (unless this is 1)
5151
- // If object is the note, this means field [[#]] (note number context)
5152
- // If this is undefined (empty string) display a double question mark
5153
+ // Return the numeric value of this note field as a numeric string
5154
+ // followed by its unit (unless this is 1).
5155
+ // If object is the note, this means field [[#]] (note number context).
5156
+ // If this is undefined (empty string) display a double question mark.
5153
5157
  if(this.object === this.note) return this.note.numberContext || '\u2047';
5154
5158
  let v = VM.UNDEFINED;
5155
5159
  const t = MODEL.t;
5156
5160
  if(Array.isArray(this.object)) {
5157
- // Object is a vector
5161
+ // Object is a vector.
5158
5162
  if(t < this.object.length) v = this.object[t];
5159
5163
  } else if(this.object.hasOwnProperty('c') &&
5160
5164
  this.object.hasOwnProperty('u')) {
5161
- // Object holds link lists for cluster balance computation
5165
+ // Object holds link lists for cluster balance computation.
5162
5166
  v = MODEL.flowBalance(this.object, t);
5163
5167
  } else if(this.object instanceof Expression) {
5164
- // Object is an expression
5168
+ // Object is an expression.
5169
+ this.object.method_object_prefix = this.prefix;
5165
5170
  v = this.object.result(t, this.wildcard_number);
5166
5171
  } else if(typeof this.object === 'number') {
5167
5172
  v = this.object;
@@ -5237,6 +5242,14 @@ class Note extends ObjectWithXYWH {
5237
5242
  return this.cluster.numberContext;
5238
5243
  }
5239
5244
 
5245
+ get methodPrefix() {
5246
+ // Return the most likely candidate prefix to be used for method fields.
5247
+ const n = this.nearbyNode;
5248
+ if(n instanceof Cluster) return n.name;
5249
+ if(this.cluster === MODEL.top_cluster) return '';
5250
+ return this.cluster.name;
5251
+ }
5252
+
5240
5253
  get nearbyNode() {
5241
5254
  // Return a node in the cluster of this note that is closest to this
5242
5255
  // note (Euclidian distance between center points), but with at most
@@ -5389,12 +5402,22 @@ class Note extends ObjectWithXYWH {
5389
5402
  if(!obj) {
5390
5403
  const m = MODEL.equations_dataset.modifiers[UI.nameToID(ena[0])];
5391
5404
  if(m) {
5392
- UI.warn('Methods cannot be evaluated without prefix');
5405
+ const mp = this.methodPrefix;
5406
+ if(mp) {
5407
+ if(m.expression.isEligible(mp)) {
5408
+ this.fields.push(new NoteField(this, tag, m.expression, to_unit,
5409
+ multiplier, false, mp));
5410
+ } else {
5411
+ UI.warn(`Prefix "${mp}" is not eligible for method "${m.selector}"`);
5412
+ }
5413
+ } else {
5414
+ UI.warn('Methods cannot be evaluated without prefix');
5415
+ }
5393
5416
  } else {
5394
5417
  UI.warn(`Unknown model entity "${en}"`);
5395
5418
  }
5396
5419
  } else if(obj instanceof DatasetModifier) {
5397
- // NOTE: equations are (for now) dimenssonless => unit '1'.
5420
+ // NOTE: Equations are (for now) dimensionless => unit '1'.
5398
5421
  if(obj.dataset !== MODEL.equations_dataset) {
5399
5422
  from_unit = obj.dataset.scale_unit;
5400
5423
  multiplier = MODEL.unitConversionMultiplier(from_unit, to_unit);
@@ -9726,21 +9749,13 @@ class ChartVariable {
9726
9749
  // this indicates that it is a Wildcard selector or a method, and
9727
9750
  // that the specified result vector should be used.
9728
9751
  if(this.wildcard_index !== false) {
9729
- // NOTE: A wildcard index (a number) can also indicate that this
9730
- // variable is a method, so check for a leading colon.
9731
- if(eqn.startsWith(':')) {
9732
- // For methods, use "entity name or prefix: method" as variable
9733
- // name, so first get the method object prefix, expand it if
9734
- // it identifies a specific model entity, and then append the
9735
- // method name (leading colon replaced by the prefixer ": ").
9736
- const
9737
- mop = this.object.expression.method_object_list[this.wildcard_index],
9738
- obj = MODEL.objectByID(mop);
9739
- eqn = (obj ? obj.displayName : (mop || '')) +
9740
- UI.PREFIXER + eqn.substring(1);
9741
- } else {
9742
- eqn = eqn.replace('??', this.wildcard_index);
9743
- }
9752
+ eqn = eqn.replace('??', this.wildcard_index);
9753
+ } else if(eqn.startsWith(':')) {
9754
+ // For methods, use "entity name or prefix: method" as variable
9755
+ // name, so first get the method object prefix, expand it if
9756
+ // it identifies a specific model entity, and then append the
9757
+ // method name (leading colon replaced by the prefixer ": ").
9758
+ eqn = this.chart.prefix + UI.PREFIXER + eqn.substring(1);
9744
9759
  }
9745
9760
  return eqn + sf;
9746
9761
  }
@@ -9759,8 +9774,8 @@ class ChartVariable {
9759
9774
  }
9760
9775
 
9761
9776
  get asXML() {
9762
- // NOTE: a "black-boxed" model can comprise charts showing "anonymous"
9763
- // entities, so the IDs of these entities must then be changed
9777
+ // NOTE: A "black-boxed" model can comprise charts showing "anonymous"
9778
+ // entities, so the IDs of these entities must then be changed.
9764
9779
  let id = this.object.identifier;
9765
9780
  if(MODEL.black_box_entities.hasOwnProperty(id)) {
9766
9781
  id = UI.nameToID(MODEL.black_box_entities[id]);
@@ -9780,7 +9795,7 @@ class ChartVariable {
9780
9795
  }
9781
9796
 
9782
9797
  get lowestValueInVector() {
9783
- // Returns the computed statistical minimum OR vector[0] (if valid & lower)
9798
+ // Return the computed statistical minimum OR vector[0] (if valid & lower).
9784
9799
  let v = this.minimum;
9785
9800
  if(this.vector.length > 0) v = this.vector[0];
9786
9801
  if(v < VM.MINUS_INFINITY || v > VM.PLUS_INFINITY || v > this.minimum) {
@@ -9790,7 +9805,7 @@ class ChartVariable {
9790
9805
  }
9791
9806
 
9792
9807
  get highestValueInVector() {
9793
- // Returns the computed statistical maximum OR vector[0] (if valid & higher)
9808
+ // Return the computed statistical maximum OR vector[0] (if valid & higher).
9794
9809
  let v = this.maximum;
9795
9810
  if(this.vector.length > 0) v = this.vector[0];
9796
9811
  if(v < VM.MINUS_INFINITY || v > VM.PLUS_INFINITY || v < this.maximum) {
@@ -9801,12 +9816,12 @@ class ChartVariable {
9801
9816
 
9802
9817
  initFromXML(node) {
9803
9818
  let id = xmlDecoded(nodeContentByTag(node, 'object-id'));
9804
- // NOTE: automatic conversion of former top cluster name
9819
+ // NOTE: Automatic conversion of former top cluster name.
9805
9820
  if(id === UI.FORMER_TOP_CLUSTER_NAME.toLowerCase()) {
9806
9821
  id = UI.nameToID(UI.TOP_CLUSTER_NAME);
9807
9822
  }
9808
9823
  if(IO_CONTEXT) {
9809
- // NOTE: actualName also works for entity IDs
9824
+ // NOTE: actualName also works for entity IDs.
9810
9825
  id = UI.nameToID(IO_CONTEXT.actualName(id));
9811
9826
  }
9812
9827
  const obj = MODEL.objectByID(id);
@@ -9847,7 +9862,7 @@ class ChartVariable {
9847
9862
  }
9848
9863
  // Compute vector and statistics only if vector is still empty.
9849
9864
  if(this.vector.length > 0) return;
9850
- // NOTE: expression vectors start at t = 0 with initial values that
9865
+ // NOTE: Expression vectors start at t = 0 with initial values that
9851
9866
  // should not be included in statistics.
9852
9867
  let v,
9853
9868
  av = null,
@@ -9868,7 +9883,7 @@ class ChartVariable {
9868
9883
  this.chart.time_scale, tsteps, 1);
9869
9884
  t_end = tsteps;
9870
9885
  } else {
9871
- // Get the variable's own value (number, vector or expression)
9886
+ // Get the variable's own value (number, vector or expression).
9872
9887
  if(this.object instanceof Dataset) {
9873
9888
  if(this.attribute) {
9874
9889
  av = this.object.attributeExpression(this.attribute);
@@ -9886,7 +9901,7 @@ class ChartVariable {
9886
9901
  }
9887
9902
  t_end = MODEL.end_period - MODEL.start_period + 1;
9888
9903
  }
9889
- // NOTE: when a chart combines run results with dataset vectors, the
9904
+ // NOTE: When a chart combines run results with dataset vectors, the
9890
9905
  // latter may be longer than the # of time steps displayed in the chart.
9891
9906
  t_end = Math.min(t_end, this.chart.total_time_steps);
9892
9907
  this.N = t_end;
@@ -9902,7 +9917,9 @@ class ChartVariable {
9902
9917
  } else if(av instanceof Expression) {
9903
9918
  // Attribute value is an expression. If this chart variable has
9904
9919
  // its wildcard vector index set, evaluate the expression with
9905
- // this index as context number.
9920
+ // this index as context number. Likewise, set the method object
9921
+ // prefix.
9922
+ av.method_object_prefix = this.chart.prefix;
9906
9923
  v = av.result(t, this.wildcard_index);
9907
9924
  } else {
9908
9925
  // Attribute value must be a number.
@@ -10040,7 +10057,8 @@ class Chart {
10040
10057
  this.value_range = 0;
10041
10058
  this.show_title = true;
10042
10059
  this.legend_position = 'none';
10043
- this.variables = [];
10060
+ this.preferred_prefix = '';
10061
+ this.variables = [];
10044
10062
  // SVG string to display the chart
10045
10063
  this.svg = '';
10046
10064
  // Properties of rectangular chart area
@@ -10052,8 +10070,35 @@ class Chart {
10052
10070
  }
10053
10071
 
10054
10072
  get displayName() {
10073
+ // Charts are identified by their title.
10055
10074
  return this.title;
10056
10075
  }
10076
+
10077
+ get prefix() {
10078
+ // The prefix is used to further specify method variables.
10079
+ if(this.preferred_prefix) return this.preferred_prefix;
10080
+ const parts = this.title.split(UI.PREFIXER);
10081
+ parts.pop();
10082
+ // Now `parts` is empty when the title contains no colon+space.
10083
+ return parts.join(UI.PREFIXER);
10084
+ }
10085
+
10086
+ get possiblePrefixes() {
10087
+ // Return list of prefixes that are eligible for all method variables.
10088
+ let pp = null;
10089
+ for(const v of this.variables) {
10090
+ if(v.object instanceof DatasetModifier && v.object.selector.startsWith(':')) {
10091
+ if(pp) {
10092
+ pp = intersection(pp, Object.keys(v.object.expression.eligible_prefixes));
10093
+ } else {
10094
+ pp = Object.keys(v.object.expression.eligible_prefixes);
10095
+ }
10096
+ }
10097
+ }
10098
+ if(pp) pp.sort();
10099
+ // Always return a list.
10100
+ return pp || [];
10101
+ }
10057
10102
 
10058
10103
  get asXML() {
10059
10104
  let xml = '';
@@ -10125,10 +10170,10 @@ class Chart {
10125
10170
  const eq = obj instanceof DatasetModifier;
10126
10171
  // No equation and no attribute specified? Then assume default.
10127
10172
  if(!eq && a === '') a = obj.defaultAttribute;
10128
- if(eq && (n.indexOf('??') >= 0 || obj.expression.isMethod)) {
10129
- // Special case: for wildcard equations and methods, prompt the
10130
- // modeler which wildcard possibilities to add UNLESS this is an
10131
- // untitled "dummy" chart used to report outcomes.
10173
+ if(eq && n.indexOf('??') >= 0) {
10174
+ // Special case: for wildcard equations, prompt the modeler which
10175
+ // wildcard possibilities to add UNLESS this is an untitled "dummy" chart
10176
+ // used to report outcomes.
10132
10177
  if(this.title) {
10133
10178
  CHART_MANAGER.promptForWildcardIndices(this, obj);
10134
10179
  } else {
@@ -12436,7 +12481,7 @@ class Experiment {
12436
12481
  const rr = this.runs[rnr].results[vi];
12437
12482
  if(rr) {
12438
12483
  // NOTE: Only experiment variables have vector data.
12439
- if(rr.x_variable && vi <= rr.N) {
12484
+ if(rr.x_variable && t <= rr.N) {
12440
12485
  row.push(numval(rr.vector[t], prec));
12441
12486
  } else {
12442
12487
  row.push('');
@@ -866,8 +866,8 @@ function cleanXML(node) {
866
866
  }
867
867
 
868
868
  function parseXML(xml) {
869
- // Parses string `xml` into an XML document, and returns its root node
870
- // (or null if errors)
869
+ // Parse string `xml` into an XML document, and returns its root node
870
+ // (or null if errors).
871
871
  xml = XML_PARSER.parseFromString(customizeXML(xml), 'application/xml');
872
872
  const
873
873
  de = xml.documentElement,
@@ -878,8 +878,8 @@ function parseXML(xml) {
878
878
  }
879
879
 
880
880
  function childNodeByTag(node, tag) {
881
- // Returns the XML child node of `node` having node name `tag`, or NULL if
882
- // no such child node exists
881
+ // Return the XML child node of `node` having node name `tag`, or NULL if
882
+ // no such child node exists.
883
883
  let cn = null;
884
884
  for (let i = 0; i < node.childNodes.length; i++) {
885
885
  if(node.childNodes[i].tagName === tag) {
@@ -891,19 +891,19 @@ function childNodeByTag(node, tag) {
891
891
  }
892
892
 
893
893
  function nodeContentByTag(node, tag) {
894
- // Returns the text content of the child node of `node` having name `tag`,
895
- // or the empty string if no such node exists
894
+ // Return the text content of the child node of `node` having name `tag`,
895
+ // or the empty string if no such node exists.
896
896
  return nodeContent(childNodeByTag(node, tag));
897
897
  }
898
898
 
899
899
  function nodeContent(node) {
900
- // Returns the text content of XML element `node`
900
+ // Return the text content of XML element `node`.
901
901
  if(node) {
902
- // For text nodes, return their value
902
+ // For text nodes, return their value.
903
903
  if(node.nodeType === 3) return node.nodeValue;
904
- // For empty nodes, return empty string
904
+ // For empty nodes, return empty string.
905
905
  if(node.childNodes.length === 0) return '';
906
- // If first child is text, return its value
906
+ // If first child is text, return its value.
907
907
  const fcn = node.childNodes.item(0);
908
908
  if(fcn && fcn.nodeType === 3) return fcn.nodeValue;
909
909
  console.log('UNEXPECTED XML', fcn.nodeType, node);
@@ -912,8 +912,8 @@ function nodeContent(node) {
912
912
  }
913
913
 
914
914
  function nodeParameterValue(node, param) {
915
- // Returns the value of parameter `param` as string if `node` has
916
- // this parameter, otherwise the empty string
915
+ // Return the value of parameter `param` as string if `node` has
916
+ // this parameter, otherwise the empty string.
917
917
  const a = node.getAttribute(param);
918
918
  return a || '';
919
919
  }
@@ -309,7 +309,7 @@ class Expression {
309
309
  if(DEBUGGING) {
310
310
  // Show the "time step stack" for --START and --STOP
311
311
  if(action.startsWith('--') || action.startsWith('"')) {
312
- action = `[${step.join(', ')}] ${action}`;
312
+ action = `[${this.step.join(', ')}] ${action}`;
313
313
  }
314
314
  console.log(action);
315
315
  }
@@ -2903,7 +2903,11 @@ class VirtualMachine {
2903
2903
  vlist = [],
2904
2904
  xlist = [];
2905
2905
  for(const x of this.call_stack) {
2906
- vlist.push(x.object.displayName + '|' + x.attribute);
2906
+ // For equations, only show the attribute.
2907
+ const ons = (x.object === MODEL.equations_dataset ?
2908
+ (x.attribute.startsWith(':') ? x.method_object_prefix : '') :
2909
+ x.object.displayName + '|');
2910
+ vlist.push(ons + x.attribute);
2907
2911
  // Trim spaces around all object-attribute separators in the
2908
2912
  // expression as entered by the modeler.
2909
2913
  xlist.push(x.text.replace(/\s*\|\s*/g, '|'));
@@ -6933,7 +6937,8 @@ function VMI_push_method(x, args) {
6933
6937
  // and possibly also the entity to be used as its object.
6934
6938
  // NOTE: Methods can only be called "as is" (without prefix) in a
6935
6939
  // method expression. The object of such "as is" method calls is
6936
- // the object of the calling method expression `x`.
6940
+ // the object of the calling method expression `x`, or for chart
6941
+ // variables the method object selected for the chart.
6937
6942
  const
6938
6943
  method = args[0].meq,
6939
6944
  mex = method.expression,