@trebco/treb 32.5.0 → 32.6.4

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trebco/treb",
3
- "version": "32.5.0",
3
+ "version": "32.6.4",
4
4
  "license": "LGPL-3.0-or-later",
5
5
  "homepage": "https://treb.app",
6
6
  "repository": {
@@ -27,18 +27,22 @@ import type { Table } from './table';
27
27
  import type { DataValidation, AnnotationType, ConditionalFormat } from 'treb-data-model';
28
28
 
29
29
  export interface CellParseResult {
30
- row: number,
31
- column: number,
30
+ row: number;
31
+ column: number;
32
32
  type: SerializedValueType; // ValueType,
33
- value: number|string|undefined|boolean,
34
- calculated?: number|string|undefined|boolean,
33
+ value: number|string|undefined|boolean;
34
+ calculated?: number|string|undefined|boolean;
35
35
  calculated_type?: SerializedValueType; // ValueType,
36
- style_ref?: number,
37
- hyperlink?: string,
38
- validation?: DataValidation,
39
- merge_area?: IArea,
40
- area?: IArea,
41
- table?: Table,
36
+ style_ref?: number;
37
+ hyperlink?: string;
38
+ validation?: DataValidation;
39
+ merge_area?: IArea;
40
+ area?: IArea;
41
+
42
+ /** dynamic arrays */
43
+ spill?: IArea;
44
+
45
+ table?: Table;
42
46
  }
43
47
 
44
48
  export interface AnchoredAnnotation {
@@ -1221,11 +1221,16 @@ export class Calculator extends Graph {
1221
1221
  public AttachSpillData(area: Area, cells?: Cells) {
1222
1222
 
1223
1223
  if (!cells) {
1224
+
1225
+ // can we assume active sheet here? actually I guess not, we'll
1226
+ // need to set that...
1227
+
1224
1228
  const sheet = area.start.sheet_id ? this.model.sheets.Find(area.start.sheet_id) : undefined;
1225
1229
  cells = sheet?.cells;
1226
1230
  }
1227
1231
 
1228
1232
  if (!cells) {
1233
+ console.info({area, cells});
1229
1234
  throw new Error('invalid sheet ID in attach spill data');
1230
1235
  }
1231
1236
 
@@ -207,6 +207,7 @@ export interface HistogramData extends ColumnDataBaseType {
207
207
  export interface LineBaseData extends ChartDataBaseType {
208
208
  series?: NumberOrUndefinedArray[];
209
209
  series2?: SeriesType[];
210
+ stacked_series?: SeriesType[];
210
211
  scale: RangeScale;
211
212
  x_scale?: RangeScale;
212
213
  titles?: string[];
@@ -262,6 +263,7 @@ export interface BarData extends LineBaseData {
262
263
  type: 'bar';
263
264
  round?: boolean;
264
265
  space?: number;
266
+ stacked?: boolean;
265
267
  }
266
268
 
267
269
  export interface DonutDataBaseType extends ChartDataBaseType {
@@ -1001,8 +1001,11 @@ export const CreateColumnChart = (
1001
1001
 
1002
1002
  const [data, labels, args_title, args_options] = args;
1003
1003
 
1004
- const series: SeriesType[] = TransformSeriesData(data);
1005
- const common = CommonData(series);
1004
+ const options = args_options?.toString() || undefined;
1005
+ const stacked = /stacked/i.test(options || '');
1006
+
1007
+ let series: SeriesType[] = TransformSeriesData(data);
1008
+ let common = CommonData(series);
1006
1009
 
1007
1010
  let category_labels: string[] | undefined;
1008
1011
 
@@ -1041,15 +1044,50 @@ export const CreateColumnChart = (
1041
1044
 
1042
1045
  }
1043
1046
 
1047
+ let stacked_series: SeriesType[]|undefined = undefined;
1048
+ const legend = common.legend; // in case we munge it
1049
+
1050
+ if (stacked) {
1051
+ stacked_series = series;
1052
+
1053
+ const map: Map<number, number> = new Map();
1054
+ // const label_map: Map<number, string> = new Map();
1055
+
1056
+ for (const entry of series) {
1057
+ for (const [index, key] of entry.x.data.entries()) {
1058
+ if (key !== undefined) {
1059
+ const value = entry.y.data[index] || 0;
1060
+ map.set(key, value + (map.get(key) || 0));
1061
+ }
1062
+ }
1063
+ }
1064
+
1065
+ const x_data = Array.from(map.keys()).sort((a, b) => a - b);
1066
+ const y_data = x_data.map(key => map.get(key) || 0);
1067
+
1068
+ // console.info({stacked_series, map, x_data, y_data});
1069
+
1070
+ series = [{
1071
+ x: { data: x_data, format: stacked_series[0]?.x?.format },
1072
+ y: { data: y_data, format: stacked_series[0]?.y?.format },
1073
+ }];
1074
+
1075
+ series[0].x.range = ArrayMinMax(x_data);
1076
+ series[0].y.range = ArrayMinMax(y_data);
1077
+
1078
+ common = CommonData(series, Math.min(0, ...y_data));
1079
+
1080
+ }
1081
+
1044
1082
  const title = args_title?.toString() || undefined;
1045
- const options = args_options?.toString() || undefined;
1046
1083
 
1047
1084
  const chart_data = {
1048
1085
  type,
1049
- legend: common.legend,
1086
+ legend, // legend: common.legend,
1050
1087
  // legend_position: LegendPosition.right,
1051
1088
  legend_style: LegendStyle.marker,
1052
1089
  series2: series,
1090
+ stacked_series,
1053
1091
  scale: common.y.scale,
1054
1092
  title,
1055
1093
  y_labels: type === 'bar' ? category_labels : common.y.labels, // swapped
@@ -1057,6 +1095,9 @@ export const CreateColumnChart = (
1057
1095
  };
1058
1096
 
1059
1097
  if (options) {
1098
+
1099
+ (chart_data as BarData).stacked = stacked;
1100
+
1060
1101
  (chart_data as BarData).round = /round/i.test(options);
1061
1102
  (chart_data as ChartDataBaseType).data_labels = /labels/i.test(options);
1062
1103
 
@@ -599,23 +599,33 @@ export class DefaultChartRenderer implements ChartRendererType {
599
599
  corners = [half_width, half_width, 0, 0];
600
600
  }
601
601
 
602
- for (let s = 0; s < series_count; s++) {
603
- const series = chart_data.series2[s];
604
- const color_index = typeof series.index === 'number' ? series.index : s + 1;
602
+ if (chart_data.stacked_series) {
605
603
 
606
- for (let i = 0; i < series.y.data.length; i++ ){
607
- const value = series.y.data[i];
608
- // const format = NumberFormatCache.Get(series.y.format || '0.00');
604
+ // step by group (stacked series)
605
+ for (let s = 0; s < chart_data.stacked_series[0].y.data.length; s++ ){
609
606
 
610
- if (typeof value === 'number') {
607
+ const x = (area.left + s * column_width + space) ; // + s * width;
608
+ let y = 0;
609
+
610
+ // now check each series, grab s-th value
611
+ for (let t = 0; t < chart_data.stacked_series.length; t++) {
612
+
613
+ const series = chart_data.stacked_series[t];
614
+ const color_index = typeof series.index === 'number' ? series.index : t + 1;
611
615
 
612
- // const x = Math.round(area.left + i * column_width + space) + s * width;
613
- const x = (area.left + i * column_width + space) + s * width;
614
-
615
616
  let height = 0;
616
- let y = 0;
617
617
  // let negative = false;
618
618
 
619
+ const value = (series.y.data[s] || 0);
620
+
621
+ if (t === 0) {
622
+ y = Math.min(chart_data.scale.min || 0, value);
623
+ }
624
+
625
+ height = Util.ApplyScale(value, area.height, chart_data.scale);
626
+ const base = area.bottom - height - Util.ApplyScale(y, area.height, chart_data.scale);
627
+
628
+ /*
619
629
  if (zero) {
620
630
  if (value > 0) {
621
631
  height = Util.ApplyScale(value + chart_data.scale.min, area.height, chart_data.scale);
@@ -631,27 +641,77 @@ export class DefaultChartRenderer implements ChartRendererType {
631
641
  height = Util.ApplyScale(value, area.height, chart_data.scale);
632
642
  y = area.bottom - height;
633
643
  }
644
+ */
634
645
 
635
- // const bar_title = chart_data.titles ? chart_data.titles[i] : undefined;
636
- const bar_title = undefined;
646
+ let bar_title = undefined;
647
+ let label = undefined;
648
+ let label_point = undefined;
637
649
 
638
- if (height) {
650
+ this.renderer.RenderRectangle(new Area(
651
+ x, base, x + width, base + height,
652
+ ), corners, ['chart-column', `series-${color_index}`], bar_title || undefined, label, label_point);
639
653
 
640
- const label = (chart_data.data_labels && !!series.y.labels) ? series.y.labels[i] : '';
641
- const label_point = {
642
- x: Math.round(x + width / 2),
643
- y: Math.round(y - 10),
644
- };
654
+ y += value;
645
655
 
646
- this.renderer.RenderRectangle(new Area(
647
- x, y, x + width, y + height,
648
- ), corners, ['chart-column', `series-${color_index}`], bar_title || undefined, label, label_point);
649
- }
650
656
  }
657
+
658
+
651
659
  }
660
+ }
661
+ else {
662
+ for (let s = 0; s < series_count; s++) {
663
+ const series = chart_data.series2[s];
664
+ const color_index = typeof series.index === 'number' ? series.index : s + 1;
665
+
666
+ for (let i = 0; i < series.y.data.length; i++ ){
667
+ const value = series.y.data[i];
668
+
669
+ if (typeof value === 'number') {
670
+
671
+ // const x = Math.round(area.left + i * column_width + space) + s * width;
672
+ const x = (area.left + i * column_width + space) + s * width;
673
+
674
+ let height = 0;
675
+ let y = 0;
676
+ // let negative = false;
677
+
678
+ if (zero) {
679
+ if (value > 0) {
680
+ height = Util.ApplyScale(value + chart_data.scale.min, area.height, chart_data.scale);
681
+ y = area.bottom - height - zero;
682
+ }
683
+ else {
684
+ height = Util.ApplyScale(chart_data.scale.min - value, area.height, chart_data.scale);
685
+ y = area.bottom - zero; // // area.bottom - height - zero;
686
+ // negative = true;
687
+ }
688
+ }
689
+ else {
690
+ height = Util.ApplyScale(value, area.height, chart_data.scale);
691
+ y = area.bottom - height;
692
+ }
693
+
694
+ // const bar_title = chart_data.titles ? chart_data.titles[i] : undefined;
695
+ const bar_title = undefined;
696
+
697
+ if (height) {
652
698
 
699
+ const label = (chart_data.data_labels && !!series.y.labels) ? series.y.labels[i] : '';
700
+ const label_point = {
701
+ x: Math.round(x + width / 2),
702
+ y: Math.round(y - 10),
703
+ };
704
+
705
+ this.renderer.RenderRectangle(new Area(
706
+ x, y, x + width, y + height,
707
+ ), corners, ['chart-column', `series-${color_index}`], bar_title || undefined, label, label_point);
708
+ }
709
+ }
710
+ }
711
+
712
+ }
653
713
  }
654
-
714
+
655
715
  }
656
716
 
657
717
  }
@@ -3011,6 +3011,15 @@ export class Sheet {
3011
3011
  this.name = data.name || ''; // wtf is this?
3012
3012
  }
3013
3013
 
3014
+ // patching from import
3015
+ for (const cell of this.cells.Iterate()) {
3016
+ if (cell.spill) {
3017
+ if (!cell.spill.start.sheet_id) {
3018
+ cell.spill.SetSheetID(this.id);
3019
+ }
3020
+ }
3021
+ }
3022
+
3014
3023
  if (data.tab_color) {
3015
3024
  this.tab_color = data.tab_color;
3016
3025
  }
@@ -26,7 +26,7 @@ import Base64JS from 'base64-js';
26
26
 
27
27
  import type { AnchoredChartDescription, AnchoredImageDescription, AnchoredTextBoxDescription} from './workbook';
28
28
  import { ChartType, ConditionalFormatOperators, Workbook } from './workbook';
29
- import type { ParseResult } from 'treb-parser';
29
+ import type { ExpressionUnit, ParseResult, UnitCall } from 'treb-parser';
30
30
  import { DecimalMarkType, Parser } from 'treb-parser';
31
31
  import type { RangeType, AddressType, HyperlinkType } from './address-type';
32
32
  import { is_range, ShiftRange, InRange, is_address } from './address-type';
@@ -42,6 +42,7 @@ import { ColumnWidthToPixels } from './column-width';
42
42
  import type { DataValidation, AnnotationType } from 'treb-data-model';
43
43
  import { ZipWrapper } from './zip-wrapper';
44
44
  import type { ConditionalFormat } from 'treb-data-model';
45
+ import { LookupMetadata, type MetadataFlags } from './metadata';
45
46
 
46
47
  interface SharedFormula {
47
48
  row: number;
@@ -57,6 +58,7 @@ interface CellElementType {
57
58
  r?: string;
58
59
  t?: string;
59
60
  s?: string;
61
+ cm?: string;
60
62
  };
61
63
  v?: string|number|{
62
64
  t$: string;
@@ -147,6 +149,7 @@ export class Importer {
147
149
  element: CellElementType,
148
150
  shared_formulae: SharedFormulaMap,
149
151
  arrays: RangeType[],
152
+ dynamic_arrays: RangeType[],
150
153
  merges: RangeType[],
151
154
  links: HyperlinkType[],
152
155
  // validations: Array<{ address: ICellAddress, validation: DataValidation }>,
@@ -165,6 +168,15 @@ export class Importer {
165
168
  return undefined;
166
169
  }
167
170
 
171
+ // new (to us) metadata
172
+ let metadata_flags: MetadataFlags = {};
173
+ if (element.a$?.cm) {
174
+ const cm_index = Number(element.a$?.cm);
175
+ if (this.workbook?.metadata) {
176
+ metadata_flags = LookupMetadata(this.workbook.metadata, 'cell', cm_index).flags;
177
+ }
178
+ }
179
+
168
180
  // console.info(element);
169
181
 
170
182
  let value: undefined | number | boolean | string;
@@ -213,6 +225,8 @@ export class Importer {
213
225
 
214
226
  if (formula) {
215
227
 
228
+ // console.info("F", formula);
229
+
216
230
  // doing it like this is sloppy (also does not work properly).
217
231
  value = '=' + formula.replace(/^_xll\./g, '');
218
232
 
@@ -221,10 +235,66 @@ export class Importer {
221
235
  value = formula;
222
236
  }
223
237
  else {
238
+
224
239
  const parse_result = this.parser.Parse(formula); // l10n?
225
240
  if (parse_result.expression) {
226
- this.parser.Walk(parse_result.expression, (unit) => {
241
+
242
+ const TrimPrefixes = (name: string) => {
243
+
244
+ if (/^_xll\./.test(name)) {
245
+ name = name.substring(5);
246
+ }
247
+ if (/^_xlfn\./.test(name)) {
248
+ console.info("xlfn:", name);
249
+ name = name.substring(6);
250
+ }
251
+ if (/^_xlws\./.test(name)) {
252
+ console.info("xlws:", name);
253
+ name = name.substring(6);
254
+ }
255
+
256
+ return name;
257
+ };
258
+
259
+ const TreeWalker = (unit: ExpressionUnit) => {
260
+
227
261
  if (unit.type === 'call') {
262
+
263
+ // if we see _xlfn.SINGLE, translate that into
264
+ // @ + the name of the first parameter...
265
+ //
266
+ // this is solving the case where single appears, but it
267
+ // doesn't solve the case where it does not appear -- they
268
+ // may be using the array flag of the cell as an indicator?
269
+ // but how then do they know it _should_ be an array? not
270
+ // sure, and I don't see any other indication.
271
+
272
+ if (/^_xlfn\.single/i.test(unit.name)) {
273
+
274
+ const first = unit.args[0];
275
+ if (first.type === 'call') {
276
+
277
+ // we could do this in place, we don't need to copy...
278
+ // although it seems like a good idea. also watch out,
279
+ // these SINGLEs could be nested.
280
+
281
+ const replacement: UnitCall = JSON.parse(JSON.stringify(first));
282
+ replacement.name = '@' + TrimPrefixes(replacement.name);
283
+
284
+ for (let i = 0; i < replacement.args.length; i++) {
285
+ replacement.args[i] = this.parser.Walk2(replacement.args[i], TreeWalker);
286
+ }
287
+
288
+ return replacement;
289
+ }
290
+ else {
291
+ console.info("_xlfn.SINGLE unexpected argument", unit.args[0]);
292
+ }
293
+ }
294
+
295
+ unit.name = TrimPrefixes(unit.name);
296
+
297
+ /*
228
298
  if (/^_xll\./.test(unit.name)) {
229
299
  unit.name = unit.name.substring(5);
230
300
  }
@@ -236,9 +306,15 @@ export class Importer {
236
306
  console.info("xlws:", unit.name);
237
307
  unit.name = unit.name.substring(6);
238
308
  }
309
+ */
310
+
239
311
  }
240
312
  return true;
241
- });
313
+
314
+ };
315
+
316
+ parse_result.expression = this.parser.Walk2(parse_result.expression, TreeWalker);
317
+
242
318
  value = '=' + this.parser.Render(parse_result.expression, { missing: '' });
243
319
  }
244
320
  }
@@ -274,10 +350,38 @@ export class Importer {
274
350
  }
275
351
  }
276
352
 
353
+ //
354
+ // arrays and spill/dynamic arrays
355
+ //
356
+
277
357
  if (typeof element.f !== 'string' && element.f.a$?.t === 'array') {
278
358
  const translated = sheet.TranslateAddress(element.f.a$.ref || '');
279
- if (is_range(translated)) {
280
- arrays.push(ShiftRange(translated, -1, -1));
359
+
360
+ // why are we checking "is_range" here? this should be valid
361
+ // even if the ref attribute is one cell, if it explicitly
362
+ // says t="array"
363
+
364
+ // we will need to adjust it, though? yes, because the lists
365
+ // only accept ranges. note that range type has a superfluous
366
+ // sheet parameter? ...
367
+
368
+ let range = translated;
369
+ if (!is_range(range)) {
370
+ range = {
371
+ to: { ...range },
372
+ from: { ...range },
373
+ sheet: range.sheet,
374
+ };
375
+ }
376
+
377
+ // if (is_range(translated))
378
+ {
379
+ if (metadata_flags['dynamic-array']) {
380
+ dynamic_arrays.push(ShiftRange(range, -1, -1));
381
+ }
382
+ else {
383
+ arrays.push(ShiftRange(range, -1, -1));
384
+ }
281
385
  }
282
386
  }
283
387
 
@@ -317,12 +421,14 @@ export class Importer {
317
421
  // but perhaps we should check that... although at this point we have
318
422
  // already added the array so we need to check for root
319
423
 
320
- for (const array of arrays) {
321
- if (InRange(array, shifted) && (shifted.row !== array.from.row || shifted.col !== array.from.col)) {
322
- calculated_type = type;
323
- calculated_value = value;
324
- value = undefined;
325
- type = 'undefined'; // ValueType.undefined;
424
+ for (const set of [arrays, dynamic_arrays]) {
425
+ for (const array of set) {
426
+ if (InRange(array, shifted) && (shifted.row !== array.from.row || shifted.col !== array.from.col)) {
427
+ calculated_type = type;
428
+ calculated_value = value;
429
+ value = undefined;
430
+ type = 'undefined'; // ValueType.undefined;
431
+ }
326
432
  }
327
433
  }
328
434
 
@@ -369,6 +475,20 @@ export class Importer {
369
475
  }
370
476
  }
371
477
 
478
+ for (const range of dynamic_arrays) {
479
+ if (InRange(range, shifted)) {
480
+ result.spill = {
481
+ start: {
482
+ row: range.from.row,
483
+ column: range.from.col,
484
+ }, end: {
485
+ row: range.to.row,
486
+ column: range.to.col,
487
+ },
488
+ }
489
+ }
490
+ }
491
+
372
492
  for (const range of arrays) {
373
493
  if (InRange(range, shifted)) {
374
494
  result.area = {
@@ -675,6 +795,7 @@ export class Importer {
675
795
  const data: CellParseResult[] = [];
676
796
  const shared_formulae: {[index: string]: SharedFormula} = {};
677
797
  const arrays: RangeType[] = [];
798
+ const dynamic_arrays: RangeType[] = [];
678
799
  const merges: RangeType[] = [];
679
800
  const conditional_formats: ConditionalFormat[] = [];
680
801
  const links: HyperlinkType[] = [];
@@ -967,7 +1088,7 @@ export class Importer {
967
1088
  const cells = row.c ? Array.isArray(row.c) ? row.c : [row.c] : [];
968
1089
 
969
1090
  for (const element of cells) {
970
- const cell = this.ParseCell(sheet, element as unknown as CellElementType, shared_formulae, arrays, merges, links); // , validations);
1091
+ const cell = this.ParseCell(sheet, element as unknown as CellElementType, shared_formulae, arrays, dynamic_arrays, merges, links); // , validations);
971
1092
  if (cell) {
972
1093
  data.push(cell);
973
1094
  }
@@ -1254,12 +1375,21 @@ export class Importer {
1254
1375
 
1255
1376
  break;
1256
1377
 
1378
+ case ChartType.Histogram:
1379
+ type = 'treb-chart';
1380
+ func = 'Histogram.Plot';
1381
+ if (series?.length) {
1382
+ // ...
1383
+ }
1384
+ args[1] = descriptor.chart.title;
1385
+ break;
1386
+
1257
1387
  case ChartType.Box:
1258
1388
  type = 'treb-chart';
1259
1389
  func = 'Box.Plot';
1260
1390
  if (series?.length) {
1261
1391
  args[0] = `Group(${series.map(s => `Series(${s.title || ''},,${s.values||''})`).join(', ')})`;
1262
- console.info("S?", {series}, args[0])
1392
+ // console.info("S?", {series}, args[0])
1263
1393
  }
1264
1394
  args[1] = descriptor.chart.title;
1265
1395
  break;
@@ -1317,6 +1447,10 @@ export class Importer {
1317
1447
  args[1] = series[0]?.categories || '';
1318
1448
  }
1319
1449
 
1450
+ if (descriptor.chart.type === ChartType.Column && descriptor.chart.flags?.includes('stacked')) {
1451
+ args[3] = '"stacked"';
1452
+ }
1453
+
1320
1454
  break;
1321
1455
  }
1322
1456