linny-r 2.0.6 → 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/README.md +6 -6
- package/package.json +1 -1
- package/static/index.html +16 -1
- package/static/linny-r.css +4 -2
- package/static/scripts/linny-r-ctrl.js +42 -40
- package/static/scripts/linny-r-gui-chart-manager.js +7 -4
- package/static/scripts/linny-r-gui-constraint-editor.js +7 -3
- package/static/scripts/linny-r-gui-controller.js +17 -10
- 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-gui-model-autosaver.js +2 -2
- package/static/scripts/linny-r-gui-monitor.js +1 -1
- package/static/scripts/linny-r-gui-paper.js +29 -28
- package/static/scripts/linny-r-model.js +23 -19
- package/static/scripts/linny-r-utils.js +41 -5
- package/static/scripts/linny-r-vm.js +173 -26
package/README.md
CHANGED
@@ -36,8 +36,8 @@ Linny-R is developed as a JavaScript package, and requires that **Node.js**
|
|
36
36
|
is installed on your computer. This software can be downloaded from
|
37
37
|
<a href="https://nodejs.org" target="_blank">https://nodejs.org</a>.
|
38
38
|
Make sure that you choose the correct installer for your computer.
|
39
|
-
Linny-R is developed using the _current_ release. Presently (
|
40
|
-
this is
|
39
|
+
Linny-R is developed using the _current_ release. Presently (December 2024)
|
40
|
+
this is 23.3.0.
|
41
41
|
|
42
42
|
Run the installer and accept the default settings.
|
43
43
|
There is <u>**no**</u> need to install the optional _Tools for Native Modules_.
|
@@ -48,7 +48,7 @@ Verify the installation by typing:
|
|
48
48
|
|
49
49
|
``node --version``
|
50
50
|
|
51
|
-
The response should be the version number of Node.js, for example:
|
51
|
+
The response should be the version number of Node.js, for example: v23.3.0.
|
52
52
|
|
53
53
|
## Installing Linny-R
|
54
54
|
It is advisable to install Linny-R in a directory on your computer, **not**
|
@@ -300,9 +300,9 @@ The Linny-R GUI should show in your browser window, while in the CLI you
|
|
300
300
|
should see a long series of server log messages like:
|
301
301
|
|
302
302
|
<pre>
|
303
|
-
[2024-
|
304
|
-
[2024-
|
305
|
-
[2024-
|
303
|
+
[2024-12-01 22:55:17] Static file: /index.html
|
304
|
+
[2024-12-01 22:55:17] Static file: /scripts/iro.min.js
|
305
|
+
[2024-12-01 22:55:17] Static file: /images/open.png
|
306
306
|
... etc.
|
307
307
|
</pre>
|
308
308
|
|
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
@@ -10,7 +10,7 @@ file that implements the graphical user interface for Linny-R.
|
|
10
10
|
*/
|
11
11
|
|
12
12
|
/*
|
13
|
-
Copyright (c) 2017-
|
13
|
+
Copyright (c) 2017-2025 Delft University of Technology
|
14
14
|
|
15
15
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
16
16
|
of this software and associated documentation files (the "Software"), to deal
|
@@ -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;
|
@@ -822,7 +824,7 @@ div.checked.not-same-not-changed {
|
|
822
824
|
}
|
823
825
|
|
824
826
|
#restore-dlg {
|
825
|
-
width:
|
827
|
+
width: 450px;
|
826
828
|
max-height: 219px;
|
827
829
|
}
|
828
830
|
|
@@ -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) {
|
@@ -875,9 +876,9 @@ class ChartManager {
|
|
875
876
|
// CLASS SensitivityAnalysis provides the sensitivity analysis functionality
|
876
877
|
class SensitivityAnalysis {
|
877
878
|
constructor() {
|
878
|
-
// Initialize main dialog properties
|
879
|
+
// Initialize main dialog properties.
|
879
880
|
this.reset();
|
880
|
-
// Sensitivity analysis creates & disposes an experiment and a chart
|
881
|
+
// Sensitivity analysis creates & disposes an experiment and a chart.
|
881
882
|
this.experiment_title = '___SENSITIVITY_ANALYSIS___';
|
882
883
|
this.chart_title = '___SENSITIVITY_ANALYSIS_CHART___';
|
883
884
|
}
|
@@ -912,7 +913,7 @@ class SensitivityAnalysis {
|
|
912
913
|
// a dummy chart is created that includes all these outcomes as *chart*
|
913
914
|
// variables.
|
914
915
|
if(!this.experiment) {
|
915
|
-
// Clear results from previous analysis
|
916
|
+
// Clear results from previous analysis.
|
916
917
|
this.clearResults();
|
917
918
|
this.parameters = [];
|
918
919
|
for(let i = 0; i < MODEL.sensitivity_parameters.length; i++) {
|
@@ -924,7 +925,7 @@ class SensitivityAnalysis {
|
|
924
925
|
if(oax) {
|
925
926
|
this.parameters.push(oax);
|
926
927
|
} else if(vn.length === 1 && obj instanceof Dataset) {
|
927
|
-
// Dataset without selector => push the dataset vector
|
928
|
+
// Dataset without selector => push the dataset vector.
|
928
929
|
this.parameters.push(obj.vector);
|
929
930
|
} else {
|
930
931
|
UI.alert(`Parameter ${p} is not a dataset or expression`);
|
@@ -938,23 +939,24 @@ class SensitivityAnalysis {
|
|
938
939
|
this.experiment = new Experiment(this.experiment_title);
|
939
940
|
this.experiment.charts = [this.chart];
|
940
941
|
this.experiment.inferVariables();
|
941
|
-
// This experiment always uses the same combination: the base selectors
|
942
|
+
// This experiment always uses the same combination: the base selectors.
|
942
943
|
const bs = MODEL.base_case_selectors.split(' ');
|
943
944
|
this.experiment.combinations = [];
|
944
|
-
// Add this combination N+1 times for N parameters
|
945
|
+
// Add this combination N+1 times for N parameters.
|
945
946
|
for(let i = 0; i <= this.parameters.length; i++) {
|
946
947
|
this.experiment.combinations.push(bs);
|
947
948
|
}
|
948
|
-
// NOTE:
|
949
|
+
// NOTE: Model settings will not be changed, but will be restored after
|
950
|
+
// each run => store the original settings.
|
949
951
|
this.experiment.original_model_settings = MODEL.settingsString;
|
950
952
|
this.experiment.original_round_sequence = MODEL.round_sequence;
|
951
953
|
}
|
952
|
-
// Change the button (GUI only -- console will return FALSE)
|
954
|
+
// Change the button (GUI only -- console will return FALSE).
|
953
955
|
const paused = this.resumeButtons();
|
954
956
|
if(!paused) {
|
955
957
|
this.experiment.time_started = new Date().getTime();
|
956
958
|
this.experiment.active_combination_index = 0;
|
957
|
-
// NOTE:
|
959
|
+
// NOTE: Start with base case run, hence no active parameter yet.
|
958
960
|
MODEL.running_experiment = this.experiment;
|
959
961
|
}
|
960
962
|
// Let the experiment manager do the work!!
|
@@ -962,31 +964,31 @@ class SensitivityAnalysis {
|
|
962
964
|
}
|
963
965
|
|
964
966
|
processRestOfRun() {
|
965
|
-
// This method is called by the experiment manager after
|
967
|
+
// This method is called by the experiment manager after a SA run.
|
966
968
|
const x = MODEL.running_experiment;
|
967
969
|
if(!x) return;
|
968
|
-
// Double-check that indeed the SA experiment is running
|
970
|
+
// Double-check that indeed the SA experiment is running.
|
969
971
|
if(x !== this.experiment) {
|
970
972
|
UI.alert('ERROR: Expected SA experiment run, but got ' + x.title);
|
971
973
|
return;
|
972
974
|
}
|
973
975
|
const aci = x.active_combination_index;
|
974
|
-
// Always add solver messages
|
976
|
+
// Always add solver messages.
|
975
977
|
x.runs[aci].addMessages();
|
976
|
-
// NOTE:
|
977
|
-
// loading , as the actual experiment is not stored
|
978
|
+
// NOTE: Use a "dummy experiment object" to ensure proper XML saving and
|
979
|
+
// loading , as the actual experiment is not stored.
|
978
980
|
x.runs.experiment = {title: SENSITIVITY_ANALYSIS.experiment_title};
|
979
|
-
// Add run to the sensitivity analysis
|
981
|
+
// Add run to the sensitivity analysis.
|
980
982
|
MODEL.sensitivity_runs.push(x.runs[aci]);
|
981
983
|
this.showProgress('Run #' + aci);
|
982
|
-
// See if more runs should be done
|
984
|
+
// See if more runs should be done.
|
983
985
|
const n = x.combinations.length;
|
984
986
|
if(!VM.halted && aci < n - 1) {
|
985
987
|
if(this.must_pause) {
|
986
988
|
this.pausedButtons(aci);
|
987
989
|
UI.setMessage('');
|
988
990
|
} else {
|
989
|
-
// NOTE:
|
991
|
+
// NOTE: Use aci because run #0 is the base case w/o active parameter.
|
990
992
|
MODEL.active_sensitivity_parameter = this.parameters[aci];
|
991
993
|
x.active_combination_index++;
|
992
994
|
setTimeout(() => EXPERIMENT_MANAGER.runModel(), 5);
|
@@ -1001,31 +1003,31 @@ class SensitivityAnalysis {
|
|
1001
1003
|
} else {
|
1002
1004
|
this.showCheckmark(msecToTime(x.time_stopped - x.time_started));
|
1003
1005
|
}
|
1004
|
-
// No more runs => perform wrap-up
|
1005
|
-
// Restore original model settings
|
1006
|
+
// No more runs => perform wrap-up.
|
1007
|
+
// (1) Restore original model settings.
|
1006
1008
|
MODEL.running_experiment = null;
|
1007
1009
|
MODEL.active_sensitivity_parameter = null;
|
1008
1010
|
MODEL.parseSettings(x.original_model_settings);
|
1009
1011
|
MODEL.round_sequence = x.original_round_sequence;
|
1010
|
-
// Reset the Virtual Machine so t=0 at the status line,
|
1011
|
-
//
|
1012
|
+
// (2) Reset the Virtual Machine so t=0 at the status line, and ALL
|
1013
|
+
// expressions are reset as well.
|
1012
1014
|
VM.reset();
|
1013
|
-
// Free the SA experiment and SA chart
|
1015
|
+
// Free the SA experiment and SA chart.
|
1014
1016
|
this.experiment = null;
|
1015
1017
|
this.chart = null;
|
1016
|
-
// Reset buttons (GUI only)
|
1018
|
+
// Reset buttons (GUI only).
|
1017
1019
|
this.readyButtons();
|
1018
1020
|
}
|
1019
1021
|
this.updateDialog();
|
1020
|
-
// Reset the model, as results of last run will be showing still
|
1022
|
+
// Reset the model, as results of last run will be showing still.
|
1021
1023
|
UI.resetModel();
|
1022
1024
|
CHART_MANAGER.resetChartVectors();
|
1023
|
-
// NOTE:
|
1025
|
+
// NOTE: Clear chart only when done (charts do not update during experiment).
|
1024
1026
|
if(!MODEL.running_experiment) CHART_MANAGER.updateDialog();
|
1025
1027
|
}
|
1026
1028
|
|
1027
1029
|
stop() {
|
1028
|
-
// Interrupt solver but retain data on server (and no resume)
|
1030
|
+
// Interrupt solver but retain data on server (and no resume).
|
1029
1031
|
VM.halt();
|
1030
1032
|
this.readyButtons();
|
1031
1033
|
this.showProgress('');
|
@@ -1033,13 +1035,13 @@ class SensitivityAnalysis {
|
|
1033
1035
|
}
|
1034
1036
|
|
1035
1037
|
clearResults() {
|
1036
|
-
// Clear results and reset control buttons
|
1038
|
+
// Clear results, and reset control buttons.
|
1037
1039
|
MODEL.sensitivity_runs.length = 0;
|
1038
1040
|
this.selected_run = -1;
|
1039
1041
|
}
|
1040
1042
|
|
1041
1043
|
computeData(sas) {
|
1042
|
-
// Compute data value or status for statistic `sas
|
1044
|
+
// Compute data value or status for statistic `sas`.
|
1043
1045
|
this.perc = {};
|
1044
1046
|
this.shade = {};
|
1045
1047
|
this.data = {};
|
@@ -1047,12 +1049,12 @@ class SensitivityAnalysis {
|
|
1047
1049
|
ol = MODEL.sensitivity_outcomes.length,
|
1048
1050
|
rl = MODEL.sensitivity_runs.length;
|
1049
1051
|
if(ol === 0) return;
|
1050
|
-
// Always find highest relative change
|
1052
|
+
// Always find highest relative change.
|
1051
1053
|
let max_dif = 0;
|
1052
1054
|
for(let i = 0; i < ol; i++) {
|
1053
1055
|
this.data[i] = [];
|
1054
1056
|
for(let j = 0; j < rl; j++) {
|
1055
|
-
// Get the selected statistic for each run to get an array of numbers
|
1057
|
+
// Get the selected statistic for each run to get an array of numbers.
|
1056
1058
|
const rr = MODEL.sensitivity_runs[j].results[i];
|
1057
1059
|
if(!rr) {
|
1058
1060
|
this.data[i].push(VM.UNDEFINED);
|
@@ -1076,7 +1078,7 @@ class SensitivityAnalysis {
|
|
1076
1078
|
this.data[i].push(rr.last);
|
1077
1079
|
}
|
1078
1080
|
}
|
1079
|
-
// Compute relative change
|
1081
|
+
// Compute the relative change.
|
1080
1082
|
let bsv = this.data[i][0];
|
1081
1083
|
if(Math.abs(bsv) < VM.NEAR_ZERO) bsv = 0;
|
1082
1084
|
this.perc[i] = [];
|
@@ -1097,26 +1099,26 @@ class SensitivityAnalysis {
|
|
1097
1099
|
for(let j = 1; j < this.data[i].length; j++) this.perc[i].push('-');
|
1098
1100
|
}
|
1099
1101
|
}
|
1100
|
-
// Now use max_dif to compute shades
|
1102
|
+
// Now use max_dif to compute shades.
|
1101
1103
|
for(let i = 0; i < ol; i++) {
|
1102
1104
|
this.shade[i] = [];
|
1103
|
-
// Color scale range is -max ... +max (0 in center => white)
|
1105
|
+
// Color scale range is -max ... +max (0 in center => white).
|
1104
1106
|
for(let j = 0; j < this.perc[i].length; j++) {
|
1105
1107
|
const p = this.perc[i][j];
|
1106
1108
|
this.shade[i].push(p === VM.UNDEFINED || max_dif < VM.NEAR_ZERO ?
|
1107
1109
|
0.5 : (p / max_dif + 1) / 2);
|
1108
1110
|
}
|
1109
|
-
// Convert to sig4Dig
|
1111
|
+
// Convert to sig4Dig.
|
1110
1112
|
for(let j = 0; j < this.data[i].length; j++) {
|
1111
1113
|
this.data[i][j] = VM.sig4Dig(this.data[i][j]);
|
1112
1114
|
}
|
1113
|
-
// Format data such that they all have same number of decimals
|
1115
|
+
// Format data such that they all have same number of decimals.
|
1114
1116
|
if(this.relative_scale && this.perc[i][0] !== '-') {
|
1115
1117
|
for(let j = 0; j < this.perc[i].length; j++) {
|
1116
1118
|
this.perc[i][j] = VM.sig4Dig(this.perc[i][j]);
|
1117
1119
|
}
|
1118
1120
|
uniformDecimals(this.perc[i]);
|
1119
|
-
// NOTE:
|
1121
|
+
// NOTE: Only consider data of base scenario.
|
1120
1122
|
this.data[i][0] = VM.sig4Dig(this.data[i][0]);
|
1121
1123
|
} else {
|
1122
1124
|
uniformDecimals(this.data[i]);
|
@@ -1125,11 +1127,11 @@ class SensitivityAnalysis {
|
|
1125
1127
|
}
|
1126
1128
|
|
1127
1129
|
resumeButtons() {
|
1128
|
-
// Console experiments cannot be paused, and hence not resumed
|
1130
|
+
// Console experiments cannot be paused, and hence not resumed.
|
1129
1131
|
return false;
|
1130
1132
|
}
|
1131
1133
|
|
1132
|
-
// Dummy methods: actions that are meaningful only for the graphical UI
|
1134
|
+
// Dummy methods: actions that are meaningful only for the graphical UI.
|
1133
1135
|
updateDialog() {}
|
1134
1136
|
showCheckmark() {}
|
1135
1137
|
showProgress() {}
|
@@ -1143,7 +1145,7 @@ class SensitivityAnalysis {
|
|
1143
1145
|
// Class ExperimentManager controls the collection of experiments of the model
|
1144
1146
|
class ExperimentManager {
|
1145
1147
|
constructor() {
|
1146
|
-
// NOTE:
|
1148
|
+
// NOTE: The properties below are relevant only for the GUI.
|
1147
1149
|
this.experiment_table = null;
|
1148
1150
|
this.focal_table = null;
|
1149
1151
|
}
|
@@ -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
|
}
|
@@ -283,7 +283,8 @@ class ConstraintEditor {
|
|
283
283
|
i = this.selected_point,
|
284
284
|
pts = this.selected.points,
|
285
285
|
li = pts.length - 1,
|
286
|
-
|
286
|
+
// NOTE: Use a copy of the selected point, or it will not be updated.
|
287
|
+
p = pts[this.selected_point].slice(),
|
287
288
|
minx = (i === 0 ? 0 : (i === li ? 100 : pts[i - 1][0])),
|
288
289
|
maxx = (i === 0 ? 0 : (i === li ? 100 : pts[i + 1][0]));
|
289
290
|
let cx = false,
|
@@ -317,6 +318,9 @@ class ConstraintEditor {
|
|
317
318
|
p[1] = Math.round(3 * p[1]) / 3;
|
318
319
|
}
|
319
320
|
}
|
321
|
+
this.dragged_point = this.selected_point;
|
322
|
+
this.movePoint(p[0], p[1]);
|
323
|
+
this.dragged_point = -1;
|
320
324
|
this.draw();
|
321
325
|
this.updateEquation();
|
322
326
|
}
|
@@ -670,7 +674,7 @@ class ConstraintEditor {
|
|
670
674
|
}
|
671
675
|
|
672
676
|
deleteSelector() {
|
673
|
-
// Delete modifier from selected dataset
|
677
|
+
// Delete modifier from selected dataset.
|
674
678
|
if(!this.selected) return;
|
675
679
|
const
|
676
680
|
bl = this.selected,
|
@@ -1156,7 +1160,7 @@ class ConstraintEditor {
|
|
1156
1160
|
}
|
1157
1161
|
|
1158
1162
|
updateConstraint() {
|
1159
|
-
//
|
1163
|
+
// Update the edited constraint, or add a new constraint to the model.
|
1160
1164
|
// TO DO: prepare for undo
|
1161
1165
|
if(this.edited_constraint === null) {
|
1162
1166
|
this.edited_constraint = MODEL.addConstraint(this.from_node, this.to_node);
|
@@ -13,7 +13,7 @@ handler functions.
|
|
13
13
|
*/
|
14
14
|
|
15
15
|
/*
|
16
|
-
Copyright (c) 2017-
|
16
|
+
Copyright (c) 2017-2025 Delft University of Technology
|
17
17
|
|
18
18
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
19
19
|
of this software and associated documentation files (the "Software"), to deal
|
@@ -3673,7 +3673,7 @@ console.log('HERE name conflicts', name_conflicts, mapping);
|
|
3673
3673
|
UI.notify('To diagnose unbounded problems, values beyond 1e+10 ' +
|
3674
3674
|
'are considered as infinite (\u221E)');
|
3675
3675
|
}
|
3676
|
-
// Some changes may necessitate redrawing the diagram
|
3676
|
+
// Some changes may necessitate redrawing the diagram.
|
3677
3677
|
let cb = UI.boxChecked('settings-align-to-grid'),
|
3678
3678
|
redraw = !model.align_to_grid && cb;
|
3679
3679
|
model.align_to_grid = cb;
|
@@ -3688,7 +3688,7 @@ console.log('HERE name conflicts', name_conflicts, mapping);
|
|
3688
3688
|
cb = UI.boxChecked('settings-block-arrows');
|
3689
3689
|
redraw = redraw || cb !== model.show_block_arrows;
|
3690
3690
|
model.show_block_arrows = cb;
|
3691
|
-
// Changes affecting run length (hence vector lengths) require a model reset
|
3691
|
+
// Changes affecting run length (hence vector lengths) require a model reset.
|
3692
3692
|
let reset = false;
|
3693
3693
|
reset = reset || (ts != model.time_scale);
|
3694
3694
|
model.time_scale = ts;
|
@@ -4283,7 +4283,7 @@ console.log('HERE name conflicts', name_conflicts, mapping);
|
|
4283
4283
|
const
|
4284
4284
|
md = this.modals.link,
|
4285
4285
|
l = this.edited_object;
|
4286
|
-
// Check whether all input fields are valid
|
4286
|
+
// Check whether all input fields are valid.
|
4287
4287
|
if(!this.updateExpressionInput('link-R', 'rate', l.relative_rate)) {
|
4288
4288
|
return false;
|
4289
4289
|
}
|
@@ -4318,8 +4318,10 @@ console.log('HERE name conflicts', name_conflicts, mapping);
|
|
4318
4318
|
`</strong> will cause issues for ${VM.LM_SYMBOLS[m]} link`);
|
4319
4319
|
}
|
4320
4320
|
}
|
4321
|
-
// NOTE:
|
4322
|
-
// point value between 0 and 1
|
4321
|
+
// NOTE: Share of cost is input as a percentage, but stored as a floating
|
4322
|
+
// point value between 0 and 1.
|
4323
|
+
// If SoC is changed, *all* output links must be redrawn.
|
4324
|
+
const soc_change = (l.share_of_cost !== soc / 100);
|
4323
4325
|
l.share_of_cost = soc / 100;
|
4324
4326
|
if(md.group.length > 1) {
|
4325
4327
|
// NOTE: Special care must be taken to not set special multipliers
|
@@ -4330,10 +4332,15 @@ console.log('HERE name conflicts', name_conflicts, mapping);
|
|
4330
4332
|
MODEL.focal_cluster.clearAllProcesses();
|
4331
4333
|
UI.drawDiagram(MODEL);
|
4332
4334
|
} else {
|
4333
|
-
|
4334
|
-
|
4335
|
-
|
4336
|
-
|
4335
|
+
if(soc_change) {
|
4336
|
+
// Redraw process with its links so that all SoC labels are updated.
|
4337
|
+
this.on_arrow.from_node.drawWithLinks();
|
4338
|
+
} else {
|
4339
|
+
// Only redraw the arrow shape that represents the edited link.
|
4340
|
+
this.paper.drawArrow(this.on_arrow);
|
4341
|
+
// Redraw the FROM node if link has become (or no longer is) "first commit".
|
4342
|
+
if(redraw) this.drawObject(this.on_arrow.from_node);
|
4343
|
+
}
|
4337
4344
|
}
|
4338
4345
|
md.hide();
|
4339
4346
|
}
|
@@ -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');
|
@@ -12,7 +12,7 @@ dialogs, the main drawing canvas, and event handler functions.
|
|
12
12
|
*/
|
13
13
|
|
14
14
|
/*
|
15
|
-
Copyright (c) 2017-
|
15
|
+
Copyright (c) 2017-2025 Delft University of Technology
|
16
16
|
|
17
17
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
18
18
|
of this software and associated documentation files (the "Software"), to deal
|
@@ -165,7 +165,7 @@ class ModelAutoSaver {
|
|
165
165
|
html += ['<tr class="dataset" style="color: gray" ',
|
166
166
|
'onclick="FILE_MANAGER.loadAutoSavedModel(\'',
|
167
167
|
m.name,'\');"><td class="restore-name">', m.name, '</td><td>',
|
168
|
-
m.date.substring(
|
168
|
+
m.date.substring(0, 16).replace('T', ' '),
|
169
169
|
'</td><td style="text-align: right">',
|
170
170
|
bytes[0], '</td><td>', bytes[1], '</td><td style="width:15px">',
|
171
171
|
'<img class="del-asm-btn" src="images/delete.png" ',
|