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 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 (June 2024)
40
- this is 22.2.0.
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: v22.2.0.
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-06-11 22:55:17] Static file: /index.html
304
- [2024-06-11 22:55:17] Static file: /scripts/iro.min.js
305
- [2024-06-11 22:55:17] Static file: /images/open.png
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "linny-r",
3
- "version": "2.0.6",
3
+ "version": "2.0.8",
4
4
  "description": "Executable graphical language with WYSIWYG editor for MILP models",
5
5
  "main": "server.js",
6
6
  "scripts": {
package/static/index.html CHANGED
@@ -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 graph having one or more variables.
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">
@@ -10,7 +10,7 @@ file that implements the graphical user interface for Linny-R.
10
10
  */
11
11
 
12
12
  /*
13
- Copyright (c) 2017-2024 Delft University of Technology
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: 440px;
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, '_').replace("'", '\u2019');
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: model settings will not be changed, but nevertheless restored
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: start with base case run, hence no active parameter yet
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 an SA run
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: use a "dummy experiment object" to ensure proper XML saving and
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: use aci because run #0 is the base case w/o active parameter
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
- // and ALL expressions are reset as well
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: clear chart only when done (charts do not update during experiment)
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: only consider data of base scenario
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: the properties below are relevant only for the GUI
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
- [v.displayName, n, data[nr][0], data[nr][1], data[nr][2],
1008
- data[nr][3], data[nr][4],
1009
- v.non_zero_tally, v.exceptions].join('</td><td>'),
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
- p = pts[this.selected_point],
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
- // Updates the edited constraint, or adds a new constraint to the model
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-2024 Delft University of Technology
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: share of cost is input as a percentage, but stored as a floating
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
- // Redraw the arrow shape that represents the edited link
4334
- this.paper.drawArrow(this.on_arrow);
4335
- // Redraw the FROM node if link has become (or no longer is) "first commit"
4336
- if(redraw) this.drawObject(this.on_arrow.from_node);
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-2024 Delft University of Technology
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(1, 16).replace('T', ' '),
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" ',