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 +1 -1
- package/static/index.html +35 -0
- package/static/linny-r.css +85 -4
- package/static/scripts/linny-r-ctrl.js +8 -1
- package/static/scripts/linny-r-gui-chart-manager.js +80 -16
- package/static/scripts/linny-r-gui-constraint-editor.js +154 -29
- package/static/scripts/linny-r-gui-controller.js +4 -4
- package/static/scripts/linny-r-gui-equation-manager.js +19 -0
- package/static/scripts/linny-r-gui-expression-editor.js +1 -1
- package/static/scripts/linny-r-gui-monitor.js +1 -0
- package/static/scripts/linny-r-model.js +96 -50
- package/static/scripts/linny-r-vm.js +13 -12
package/package.json
CHANGED
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">
|
package/static/linny-r.css
CHANGED
@@ -1922,8 +1922,8 @@ div.menu-item:hover {
|
|
1922
1922
|
#constraint-point {
|
1923
1923
|
position: absolute;
|
1924
1924
|
top: 393px;
|
1925
|
-
left:
|
1926
|
-
width:
|
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:
|
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:
|
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
|
-
|
336
|
-
|
337
|
-
|
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', '#
|
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(
|
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/
|
193
|
-
|
194
|
-
|
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/
|
197
|
-
|
198
|
-
|
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
|
-
//
|
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
|
-
//
|
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,
|
415
|
-
newy = Math.min(100, Math.max(0,
|
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
|
-
|
435
|
-
|
436
|
-
|
437
|
-
|
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
|
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:
|
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:
|
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
|
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% -
|
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"
|
@@ -170,11 +170,11 @@ class LinnyRModel {
|
|
170
170
|
olist.push(ds.displayName);
|
171
171
|
}
|
172
172
|
}
|
173
|
-
//
|
173
|
+
// Also add all outcome equation selectors.
|
174
174
|
const dsm = this.equations_dataset.modifiers;
|
175
|
-
//
|
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 -
|
1597
|
-
note.y += nbp.node.y -
|
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
|
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
|
-
|
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.
|
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
|
-
|
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
|
-
|
8828
|
-
|
8829
|
-
|
8830
|
-
|
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
|
-
|
9365
|
-
|
9366
|
-
|
9367
|
-
|
9368
|
-
|
9369
|
-
|
9370
|
-
//
|
9371
|
-
|
9372
|
-
|
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
|
-
//
|
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,
|
9568
|
-
//
|
9569
|
-
//
|
9570
|
-
|
9571
|
-
|
9572
|
-
|
9573
|
-
|
9574
|
-
|
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:
|
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
|
2976
|
-
|
2977
|
-
|
2978
|
-
|
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.
|
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
|
-
|
8097
|
-
|
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,
|