linny-r 1.9.3 → 2.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +172 -126
  3. package/console.js +2 -1
  4. package/package.json +1 -1
  5. package/post-install.js +93 -37
  6. package/server.js +73 -29
  7. package/static/images/eq-negated.png +0 -0
  8. package/static/images/power.png +0 -0
  9. package/static/images/tex.png +0 -0
  10. package/static/index.html +226 -11
  11. package/static/linny-r.css +458 -8
  12. package/static/scripts/linny-r-ctrl.js +6 -4
  13. package/static/scripts/linny-r-gui-actor-manager.js +1 -1
  14. package/static/scripts/linny-r-gui-chart-manager.js +20 -13
  15. package/static/scripts/linny-r-gui-constraint-editor.js +410 -50
  16. package/static/scripts/linny-r-gui-controller.js +138 -21
  17. package/static/scripts/linny-r-gui-dataset-manager.js +28 -20
  18. package/static/scripts/linny-r-gui-documentation-manager.js +11 -3
  19. package/static/scripts/linny-r-gui-equation-manager.js +1 -1
  20. package/static/scripts/linny-r-gui-experiment-manager.js +1 -1
  21. package/static/scripts/linny-r-gui-expression-editor.js +7 -1
  22. package/static/scripts/linny-r-gui-file-manager.js +63 -19
  23. package/static/scripts/linny-r-gui-finder.js +1 -1
  24. package/static/scripts/linny-r-gui-model-autosaver.js +1 -1
  25. package/static/scripts/linny-r-gui-monitor.js +1 -1
  26. package/static/scripts/linny-r-gui-paper.js +108 -25
  27. package/static/scripts/linny-r-gui-power-grid-manager.js +529 -0
  28. package/static/scripts/linny-r-gui-receiver.js +1 -1
  29. package/static/scripts/linny-r-gui-repository-browser.js +1 -1
  30. package/static/scripts/linny-r-gui-scale-unit-manager.js +1 -1
  31. package/static/scripts/linny-r-gui-sensitivity-analysis.js +1 -1
  32. package/static/scripts/linny-r-gui-tex-manager.js +110 -0
  33. package/static/scripts/linny-r-gui-undo-redo.js +1 -1
  34. package/static/scripts/linny-r-milp.js +1 -1
  35. package/static/scripts/linny-r-model.js +982 -123
  36. package/static/scripts/linny-r-utils.js +3 -3
  37. package/static/scripts/linny-r-vm.js +731 -252
  38. package/static/show-diff.html +1 -1
  39. package/static/show-png.html +1 -1
@@ -11,7 +11,7 @@ functionality for the Linny-R File Manager.
11
11
  */
12
12
 
13
13
  /*
14
- Copyright (c) 2017-2023 Delft University of Technology
14
+ Copyright (c) 2017-2024 Delft University of Technology
15
15
 
16
16
  Permission is hereby granted, free of charge, to any person obtaining a copy
17
17
  of this software and associated documentation files (the "Software"), to deal
@@ -49,6 +49,14 @@ class GUIFileManager {
49
49
  getRemoteData(dataset, url) {
50
50
  // Gets data from a URL, or from a file on the local host
51
51
  if(url === '') return;
52
+ if(url.indexOf('%') >= 0) {
53
+ // Expand %i, %j and %k if used in the URL.
54
+ const letters = ['i', 'j', 'k'];
55
+ for(let i = 0; i < letters.length; i++) {
56
+ const l = letters[i];
57
+ url = url.replaceAll('%' + l, valueOfIndexVariable(l));
58
+ }
59
+ }
52
60
  // NOTE: add this dataset to the "loading" list...
53
61
  addDistinct(dataset, MODEL.loading_datasets);
54
62
  // ... and allow for 3 more seconds (6 times 500 ms) to complete
@@ -63,19 +71,29 @@ class GUIFileManager {
63
71
  })
64
72
  .then((data) => {
65
73
  if(data !== '' && UI.postResponseOK(data)) {
66
- // Server must return either semicolon-separated or
67
- // newline-separated string of numbers
68
- if(data.indexOf(';') < 0) {
69
- // If no semicolon found, replace newlines by semicolons
70
- data = data.trim().split('\n').join(';');
71
- }
72
- // Remove all white space
73
- data = data.replace(/\s+/g, '');
74
- // Show data in text area when the SERIES dialog is visible
75
- if(!UI.hidden('series-modal')) {
76
- DATASET_MANAGER.series_data.value = data.split(';').join('\n');
74
+ if(dataset instanceof BoundLine) {
75
+ // Server must return semicolon-separated list of white-
76
+ // space-separated list of numbers.
77
+ dataset.unpackPointDataString(data);
78
+ // Show data in boundline data modal when it is visible.
79
+ if(!UI.hidden('boundline-data-modal')) {
80
+ CONSTRAINT_EDITOR.stopEditing(false);
81
+ }
77
82
  } else {
78
- dataset.unpackDataString(data);
83
+ // Server must return either semicolon-separated or
84
+ // newline-separated string of numbers
85
+ if(data.indexOf(';') < 0) {
86
+ // If no semicolon found, replace newlines by semicolons
87
+ data = data.trim().split('\n').join(';');
88
+ }
89
+ // Remove all white space
90
+ data = data.replace(/\s+/g, '');
91
+ // Show data in text area when the SERIES dialog is visible
92
+ if(!UI.hidden('series-modal')) {
93
+ DATASET_MANAGER.series_data.value = data.split(';').join('\n');
94
+ } else {
95
+ dataset.unpackDataString(data);
96
+ }
79
97
  }
80
98
  // NOTE: remove dataset from the "loading" list
81
99
  const i = MODEL.loading_datasets.indexOf(dataset);
@@ -235,7 +253,12 @@ class GUIFileManager {
235
253
  code.focus();
236
254
  }
237
255
 
238
- saveModel() {
256
+ saveModel(ws=false) {
257
+ // Save the current model either as a download (directly from the browser),
258
+ // or in the user workspace (via the server) when the Save button is
259
+ // Shift-clicked.
260
+ // NOTE: The File manager keeps track of which option to use.
261
+ this.save_to_workspace = ws;
239
262
  MODEL.clearSelection();
240
263
  if(MODEL.encrypt) {
241
264
  const md = UI.modals.password;
@@ -252,7 +275,17 @@ class GUIFileManager {
252
275
  }
253
276
 
254
277
  pushModelToBrowser(xml) {
278
+ // Save model as .lnr file.
255
279
  UI.setMessage('Model file size: ' + UI.sizeInBytes(xml.length));
280
+ // NOTE: Since version 2.0.2, Shift-click on the Save button means
281
+ // that the model should be saved in the user workspace.
282
+ if(this.save_to_workspace) {
283
+ // Immediately reset the flag...
284
+ this.save_to_workspace = false;
285
+ // ... but pass is on to the auto-save routine.
286
+ this.storeAutoSavedModel(true);
287
+ return;
288
+ }
256
289
  const el = document.getElementById('xml-saver');
257
290
  el.href = 'data:attachment/text,' + encodeURI(xml);
258
291
  console.log('Encoded file size:', el.href.length);
@@ -263,6 +296,9 @@ class GUIFileManager {
263
296
  'If it does not download, store it in a repository');
264
297
  }
265
298
  el.click();
299
+ // Clear the HREF after 3 seconds or it may use a lot of memory.
300
+ setTimeout(
301
+ () => { document.getElementById('xml-saver').href = ''; }, 3000);
266
302
  UI.normalCursor();
267
303
  }
268
304
 
@@ -271,7 +307,7 @@ class GUIFileManager {
271
307
  md = UI.modals.password,
272
308
  code = md.element('code'),
273
309
  pwd = code.value;
274
- // NOTE: immediately clear password field
310
+ // NOTE: Immediately clear password field.
275
311
  code.value = '';
276
312
  md.hide();
277
313
  if(pwd !== md.encryption_code) {
@@ -280,7 +316,7 @@ class GUIFileManager {
280
316
  }
281
317
  UI.setMessage('Encrypting...');
282
318
  UI.waitingCursor();
283
- // Wait for key (NOTE: asynchronous functions defined in linny-r.js)
319
+ // Wait for key (NOTE: asynchronous functions defined in linny-r.js).
284
320
  encryptionKey(pwd)
285
321
  .then((key) => encryptMessage(MODEL.asXML.replace(/#/g, '%23'), key)
286
322
  .then((enc) => this.pushModelToBrowser(MODEL.asEncryptedXML(enc)))
@@ -311,8 +347,12 @@ class GUIFileManager {
311
347
  .catch((err) => UI.warn(UI.WARNING.NO_CONNECTION, err));
312
348
  }
313
349
 
314
- storeAutoSavedModel() {
315
- // Stores the current model in the local auto-save directory
350
+ storeAutoSavedModel(workspace=false) {
351
+ // Store the current model in the local auto-save directory, or in
352
+ // the local models directory when `workspace` = TRUE.
353
+ // NOTE: Always reset the "save to workspace" flag, because it may
354
+ // have not been cleared when the user canceled encryption.
355
+ this.save_to_workspace = false;
316
356
  const bcl = document.getElementById('autosave-btn').classList;
317
357
  if(MODEL.running_experiment) {
318
358
  console.log('No autosaving while running an experiment');
@@ -324,7 +364,8 @@ class GUIFileManager {
324
364
  file: REPOSITORY_BROWSER.asFileName(
325
365
  (MODEL.name || 'no-name') + '_by_' +
326
366
  (MODEL.author || 'no-author')),
327
- xml: MODEL.asXML
367
+ xml: MODEL.asXML,
368
+ wsd: workspace
328
369
  }))
329
370
  .then((response) => {
330
371
  if(!response.ok) {
@@ -338,6 +379,9 @@ class GUIFileManager {
338
379
  AUTO_SAVE.interval = 0;
339
380
  AUTO_SAVE.not_implemented = true;
340
381
  console.log('Auto-save disabled');
382
+ } else if(workspace) {
383
+ // Notify user where the model file has been stored.
384
+ UI.notify(data);
341
385
  }
342
386
  bcl.remove('stay-activ');
343
387
  })
@@ -13,7 +13,7 @@ model.
13
13
  */
14
14
 
15
15
  /*
16
- Copyright (c) 2017-2023 Delft University of Technology
16
+ Copyright (c) 2017-2024 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
@@ -12,7 +12,7 @@ dialogs, the main drawing canvas, and event handler functions.
12
12
  */
13
13
 
14
14
  /*
15
- Copyright (c) 2017-2023 Delft University of Technology
15
+ Copyright (c) 2017-2024 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
@@ -11,7 +11,7 @@ for the Linny-R Monitor dialog.
11
11
  */
12
12
 
13
13
  /*
14
- Copyright (c) 2017-2023 Delft University of Technology
14
+ Copyright (c) 2017-2024 Delft University of Technology
15
15
 
16
16
  Permission is hereby granted, free of charge, to any person obtaining a copy
17
17
  of this software and associated documentation files (the "Software"), to deal
@@ -11,7 +11,7 @@ functionality for the Linny-R model editor.
11
11
  */
12
12
 
13
13
  /*
14
- Copyright (c) 2017-2023 Delft University of Technology
14
+ Copyright (c) 2017-2024 Delft University of Technology
15
15
 
16
16
  Permission is hereby granted, free of charge, to any person obtaining a copy
17
17
  of this software and associated documentation files (the "Software"), to deal
@@ -998,7 +998,7 @@ class Paper {
998
998
  let cnb, proc, prod, fnx, fny, fnw, fnh, tnx, tny, tnw, tnh,
999
999
  cp, rr, aa, bb, dd, nn, af, l, s, w, tw, th, bpx, bpy, epx, epy,
1000
1000
  sda, stroke_color, stroke_width, arrow_start, arrow_end,
1001
- font_color, font_weight, luc = null;
1001
+ font_color, font_weight, luc = null, grid = null;
1002
1002
  // Get the main arrow attributes
1003
1003
  const
1004
1004
  from_nb = arrw.from_node,
@@ -1243,9 +1243,12 @@ class Paper {
1243
1243
  prod = luc.from_node;
1244
1244
  }
1245
1245
  // NOTE: `luc` may also be a constraint!
1246
- if(luc instanceof Link && luc.is_feedback) {
1247
- sda = UI.sda.long_dash_dot;
1248
- arrow_end = this.feedback_triangle;
1246
+ if(luc instanceof Link) {
1247
+ grid = proc.grid;
1248
+ if(luc.is_feedback) {
1249
+ sda = UI.sda.long_dash_dot;
1250
+ arrow_end = this.feedback_triangle;
1251
+ }
1249
1252
  }
1250
1253
  // Data link => dotted line
1251
1254
  if(luc.dataOnly) {
@@ -1474,11 +1477,27 @@ class Paper {
1474
1477
  epy = arrw.from_y + (shift + bi) * dy / l;
1475
1478
  font_color = this.palette.produced;
1476
1479
  }
1477
- // Draw the rate in a semi-transparent white roundbox.
1478
- arrw.shape.addRect(epx, epy, tw, th,
1479
- {fill: 'white', opacity: 0.8, rx: 2, ry: 2});
1480
- arrw.shape.addNumber(epx, epy, s, {fill: font_color, 'font-style': rrfs});
1481
-
1480
+ let with_rate = true;
1481
+ if(grid) {
1482
+ // For power links, only draw the rate when the model has been run
1483
+ // and the actual flow is less than the process level (so the rate
1484
+ // then reflects the loss).
1485
+ const
1486
+ absf = Math.abs(af),
1487
+ apl = Math.abs(proc.actualLevel(MODEL.t));
1488
+ with_rate = MODEL.solved && apl - absf > VM.SIG_DIF_FROM_ZERO;
1489
+ font_color = 'gray';
1490
+ s = VM.sig4Dig(absf / apl);
1491
+ bb = this.numberSize(s);
1492
+ th = bb.height;
1493
+ tw = Math.max(th, bb.width);
1494
+ }
1495
+ if(with_rate) {
1496
+ // Draw the rate in a semi-transparent white roundbox.
1497
+ arrw.shape.addRect(epx, epy, tw, th,
1498
+ {fill: 'white', opacity: 0.8, rx: 2, ry: 2});
1499
+ arrw.shape.addNumber(epx, epy, s, {fill: font_color, 'font-style': rrfs});
1500
+ }
1482
1501
  // Draw the share of cost (only if relevant and > 0) behind the rate
1483
1502
  // in a pale yellow filled box.
1484
1503
  if(MODEL.infer_cost_prices && luc.share_of_cost > 0) {
@@ -1514,11 +1533,13 @@ class Paper {
1514
1533
  }
1515
1534
 
1516
1535
  // Draw the actual flow
1517
- if(l > 0 && af < VM.UNDEFINED && Math.abs(af) > VM.SIG_DIF_FROM_ZERO) {
1536
+ const absf = Math.abs(af);
1537
+ if(l > 0 && af < VM.UNDEFINED && absf > VM.SIG_DIF_FROM_ZERO) {
1518
1538
  const ffill = {fill:'white', opacity:0.8};
1519
1539
  if(luc || mf[0] == 1) {
1520
- // Draw flow data halfway the arrow only if calculated and non-zero
1521
- s = VM.sig4Dig(af);
1540
+ // Draw flow data halfway the arrow only if calculated and non-zero.
1541
+ // NOTE: Power flows are always absolute flows.
1542
+ s = VM.sig4Dig(grid ? absf : af);
1522
1543
  bb = this.numberSize(s, 10, 700);
1523
1544
  tw = bb.width/2;
1524
1545
  th = bb.height/2;
@@ -1861,11 +1882,14 @@ class Paper {
1861
1882
  const
1862
1883
  bl = c.bound_lines[i],
1863
1884
  // Draw thumbnail in shades of the arrow color, but use black
1864
- // for regular color or the filled areas turn out too light
1885
+ // for regular color or the filled areas turn out too light.
1865
1886
  clr = (stroke_color === this.palette.node_rim ? 'black' : stroke_color);
1887
+ // Set the boundline point coordinates (TRUE indicates: also compute
1888
+ // the thumbnail SVG).
1889
+ bl.setDynamicPoints(MODEL.t, true);
1866
1890
  el = this.newSVGElement('path');
1867
1891
  if(bl.type === VM.EQ) {
1868
- // For EQ bound lines, draw crisp line on silver background
1892
+ // For EQ bound lines, draw crisp line on silver background.
1869
1893
  this.addSVGAttributes(el,
1870
1894
  {d: bl.contour_path, fill: 'none', stroke: clr, 'stroke-width': 30});
1871
1895
  } else {
@@ -1875,7 +1899,7 @@ class Paper {
1875
1899
  svg.appendChild(el);
1876
1900
  }
1877
1901
  // Draw the share of cost (only if relevant and non-zero) near tail
1878
- // (or head if Y->X) of arrow in a pale yellow filled box
1902
+ // (or head if Y->X) of arrow in a pale yellow filled box.
1879
1903
  if(MODEL.infer_cost_prices && c.share_of_cost) {
1880
1904
  let s = VM.sig4Dig(c.share_of_cost * 100) + '%',
1881
1905
  bb = this.numberSize(s, 7),
@@ -2017,7 +2041,11 @@ class Paper {
2017
2041
  // Negative level => more reddish stroke and font
2018
2042
  font_color = this.palette.compound_flow;
2019
2043
  stroke_color = font_color;
2020
- if(lb < -VM.NEAR_ZERO) bar_ratio = l / lb;
2044
+ if(proc.grid) {
2045
+ bar_ratio = l / -ub;
2046
+ } else if(lb < -VM.NEAR_ZERO) {
2047
+ bar_ratio = l / lb;
2048
+ }
2021
2049
  stroke_width = 1.25;
2022
2050
  } else {
2023
2051
  font_color = this.palette.active_process;
@@ -2027,7 +2055,9 @@ class Paper {
2027
2055
  }
2028
2056
  // For options, set longer-dashed rim if committed at time <= t
2029
2057
  const fcn = (is_fc_option ? proc : fc_option_node);
2030
- if(fcn && fcn.start_ups.length > 0 && MODEL.t >= fcn.start_ups[0]) {
2058
+ // NOTE: When initial level > 0, option is already committed at t=0.
2059
+ if(fcn && (fcn.actualLevel(0) > 0 ||
2060
+ (fcn.start_ups.length > 0 && MODEL.t >= fcn.start_ups[0]))) {
2031
2061
  sda = UI.sda.longer_dash;
2032
2062
  }
2033
2063
  } else if(il) {
@@ -2052,7 +2082,7 @@ class Paper {
2052
2082
  hw = proc.width / 2;
2053
2083
  hh = proc.height / 2;
2054
2084
  }
2055
- // Draw frame using colors as defined above
2085
+ // Draw frame using colors as defined above.
2056
2086
  proc.shape.addRect(x, y, 2 * hw, 2 * hh,
2057
2087
  {fill: fill_color, stroke: stroke_color, 'stroke-width': stroke_width,
2058
2088
  'stroke-dasharray': sda, 'stroke-linecap': 'round'});
@@ -2062,10 +2092,19 @@ class Paper {
2062
2092
  const
2063
2093
  hsw = stroke_width / 2,
2064
2094
  hbl = hh * bar_ratio - hsw;
2065
- // NOTE: when level < 0, bar drops down from top
2066
- proc.shape.addRect(x + hw - 4 - hsw,
2067
- (l < 0 ? y - hh + hbl + hsw : y + hh - hbl - hsw),
2068
- 8, 2 * hbl, {fill: bar_color, stroke: 'none'});
2095
+ // Collapesed grid processes display a "wire" instead of a bar.
2096
+ if(proc.grid && proc.collapsed) {
2097
+ proc.shape.addPath(
2098
+ ['M', x - hw + 0.5, ',', y - hh/2, 'L', x + hw - 0.5, ',', y - hh/2],
2099
+ // NOTE: Use *squared* bar ratio to reflect quadratic losses.
2100
+ {fill: 'none', stroke: proc.grid.color,
2101
+ 'stroke-width': hh * bar_ratio * bar_ratio});
2102
+ } else {
2103
+ // NOTE: when level < 0, bar drops down from top
2104
+ proc.shape.addRect(x + hw - 4 - hsw,
2105
+ (l < 0 ? y - hh + hbl + hsw : y + hh - hbl - hsw),
2106
+ 8, 2 * hbl, {fill: bar_color, stroke: 'none'});
2107
+ }
2069
2108
  }
2070
2109
  // If semi-continuous, add a double rim 2 px above the bottom line
2071
2110
  if(proc.level_to_zero) {
@@ -2073,6 +2112,42 @@ class Paper {
2073
2112
  proc.shape.addPath(['M', x - hw, ',', bly, 'L', x + hw, ',', bly],
2074
2113
  {'fill': 'none', stroke: stroke_color, 'stroke-width': 0.6});
2075
2114
  }
2115
+ // If grid element, add colored strip at bottom.
2116
+ if(proc.grid) {
2117
+ proc.shape.addRect(x, y + hh - 3.3, 2*hw - 1.5, 6,
2118
+ {'fill': proc.grid.color, stroke: 'none'});
2119
+ // If grid enforces Kirchhoff's voltage law and/or losses, length
2120
+ // matters, so draw a white horizontal line through the strip that
2121
+ // is proportional to the length property of the process.
2122
+ if(MODEL.solved &&
2123
+ (proc.grid.kirchhoff || proc.grid.loss_approximation)) {
2124
+ const
2125
+ maxl = Math.max(1, POWER_GRID_MANAGER.max_length),
2126
+ w = (2 * hw - 8) * proc.length_in_km / maxl,
2127
+ bly = y + hh - 3.3;
2128
+ proc.shape.addPath(
2129
+ ['M', x - w/2, ',', bly, 'L', x + w/2, ',', bly],
2130
+ {'fill': 'none', stroke: 'white', 'stroke-width': 1.5,
2131
+ 'stroke-linecap': 'round'});
2132
+ }
2133
+ // If process has no capacity, cross it out.
2134
+ if(ub <= VM.NEAR_ZERO) {
2135
+ proc.shape.addPath(
2136
+ ['M', x - hw + 0.8, ',', y - hh + 0.5,
2137
+ 'L', x + hw - 0.5, ',', y + hh - 0.5,
2138
+ 'M', x - hw + 0.8, ',', y + hh - 0.5,
2139
+ 'L', x + hw - 0.5, ',', y - hh + 0.5],
2140
+ {fill: 'none', stroke: 'white', 'stroke-width': 2,
2141
+ 'stroke-linecap': 'round'});
2142
+ proc.shape.addPath(
2143
+ ['M', x - hw + 0.8, ',', y - hh + 0.5,
2144
+ 'L', x + hw - 0.5, ',', y + hh - 0.5,
2145
+ 'M', x - hw + 0.8, ',', y + hh - 0.5,
2146
+ 'L', x + hw - 0.5, ',', y - hh + 0.5],
2147
+ {fill: 'none', stroke: proc.grid.color, 'stroke-width': 1,
2148
+ 'stroke-linecap': 'round'});
2149
+ }
2150
+ }
2076
2151
  if(!proc.collapsed) {
2077
2152
  // If model has been computed or initial level is non-zero, draw
2078
2153
  // production level in upper right corner
@@ -2102,6 +2177,10 @@ class Paper {
2102
2177
  proc.shape.addNumber(cx, cy, s,
2103
2178
  {'font-size': 9, 'fill': font_color, 'font-weight': 700});
2104
2179
  }
2180
+ if(proc.grid && POWER_GRID_MANAGER.inCycle(proc)) {
2181
+ proc.shape.addText(x + hw - 2, y - hh + bh + 3, '\u27F3',
2182
+ {'font-size': 9, fill: 'black', 'text-anchor':'end'});
2183
+ }
2105
2184
  }
2106
2185
  // Draw boundaries in upper left corner
2107
2186
  // NOTE: their expressions should have been computed
@@ -2118,11 +2197,11 @@ class Paper {
2118
2197
  } else {
2119
2198
  const ubs = (ub >= VM.PLUS_INFINITY && !proc.upper_bound.defined ?
2120
2199
  '\u221E' : VM.sig4Dig(ub));
2121
- if(Math.abs(lb) > VM.NEAR_ZERO) {
2200
+ if(Math.abs(lb) > VM.NEAR_ZERO && !proc.grid) {
2122
2201
  // If lb <> 0 then lb...ub (with ellipsis).
2123
2202
  s += '\u2026' + ubs;
2124
2203
  } else {
2125
- // If lb = 0 show only the upper bound.
2204
+ // If gid process or lb = 0, show only the upper bound.
2126
2205
  s = ubs;
2127
2206
  lbw = 0;
2128
2207
  }
@@ -2137,6 +2216,10 @@ class Paper {
2137
2216
  ty = y - hh + sh/2 + 1;
2138
2217
  proc.shape.addNumber(tx + btw/2, ty, s,
2139
2218
  {fill: 'black', 'font-style': bfs});
2219
+ if(proc.grid) {
2220
+ proc.shape.addText(tx + 1, ty + 8, proc.grid.power_unit,
2221
+ {'font-size': 6, fill: 'black', 'text-anchor':'start'});
2222
+ }
2140
2223
  // Show start/stop-related status right of the process boundaries.
2141
2224
  // NOTE: lb must be > 0 for start/stop to work.
2142
2225
  if(proc.level_to_zero && lbw) {