linny-r 2.1.5 → 2.1.7

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.
@@ -50,12 +50,27 @@ class Finder {
50
50
  this.chart_btn = document.getElementById('finder-chart-btn');
51
51
  this.chart_btn.addEventListener(
52
52
  'click', () => FINDER.confirmAddChartVariables());
53
+ this.table_btn = document.getElementById('finder-table-btn');
54
+ this.table_btn.addEventListener(
55
+ 'click', () => FINDER.toggleViewAttributes());
56
+ this.experiment_btn = document.getElementById('finder-experiment-btn');
57
+ this.experiment_btn.addEventListener(
58
+ 'click', () => FINDER.toggleViewExperiment());
53
59
  this.copy_btn = document.getElementById('finder-copy-btn');
54
60
  this.copy_btn.addEventListener(
55
61
  'click', (event) => FINDER.copyAttributesToClipboard(event.shiftKey));
62
+ this.entity_scroll_area = document.getElementById('finder-scroll-area');
63
+ this.entity_scroll_area.addEventListener(
64
+ 'scroll', () => FINDER.scrollEntityArea());
56
65
  this.entity_table = document.getElementById('finder-table');
57
66
  this.item_table = document.getElementById('finder-item-table');
58
67
  this.expression_table = document.getElementById('finder-expression-table');
68
+ this.data_pane = document.getElementById('finder-data-pane');
69
+ this.data_header = document.getElementById('finder-data-header');
70
+ this.data_scroll_area = document.getElementById('finder-data-scroll-area');
71
+ this.data_scroll_area.addEventListener(
72
+ 'scroll', () => FINDER.scrollDataArea());
73
+ this.data_table = document.getElementById('finder-data-table');
59
74
 
60
75
  // The Confirm add chart variables modal.
61
76
  this.add_chart_variables_modal = new ModalDialog('confirm-add-chart-variables');
@@ -97,6 +112,8 @@ class Finder {
97
112
  // Product cluster index "remembers" for which cluster a product was
98
113
  // last revealed, so it can reveal the next cluster when clicked again.
99
114
  this.product_cluster_index = 0;
115
+ this.tabular_view = false;
116
+ this.experiment_view = false;
100
117
  }
101
118
 
102
119
  doubleClicked(obj) {
@@ -150,8 +167,23 @@ class Finder {
150
167
  let imgs = '';
151
168
  this.entities.length = 0;
152
169
  this.filtered_types.length = 0;
153
- // No list unless a pattern OR a specified SUB-set of entity types.
154
- if(fp || et && et !== VM.entity_letters) {
170
+ if(this.experiment_view) {
171
+ // List outcome variables of selected experiment.
172
+ const x = EXPERIMENT_MANAGER.selected_experiment;
173
+ if(x) {
174
+ x.inferVariables();
175
+ for(const v of x.variables) {
176
+ const obj = v.object;
177
+ if(et !== VM.entity_letters && et.indexOf(obj.typeLetter) >= 0) {
178
+ if(!fp || patternMatch(obj.displayName, this.filter_pattern)) {
179
+ this.entities.push(v);
180
+ enl.push(v.displayName);
181
+ }
182
+ }
183
+ }
184
+ }
185
+ } else if(fp || et && et !== VM.entity_letters) {
186
+ // No list unless a pattern OR a specified SUB-set of entity types.
155
187
  if(et.indexOf('A') >= 0) {
156
188
  imgs += '<img src="images/actor.png">';
157
189
  for(let k in MODEL.actors) if(MODEL.actors.hasOwnProperty(k)) {
@@ -315,32 +347,44 @@ class Finder {
315
347
  }
316
348
  }
317
349
  }
350
+ // NOTE: Pass TRUE to indicate "comparison of identifiers".
318
351
  enl.sort((a, b) => UI.compareFullNames(a, b, true));
319
352
  }
320
353
  document.getElementById('finder-entity-imgs').innerHTML = imgs;
321
- let seid = 'etr';
322
- for(let i = 0; i < enl.length; i++) {
323
- const e = MODEL.objectByID(enl[i]);
324
- if(e === se) seid += i;
325
- el.push(['<tr id="etr', i, '" class="dataset',
326
- (e === se ? ' sel-set' : ''), '" onclick="FINDER.selectEntity(\'',
327
- enl[i], '\', event.altKey);" onmouseover="FINDER.showInfo(\'', enl[i],
328
- '\', event.shiftKey);"><td draggable="true" ',
329
- 'ondragstart="FINDER.drag(event);"><img class="finder" src="images/',
330
- e.type.toLowerCase(), '.png">', e.displayName,
331
- '</td></tr>'].join(''));
354
+ let n = enl.length,
355
+ seid = 'etr';
356
+ for(let i = 0; i < n; i++) {
357
+ if(this.experiment_view) {
358
+ el.push(['<tr id="etr', i, '" class="dataset"><td>',
359
+ '<div class="series">', enl[i], '</div></td></tr>'].join(''));
360
+ } else {
361
+ const e = MODEL.objectByID(enl[i]);
362
+ if(e === se) seid += i;
363
+ el.push(['<tr id="etr', i, '" class="dataset',
364
+ (e === se ? ' sel-set' : ''), '" onclick="FINDER.selectEntity(\'',
365
+ enl[i], '\', event.altKey);" onmouseover="FINDER.showInfo(\'', enl[i],
366
+ '\', event.shiftKey);"><td draggable="true" ',
367
+ 'ondragstart="FINDER.drag(event);"><img class="finder" src="images/',
368
+ e.type.toLowerCase(), '.png">', e.displayName,
369
+ '</td></tr>'].join(''));
370
+ }
332
371
  }
333
372
  // NOTE: Reset `selected_entity` if not in the new list.
334
373
  if(seid === 'etr') this.selected_entity = null;
335
374
  this.entity_table.innerHTML = el.join('');
336
375
  UI.scrollIntoView(document.getElementById(seid));
337
- document.getElementById('finder-count').innerHTML = pluralS(
338
- el.length, 'entity', 'entities');
339
- // Only show the edit button if all filtered entities are of the
340
- // same type.
341
- let n = el.length;
376
+ document.getElementById('finder-count').innerHTML = pluralS(n,
377
+ 'entity', 'entities');
342
378
  this.edit_btn.style.display = 'none';
379
+ this.chart_btn.style.display = 'none';
380
+ this.table_btn.style.display = 'none';
343
381
  this.copy_btn.style.display = 'none';
382
+ /*
383
+ // Show the experiment button only when at least 1 experiment exists.
384
+ this.experiment_btn.style.display = (MODEL.experiments.length ?
385
+ 'inline-block' : 'none');
386
+ */
387
+ // Only show other buttons if the set of filtered entities is not empty.
344
388
  if(n > 0) {
345
389
  this.copy_btn.style.display = 'inline-block';
346
390
  if(CHART_MANAGER.visible && CHART_MANAGER.chart_index >= 0) {
@@ -351,13 +395,27 @@ class Finder {
351
395
  this.chart_btn.style.display = 'inline-block';
352
396
  }
353
397
  }
398
+ // NOTE: Enable editing and tabular view only when filter results
399
+ // in a single entity type.
354
400
  n = this.entityGroup.length;
355
401
  if(n > 0) {
356
402
  this.edit_btn.title = 'Edit attributes of ' +
357
403
  pluralS(n, this.entities[0].type.toLowerCase());
358
404
  this.edit_btn.style.display = 'inline-block';
405
+ this.table_btn.style.display = 'inline-block';
359
406
  }
360
407
  }
408
+ // Show toggle button status.
409
+ if(this.tabular_view) {
410
+ this.table_btn.classList.add('stay-activ');
411
+ } else {
412
+ this.table_btn.classList.remove('stay-activ');
413
+ }
414
+ if(this.experiment_view) {
415
+ this.experiment_btn.classList.add('stay-activ');
416
+ } else {
417
+ this.experiment_btn.classList.remove('stay-activ');
418
+ }
361
419
  this.updateRightPane();
362
420
  }
363
421
 
@@ -379,10 +437,10 @@ class Finder {
379
437
  if(this.filtered_types.length === 1 && ft !== 'E') {
380
438
  for(const e of this.entities) {
381
439
  // Exclude "no actor" and top cluster.
382
- if(e.name && e.name !== '(no_actor)' && e.name !== '(top_cluster)' &&
440
+ if(!e.name || (e.name !== '(no_actor)' && e.name !== '(top_cluster)' &&
383
441
  // Also exclude actor cash flow data products because
384
442
  // many of their properties should not be changed.
385
- !e.name.startsWith('$')) {
443
+ !e.name.startsWith('$'))) {
386
444
  eg.push(e);
387
445
  }
388
446
  }
@@ -406,7 +464,13 @@ class Finder {
406
464
  for(const a of ca) {
407
465
  html += `<option value="${a}">${VM.attribute_names[a]}</option>`;
408
466
  }
409
- md.element('attribute').innerHTML = html;
467
+ if(html) {
468
+ md.element('attr-of').style.display = 'inline-block';
469
+ md.element('attribute').innerHTML = html;
470
+ } else {
471
+ md.element('attr-of').style.display = 'none';
472
+ md.element('attribute').innerHTML = '';
473
+ }
410
474
  md.element('count').innerText = et;
411
475
  md.show();
412
476
  }
@@ -440,7 +504,33 @@ class Finder {
440
504
  md.hide();
441
505
  }
442
506
 
507
+ scrollEntityArea() {
508
+ // When in tabular view, the data table must scroll along with the
509
+ // entity table.
510
+ if(this.tabular_view) {
511
+ this.data_scroll_area.scrollTop = this.entity_scroll_area.scrollTop;
512
+ }
513
+ }
514
+
515
+ scrollDataArea() {
516
+ // When in tabular view, the entity table must scroll along with the
517
+ // data table.
518
+ if(this.tabular_view) {
519
+ this.entity_scroll_area.scrollTop = this.data_scroll_area.scrollTop;
520
+ }
521
+ }
522
+
443
523
  updateRightPane() {
524
+ // Right pane can display attribute data...
525
+ if(this.tabular_view) {
526
+ this.data_pane.style.display = 'block';
527
+ this.updateTabularView();
528
+ return;
529
+ }
530
+ // ... or no data...
531
+ this.data_pane.style.display = 'none';
532
+ this.data_table.innerHTML = '';
533
+ // ... but information on the occurence of the selected entity.
444
534
  const
445
535
  se = this.selected_entity,
446
536
  occ = [], // list with occurrences (clusters, processes or charts)
@@ -622,6 +712,152 @@ class Finder {
622
712
  document.getElementById('finder-expression-hdr').innerHTML =
623
713
  pluralS(el.length, 'expression');
624
714
  }
715
+
716
+ toggleViewAttributes() {
717
+ // Show/hide tabular display of entity attributes.
718
+ this.tabular_view = !this.tabular_view;
719
+ this.updateRightPane();
720
+ if(this.tabular_view) {
721
+ this.table_btn.classList.add('stay-activ');
722
+ } else {
723
+ this.table_btn.classList.remove('stay-activ');
724
+ }
725
+ }
726
+
727
+ toggleViewExperiment() {
728
+ // Switch between model entities and experiment outcomes.
729
+ this.experiment_view = !this.experiment_view;
730
+ if(this.experiment_view) this.tabular_view = true;
731
+ this.updateDialog();
732
+ }
733
+
734
+ updateTabularView() {
735
+ // Display data values when tabular view is active.
736
+ if(!this.entities.length ||
737
+ (this.filtered_types.length !== 1 && !this.experiment_view)) {
738
+ this.data_table.innerHTML = '';
739
+ return;
740
+ }
741
+ const
742
+ special = ['\u221E', '-\u221E', '\u2047', '\u00A2'],
743
+ rows = [],
744
+ etl = this.entities[0].typeLetter,
745
+ data_list = [],
746
+ data = {};
747
+ // Collect data and sort list by name, so it coresponds with the
748
+ // entities listed in the left pane.
749
+ if(this.experiment_view) {
750
+ // Get selected runs.
751
+ const
752
+ x = EXPERIMENT_MANAGER.selected_experiment,
753
+ runs = (x ? x.chart_combinations : []);
754
+ if(!runs.length) {
755
+ UI.notify('');
756
+ this.data_table.innerHTML = '';
757
+ return;
758
+ }
759
+ // Add aray for each run.
760
+ data[0] = [];
761
+ for(const e of this.entities) {
762
+ const run_data = {name: e.object.displayName};
763
+ // Add value for each run.
764
+ data_list.push(run_data);
765
+ }
766
+ data_list.sort((a, b) => UI.compareFullNames(a.name, b.name));
767
+ } else {
768
+ for(const e of this.entities) data_list.push(e.attributes);
769
+ data_list.sort((a, b) => UI.compareFullNames(a.name, b.name));
770
+ // The data "matrix" then holds values as an array per attribute code.
771
+ // NOTE: Datasets are special in that their data is a multi-line
772
+ // string of tab-separated key-value pairs where the first pair has no
773
+ // key (dataset default value) and the other pairs have a dataset
774
+ // modifier selector as key.
775
+ if(etl === 'D') {
776
+ // First compile the list of unique selectors.
777
+ const sel = [];
778
+ for(const ed of data_list) {
779
+ // NOTE: Dataset modifier lines start with a tab.
780
+ const lines = ed.D.split('\n\t');
781
+ // Store default value in entity data object for second iteration.
782
+ ed.dv = VM.sig4Dig(safeStrToFloat(lines[0].trim(), 0));
783
+ for(let i = 1; i < lines.length; i++) {
784
+ const pair = lines[i].split('\t');
785
+ if(pair[0]) {
786
+ addDistinct(pair[0], sel);
787
+ // Store pair value in entity data object for second iteration.
788
+ ed[pair[0]] = (pair.length > 1 ? pair[1] : '');
789
+ }
790
+ }
791
+ }
792
+ sel.sort(compareSelectors);
793
+ // Initialize arrays for default values and for selectors.
794
+ // NOTE: The parentheses of '(default)'ensure that there is no doubling
795
+ // with a selector defined by the modeler.
796
+ data['(default)'] = [];
797
+ for(const s of sel) data[s] = [];
798
+ // Perform second iteration.
799
+ for(const ed of data_list) {
800
+ data['(default)'].push(ed.dv);
801
+ for(const s of sel) {
802
+ if(ed[s]) {
803
+ const f = parseFloat(ed[s]);
804
+ data[s].push(isNaN(f) ? ed[s] : VM.sig4Dig(f));
805
+ } else {
806
+ // Empty string to denote "no modifier => not calculated".
807
+ data[s].push('\u2047');
808
+ }
809
+ }
810
+ }
811
+ } else {
812
+ // Initialize array per selector.
813
+ let atcodes = VM.attribute_codes[etl];
814
+ if(!MODEL.solved) atcodes = complement(atcodes, VM.level_based_attr);
815
+ if(!MODEL.infer_cost_prices) atcodes = complement(atcodes, ['CP', 'HCP', 'SOC']);
816
+ for(const ac of atcodes) data[ac] = [];
817
+ for(const ed of data_list) {
818
+ for(const ac of atcodes) {
819
+ let v = ed[ac];
820
+ if(v === '') {
821
+ // Empty strings denote "undefined".
822
+ v = '\u2047';
823
+ // Keep special values such as infinity and exception codes.
824
+ } else if(special.indexOf(v) < 0) {
825
+ // When model is not solved, expression values will be the
826
+ // expression string, and this is likely to be not parsable.
827
+ const f = parseFloat(v);
828
+ if(isNaN(f)) {
829
+ v = '\u2297'; // Circled X to denote "not computed".
830
+ } else {
831
+ v = VM.sig4Dig(parseFloat(f.toPrecision(4)));
832
+ }
833
+ }
834
+ data[ac].push(v);
835
+ }
836
+ }
837
+ }
838
+ }
839
+ // Create header.
840
+ const
841
+ keys = Object.keys(data),
842
+ row = [],
843
+ perc = (97 / keys.length).toPrecision(3),
844
+ style = `min-width: ${perc}%; max-width: ${perc}%`;
845
+ for(const k of keys) {
846
+ row.push(`<td style="${style}">${k}</td>`);
847
+ }
848
+ this.data_header.innerHTML = '<tr>' + row.join('') + '</tr>';
849
+ // Format each array with uniform decimals.
850
+ for(const k of keys) uniformDecimals(data[k]);
851
+ const n = data_list.length;
852
+ for(let index = 0; index < n; index++) {
853
+ const row = [];
854
+ for(const k of keys) {
855
+ row.push(`<td style="${style}">${data[k][index]}</td>`);
856
+ }
857
+ rows.push('<tr>' + row.join('') + '</tr>');
858
+ }
859
+ this.data_table.innerHTML = rows.join('');
860
+ }
625
861
 
626
862
  drag(ev) {
627
863
  // Start dragging the selected entity.
@@ -977,9 +1213,11 @@ class Finder {
977
1213
  // Also update the draggable dialogs that may be affected.
978
1214
  UI.updateControllerDialogs('CDEFIJX');
979
1215
  }
980
-
1216
+
981
1217
  copyAttributesToClipboard(shift) {
982
1218
  // Copy relevant entity attributes as tab-separated text to clipboard.
1219
+ // When copy button is Shift-clicked, only data for the selected entity
1220
+ // is copied.
983
1221
  // NOTE: All entity types have "get" method `attributes` that returns an
984
1222
  // object that for each defined attribute (and if model has been
985
1223
  // solved also each inferred attribute) has a property with its value.
@@ -173,6 +173,12 @@ class PowerGridManager {
173
173
  MODEL.ignore_KVL = UI.boxChecked('power-grids-KVL');
174
174
  MODEL.ignore_power_losses = UI.boxChecked('power-grids-losses');
175
175
  this.dialog.hide();
176
+ const pg_btn = document.getElementById('settings-power-btn');
177
+ if(MODEL.ignore_grid_capacity || MODEL.ignore_KVL || MODEL.ignore_power_losses) {
178
+ pg_btn.classList.add('ignore');
179
+ } else {
180
+ pg_btn.classList.remove('ignore');
181
+ }
176
182
  }
177
183
 
178
184
  selectPowerGrid(event, id, focus) {
@@ -581,7 +581,13 @@ module.exports = class MILPSolver {
581
581
  col++;
582
582
  }
583
583
  // Return near-zero values as 0.
584
- x_values.push(Math.abs(x_dict[v]) < this.near_zero ? 0 : x_dict[v]);
584
+ let xv = x_dict[v];
585
+ const xfv = parseFloat(xv);
586
+ if(xfv && Math.abs(xfv) < this.near_zero) {
587
+ console.log('NOTE: Truncated ', xfv, ' to zero for variable', v);
588
+ xv = '0';
589
+ }
590
+ x_values.push(xv);
585
591
  col++;
586
592
  }
587
593
  // Add zeros to vector for remaining columns.