@trebco/treb 28.10.5 → 28.13.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.
@@ -309,6 +309,17 @@ export const Equals = (a: UnionValue, b: UnionValue): UnionValue => {
309
309
 
310
310
  }
311
311
 
312
+ // this is standard (icase) string equality. we might also need
313
+ // to handle wildcard string matching, although it's not the
314
+ // default case for = operators.
315
+
316
+ if (a.type === ValueType.string && b.type === ValueType.string) {
317
+ return {
318
+ type: ValueType.boolean,
319
+ value: a.value.toLowerCase() === b.value.toLowerCase(),
320
+ };
321
+ }
322
+
312
323
  return { type: ValueType.boolean, value: a.value == b.value }; // note ==
313
324
  };
314
325
 
@@ -302,3 +302,58 @@ export const ApplyAsArray2 = (base: (a: any, b: any, ...rest: any[]) => UnionVal
302
302
  }
303
303
  };
304
304
 
305
+
306
+ /**
307
+ * parse a string with wildcards into a regex pattern
308
+ *
309
+ * from
310
+ * https://exceljet.net/glossary/wildcard
311
+ *
312
+ * Excel has 3 wildcards you can use in your formulas:
313
+ *
314
+ * Asterisk (*) - zero or more characters
315
+ * Question mark (?) - any one character
316
+ * Tilde (~) - escape for literal character (~*) a literal question mark (~?), or a literal tilde (~~)
317
+ *
318
+ * they're pretty liberal with escaping, nothing is an error, just roll with it
319
+ *
320
+ */
321
+ export const ParseWildcards = (text: string): string => {
322
+
323
+ const result: string[] = [];
324
+ const length = text.length;
325
+
326
+ const escaped_chars = '[\\^$.|?*+()';
327
+
328
+ for (let i = 0; i < length; i++) {
329
+ let char = text[i];
330
+ switch (char) {
331
+
332
+ case '*':
333
+ result.push('.', '*');
334
+ break;
335
+
336
+ case '?':
337
+ result.push('.');
338
+ break;
339
+
340
+ case '~':
341
+ char = text[++i] || '';
342
+
343
+ // eslint-disable-next-line no-fallthrough
344
+ default:
345
+ for (let j = 0; j < escaped_chars.length; j++) {
346
+ if (char === escaped_chars[j]) {
347
+ result.push('\\');
348
+ break;
349
+ }
350
+ }
351
+ result.push(char);
352
+ break;
353
+
354
+ }
355
+ }
356
+
357
+ return result.join('');
358
+
359
+ };
@@ -310,7 +310,7 @@ export const TransformSeriesData = (raw_data?: UnionValue, default_x?: UnionValu
310
310
  };
311
311
 
312
312
  /** get a unified scale, and formats */
313
- export const CommonData = (series: SeriesType[], y_floor?: number, y_ceiling?: number) => {
313
+ export const CommonData = (series: SeriesType[], y_floor?: number, y_ceiling?: number, x_floor?: number, x_ceiling?: number) => {
314
314
 
315
315
  let x_format = '';
316
316
  let y_format = '';
@@ -362,7 +362,14 @@ export const CommonData = (series: SeriesType[], y_floor?: number, y_ceiling?: n
362
362
  }
363
363
  }
364
364
  }
365
-
365
+
366
+ if (typeof x_floor !== 'undefined') {
367
+ x_min = Math.min(x_min, x_floor);
368
+ }
369
+ if (typeof x_ceiling !== 'undefined') {
370
+ x_min = Math.max(x_min, x_ceiling);
371
+ }
372
+
366
373
  if (typeof y_floor !== 'undefined') {
367
374
  y_min = Math.min(y_min, y_floor);
368
375
  }
@@ -438,7 +445,21 @@ const ApplyLabels = (series_list: SeriesType[], pattern: string, category_labels
438
445
  export const CreateBubbleChart = (args: UnionValue[]): ChartData => {
439
446
 
440
447
  const series: SeriesType[] = TransformSeriesData(args[0]);
441
- const common = CommonData(series);
448
+
449
+ let y_floor: number|undefined = undefined;
450
+ let x_floor: number|undefined = undefined;
451
+
452
+ for (const entry of series) {
453
+
454
+ if (typeof entry.x.range?.min === 'number' && entry.x.range.min > 0 && entry.x.range.min < 50) {
455
+ x_floor = 0;
456
+ }
457
+ if (typeof entry.y.range?.min === 'number' && entry.y.range.min > 0 && entry.y.range.min < 50) {
458
+ y_floor = 0;
459
+ }
460
+ }
461
+
462
+ const common = CommonData(series, y_floor, undefined, x_floor);
442
463
  const title = args[1]?.toString() || undefined;
443
464
  const options = args[2]?.toString() || undefined;
444
465
 
@@ -196,6 +196,8 @@ export class DefaultChartRenderer implements ChartRendererType {
196
196
 
197
197
  // now do type-specific rendering
198
198
 
199
+ let zeros: number[]|undefined = [];
200
+
199
201
  switch (chart_data.type) {
200
202
  case 'scatter':
201
203
  this.renderer.RenderPoints(area, chart_data.x, chart_data.y, 'mc mc-correlation series-1');
@@ -203,10 +205,17 @@ export class DefaultChartRenderer implements ChartRendererType {
203
205
 
204
206
  case 'bubble':
205
207
 
208
+ if (chart_data.x_scale.min <= 0 && chart_data.x_scale.max >= 0) {
209
+ zeros[0] = Math.round(Math.abs(chart_data.x_scale.min / chart_data.x_scale.step));
210
+ }
211
+ if (chart_data.y_scale.min <= 0 && chart_data.y_scale.max >= 0) {
212
+ zeros[1] = Math.round(Math.abs(chart_data.y_scale.max / chart_data.y_scale.step));
213
+ }
214
+
206
215
  this.renderer.RenderGrid(area,
207
216
  chart_data.y_scale.count,
208
217
  chart_data.x_scale.count + 1, // (sigh)
209
- 'chart-grid');
218
+ 'chart-grid', zeros);
210
219
 
211
220
  for (const [index, series] of chart_data.series.entries()) {
212
221
  const series_index = (typeof series.index === 'number') ? series.index : index + 1;
@@ -968,23 +968,54 @@ export class ChartRenderer {
968
968
 
969
969
  }
970
970
 
971
- public RenderGrid(area: Area, y_count: number, x_count = 0, classes?: string | string[]): void {
971
+ public RenderGrid(area: Area, y_count: number, x_count = 0, classes?: string | string[], zeros?: number[]): void {
972
972
 
973
973
  const d: string[] = [];
974
+ const d2: string[] = [];
974
975
 
975
976
  let step = area.height / y_count;
976
977
  for (let i = 0; i <= y_count; i++) {
978
+
977
979
  const y = Math.round(area.top + step * i) - 0.5;
978
- d.push(`M${area.left} ${y} L${area.right} ${y}`);
980
+
981
+ if (zeros && zeros[1] === i) {
982
+ d2.push(`M${area.left} ${y} L${area.right} ${y}`);
983
+ }
984
+ else {
985
+ d.push(`M${area.left} ${y} L${area.right} ${y}`);
986
+ }
979
987
  }
980
988
 
981
989
  step = area.width / (x_count - 1);
982
990
  for (let i = 0; i < x_count; i++) {
991
+
983
992
  const x = Math.round(area.left + step * i) - 0.5;
984
- d.push(`M${x} ${area.top} L${x} ${area.bottom}`);
993
+
994
+ if (zeros && zeros[0] === i) {
995
+ d2.push(`M${x} ${area.top} L${x} ${area.bottom}`);
996
+ }
997
+ else {
998
+ d.push(`M${x} ${area.top} L${x} ${area.bottom}`);
999
+ }
985
1000
  }
986
1001
 
987
1002
  this.group.appendChild(SVGNode('path', {d, class: classes}));
1003
+ if (d2.length) {
1004
+
1005
+ if (classes) {
1006
+ if (!Array.isArray(classes)) {
1007
+ classes = classes + ' zero';
1008
+ }
1009
+ else {
1010
+ classes.push('zero');
1011
+ }
1012
+ }
1013
+ else {
1014
+ classes = 'zero';
1015
+ }
1016
+
1017
+ this.group.appendChild(SVGNode('path', {d: d2, class: classes}));
1018
+ }
988
1019
 
989
1020
  }
990
1021
 
@@ -104,6 +104,11 @@
104
104
  /* grid */
105
105
  .chart-grid, .chart-ticks {
106
106
  stroke: var(--treb-chart-grid-color, #ddd);
107
+
108
+ &.zero {
109
+ stroke: var(--treb-chart-grid-zero-color, var(--treb-chart-grid-color, #999));
110
+ }
111
+
107
112
  }
108
113
 
109
114
  /* mouse elements */
@@ -42,6 +42,8 @@ import type {
42
42
  CondifionalFormatExpressionOptions,
43
43
  ConditionalFormatCellMatchOptions,
44
44
  ConditionalFormatCellMatch,
45
+ MacroFunction,
46
+ SerializedNamedExpression,
45
47
  } from 'treb-grid';
46
48
 
47
49
  import {
@@ -413,7 +415,7 @@ export class EmbeddedSpreadsheet<USER_DATA_TYPE = unknown> {
413
415
  }
414
416
 
415
417
  /** for destruction */
416
- protected view?: HTMLElement;
418
+ protected view_node?: HTMLElement;
417
419
 
418
420
  /** for destruction */
419
421
  protected key_listener?: (event: KeyboardEvent) => void;
@@ -844,7 +846,7 @@ export class EmbeddedSpreadsheet<USER_DATA_TYPE = unknown> {
844
846
  // elements. but we don't add a default; rather we use a template
845
847
 
846
848
  const template = container.querySelector('.treb-view-template') as HTMLTemplateElement;
847
- this.view = template.content.firstElementChild?.cloneNode(true) as HTMLElement;
849
+ this.view_node = template.content.firstElementChild?.cloneNode(true) as HTMLElement;
848
850
 
849
851
  // this is a little weird but we're inserting at the front. the
850
852
  // reason for this is that we only want to use one resize handle,
@@ -852,13 +854,13 @@ export class EmbeddedSpreadsheet<USER_DATA_TYPE = unknown> {
852
854
 
853
855
  // we could work around this, really we're just being lazy.
854
856
 
855
- container.prepend(this.view);
857
+ container.prepend(this.view_node);
856
858
 
857
859
 
858
860
  // this.node = container;
859
861
  // this.node = this.view;
860
862
 
861
- this.view.addEventListener('focusin', () => {
863
+ this.view_node.addEventListener('focusin', () => {
862
864
  if (this.focus_target !== this) {
863
865
  this.Publish({ type: 'focus-view' });
864
866
  this.focus_target = this;
@@ -878,14 +880,14 @@ export class EmbeddedSpreadsheet<USER_DATA_TYPE = unknown> {
878
880
 
879
881
  // const view = container.querySelector('.treb-view') as HTMLElement;
880
882
 
881
- this.grid.Initialize(this.view, toll_initial_render);
883
+ this.grid.Initialize(this.view_node, toll_initial_render);
882
884
 
883
885
  // dnd
884
886
 
885
887
  if (this.options.dnd) {
886
- this.view.addEventListener('dragenter', (event) => this.HandleDrag(event));
887
- this.view.addEventListener('dragover', (event) => this.HandleDrag(event));
888
- this.view.addEventListener('drop', (event) => this.HandleDrop(event));
888
+ this.view_node.addEventListener('dragenter', (event) => this.HandleDrag(event));
889
+ this.view_node.addEventListener('dragover', (event) => this.HandleDrag(event));
890
+ this.view_node.addEventListener('drop', (event) => this.HandleDrop(event));
889
891
  }
890
892
 
891
893
  // set up grid events
@@ -1244,19 +1246,19 @@ export class EmbeddedSpreadsheet<USER_DATA_TYPE = unknown> {
1244
1246
  sheet.grid.grid_events.CancelAll();
1245
1247
  sheet.events.CancelAll();
1246
1248
 
1247
- if (sheet.view?.parentElement) {
1249
+ if (sheet.view_node?.parentElement) {
1248
1250
 
1249
1251
  // remove listener
1250
1252
  if (sheet.key_listener) {
1251
- sheet.view.parentElement.removeEventListener('keydown', sheet.key_listener);
1253
+ sheet.view_node.parentElement.removeEventListener('keydown', sheet.key_listener);
1252
1254
  }
1253
1255
 
1254
1256
  // remove node
1255
- sheet.view.parentElement.removeChild(sheet.view);
1257
+ sheet.view_node.parentElement.removeChild(sheet.view_node);
1256
1258
  }
1257
1259
 
1258
1260
  // in case other view was focused
1259
- this.view?.focus();
1261
+ this.view_node?.focus();
1260
1262
 
1261
1263
  // usually this results in us getting larger, we may need to update
1262
1264
  this.Resize();
@@ -1285,7 +1287,7 @@ export class EmbeddedSpreadsheet<USER_DATA_TYPE = unknown> {
1285
1287
  const view = this.CreateView();
1286
1288
  view.grid.EnsureActiveSheet(true);
1287
1289
 
1288
- view.view?.addEventListener('focusin', () => {
1290
+ view.view_node?.addEventListener('focusin', () => {
1289
1291
  if (this.focus_target !== view) {
1290
1292
  this.Publish({ type: 'focus-view' });
1291
1293
  this.focus_target = view;
@@ -2244,12 +2246,16 @@ export class EmbeddedSpreadsheet<USER_DATA_TYPE = unknown> {
2244
2246
  this.parser.flags.r1c1 = r1c1;
2245
2247
 
2246
2248
  if (argument_separator === ',') {
2247
- this.parser.argument_separator = ArgumentSeparatorType.Comma;
2248
- this.parser.decimal_mark = DecimalMarkType.Period;
2249
+ this.parser.SetLocaleSettings(DecimalMarkType.Period);
2250
+
2251
+ // this.parser.argument_separator = ArgumentSeparatorType.Comma;
2252
+ // this.parser.decimal_mark = DecimalMarkType.Period;
2249
2253
  }
2250
2254
  else {
2251
- this.parser.argument_separator = ArgumentSeparatorType.Semicolon;
2252
- this.parser.decimal_mark = DecimalMarkType.Comma;
2255
+ this.parser.SetLocaleSettings(DecimalMarkType.Comma);
2256
+
2257
+ // this.parser.argument_separator = ArgumentSeparatorType.Semicolon;
2258
+ // this.parser.decimal_mark = DecimalMarkType.Comma;
2253
2259
  }
2254
2260
 
2255
2261
  const result = this.parser.Parse(formula);
@@ -2354,12 +2360,16 @@ export class EmbeddedSpreadsheet<USER_DATA_TYPE = unknown> {
2354
2360
  */
2355
2361
 
2356
2362
  if (argument_separator === ',') {
2357
- this.parser.argument_separator = ArgumentSeparatorType.Comma;
2358
- this.parser.decimal_mark = DecimalMarkType.Period;
2363
+ this.parser.SetLocaleSettings(DecimalMarkType.Period);
2364
+
2365
+ // this.parser.argument_separator = ArgumentSeparatorType.Comma;
2366
+ // this.parser.decimal_mark = DecimalMarkType.Period;
2359
2367
  }
2360
2368
  else {
2361
- this.parser.argument_separator = ArgumentSeparatorType.Semicolon;
2362
- this.parser.decimal_mark = DecimalMarkType.Comma;
2369
+ this.parser.SetLocaleSettings(DecimalMarkType.Comma);
2370
+
2371
+ // this.parser.argument_separator = ArgumentSeparatorType.Semicolon;
2372
+ // this.parser.decimal_mark = DecimalMarkType.Comma;
2363
2373
  }
2364
2374
 
2365
2375
  // const r1c1_state = this.parser.flags.r1c1;
@@ -2749,13 +2759,14 @@ export class EmbeddedSpreadsheet<USER_DATA_TYPE = unknown> {
2749
2759
 
2750
2760
  // FIXME: type
2751
2761
 
2752
- const serialized: SerializedModel = this.grid.Serialize({
2762
+ const serialized: SerializedModel = this.Serialize({ // this.grid.Serialize({
2753
2763
  rendered_values: true,
2754
2764
  expand_arrays: true,
2755
2765
  export_colors: true,
2756
2766
  decorated_cells: true,
2757
2767
  tables: true,
2758
2768
  share_resources: false,
2769
+ export_functions: true,
2759
2770
  });
2760
2771
 
2761
2772
  // why do _we_ put this in, instead of the grid method?
@@ -3523,6 +3534,9 @@ export class EmbeddedSpreadsheet<USER_DATA_TYPE = unknown> {
3523
3534
  /**
3524
3535
  * Create a macro function.
3525
3536
  *
3537
+ * FIXME: this needs a control for argument separator, like other
3538
+ * functions that use formulas (@see SetRange)
3539
+ *
3526
3540
  * @public
3527
3541
  */
3528
3542
  public DefineFunction(name: string, argument_names: string | string[] = '', function_def = '0'): void {
@@ -3592,7 +3606,8 @@ export class EmbeddedSpreadsheet<USER_DATA_TYPE = unknown> {
3592
3606
  ...options,
3593
3607
  };
3594
3608
 
3595
- const grid_data = this.grid.Serialize(options);
3609
+ // const grid_data = this.grid.Serialize(options);
3610
+ const grid_data = this.Serialize(options);
3596
3611
 
3597
3612
  // NOTE: these are not really env vars. we replace them at build time
3598
3613
  // via a webpack plugin. using the env syntax lets them look "real" at
@@ -4405,6 +4420,121 @@ export class EmbeddedSpreadsheet<USER_DATA_TYPE = unknown> {
4405
4420
 
4406
4421
  // --- internal (protected) methods ------------------------------------------
4407
4422
 
4423
+ // --- moved from grid/grid base ---------------------------------------------
4424
+
4425
+
4426
+ /**
4427
+ * serialize data model. moved from grid/grid base. this is moved so we
4428
+ * have access to the calculator, which we want so we can do function
4429
+ * translation on some new functions that don't necessarily map 1:1 to
4430
+ * XLSX functions. we can also do cleanup on functions where we're less
4431
+ * strict about arguments (ROUND, for example).
4432
+ *
4433
+ */
4434
+ protected Serialize(options: SerializeOptions = {}): SerializedModel {
4435
+
4436
+ const active_sheet = this.grid.active_sheet; // I thought this was in view? (...)
4437
+
4438
+ active_sheet.selection = JSON.parse(JSON.stringify(this.grid.GetSelection()));
4439
+
4440
+ // same for scroll offset
4441
+
4442
+ const scroll_offset = this.grid.ScrollOffset();
4443
+ if (scroll_offset) {
4444
+ active_sheet.scroll_offset = scroll_offset; // this.grid.layout.scroll_offset;
4445
+ }
4446
+
4447
+ // NOTE: annotations moved to sheets, they will be serialized in the sheets
4448
+
4449
+ const sheet_data = this.model.sheets.list.map((sheet) => sheet.toJSON(options));
4450
+
4451
+ // OK, not serializing tables in cells anymore. old comment about this:
4452
+ //
4453
+ // at the moment, tables are being serialized in cells. if we put them
4454
+ // in here, then we have two records of the same data. that would be bad.
4455
+ // I think this is probably the correct place, but if we put them here
4456
+ // we need to stop serializing in cells. and I'm not sure that there are
4457
+ // not some side-effects to that. hopefully not, but (...)
4458
+ //
4459
+
4460
+ let tables: Table[] | undefined;
4461
+ if (this.model.tables.size > 0) {
4462
+ tables = Array.from(this.model.tables.values());
4463
+ }
4464
+
4465
+ // NOTE: moving into a structured object (the sheet data is also structured,
4466
+ // of course) but we are moving things out of sheet (just named ranges atm))
4467
+
4468
+ let macro_functions: MacroFunction[] | undefined;
4469
+
4470
+ if (this.model.macro_functions.size) {
4471
+ macro_functions = [];
4472
+ for (const macro of this.model.macro_functions.values()) {
4473
+ macro_functions.push({
4474
+ ...macro,
4475
+ expression: undefined,
4476
+ });
4477
+ }
4478
+ }
4479
+
4480
+ // when serializing named expressions, we have to make sure
4481
+ // that there's a sheet name in any address/range.
4482
+
4483
+ const named_expressions: SerializedNamedExpression[] = [];
4484
+ if (this.model.named_expressions) {
4485
+
4486
+ for (const [name, expr] of this.model.named_expressions) {
4487
+ this.parser.Walk(expr, unit => {
4488
+ if (unit.type === 'address' || unit.type === 'range') {
4489
+ const test = unit.type === 'range' ? unit.start : unit;
4490
+
4491
+ test.absolute_column = test.absolute_row = true;
4492
+
4493
+ if (!test.sheet) {
4494
+ if (test.sheet_id) {
4495
+ const sheet = this.model.sheets.Find(test.sheet_id);
4496
+ if (sheet) {
4497
+ test.sheet = sheet.name;
4498
+ }
4499
+ }
4500
+ if (!test.sheet) {
4501
+ test.sheet = active_sheet.name;
4502
+ }
4503
+ }
4504
+
4505
+ if (unit.type === 'range') {
4506
+ unit.end.absolute_column = unit.end.absolute_row = true;
4507
+ }
4508
+
4509
+ return false;
4510
+ }
4511
+ else if (unit.type === 'call' && options.export_functions) {
4512
+ // ...
4513
+ }
4514
+ return true;
4515
+ });
4516
+ const rendered = this.parser.Render(expr, { missing: '' });
4517
+ named_expressions.push({
4518
+ name, expression: rendered
4519
+ });
4520
+ }
4521
+ }
4522
+
4523
+ return {
4524
+ sheet_data,
4525
+ active_sheet: active_sheet.id,
4526
+ named_ranges: this.model.named_ranges.Count() ?
4527
+ this.model.named_ranges.Serialize() :
4528
+ undefined,
4529
+ macro_functions,
4530
+ tables,
4531
+ named_expressions: named_expressions.length ? named_expressions : undefined,
4532
+ };
4533
+
4534
+ }
4535
+
4536
+ // --- /moved ----------------------------------------------------------------
4537
+
4408
4538
  /**
4409
4539
  *
4410
4540
  */
@@ -5531,14 +5661,18 @@ export class EmbeddedSpreadsheet<USER_DATA_TYPE = unknown> {
5531
5661
  // FIXME: also we should unify on types for decimal, argument separator
5532
5662
 
5533
5663
  if (data.decimal_mark === '.') {
5534
- parser.decimal_mark = DecimalMarkType.Period;
5535
- parser.argument_separator = ArgumentSeparatorType.Comma;
5664
+ // parser.decimal_mark = DecimalMarkType.Period;
5665
+ // parser.argument_separator = ArgumentSeparatorType.Comma;
5666
+ parser.SetLocaleSettings(DecimalMarkType.Period);
5667
+
5536
5668
  target_decimal_mark = DecimalMarkType.Comma;
5537
5669
  target_argument_separator = ArgumentSeparatorType.Semicolon;
5538
5670
  }
5539
5671
  else {
5540
- parser.decimal_mark = DecimalMarkType.Comma;
5541
- parser.argument_separator = ArgumentSeparatorType.Semicolon;
5672
+ // parser.decimal_mark = DecimalMarkType.Comma;
5673
+ // parser.argument_separator = ArgumentSeparatorType.Semicolon;
5674
+ parser.SetLocaleSettings(DecimalMarkType.Comma);
5675
+
5542
5676
  target_decimal_mark = DecimalMarkType.Period;
5543
5677
  target_argument_separator = ArgumentSeparatorType.Comma;
5544
5678
  }
@@ -33,6 +33,7 @@
33
33
  --treb-autocomplete-tooltip-color: #fff;
34
34
  --treb-chart-background: #000;
35
35
  --treb-chart-grid-color: #976;
36
+ --treb-chart-grid-zero-color: #fdd;
36
37
  --treb-chart-text-color: #fff;
37
38
  --treb-dialog-background: #000;
38
39
  --treb-dialog-color: #fff;