@trebco/treb 28.10.0 → 28.11.1

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.
@@ -714,6 +714,9 @@ export class Exporter {
714
714
  /** overload for return type */
715
715
  public NormalizeAddress(unit: UnitRange, sheet: SerializedSheet): UnitRange;
716
716
 
717
+ /** extra overload */
718
+ public NormalizeAddress<UNIT = UnitAddress|UnitRange>(unit: UNIT, sheet: SerializedSheet): UNIT;
719
+
717
720
  /**
718
721
  * for charts we need addresses to be absolute ($) and ensure there's
719
722
  * a sheet name -- use the active sheet if it's not explicitly referenced
@@ -738,6 +741,19 @@ export class Exporter {
738
741
 
739
742
  }
740
743
 
744
+ public EnsureRange(unit: UnitAddress|UnitRange): UnitRange {
745
+ if (unit.type === 'range') {
746
+ return unit;
747
+ }
748
+ return {
749
+ type: 'range',
750
+ start: unit,
751
+ end: unit,
752
+ label: unit.label,
753
+ id: unit.id,
754
+ position: unit.position,
755
+ };
756
+ }
741
757
 
742
758
  /**
743
759
  * new-style annotation layout (kind of a two-cell anchor) to two-cell anchor
@@ -909,10 +925,13 @@ export class Exporter {
909
925
  }
910
926
  else if (/series/i.test(arg.name)) {
911
927
 
912
- const [label, x, y] = arg.args; // y is required
928
+ const [label, x, y, z] = arg.args; // y is required
913
929
 
914
- if (y && y.type === 'range') {
915
- options.data.push(this.NormalizeAddress(y, sheet_source));
930
+ // FIXME: could be address also [x, y]
931
+
932
+ if (y && (y.type === 'range' || y.type === 'address')) {
933
+
934
+ options.data.push(this.EnsureRange(this.NormalizeAddress(y, sheet_source)));
916
935
 
917
936
  if (label) {
918
937
 
@@ -933,9 +952,16 @@ export class Exporter {
933
952
  }
934
953
 
935
954
  if (!options.labels2) { options.labels2 = []; }
936
- if (x && x.type === 'range') {
937
- options.labels2[options.data.length - 1] = this.NormalizeAddress(x, sheet_source);
955
+
956
+ if (x && (x.type === 'range' || x.type === 'address')) {
957
+ options.labels2[options.data.length - 1] = this.EnsureRange(this.NormalizeAddress(x, sheet_source));
958
+ }
959
+
960
+ if (z && (z.type === 'range' || z.type === 'address')) {
961
+ if (!options.labels3) { options.labels3 = []; }
962
+ options.labels3[options.data.length - 1] = this.EnsureRange(this.NormalizeAddress(z, sheet_source));
938
963
  }
964
+
939
965
  }
940
966
  else {
941
967
  console.info('invalid series missing Y', {y, arg, ref});
@@ -951,30 +977,39 @@ export class Exporter {
951
977
  const parse_result = this.parser.Parse(annotation.formula || '');
952
978
  if (parse_result.expression && parse_result.expression.type === 'call') {
953
979
 
954
- let type = '';
980
+ let type = ''; // FIXME
981
+
955
982
  switch (parse_result.expression.name.toLowerCase()) {
956
983
  case 'line.chart':
957
984
  type = 'scatter';
958
985
  break;
986
+
987
+ case 'bubble.chart':
988
+ type = 'bubble';
989
+ break;
990
+
959
991
  case 'scatter.line':
960
992
  type = 'scatter2';
961
993
  break;
994
+
962
995
  case 'donut.chart':
963
996
  type = 'donut';
964
997
  break;
998
+
965
999
  case 'bar.chart':
966
1000
  type = 'bar';
967
1001
  break;
1002
+
968
1003
  case 'column.chart':
969
1004
  type = 'column';
970
1005
  break;
971
1006
  }
972
1007
 
973
- if (type === 'column' || type === 'donut' || type === 'bar' || type === 'scatter' || type === 'scatter2') {
1008
+ if (type === 'column' || type === 'donut' || type === 'bar' || type === 'scatter' || type === 'scatter2' || type === 'bubble') {
974
1009
 
975
1010
  const options: ChartOptions = { type, data: [] };
976
1011
 
977
- const title_index = (type === 'scatter2') ? 1 : 2;
1012
+ const title_index = (type === 'scatter2' || type === 'bubble') ? 1 : 2;
978
1013
  const title_arg = parse_result.expression.args[title_index];
979
1014
 
980
1015
  if (title_arg && title_arg.type === 'literal') {
@@ -1000,7 +1035,7 @@ export class Exporter {
1000
1035
 
1001
1036
  if (parse_result.expression.args[0]) {
1002
1037
  const arg0 = parse_result.expression.args[0];
1003
- if (type === 'scatter2' || type === 'bar' || type === 'column' || type === 'scatter') {
1038
+ if (type === 'scatter2' || type === 'bar' || type === 'column' || type === 'scatter' || type === 'bubble') {
1004
1039
  parse_series(arg0, options, sheet_source.name);
1005
1040
  }
1006
1041
  else if (arg0.type === 'range') {
@@ -1045,7 +1080,7 @@ export class Exporter {
1045
1080
  */
1046
1081
  }
1047
1082
 
1048
- if (type !== 'scatter2') {
1083
+ if (type !== 'scatter2' && type !== 'bubble') {
1049
1084
  if (parse_result.expression.args[1] && parse_result.expression.args[1].type === 'range') {
1050
1085
  options.labels = this.NormalizeAddress(parse_result.expression.args[1], sheet_source);
1051
1086
  }
@@ -1064,6 +1099,10 @@ export class Exporter {
1064
1099
  options.smooth = true;
1065
1100
  }
1066
1101
  }
1102
+ else if (type === 'bubble') {
1103
+ // ...
1104
+ // console.info({parse_result});
1105
+ }
1067
1106
 
1068
1107
  // FIXME: fix this type (this happened when we switched from annotation
1069
1108
  // class to a data interface)
@@ -917,6 +917,27 @@ export class Importer {
917
917
  const series = descriptor.chart?.series;
918
918
 
919
919
  switch(descriptor.chart.type) {
920
+
921
+ case ChartType.Bubble:
922
+ type = 'treb-chart';
923
+ func = 'Bubble.Chart';
924
+
925
+ if (series && series.length) {
926
+ args[0] = `Group(${series.map(s => `Series(${
927
+ [
928
+ s.title || '',
929
+ s.values || '',
930
+ s.categories || '',
931
+ s.bubble_size || '',
932
+
933
+ ].join(', ')
934
+ })`).join(', ')})`;
935
+ }
936
+
937
+ args[1] = descriptor.chart.title;
938
+
939
+ break;
940
+
920
941
  case ChartType.Scatter:
921
942
  type = 'treb-chart';
922
943
  func = 'Scatter.Line';
@@ -69,12 +69,13 @@ export const ConditionalFormatOperators: Record<string, string> = {
69
69
  };
70
70
 
71
71
  export enum ChartType {
72
- Unknown = 0, Column, Bar, Line, Scatter, Donut, Pie
72
+ Unknown = 0, Column, Bar, Line, Scatter, Donut, Pie, Bubble
73
73
  }
74
74
 
75
75
  export interface ChartSeries {
76
76
  values?: string;
77
77
  categories?: string;
78
+ bubble_size?: string;
78
79
  title?: string;
79
80
  }
80
81
 
@@ -411,7 +412,7 @@ export class Workbook {
411
412
 
412
413
  }
413
414
 
414
- const ParseSeries = (node: any, scatter = false): ChartSeries[] => {
415
+ const ParseSeries = (node: any, type?: ChartType): ChartSeries[] => {
415
416
 
416
417
  const series: ChartSeries[] = [];
417
418
 
@@ -447,7 +448,7 @@ export class Workbook {
447
448
  }
448
449
  }
449
450
 
450
- if (scatter) {
451
+ if (type === ChartType.Scatter || type === ChartType.Bubble) {
451
452
  const x = XMLUtils.FindChild(series_node, 'c:xVal/c:numRef/c:f');
452
453
  if (x) {
453
454
  series_data.categories = x; // .text?.toString();
@@ -456,6 +457,14 @@ export class Workbook {
456
457
  if (y) {
457
458
  series_data.values = y; // .text?.toString();
458
459
  }
460
+
461
+ if (type === ChartType.Bubble) {
462
+ const z = XMLUtils.FindChild(series_node, 'c:bubbleSize/c:numRef/c:f');
463
+ if (z) {
464
+ series_data.bubble_size = z; // .text?.toString();
465
+ }
466
+ }
467
+
459
468
  }
460
469
  else {
461
470
  const value_node = XMLUtils.FindChild(series_node, 'c:val/c:numRef/c:f');
@@ -522,9 +531,23 @@ export class Workbook {
522
531
  node = XMLUtils.FindChild(xml, 'c:chartSpace/c:chart/c:plotArea/c:scatterChart');
523
532
  if (node) {
524
533
  result.type = ChartType.Scatter;
525
- result.series = ParseSeries(node, true);
534
+ result.series = ParseSeries(node, ChartType.Scatter);
526
535
  }
527
536
  }
537
+
538
+ if (!node) {
539
+ node = XMLUtils.FindChild(xml, 'c:chartSpace/c:chart/c:plotArea/c:bubbleChart');
540
+ if (node) {
541
+ result.type = ChartType.Bubble;
542
+ result.series = ParseSeries(node, ChartType.Bubble);
543
+ console.info("Bubble series?", result.series);
544
+ }
545
+ }
546
+
547
+ if (!node) {
548
+ console.info("Chart type not handled");
549
+ }
550
+
528
551
  // console.info("RX?", result);
529
552
 
530
553
  return result;
@@ -22,7 +22,7 @@
22
22
  export { Grid } from './types/grid';
23
23
  export { GridBase } from './types/grid_base';
24
24
  export { Sheet } from './types/sheet';
25
- export { DataModel, type MacroFunction } from './types/data_model';
25
+ export { DataModel, type MacroFunction, type ConnectedElementType } from './types/data_model';
26
26
  export type { SerializedNamedExpression, SerializedModel } from './types/data_model';
27
27
  export * from './types/grid_events';
28
28
  export type { SerializedSheet, FreezePane } from './types/sheet_types';
@@ -26,6 +26,12 @@ import { NamedRangeCollection } from './named_range';
26
26
  import { type ExpressionUnit, type UnitAddress, type UnitStructuredReference, type UnitRange, Parser, QuotedSheetNameRegex } from 'treb-parser';
27
27
  import { Area, IsCellAddress, Style } from 'treb-base-types';
28
28
 
29
+ export interface ConnectedElementType {
30
+ formula: string;
31
+ update?: (instance: ConnectedElementType) => void;
32
+ internal?: unknown; // opaque type to prevent circular dependencies
33
+ }
34
+
29
35
  export interface SerializedMacroFunction {
30
36
  name: string;
31
37
  function_def: string;
@@ -444,6 +450,32 @@ export class DataModel {
444
450
  return address; // already range or address
445
451
 
446
452
  }
453
+
454
+ public AddConnectedElement(connected_element: ConnectedElementType): number {
455
+ const id = this.connected_element_id++;
456
+ this.connected_elements.set(id, connected_element);
457
+ return id;
458
+ }
459
+
460
+ public RemoveConnectedElement(id: number) {
461
+ const element = this.connected_elements.get(id);
462
+ this.connected_elements.delete(id);
463
+ return element;
464
+ }
465
+
466
+ /**
467
+ * identifier for connected elements, used to manage. these need to be
468
+ * unique in the lifetime of a model instance, but no more than that.
469
+ */
470
+ protected connected_element_id = 100;
471
+
472
+ /**
473
+ * these are intentionally NOT serialized. they're ephemeral, created
474
+ * at runtime and not persistent.
475
+ *
476
+ * @internal
477
+ */
478
+ public connected_elements: Map<number, ConnectedElementType> = new Map();
447
479
 
448
480
  }
449
481
 
@@ -2270,6 +2270,16 @@ export class GridBase {
2270
2270
  }
2271
2271
  }
2272
2272
 
2273
+ for (const element of this.model.connected_elements.values()) {
2274
+ if (element.formula) {
2275
+ const updated = this.PatchExpressionSheetName(element.formula, old_name, name);
2276
+ if (updated) {
2277
+ element.formula = updated;
2278
+ changes++;
2279
+ }
2280
+ }
2281
+ }
2282
+
2273
2283
  return changes;
2274
2284
 
2275
2285
  }
@@ -2850,6 +2860,18 @@ export class GridBase {
2850
2860
  row_count: command.count
2851
2861
  });
2852
2862
 
2863
+ // connected elements
2864
+ for (const external of this.model.connected_elements.values()) {
2865
+ if (external.formula) {
2866
+ const modified = this.PatchFormulasInternal(external.formula,
2867
+ command.before_row, command.count, 0, 0,
2868
+ target_sheet.name.toLowerCase(), false);
2869
+ if (modified) {
2870
+ external.formula = modified;
2871
+ }
2872
+ }
2873
+ }
2874
+
2853
2875
  // see InsertColumnsInternal re: tables. rows are less complicated,
2854
2876
  // except that if you delete the header row we want to remove the
2855
2877
  // table entirely.
@@ -3172,6 +3194,18 @@ export class GridBase {
3172
3194
  before_row: 0,
3173
3195
  row_count: 0 });
3174
3196
 
3197
+ // connected elements
3198
+ for (const element of this.model.connected_elements.values()) {
3199
+ if (element.formula) {
3200
+ const modified = this.PatchFormulasInternal(element.formula,
3201
+ 0, 0, command.before_column, command.count,
3202
+ target_sheet.name.toLowerCase(), false);
3203
+ if (modified) {
3204
+ element.formula = modified;
3205
+ }
3206
+ }
3207
+ }
3208
+
3175
3209
  // patch tables. we removed this from the sheet routine entirely,
3176
3210
  // we need to rebuild any affected tables now.
3177
3211
 
@@ -395,3 +395,9 @@ export interface RenderOptions {
395
395
  long_structured_references: boolean;
396
396
  table_name: string;
397
397
  }
398
+
399
+ export interface PersistedParserConfig {
400
+ flags: Partial<ParserFlags>;
401
+ argument_separator: ArgumentSeparatorType;
402
+ decimal_mark: DecimalMarkType;
403
+ }
@@ -33,7 +33,8 @@ import type {
33
33
  UnitLiteralNumber,
34
34
  ParserFlags,
35
35
  UnitStructuredReference,
36
- RenderOptions} from './parser-types';
36
+ RenderOptions,
37
+ PersistedParserConfig} from './parser-types';
37
38
  import {
38
39
  ArgumentSeparatorType,
39
40
  DecimalMarkType
@@ -248,6 +249,52 @@ export class Parser {
248
249
  */
249
250
  protected full_reference_list: Array<UnitAddress | UnitRange | UnitIdentifier | UnitStructuredReference> = [];
250
251
 
252
+ protected parser_state: string[] = [];
253
+
254
+ /**
255
+ * save local configuration to a buffer, so it can be restored. we're doing
256
+ * this because in a lot of places we're caching parser flagss, changing
257
+ * them, and then restoring them. that's become repetitive, fragile to
258
+ * changes or new flags, and annoying.
259
+ *
260
+ * config is managed in a list with push/pop semantics. we store it as
261
+ * JSON so there's no possibility we'll accidentally mutate.
262
+ *
263
+ * FIXME: while we're at it why not migrate the separators -> flags, so
264
+ * there's a single location for this kind of state? (...TODO)
265
+ *
266
+ */
267
+ public Save() {
268
+ const config: PersistedParserConfig = {
269
+ flags: this.flags,
270
+ argument_separator: this.argument_separator,
271
+ decimal_mark: this.decimal_mark,
272
+ }
273
+ this.parser_state.push(JSON.stringify(config));
274
+ }
275
+
276
+ /**
277
+ * restore persisted config
278
+ * @see Save
279
+ */
280
+ public Restore() {
281
+ const json = this.parser_state.shift();
282
+ if (json) {
283
+ try {
284
+ const config = JSON.parse(json) as PersistedParserConfig;
285
+ this.flags = config.flags;
286
+ this.argument_separator = config.argument_separator;
287
+ this.decimal_mark = config.decimal_mark;
288
+ }
289
+ catch (err) {
290
+ console.error(err);
291
+ }
292
+ }
293
+ else {
294
+ console.warn("No parser state to restore");
295
+ }
296
+ }
297
+
251
298
  /**
252
299
  * recursive tree walk.
253
300
  *