linny-r 2.0.7 → 2.0.8
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 +16 -1
- package/static/linny-r.css +2 -0
- package/static/scripts/linny-r-ctrl.js +2 -1
- package/static/scripts/linny-r-gui-chart-manager.js +7 -4
- package/static/scripts/linny-r-gui-dataset-manager.js +130 -0
- package/static/scripts/linny-r-gui-file-manager.js +13 -0
- package/static/scripts/linny-r-utils.js +8 -0
- package/static/scripts/linny-r-vm.js +6 -0
package/package.json
CHANGED
package/static/index.html
CHANGED
@@ -1843,6 +1843,8 @@ Each line should represent the points of a different bound line.">
|
|
1843
1843
|
title="Rename selected dataset">
|
1844
1844
|
<img id="ds-clone-btn" class="btn disab" src="images/clone.png"
|
1845
1845
|
title="Clone selected dataset">
|
1846
|
+
<img id="ds-load-btn" class="btn enab" src="images/open.png"
|
1847
|
+
title="Load dataset(s) from CSV file">
|
1846
1848
|
<img id="ds-delete-btn" class="btn disab" src="images/delete.png"
|
1847
1849
|
title="Delete selected dataset"
|
1848
1850
|
style="position: absolute; right: 2px">
|
@@ -1940,6 +1942,19 @@ Start with = to find exact match, with ~ to match first characters">
|
|
1940
1942
|
<input id="rename-dataset-name" type="text" autocomplete="off">
|
1941
1943
|
</div>
|
1942
1944
|
</div>
|
1945
|
+
|
1946
|
+
<!-- the LOAD CSV dialog prompts the user for a CSV file on disk
|
1947
|
+
to be loaded -->
|
1948
|
+
<div id="load-csv-modal" class="modal">
|
1949
|
+
<div id="load-csv-dlg" class="inp-dlg">
|
1950
|
+
<div class="dlg-title">
|
1951
|
+
Load datasets from CSV file
|
1952
|
+
<img class="cancel-btn" src="images/cancel.png">
|
1953
|
+
<img class="ok-btn" src="images/ok.png">
|
1954
|
+
</div>
|
1955
|
+
<input id="load-csv-file" type="file">
|
1956
|
+
</div>
|
1957
|
+
</div>
|
1943
1958
|
|
1944
1959
|
<!-- the NEW SELECTOR dialog prompts for a new selector for a dataset -->
|
1945
1960
|
<div id="new-selector-modal" class="modal">
|
@@ -2495,7 +2510,7 @@ NOTE: * and ? will be interpreted as wildcards"
|
|
2495
2510
|
<div id="experiment-separator"></div>
|
2496
2511
|
<div id="experiment-default-message">
|
2497
2512
|
To define a meaningful experiment, a model must feature at least
|
2498
|
-
one outcome dataset, or a
|
2513
|
+
one outcome dataset, or a chart having one or more variables.
|
2499
2514
|
</div>
|
2500
2515
|
<div id="experiment-params-header">(no experiment selected)</div>
|
2501
2516
|
<div id="experiment-params-div">
|
package/static/linny-r.css
CHANGED
@@ -794,6 +794,7 @@ div.checked.not-same-not-changed {
|
|
794
794
|
|
795
795
|
/* LOAD modal dialog */
|
796
796
|
#load-dlg,
|
797
|
+
#load-csv-dlg,
|
797
798
|
#comparison-dlg {
|
798
799
|
width: 430px;
|
799
800
|
height: min-content;
|
@@ -801,6 +802,7 @@ div.checked.not-same-not-changed {
|
|
801
802
|
}
|
802
803
|
|
803
804
|
#load-xml-file,
|
805
|
+
#load-csv-file,
|
804
806
|
#comparison-xml-file {
|
805
807
|
padding: 2px;
|
806
808
|
max-width: 375px;
|
@@ -446,7 +446,8 @@ class Controller {
|
|
446
446
|
}
|
447
447
|
// NOTE: Replace single quotes by Unicode apostrophe so that they
|
448
448
|
// cannot interfere with JavaScript strings delimited by single quotes.
|
449
|
-
return name.toLowerCase().replace(/\s/g, '_')
|
449
|
+
return name.toLowerCase().replace(/\s/g, '_')
|
450
|
+
.replace("'", '\u2019').replace('"', '\uff02');
|
450
451
|
}
|
451
452
|
|
452
453
|
htmlEquationName(n) {
|
@@ -1003,10 +1003,13 @@ class GUIChartManager extends ChartManager {
|
|
1003
1003
|
if(v.visible) {
|
1004
1004
|
// NOTE: while still solving, display t-1 as N
|
1005
1005
|
const n = Math.max(0, v.N);
|
1006
|
-
html.push('<tr><td class="v-name">',
|
1007
|
-
|
1008
|
-
|
1009
|
-
|
1006
|
+
html.push('<tr><td class="v-name">', v.displayName, '</td><td>', n,
|
1007
|
+
'</td><td title="', v.minimum.toPrecision(8), '">', data[nr][0],
|
1008
|
+
'</td><td title="', v.maximum.toPrecision(8), '">', data[nr][1],
|
1009
|
+
'</td><td title="', v.mean.toPrecision(8), '">', data[nr][2],
|
1010
|
+
'</td><td title="', Math.sqrt(v.variance).toPrecision(8), '">', data[nr][3],
|
1011
|
+
'</td><td title="', v.sum.toPrecision(8), '">', data[nr][4],
|
1012
|
+
'</td><td>', v.non_zero_tally, '</td><td>', v.exceptions,
|
1010
1013
|
'</td></tr>');
|
1011
1014
|
nr++;
|
1012
1015
|
}
|
@@ -52,6 +52,8 @@ class GUIDatasetManager extends DatasetManager {
|
|
52
52
|
'click', () => DATASET_MANAGER.promptForName());
|
53
53
|
document.getElementById('ds-clone-btn').addEventListener(
|
54
54
|
'click', () => DATASET_MANAGER.cloneDataset());
|
55
|
+
document.getElementById('ds-load-btn').addEventListener(
|
56
|
+
'click', () => DATASET_MANAGER.load_csv_modal.show());
|
55
57
|
document.getElementById('ds-delete-btn').addEventListener(
|
56
58
|
'click', () => DATASET_MANAGER.deleteDataset());
|
57
59
|
document.getElementById('ds-filter-btn').addEventListener(
|
@@ -97,6 +99,11 @@ class GUIDatasetManager extends DatasetManager {
|
|
97
99
|
'click', () => DATASET_MANAGER.renameDataset());
|
98
100
|
this.rename_modal.cancel.addEventListener(
|
99
101
|
'click', () => DATASET_MANAGER.rename_modal.hide());
|
102
|
+
this.load_csv_modal = new ModalDialog('load-csv');
|
103
|
+
this.load_csv_modal.ok.addEventListener(
|
104
|
+
'click', () => FILE_MANAGER.loadCSVFile());
|
105
|
+
this.load_csv_modal.cancel.addEventListener(
|
106
|
+
'click', () => DATASET_MANAGER.load_csv_modal.hide());
|
100
107
|
this.conversion_modal = new ModalDialog('convert-modifiers');
|
101
108
|
this.conversion_modal.ok.addEventListener(
|
102
109
|
'click', () => DATASET_MANAGER.convertModifiers());
|
@@ -1149,4 +1156,127 @@ class GUIDatasetManager extends DatasetManager {
|
|
1149
1156
|
this.updateDialog();
|
1150
1157
|
}
|
1151
1158
|
|
1159
|
+
readCSVData(text) {
|
1160
|
+
// Parse text from uploaded file and create or overwrite datasets.
|
1161
|
+
const
|
1162
|
+
lines = text.trim().split(/\n/),
|
1163
|
+
n = lines.length;
|
1164
|
+
if(n < 2) {
|
1165
|
+
UI.warn('Data must have at least 2 lines: dataset names and default values');
|
1166
|
+
return false;
|
1167
|
+
}
|
1168
|
+
// Infer most likely delimiter.
|
1169
|
+
const
|
1170
|
+
tabs = text.split('\t').length - 1,
|
1171
|
+
tab0 = lines[0].split('\t').length - 1,
|
1172
|
+
tab1 = lines[1].split('\t').length - 1,
|
1173
|
+
semic0 = lines[0].split(';').length - 1,
|
1174
|
+
semics = text.split(';').length - 1 - semic0;
|
1175
|
+
let sep = '\t';
|
1176
|
+
if(!tabs || tab0 !== tab1) {
|
1177
|
+
// Tab is most likely NOT the separator.
|
1178
|
+
sep = (semics ? ';' : ',');
|
1179
|
+
}
|
1180
|
+
const
|
1181
|
+
parts = lines[0].split(sep),
|
1182
|
+
dsn = [];
|
1183
|
+
let quoted = false;
|
1184
|
+
for(let i = 0; i < parts.length; i++) {
|
1185
|
+
const
|
1186
|
+
p = parts[i],
|
1187
|
+
swq = /^\"(\"\")*($|[^\"])/.test(p),
|
1188
|
+
ewq = p.endsWith('"');
|
1189
|
+
if(!quoted && swq && !ewq) {
|
1190
|
+
dsn.push(p);
|
1191
|
+
quoted = true;
|
1192
|
+
} else if(quoted) {
|
1193
|
+
dsn[dsn.length - 1] += sep + p;
|
1194
|
+
quoted = !ewq;
|
1195
|
+
} else {
|
1196
|
+
dsn.push(p);
|
1197
|
+
}
|
1198
|
+
}
|
1199
|
+
for(let i = 0; i < dsn.length; i++) {
|
1200
|
+
const n = unquoteCSV(dsn[i].trim());
|
1201
|
+
if(!UI.validName(n)) {
|
1202
|
+
UI.warn(`Invalid dataset name "${n}" in column ${i}`);
|
1203
|
+
return false;
|
1204
|
+
}
|
1205
|
+
dsn[i] = n;
|
1206
|
+
}
|
1207
|
+
const
|
1208
|
+
ncol = dsn.length,
|
1209
|
+
dsa = [],
|
1210
|
+
dsdv = lines[1].split(sep);
|
1211
|
+
if(dsdv.length !== ncol) {
|
1212
|
+
UI.warn(`Number of default values (${dsdv.length}) does not match number of dataset names (${ncol})`);
|
1213
|
+
return false;
|
1214
|
+
}
|
1215
|
+
for(let i = 0; i < dsdv.length; i++) {
|
1216
|
+
const
|
1217
|
+
v = dsdv[i].trim(),
|
1218
|
+
sf = safeStrToFloat(v, NaN);
|
1219
|
+
if(isNaN(sf)) {
|
1220
|
+
UI.warn(`Invalid default value "${v}" in column ${i}`);
|
1221
|
+
return false;
|
1222
|
+
} else {
|
1223
|
+
dsa.push([sf]);
|
1224
|
+
}
|
1225
|
+
}
|
1226
|
+
for(let i = 2; i < n; i++) {
|
1227
|
+
const dsv = lines[i].trim().split(sep);
|
1228
|
+
if(dsv.length !== ncol) {
|
1229
|
+
UI.warn(`Number of values (${dsv.length}) on line ${i} does not match number of dataset names (${ncol})`);
|
1230
|
+
return false;
|
1231
|
+
}
|
1232
|
+
for(let j = 0; j < dsv.length; j++) {
|
1233
|
+
const
|
1234
|
+
v = dsv[j].trim(),
|
1235
|
+
sf = safeStrToFloat(v, '');
|
1236
|
+
if(sf === '' && v !== '') {
|
1237
|
+
UI.warn(`Invalid numerical value "${v}" for <strong>${dsn[j]}</strong> on line ${i}`);
|
1238
|
+
return false;
|
1239
|
+
} else {
|
1240
|
+
dsa[j].push(sf);
|
1241
|
+
}
|
1242
|
+
}
|
1243
|
+
}
|
1244
|
+
// Add or update datasets.
|
1245
|
+
let added = 0,
|
1246
|
+
updated = 0;
|
1247
|
+
for(let i = 0; i < dsn.length; i++) {
|
1248
|
+
const
|
1249
|
+
n = dsn[i],
|
1250
|
+
id = UI.nameToID(n),
|
1251
|
+
ods = MODEL.namedObjectByID(id),
|
1252
|
+
ds = ods || MODEL.addDataset(n);
|
1253
|
+
// NOTE: `ds` will now be either a new dataset or an existing one.
|
1254
|
+
if(ds) {
|
1255
|
+
// Keep track of added/updated datasets.
|
1256
|
+
const
|
1257
|
+
odv = ds.default_value,
|
1258
|
+
odata = ds.dataString;
|
1259
|
+
ds.default_value = safeStrToFloat(dsdv[i], 0);
|
1260
|
+
ds.data = dsa[i];
|
1261
|
+
if(ods) {
|
1262
|
+
if(ds.default_value !== odv || odata !== ds.dataString) updated++;
|
1263
|
+
} else {
|
1264
|
+
added++;
|
1265
|
+
}
|
1266
|
+
ds.computeStatistics();
|
1267
|
+
}
|
1268
|
+
}
|
1269
|
+
// Notify modeler of changes (if any).
|
1270
|
+
let msg = 'No datasets added or updated';
|
1271
|
+
if(added) {
|
1272
|
+
msg = pluralS(added, 'dataset') + ' added';
|
1273
|
+
if(updated) msg += ', ' + pluralS(updated, 'dataset') + ' updated';
|
1274
|
+
} else if(updated) {
|
1275
|
+
msg = pluralS(updated, 'dataset') + ' updated';
|
1276
|
+
}
|
1277
|
+
UI.notify(msg);
|
1278
|
+
this.updateDialog();
|
1279
|
+
return true;
|
1280
|
+
}
|
1281
|
+
|
1152
1282
|
} // END of class GUIDatasetManager
|
@@ -391,6 +391,19 @@ class GUIFileManager {
|
|
391
391
|
});
|
392
392
|
}
|
393
393
|
|
394
|
+
loadCSVFile() {
|
395
|
+
document.getElementById('load-csv-modal').style.display = 'none';
|
396
|
+
try {
|
397
|
+
const file = document.getElementById('load-csv-file').files[0];
|
398
|
+
if(!file) return;
|
399
|
+
const reader = new FileReader();
|
400
|
+
reader.onload = (event) => DATASET_MANAGER.readCSVData(event.target.result);
|
401
|
+
reader.readAsText(file);
|
402
|
+
} catch(err) {
|
403
|
+
UI.alert('Error while reading file: ' + err);
|
404
|
+
}
|
405
|
+
}
|
406
|
+
|
394
407
|
renderDiagramAsPNG(tight) {
|
395
408
|
// When `tight` is TRUE, add no whitespace around the diagram.
|
396
409
|
window.localStorage.removeItem('png-url');
|
@@ -225,6 +225,13 @@ function ellipsedText(text, n=50, m=10) {
|
|
225
225
|
return text.slice(0, n) + ' \u2026 ' + text.slice(text.length - m);
|
226
226
|
}
|
227
227
|
|
228
|
+
function unquoteCSV(s) {
|
229
|
+
// Returns a double-quoted string `s` without its quotes, and with
|
230
|
+
// quote pairs "" replaced by single " quotes.
|
231
|
+
if(!s.startsWith('"') || !s.endsWith('"')) return s;
|
232
|
+
return s.slice(1, -1).replaceAll('""', '"');
|
233
|
+
}
|
234
|
+
|
228
235
|
//
|
229
236
|
// Functions used when comparing two Linny-R models
|
230
237
|
//
|
@@ -1132,6 +1139,7 @@ if(NODE) module.exports = {
|
|
1132
1139
|
uniformDecimals: uniformDecimals,
|
1133
1140
|
capitalized: capitalized,
|
1134
1141
|
ellipsedText: ellipsedText,
|
1142
|
+
unquoteCSV: unquoteCSV,
|
1135
1143
|
earlierVersion: earlierVersion,
|
1136
1144
|
differences: differences,
|
1137
1145
|
markFirstDifference: markFirstDifference,
|
@@ -444,6 +444,12 @@ class Expression {
|
|
444
444
|
// expression).
|
445
445
|
if(t < 0 || this.isStatic) t = 0;
|
446
446
|
if(t >= v.length) return VM.UNDEFINED;
|
447
|
+
// Check for recursive calls.
|
448
|
+
if(v[t] === VM.COMPUTING) {
|
449
|
+
console.log('Already computing expression for', this.variableName);
|
450
|
+
console.log(this.text);
|
451
|
+
return VM.CYCLIC;
|
452
|
+
}
|
447
453
|
// NOTES:
|
448
454
|
// (1) When VM is setting up a tableau, values computed for the
|
449
455
|
// look-ahead period must be recomputed.
|