linny-r 2.0.9 → 2.0.10

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.
@@ -32,7 +32,7 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
32
32
  SOFTWARE.
33
33
  */
34
34
 
35
- // CLASS GUIDatasetManager provides the dataset dialog functionality
35
+ // CLASS GUIDatasetManager provides the dataset dialog functionality.
36
36
  class GUIDatasetManager extends DatasetManager {
37
37
  constructor() {
38
38
  super();
@@ -44,7 +44,7 @@ class GUIDatasetManager extends DatasetManager {
44
44
  'click', (event) => UI.toggleDialog(event));
45
45
  document.getElementById('ds-new-btn').addEventListener(
46
46
  // Shift-click on New button => add prefix of selected dataset
47
- // (if any) to the name field of the dialog
47
+ // (if any) to the name field of the dialog.
48
48
  'click', () => DATASET_MANAGER.promptForDataset(event.shiftKey));
49
49
  document.getElementById('ds-data-btn').addEventListener(
50
50
  'click', () => DATASET_MANAGER.editData());
@@ -58,14 +58,14 @@ class GUIDatasetManager extends DatasetManager {
58
58
  'click', () => DATASET_MANAGER.deleteDataset());
59
59
  document.getElementById('ds-filter-btn').addEventListener(
60
60
  'click', () => DATASET_MANAGER.toggleFilter());
61
- // Update when filter input text changes
61
+ // Update when filter input text changes.
62
62
  this.filter_text = document.getElementById('ds-filter-text');
63
63
  this.filter_text.addEventListener(
64
64
  'input', () => DATASET_MANAGER.changeFilter());
65
65
  this.dataset_table = document.getElementById('dataset-table');
66
- // Data properties pane
66
+ // Data properties pane.
67
67
  this.properties = document.getElementById('dataset-properties');
68
- // Toggle buttons at bottom of dialog
68
+ // Toggle buttons at bottom of dialog.
69
69
  this.blackbox = document.getElementById('dataset-blackbox');
70
70
  this.blackbox.addEventListener(
71
71
  'click', () => DATASET_MANAGER.toggleBlackBox());
@@ -75,7 +75,7 @@ class GUIDatasetManager extends DatasetManager {
75
75
  this.io_box = document.getElementById('dataset-io');
76
76
  this.io_box.addEventListener(
77
77
  'click', () => DATASET_MANAGER.toggleImportExport());
78
- // Modifier pane buttons
78
+ // Modifier pane buttons.
79
79
  document.getElementById('ds-add-modif-btn').addEventListener(
80
80
  'click', () => DATASET_MANAGER.promptForSelector('new'));
81
81
  document.getElementById('ds-rename-modif-btn').addEventListener(
@@ -86,9 +86,9 @@ class GUIDatasetManager extends DatasetManager {
86
86
  'click', () => DATASET_MANAGER.deleteModifier());
87
87
  document.getElementById('ds-convert-modif-btn').addEventListener(
88
88
  'click', () => DATASET_MANAGER.promptToConvertModifiers());
89
- // Modifier table
89
+ // Modifier table.
90
90
  this.modifier_table = document.getElementById('dataset-modif-table');
91
- // Modal dialogs
91
+ // Modal dialogs.
92
92
  this.new_modal = new ModalDialog('new-dataset');
93
93
  this.new_modal.ok.addEventListener(
94
94
  'click', () => DATASET_MANAGER.newDataset());
@@ -170,7 +170,8 @@ class GUIDatasetManager extends DatasetManager {
170
170
  }
171
171
 
172
172
  enterKey() {
173
- // Open "edit" dialog for the selected dataset or modifier expression
173
+ // Open "edit" dialog for the selected dataset or modifier expression.
174
+ if(!this.focal_table) this.focal_table = this.dataset_table;
174
175
  const srl = this.focal_table.getElementsByClassName('sel-set');
175
176
  if(srl.length > 0) {
176
177
  const r = this.focal_table.rows[srl[0].rowIndex];
@@ -190,7 +191,8 @@ class GUIDatasetManager extends DatasetManager {
190
191
  }
191
192
 
192
193
  upDownKey(dir) {
193
- // Select row above or below the selected one (if possible)
194
+ // Select row above or below the selected one (if possible).
195
+ if(!this.focal_table) this.focal_table = this.dataset_table;
194
196
  const srl = this.focal_table.getElementsByClassName('sel-set');
195
197
  if(srl.length > 0) {
196
198
  let r = this.focal_table.rows[srl[0].rowIndex + dir];
@@ -206,11 +208,24 @@ class GUIDatasetManager extends DatasetManager {
206
208
  }
207
209
  }
208
210
 
211
+ expandToShow(name) {
212
+ // Expand all prefix rows for dataset having `name` so as to make it visible.
213
+ const pn = UI.prefixesAndName(name);
214
+ if(pn.length > 1) {
215
+ pn.pop();
216
+ while(pn.length) {
217
+ addDistinct(pn.join(UI.PREFIXER).toLowerCase(), this.expanded_rows);
218
+ pn.pop();
219
+ }
220
+ this.updateDialog();
221
+ }
222
+ }
223
+
209
224
  hideCollapsedRows() {
210
- // Hides all rows except top level and immediate children of expanded.
225
+ // Hide all rows except top level and immediate children of expanded.
211
226
  for(const row of this.dataset_table.rows) {
212
227
  const
213
- // Get the first DIV in the first TD of this row
228
+ // Get the first DIV in the first TD of this row.
214
229
  first_div = row.firstChild.firstElementChild,
215
230
  btn = first_div.dataset.prefix === 'x';
216
231
  let p = row.dataset.prefix,
@@ -218,18 +233,18 @@ class GUIDatasetManager extends DatasetManager {
218
233
  show = !p || x;
219
234
  if(btn) {
220
235
  const btn_div = row.getElementsByClassName('tree-btn')[0];
221
- // Special expand/collapse row
236
+ // Special expand/collapse row.
222
237
  if(show) {
223
- // Set triangle to point down
238
+ // Set triangle to point down.
224
239
  btn_div.innerText = '\u25BC';
225
240
  } else {
226
- // Set triangle to point right
241
+ // Set triangle to point right.
227
242
  btn_div.innerText = '\u25BA';
228
- // See whether "parent prefix" is expanded
243
+ // See whether "parent prefix" is expanded.
229
244
  p = p.split(UI.PREFIXER);
230
245
  p.pop();
231
246
  p = p.join(UI.PREFIXER);
232
- // If so, then also show the row
247
+ // If so, then also show the row.
233
248
  show = (!p || this.expanded_rows.indexOf(p) >= 0);
234
249
  }
235
250
  }
@@ -238,7 +253,7 @@ class GUIDatasetManager extends DatasetManager {
238
253
  }
239
254
 
240
255
  togglePrefixRow(e) {
241
- // Shows list items of the next prefix level
256
+ // Show list items of the next prefix level.
242
257
  let r = e.target;
243
258
  while(r.tagName !== 'TR') r = r.parentNode;
244
259
  const
@@ -246,7 +261,7 @@ class GUIDatasetManager extends DatasetManager {
246
261
  i = this.expanded_rows.indexOf(p);
247
262
  if(i >= 0) {
248
263
  this.expanded_rows.splice(i, 1);
249
- // Also remove all prefixes that have `p` as prefix
264
+ // Also remove all prefixes that have `p` as prefix.
250
265
  for(let j = this.expanded_rows.length - 1; j >= 0; j--) {
251
266
  if(this.expanded_rows[j].startsWith(p + UI.PREFIXER)) {
252
267
  this.expanded_rows.splice(j, 1);
@@ -259,7 +274,7 @@ class GUIDatasetManager extends DatasetManager {
259
274
  }
260
275
 
261
276
  rowByPrefix(prefix) {
262
- // Returns first table row with the specified prefix
277
+ // Return the first table row with the specified prefix.
263
278
  if(!prefix) return null;
264
279
  let lcp = prefix.toLowerCase(),
265
280
  pl = lcp.split(': ');
@@ -278,12 +293,12 @@ class GUIDatasetManager extends DatasetManager {
278
293
  }
279
294
 
280
295
  selectPrefixRow(e) {
281
- // Selects expand/collapse prefix row
296
+ // Select expand/collapse prefix row.
282
297
  this.focal_table = this.dataset_table;
283
- // NOTE: `e` can also be a string specifying the prefix to select
298
+ // NOTE: `e` can also be a string specifying the prefix to select.
284
299
  let r = e.target || this.rowByPrefix(e);
285
300
  if(!r) return;
286
- // Modeler may have clicked on the expand/collapse triangle;
301
+ // Modeler may have clicked on the expand/collapse triangle.
287
302
  const toggle = r.classList.contains('tree-btn');
288
303
  while(r.tagName !== 'TR') r = r.parentNode;
289
304
  this.selected_prefix_row = r;
@@ -1047,7 +1062,7 @@ class GUIDatasetManager extends DatasetManager {
1047
1062
  }
1048
1063
 
1049
1064
  editData() {
1050
- // Show the Edit time series dialog
1065
+ // Show the Edit time series dialog.
1051
1066
  const
1052
1067
  ds = this.selected_dataset,
1053
1068
  md = this.series_modal,
@@ -1057,7 +1072,7 @@ class GUIDatasetManager extends DatasetManager {
1057
1072
  md.element('unit').value = ds.scale_unit;
1058
1073
  cover.style.display = (ds.array ? 'block' : 'none');
1059
1074
  md.element('time-scale').value = VM.sig4Dig(ds.time_scale);
1060
- // Add options for time unit selector
1075
+ // Add options for time unit selector.
1061
1076
  const ol = [];
1062
1077
  for(let u in VM.time_unit_shorthand) {
1063
1078
  if(VM.time_unit_shorthand.hasOwnProperty(u)) {
@@ -1067,7 +1082,7 @@ class GUIDatasetManager extends DatasetManager {
1067
1082
  }
1068
1083
  }
1069
1084
  md.element('time-unit').innerHTML = ol.join('');
1070
- // Add options for(dis)aggregation method selector
1085
+ // Add options for(dis)aggregation method selector.
1071
1086
  ol.length = 0;
1072
1087
  for(let i = 0; i < this.methods.length; i++) {
1073
1088
  ol.push(['<option value="', this.methods[i],
@@ -1075,12 +1090,12 @@ class GUIDatasetManager extends DatasetManager {
1075
1090
  '">', this.method_names[i], '</option>'].join(''));
1076
1091
  }
1077
1092
  md.element('method').innerHTML = ol.join('');
1078
- // Update the "periodic" box
1093
+ // Update the "periodic" box.
1079
1094
  UI.setBox('series-periodic', ds.periodic);
1080
- // Update the "array" box
1095
+ // Update the "array" box.
1081
1096
  UI.setBox('series-array', ds.array);
1082
1097
  md.element('url').value = ds.url;
1083
- // Show data as decimal numbers (JS default notation) on separate lines
1098
+ // Show data as decimal numbers (JS default notation) on separate lines.
1084
1099
  this.series_data.value = ds.data.join('\n');
1085
1100
  md.show('default');
1086
1101
  }
@@ -1203,7 +1218,8 @@ class GUIDatasetManager extends DatasetManager {
1203
1218
  UI.warn(`Invalid default value "${v}" in column ${i}`);
1204
1219
  return false;
1205
1220
  } else {
1206
- dsa.push([sf]);
1221
+ // Push empty list, as this will become the actual dataset without default value.
1222
+ dsa.push([]);
1207
1223
  }
1208
1224
  }
1209
1225
  for(let i = 2; i < n; i++) {
@@ -1212,7 +1228,7 @@ class GUIDatasetManager extends DatasetManager {
1212
1228
  UI.warn(`Number of values (${dsv.length}) on line ${i} does not match number of dataset names (${ncol})`);
1213
1229
  return false;
1214
1230
  }
1215
- for(let j = 0; j < dsv.length; j++) {
1231
+ for(let j = 0; j < ncol; j++) {
1216
1232
  const
1217
1233
  v = dsv[j].trim(),
1218
1234
  sf = safeStrToFloat(v, '');
@@ -1220,6 +1236,7 @@ class GUIDatasetManager extends DatasetManager {
1220
1236
  UI.warn(`Invalid numerical value "${v}" for <strong>${dsn[j]}</strong> on line ${i}`);
1221
1237
  return false;
1222
1238
  }
1239
+ dsa[j].push(sf);
1223
1240
  }
1224
1241
  }
1225
1242
  // Add or update datasets.
@@ -441,19 +441,25 @@ class GUIExperimentManager extends ExperimentManager {
441
441
  }
442
442
 
443
443
  newExperiment() {
444
- const n = this.new_modal.element('name').value.trim();
445
- const x = MODEL.addExperiment(n);
446
- if(x) {
447
- this.new_modal.hide();
448
- this.selected_experiment = x;
449
- this.updateDialog();
444
+ // NOTE: Title must be a "clean" name: no \ or | and spacing reduced to
445
+ // a single space to permit using it unambiguously in experiment result
446
+ // specifiers of variable names.
447
+ const n = UI.cleanName(this.new_modal.element('name').value);
448
+ if(n) {
449
+ const x = MODEL.addExperiment(n);
450
+ if(x) {
451
+ this.new_modal.hide();
452
+ this.selected_experiment = x;
453
+ this.updateDialog();
454
+ }
455
+ } else {
456
+ this.new_modal.element('name').focus();
457
+ return;
450
458
  }
451
459
  }
452
460
 
453
461
  promptForName() {
454
462
  if(this.selected_experiment) {
455
- this.rename_modal.element('former-name').innerHTML =
456
- this.selected_experiment.title;
457
463
  this.rename_modal.element('name').value = '';
458
464
  this.rename_modal.show('name');
459
465
  }
@@ -464,12 +470,16 @@ class GUIExperimentManager extends ExperimentManager {
464
470
  const
465
471
  nel = this.rename_modal.element('name'),
466
472
  n = UI.cleanName(nel.value);
467
- // Show modeler the "cleaned" new name
473
+ // Show modeler the "cleaned" new name.
468
474
  nel.value = n;
469
- // Keep prompt open if title is empty string
475
+ // Keep prompt open if cleaned title is empty string, or identifies
476
+ // an existing experiment.
477
+ nel.focus();
470
478
  if(n) {
471
- // Warn modeler if name already in use for some experiment
472
- if(MODEL.indexOfExperiment(n) >= 0) {
479
+ // Warn modeler if name already in use for some experiment, other than
480
+ // the selected experiment (as upper/lower case changes must be possible).
481
+ if(MODEL.indexOfExperiment(n) >= 0 &&
482
+ n.toLowerCase() !== this.selected_experiment.title.toLowerCase()) {
473
483
  UI.warn(`An experiment with title "${n}" already exists`);
474
484
  } else {
475
485
  this.selected_experiment.title = n;
@@ -197,9 +197,9 @@ NOTE: Grouping groups results in a single group, e.g., (1;2);(3;4;5) evaluates a
197
197
  let n = '',
198
198
  a = '';
199
199
  if(ids[0] === 'link') {
200
- n = document.getElementById('link-from-name').innerHTML +
200
+ n = document.getElementById('link-from-name').innerText +
201
201
  UI.LINK_ARROW +
202
- document.getElementById('link-to-name').innerHTML;
202
+ document.getElementById('link-to-name').innerText;
203
203
  } else {
204
204
  n = document.getElementById(ids[0] + '-name').value;
205
205
  if(ids[0] === 'process') {
@@ -218,6 +218,7 @@ NOTE: Grouping groups results in a single group, e.g., (1;2);(3;4;5) evaluates a
218
218
  this.obj.value = 0;
219
219
  this.updateVariableBar();
220
220
  this.clearStatusBar();
221
+ this.showPrefix(UI.entityPrefix(prop));
221
222
  md.show('text');
222
223
  }
223
224
 
@@ -257,15 +258,20 @@ NOTE: Grouping groups results in a single group, e.g., (1;2);(3;4;5) evaluates a
257
258
  // the dataset and the selector as extra parameters for the parser.
258
259
  let own = null,
259
260
  sel = '';
260
- if(!this.edited_input_id && DATASET_MANAGER.edited_expression) {
261
- own = DATASET_MANAGER.selected_dataset;
262
- sel = DATASET_MANAGER.selected_modifier.selector;
263
- } else if(!this.edited_input_id && EQUATION_MANAGER.edited_expression) {
264
- own = MODEL.equations_dataset;
265
- sel = EQUATION_MANAGER.selected_modifier.selector;
266
- } else if(!this.edited_input_id && CONSTRAINT_EDITOR.edited_expression) {
267
- own = CONSTRAINT_EDITOR.selected;
268
- sel = CONSTRAINT_EDITOR.selected_selector;
261
+ if(!this.edited_input_id) {
262
+ if(DATASET_MANAGER.edited_expression) {
263
+ own = DATASET_MANAGER.selected_dataset;
264
+ sel = DATASET_MANAGER.selected_modifier.selector;
265
+ } else if(EQUATION_MANAGER.edited_expression) {
266
+ own = MODEL.equations_dataset;
267
+ sel = EQUATION_MANAGER.selected_modifier.selector;
268
+ } else if(CONSTRAINT_EDITOR.edited_expression) {
269
+ own = CONSTRAINT_EDITOR.selected;
270
+ sel = CONSTRAINT_EDITOR.selected_selector;
271
+ } else if(UI.modals.datasetgroup.showing) {
272
+ own = UI.modals.datasetgroup.selected_ds;
273
+ sel = UI.modals.datasetgroup.selected_selector;
274
+ }
269
275
  } else {
270
276
  own = UI.edited_object;
271
277
  sel = this.edited_input_id.split('-').pop();
@@ -298,6 +304,8 @@ NOTE: Grouping groups results in a single group, e.g., (1;2);(3;4;5) evaluates a
298
304
  } else if(CONSTRAINT_EDITOR.edited_expression) {
299
305
  // NOTE: Boundline selector expressions may result in a grouping.
300
306
  CONSTRAINT_EDITOR.modifyExpression(xp.expr, xp.concatenating);
307
+ } else if(UI.modals.datasetgroup.showing) {
308
+ UI.modals.datasetgroup.modifyExpression(xp.expr);
301
309
  }
302
310
  UI.modals.expression.hide();
303
311
  return true;
@@ -308,7 +316,13 @@ NOTE: Grouping groups results in a single group, e.g., (1;2);(3;4;5) evaluates a
308
316
  this.status.style.backgroundColor = UI.color.dialog_background;
309
317
  this.status.innerHTML = '&nbsp;';
310
318
  }
311
-
319
+
320
+ showPrefix(prefix) {
321
+ // When editing an expression for a prefixed entity, show the prefix
322
+ // on the status line.
323
+ if(prefix) this.status.innerHTML = '<em>Prefix:</em> ' + prefix;
324
+ }
325
+
312
326
  namesByType(type) {
313
327
  // Returns a list of entity names of the specified types
314
328
  // (used only to generate the options of SELECT elements)
@@ -256,8 +256,8 @@ class Finder {
256
256
  }
257
257
  }
258
258
  // Also allow search for scale unit names.
259
- if(et.indexOf('U') >= 0) {
260
- imgs += '<img src="images/scale.png">';
259
+ if(et === 'U') {
260
+ imgs = '<img src="images/scale.png">';
261
261
  for(let k in MODEL.products) if(MODEL.products.hasOwnProperty(k)) {
262
262
  if(fp && !k.startsWith(UI.BLACK_BOX) && patternMatch(
263
263
  MODEL.products[k].scale_unit, this.filter_pattern)) {
@@ -266,6 +266,37 @@ class Finder {
266
266
  addDistinct('Q', this.filtered_types);
267
267
  }
268
268
  }
269
+ for(let k in MODEL.datasets) if(MODEL.datasets.hasOwnProperty(k)) {
270
+ if(fp && !k.startsWith(UI.BLACK_BOX)) {
271
+ const ds = MODEL.datasets[k];
272
+ if(ds !== MODEL.equations_dataset && patternMatch(
273
+ ds.scale_unit, this.filter_pattern)) {
274
+ enl.push(k);
275
+ this.entities.push(MODEL.datasets[k]);
276
+ addDistinct('D', this.filtered_types);
277
+ }
278
+ }
279
+ }
280
+ }
281
+ // Also allow search for dataset modifier selectors.
282
+ if(et.indexOf('S') >= 0) {
283
+ imgs = '<img src="images/dataset.png">';
284
+ for(let k in MODEL.datasets) if(MODEL.datasets.hasOwnProperty(k)) {
285
+ if(fp && !k.startsWith(UI.BLACK_BOX)) {
286
+ const ds = MODEL.datasets[k];
287
+ if(ds !== MODEL.equations_dataset) {
288
+ for(let mk in ds.modifiers) if(ds.modifiers.hasOwnProperty(mk)) {
289
+ if(patternMatch(
290
+ ds.modifiers[mk].selector, this.filter_pattern)) {
291
+ enl.push(k);
292
+ this.entities.push(MODEL.datasets[k]);
293
+ addDistinct('D', this.filtered_types);
294
+ break;
295
+ }
296
+ }
297
+ }
298
+ }
299
+ }
269
300
  }
270
301
  // Also allow search for link multiplier symbols.
271
302
  if(et.indexOf('M') >= 0) {
@@ -341,11 +372,11 @@ class Finder {
341
372
 
342
373
  get entityGroup() {
343
374
  // Returns the list of filtered entities if all are of the same type,
344
- // while excluding (no actor), (top cluster), datasets and equations.
375
+ // while excluding (no actor), (top cluster), and equations.
345
376
  const
346
377
  eg = [],
347
378
  ft = this.filtered_types[0];
348
- if(this.filtered_types.length === 1 && 'DE'.indexOf(ft) < 0) {
379
+ if(this.filtered_types.length === 1 && ft !== 'E') {
349
380
  for(const e of this.entities) {
350
381
  // Exclude "no actor" and top cluster.
351
382
  if(e.name && e.name !== '(no_actor)' && e.name !== '(top_cluster)' &&
@@ -601,7 +632,7 @@ class Finder {
601
632
  // look only for the entity types denoted by these letters.
602
633
  let ft = this.filter_input.value,
603
634
  et = VM.entity_letters;
604
- if(/^(\*|U|M|[ABCDELPQ]+)\?/i.test(ft)) {
635
+ if(/^(\*|U|M|S|[ABCDELPQ]+)\?/i.test(ft)) {
605
636
  ft = ft.split('?');
606
637
  // NOTE: *? denotes "all entity types except constraints".
607
638
  et = (ft[0] === '*' ? 'ACDELPQ' : ft[0].toUpperCase());
@@ -644,6 +675,7 @@ class Finder {
644
675
  if(UI.hidden('dataset-dlg')) {
645
676
  UI.buttons.dataset.dispatchEvent(new Event('click'));
646
677
  }
678
+ DATASET_MANAGER.expandToShow(obj.name);
647
679
  DATASET_MANAGER.selected_dataset = obj;
648
680
  DATASET_MANAGER.updateDialog();
649
681
  } else if(obj instanceof DatasetModifier) {
@@ -777,9 +809,162 @@ class Finder {
777
809
  UI.showLinkPropertiesDialog(e, 'R', false, group);
778
810
  } else if(e instanceof Cluster) {
779
811
  UI.showClusterPropertiesDialog(e, group);
812
+ } else if(e instanceof Dataset) {
813
+ this.showDatasetGroupDialog(e, group);
780
814
  }
781
815
  }
782
816
 
817
+ showDatasetGroupDialog(ds, dsl) {
818
+ // Initialize fields with properties of first element of `dsl`.
819
+ if(!dsl.length) return;
820
+ const md = UI.modals.datasetgroup;
821
+ md.group = dsl;
822
+ md.selected_ds = ds;
823
+ md.element('no-time-msg').style.display = (ds.array ? 'block' : 'none');
824
+ md.show('prefix', ds);
825
+ }
826
+
827
+ updateDatasetGroupProperties() {
828
+ // Update properties of selected group of datasets.
829
+ const md = UI.modals.datasetgroup;
830
+ if(!md.group.length) return;
831
+ // Reduce multiple spaces to a single space.
832
+ let prefix = md.element('prefix').value.replaceAll(/\s+/gi, ' ').trim();
833
+ // Trim trailing colons (also when they have spaces between them).
834
+ while(prefix.endsWith(':')) prefix = prefix.slice(0, -1).trim();
835
+ // Count the updated chart variables and expressions.
836
+ let cv_cnt = 0,
837
+ xr_cnt = 0;
838
+ // Only rename datasets if prefix has been changed.
839
+ if(prefix !== md.shared_prefix) {
840
+ // Check whether prefix is valid.
841
+ if(prefix && !UI.validName(prefix)) {
842
+ UI.warn(`Invalid prefix "${prefix}"`);
843
+ return;
844
+ }
845
+ // Add the prefixer ": " to make it a true prefix.
846
+ if(prefix) prefix += UI.PREFIXER;
847
+ let old_prefix = md.shared_prefix;
848
+ if(old_prefix) old_prefix += UI.PREFIXER;
849
+ // Check whether prefix will create name conflicts.
850
+ let nc = 0;
851
+ for(const ds of md.group) {
852
+ let nn = ds.name;
853
+ if(nn.startsWith(old_prefix)) {
854
+ nn = nn.replace(old_prefix, prefix);
855
+ const obj = MODEL.objectByName(nn);
856
+ if(obj && obj !== ds) {
857
+ console.log('Anticipated name conflict with', obj.type,
858
+ obj.displayName);
859
+ nc++;
860
+ }
861
+ }
862
+ }
863
+ if(nc > 0) {
864
+ UI.warn(`Prefix "${prefix}" will result in` +
865
+ pluralS(nc, 'name conflict'));
866
+ return;
867
+ }
868
+ // Rename the datasets -- this may affect the group.
869
+ MODEL.renamePrefixedDatasets(old_prefix, prefix, md.group);
870
+ cv_cnt += MODEL.variable_count;
871
+ xr_cnt += MODEL.expression_count;
872
+ }
873
+ // Validate input field values.
874
+ const dv = UI.validNumericInput('datasetgroup-default', 'default value');
875
+ if(dv === false) return;
876
+ const ts = UI.validNumericInput('datasetgroup-time-scale', 'time step');
877
+ if(ts === false) return;
878
+ // No issues => update *only the modified* properties of all datasets in
879
+ // the group.
880
+ const data = {
881
+ 'default': dv,
882
+ 'unit': md.element('unit').value.trim(),
883
+ 'periodic': UI.boxChecked('datasetgroup-periodic'),
884
+ 'array': UI.boxChecked('datasetgroup-array'),
885
+ 'time-scale': ts,
886
+ 'time-unit': md.element('time-unit').value,
887
+ 'method': md.element('method').value
888
+ };
889
+ for(let name in md.fields) if(md.changed[name]) {
890
+ const
891
+ prop = md.fields[name],
892
+ value = data[name];
893
+ for(const ds of md.group) ds[prop] = value;
894
+ }
895
+ // Also update the dataset modifiers.
896
+ const dsv_list = MODEL.datasetVariables;
897
+ for(const ds of md.group) {
898
+ for(const k of Object.keys(md.selectors)) {
899
+ const sel = md.selectors[k];
900
+ if(ds.modifiers.hasOwnProperty(k)) {
901
+ // If dataset `ds` has selector with key `k`,
902
+ // first check if it has been deleted.
903
+ if(sel.deleted) {
904
+ // If so, delete this modifier it from `ds`.
905
+ if(k === ds.default_selector) ds.default_selector = '';
906
+ delete ds.modifiers[k];
907
+ } else {
908
+ // If not deleted, check whether the selector was renamed.
909
+ const dsm = ds.modifiers[k];
910
+ let s = k;
911
+ if(sel.new_s) {
912
+ // If so, let `s` be the key for new selector.
913
+ s = UI.nameToID(sel.new_s);
914
+ dsm.selector = sel.new_s;
915
+ if(s !== k) {
916
+ // Add modifier with its own selector key.
917
+ ds.modifiers[s] = ds.modifiers[k];
918
+ delete ds.modifiers[k];
919
+ }
920
+ // Always update all chart variables referencing dataset + old selector.
921
+ for(const v of dsv_list) {
922
+ if(v.object === ds && v.attribute === sel.sel) {
923
+ v.attribute = sel.new_s;
924
+ cv_cnt++;
925
+ }
926
+ }
927
+ // Also replace old selector in all expressions (count these as well).
928
+ xr_cnt += MODEL.replaceAttributeInExpressions(
929
+ ds.name + '|' + sel.sel, sel.new_s);
930
+ }
931
+ // NOTE: Keep original expression unless a new expression is specified.
932
+ if(sel.new_x) {
933
+ dsm.expression.text = sel.new_x;
934
+ // Clear code so the expresion will be recompiled.
935
+ dsm.expression.code = null;
936
+ }
937
+ }
938
+ } else {
939
+ // If dataset `ds` has NO selector with key `k`, add the (new) selector.
940
+ let s = sel.sel,
941
+ id = k;
942
+ if(sel.new_s) {
943
+ s = sel.new_s;
944
+ id = UI.nameToID(sel.new_s);
945
+ }
946
+ const dsm = new DatasetModifier(ds, s);
947
+ dsm.expression.text = (sel.new_x === false ? sel.expr : sel.new_x);
948
+ ds.modifiers[id] = dsm;
949
+ }
950
+ }
951
+ // Set the new default selector (if changed).
952
+ if(md.new_defsel !== false) ds.default_selector = md.new_defsel;
953
+ }
954
+ // Notify modeler of changes (if any).
955
+ const msg = [];
956
+ if(cv_cnt) msg.push(pluralS(cv_cnt, ' chart variable'));
957
+ if(xr_cnt) msg.push(pluralS(xr_cnt, ' expression variable'));
958
+ if(msg.length) {
959
+ UI.notify('Updated ' + msg.join(' and '));
960
+ }
961
+ MODEL.cleanUpScaleUnits();
962
+ MODEL.updateDimensions();
963
+ md.hide();
964
+ // Also update the draggable dialogs that may be affected.
965
+ UI.updateControllerDialogs('CDEFIJX');
966
+ }
967
+
783
968
  copyAttributesToClipboard(shift) {
784
969
  // Copy relevant entity attributes as tab-separated text to clipboard.
785
970
  // NOTE: All entity types have "get" method `attributes` that returns an