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 +1 -1
- package/static/index.html +4 -1
- package/static/linny-r.css +18 -0
- package/static/scripts/linny-r-gui-chart-manager.js +53 -25
- package/static/scripts/linny-r-gui-controller.js +45 -27
- package/static/scripts/linny-r-gui-monitor.js +3 -2
- package/static/scripts/linny-r-model.js +89 -44
- package/static/scripts/linny-r-utils.js +12 -12
- package/static/scripts/linny-r-vm.js +8 -3
package/package.json
CHANGED
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:
|
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>
|
package/static/linny-r.css
CHANGED
@@ -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
|
-
//
|
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
|
598
|
-
|
599
|
-
|
600
|
-
|
601
|
-
|
602
|
-
|
603
|
-
|
604
|
-
|
605
|
-
|
606
|
-
|
607
|
-
|
608
|
-
|
609
|
-
|
610
|
-
|
611
|
-
|
612
|
-
|
613
|
-
|
614
|
-
|
615
|
-
|
616
|
-
|
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 =
|
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
|
-
|
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
|
-
|
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
|
2316
|
-
|
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
|
-
|
2588
|
-
|
2589
|
-
|
2590
|
-
|
2591
|
-
|
2592
|
-
|
2593
|
-
|
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 === '
|
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.
|
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,
|
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
|
-
//
|
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
|
-
|
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:
|
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
|
-
|
9730
|
-
|
9731
|
-
|
9732
|
-
|
9733
|
-
|
9734
|
-
|
9735
|
-
|
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:
|
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
|
-
//
|
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
|
-
//
|
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:
|
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:
|
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:
|
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.
|
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 &&
|
10129
|
-
// Special case: for wildcard equations
|
10130
|
-
//
|
10131
|
-
//
|
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 &&
|
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
|
-
//
|
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
|
-
//
|
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
|
-
//
|
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
|
-
//
|
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
|
-
//
|
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
|
-
|
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,
|