@trebco/treb 30.3.2 → 30.6.0

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.
@@ -25,6 +25,95 @@ import { ValueError, ArgumentError, NAError } from '../function-error';
25
25
  import { type Complex, type UnionValue, ValueType, type CellValue } from 'treb-base-types';
26
26
  import * as ComplexMath from '../complex-math';
27
27
 
28
+ const Median = (data: number[]) => {
29
+ const n = data.length;
30
+ if (n % 2) {
31
+ return data[Math.floor(n/2)];
32
+ }
33
+ else {
34
+ return (data[n/2] + data[n/2 - 1])/2;
35
+ }
36
+ };
37
+
38
+ const InterpolatedQuartiles = (data: number[], include_median = true) => {
39
+
40
+ data.sort((a, b) => a - b);
41
+ const n = data.length;
42
+
43
+ const interp = (p: number, base: number, skip: number) => {
44
+ const index = base * p + skip;
45
+ const offset = index % 1;
46
+ if (offset) {
47
+ const a = data[Math.floor(index)];
48
+ const b = data[Math.ceil(index)];
49
+ return a + (b - a) * offset;
50
+ }
51
+ else {
52
+ return data[index];
53
+ }
54
+
55
+ }
56
+
57
+ if (include_median) {
58
+ return [data[0], interp(.25, n - 1, 0), Median(data), interp(.75, n - 1, 0), data[n-1]];
59
+ }
60
+ else {
61
+ if (n % 1) {
62
+ return [data[0], interp(.25, n - 2, 0), Median(data), interp(.75, n - 2, 1), data[n-1]];
63
+ }
64
+ else {
65
+ return [data[0], interp(.25, n - 3, 0), Median(data), interp(.75, n - 3, 2), data[n-1]];
66
+ }
67
+ }
68
+
69
+ };
70
+
71
+ const Gamma = (z: Complex): Complex => {
72
+
73
+ // this is a Lanczos approximation. could be cleaned up.
74
+
75
+ const coefficients = [
76
+ 0.99999999999980993,
77
+ 676.5203681218851,
78
+ -1259.1392167224028,
79
+ 771.32342877765313,
80
+ -176.61502916214059,
81
+ 12.507343278686905,
82
+ -0.13857109526572012,
83
+ 9.9843695780195716e-6,
84
+ 1.5056327351493116e-7
85
+ ];
86
+
87
+ // generally speaking I'm against operator overloading but
88
+ // it would be a big help for complex math
89
+
90
+ const pi = Math.PI;
91
+ const sin = ComplexMath.Sin;
92
+ const div = ComplexMath.Divide;
93
+ const mul = ComplexMath.Multiply;
94
+ const cpx = (a: number) => ({ real: a, imaginary: 0 });
95
+ const add = (a: Complex, b: Complex): Complex => ({ real: a.real + b.real, imaginary: a.imaginary + b.imaginary });
96
+ const pow = ComplexMath.Power;
97
+ const exp = ComplexMath.Exp;
98
+ const inv = (a: Complex) => ({ real: -a.real, imaginary: -a.imaginary });
99
+ const prod = ComplexMath.Product;
100
+
101
+ if (z.real < 0.5) {
102
+ return div(cpx(pi), mul(sin(mul(cpx(pi), z)), Gamma({ real: 1 - z.real, imaginary: -z.imaginary })));
103
+ }
104
+
105
+ z.real -= 1;
106
+ let x = cpx(coefficients[0]);
107
+
108
+ for (let i = 1; i < coefficients.length; i++) {
109
+ x = add(x, div(cpx(coefficients[i]), add(z, cpx(i))));
110
+ }
111
+
112
+ const t = add(z, cpx(7.5));
113
+ return prod(cpx(Math.sqrt(2 * pi)), pow(t, add(z, cpx(0.5))), exp(inv(t)), x);
114
+
115
+ };
116
+
28
117
  export const Variance = (data: number[], sample = false) => {
29
118
 
30
119
  const len = data.length;
@@ -307,6 +396,48 @@ export const StatisticsFunctionLibrary: FunctionMap = {
307
396
  },
308
397
  },
309
398
 
399
+ Gamma: {
400
+ description: 'Returns the gamma function for the given value',
401
+ arguments: [{ name: 'value', boxed: true }],
402
+ fn: (value: UnionValue) => {
403
+
404
+ let complex: Complex = { real: 0, imaginary: 0 };
405
+
406
+ if (value.type === ValueType.complex) {
407
+ complex = {...value.value};
408
+ }
409
+ else if (value.type === ValueType.number) {
410
+ complex.real = value.value;
411
+ }
412
+ else {
413
+ return ArgumentError();
414
+ }
415
+
416
+ if (complex.imaginary === 0 && complex.real % 1 === 0 && complex.real <= 0) {
417
+ return ValueError();
418
+ }
419
+
420
+ const gamma = Gamma(complex);
421
+
422
+ if (Math.abs(gamma.imaginary) <= 1e-7) {
423
+ return { type: ValueType.number, value: gamma.real };
424
+ }
425
+
426
+ return {type: ValueType.complex, value: gamma};
427
+
428
+ },
429
+ },
430
+
431
+ Delta: {
432
+ arguments: [{ name: 'number', }, { name: 'number', default: 0 }],
433
+ fn: (a: CellValue, b: CellValue = 0) => {
434
+ if (typeof a !== 'number' || typeof b !== 'number') {
435
+ return ValueError();
436
+ }
437
+ return { type: ValueType.number, value: (a === b) ? 1 : 0 };
438
+ },
439
+ },
440
+
310
441
  GCD: {
311
442
  description: 'Finds the greatest common divisor of the arguments',
312
443
  arguments: [{ boxed: true }],
@@ -457,6 +588,46 @@ export const StatisticsFunctionLibrary: FunctionMap = {
457
588
  },
458
589
  },
459
590
 
591
+ 'Quartile.Inc': {
592
+ description: 'Returns the interpolated quartile of the data set (including median)',
593
+ arguments: [
594
+ { name: 'range', },
595
+ { name: 'quartile' },
596
+ ],
597
+ xlfn: true,
598
+ fn: (data: CellValue[], quartile: CellValue) => {
599
+
600
+ if (typeof quartile !== 'number' || quartile < 0 || quartile > 4 || quartile % 1) {
601
+ return ArgumentError();
602
+ }
603
+
604
+ const flat = Utils.FlattenNumbers(data);
605
+ const quartiles = InterpolatedQuartiles(flat, true);
606
+
607
+ return { type: ValueType.number, value: quartiles[quartile] };
608
+ }
609
+ },
610
+
611
+ 'Quartile.Exc': {
612
+ description: 'Returns the interpolated quartile of the data set (excluding median)',
613
+ arguments: [
614
+ { name: 'range', },
615
+ { name: 'quartile' },
616
+ ],
617
+ xlfn: true,
618
+ fn: (data: CellValue[], quartile: CellValue) => {
619
+
620
+ if (typeof quartile !== 'number' || quartile < 1 || quartile > 3 || quartile % 1) {
621
+ return ArgumentError();
622
+ }
623
+
624
+ const flat = Utils.FlattenNumbers(data);
625
+ const quartiles = InterpolatedQuartiles(flat, false);
626
+
627
+ return { type: ValueType.number, value: quartiles[quartile] };
628
+ }
629
+ },
630
+
460
631
  Median: {
461
632
  description: 'Returns the median value of the range of data',
462
633
  arguments: [
@@ -515,4 +686,5 @@ export const StatisticsFunctionAliases: {[index: string]: string} = {
515
686
  Mean: 'Average',
516
687
  'StDev': 'StDev.S',
517
688
  'Var': 'Var.S',
689
+ 'Quartile': 'Quartile.Inc',
518
690
  };
@@ -301,6 +301,31 @@ export const TextFunctionLibrary: FunctionMap = {
301
301
  },
302
302
  },
303
303
 
304
+ Upper: {
305
+ description: 'Converts text to upper case',
306
+ arguments: [{ name: 'text', unroll: true }],
307
+ fn: (text?: string) => {
308
+ if (text === null || text === undefined) {
309
+ return { type: ValueType.undefined };
310
+ }
311
+ return {
312
+ type: ValueType.string, value: text.toString().toUpperCase(),
313
+ };
314
+ },
315
+ },
316
+ Lower: {
317
+ description: 'Converts text to lower case',
318
+ arguments: [{ name: 'text', unroll: true }],
319
+ fn: (text?: string) => {
320
+ if (text === null || text === undefined) {
321
+ return { type: ValueType.undefined };
322
+ }
323
+ return {
324
+ type: ValueType.string, value: text.toString().toLowerCase(),
325
+ };
326
+ },
327
+ },
328
+
304
329
  /** canonical should be CONCAT; concatenate can be an alias */
305
330
  Concat: {
306
331
  description: 'Pastes strings together',
@@ -172,12 +172,12 @@ export const FlattenCellValues = (args: any[]): any[] => {
172
172
  * to using any, although we're still kind of hacking at it. also we
173
173
  * need to allow typed arrays (I think we've mostly gotten those out?)
174
174
  */
175
- export const FlattenCellValues = (args: (CellValue|CellValue[]|CellValue[][]|Float32Array|Float64Array)[]): CellValue[] => {
175
+ export const FlattenCellValues = (args: (CellValue|CellValue[]|CellValue[][]|Float32Array|Float64Array)[], keep_undefined = false): CellValue[] => {
176
176
 
177
177
  if (!Array.isArray(args)) { return [args]; } // special case
178
178
  return args.reduce((a: CellValue[], b) => {
179
- if (typeof b === 'undefined') return a;
180
- if (Array.isArray(b)) return a.concat(FlattenCellValues(b));
179
+ if (typeof b === 'undefined' && !keep_undefined) return a;
180
+ if (Array.isArray(b)) return a.concat(FlattenCellValues(b, keep_undefined));
181
181
  if (b instanceof Float32Array) return a.concat(Array.from(b));
182
182
  if (b instanceof Float64Array) return a.concat(Array.from(b));
183
183
  return a.concat([b]);
@@ -193,7 +193,21 @@ export const FlattenCellValues = (args: (CellValue|CellValue[]|CellValue[][]|Flo
193
193
  export const FlattenNumbers = (args: Parameters<typeof FlattenCellValues>[0]) =>
194
194
  (FlattenCellValues(args)).filter((value): value is number => typeof value === 'number');
195
195
 
196
- export const FilterIntrinsics = (data: unknown[]): (string|number|boolean|undefined)[] => {
196
+ export const FilterIntrinsics = (data: unknown[], fill = false): (string|number|boolean|undefined)[] => {
197
+
198
+ if (fill) {
199
+ return data.map((value => {
200
+ switch (typeof value) {
201
+ case 'number':
202
+ case 'undefined':
203
+ case 'string':
204
+ case 'boolean':
205
+ return value;
206
+ }
207
+ return undefined;
208
+ }));
209
+ }
210
+
197
211
  return data.filter((value): value is number|boolean|undefined|string => {
198
212
  switch (typeof value) {
199
213
  case 'number':
@@ -17,6 +17,26 @@ import type { RangeScale } from 'treb-utils';
17
17
 
18
18
  const DEFAULT_FORMAT = '#,##0.00'; // why not use "general", or whatever the usual default is?
19
19
 
20
+ export const ArrayMinMax = (data: number[]) => {
21
+
22
+ let min = data[0];
23
+ let max = data[0];
24
+
25
+ for (const entry of data) {
26
+ if (entry < min) { min = entry; }
27
+ if (entry > max) { max = entry; }
28
+ }
29
+
30
+ return { min, max };
31
+
32
+ /*
33
+ const copy = data.slice(0);
34
+ copy.sort((a, b) => a - b);
35
+ return {min: copy[0], max: copy[copy.length - 1]};
36
+ */
37
+
38
+ };
39
+
20
40
  export const ReadSeries = (data: ReferenceSeries['value']): SeriesType => {
21
41
 
22
42
  const [label, x, y, z, index, subtype, data_labels] = data;
@@ -111,10 +131,7 @@ export const ReadSeries = (data: ReferenceSeries['value']): SeriesType => {
111
131
  // in case of no values
112
132
  if (subseries.data.length) {
113
133
  const values = subseries.data.filter(value => value || value === 0) as number[];
114
- subseries.range = {
115
- min: Math.min.apply(0, values),
116
- max: Math.max.apply(0, values),
117
- };
134
+ subseries.range = ArrayMinMax(values);
118
135
  }
119
136
  }
120
137
 
@@ -174,10 +191,7 @@ export const ArrayToSeries = (array_data: ArrayUnion): SeriesType => {
174
191
  }
175
192
 
176
193
  const values = series.y.data.filter(value => value || value === 0) as number[];
177
- series.y.range = {
178
- min: Math.min.apply(0, values),
179
- max: Math.max.apply(0, values),
180
- };
194
+ series.y.range = ArrayMinMax(values);
181
195
 
182
196
  // experimenting with complex... this should only be set if we populated
183
197
  // it from complex values
@@ -185,10 +199,7 @@ export const ArrayToSeries = (array_data: ArrayUnion): SeriesType => {
185
199
  if (series.x.data.length) {
186
200
 
187
201
  const filtered: number[] = series.x.data.filter(test => typeof test === 'number') as number[];
188
- series.x.range = {
189
- min: Math.min.apply(0, filtered),
190
- max: Math.max.apply(0, filtered),
191
- }
202
+ series.x.range = ArrayMinMax(filtered);
192
203
 
193
204
  if (first_format) {
194
205
  series.x.format = first_format;
@@ -281,10 +292,7 @@ export const TransformSeriesData = (raw_data?: UnionValue, default_x?: UnionValu
281
292
  baseline_x = {
282
293
  data,
283
294
  format,
284
- range: {
285
- min: Math.min.apply(0, filtered),
286
- max: Math.max.apply(0, filtered),
287
- }
295
+ range: ArrayMinMax(filtered),
288
296
  }
289
297
  }
290
298
 
@@ -511,26 +519,27 @@ const ApplyLabels = (series_list: SeriesType[], pattern: string, category_labels
511
519
  */
512
520
  export const BoxStats = (data: number[]) => {
513
521
 
514
- const median = (data: number[]) => {
515
- const n = data.length;
522
+ // removed copying. still has 3 loops though.
523
+
524
+ const median = (data: number[], start = 0, n = data.length) => {
516
525
  if (n % 2) {
517
- return data[Math.floor(n/2)];
526
+ return data[Math.floor(n/2) + start];
518
527
  }
519
528
  else {
520
- return (data[n/2] + data[n/2 - 1])/2;
529
+ return (data[n/2 + start] + data[n/2 - 1 + start])/2;
521
530
  }
522
531
  };
523
532
 
524
533
  const n = data.length;
525
534
  const quartiles: [number, number, number] = [0, median(data), 0];
526
-
527
535
  if (n % 2) {
528
- quartiles[0] = median(data.slice(0, Math.ceil(n/2)));
529
- quartiles[2] = median(data.slice(Math.floor(n/2)));
536
+ const floor = Math.floor(n/2);
537
+ quartiles[0] = median(data, 0, Math.ceil(n/2));
538
+ quartiles[2] = median(data, floor, data.length - floor);
530
539
  }
531
540
  else {
532
- quartiles[0] = median(data.slice(0, n/2));
533
- quartiles[2] = median(data.slice(n/2));
541
+ quartiles[0] = median(data, 0, n/2);
542
+ quartiles[2] = median(data, n/2, data.length - n/2);
534
543
  }
535
544
 
536
545
  const iqr = quartiles[2] - quartiles[0];
@@ -556,7 +565,7 @@ export const BoxStats = (data: number[]) => {
556
565
  break;
557
566
  }
558
567
  }
559
-
568
+
560
569
  return {
561
570
  data,
562
571
  quartiles,
@@ -581,7 +590,13 @@ export const CreateBoxPlot = (args: UnionValue[]): ChartData => {
581
590
  let max_n = 0;
582
591
 
583
592
  const stats: BoxPlotData['data'] = series.map(series => {
584
- const data = series.y.data.slice(0).filter((test): test is number => test !== undefined).sort((a, b) => a - b);
593
+ // const data = series.y.data.slice(0).filter((test): test is number => test !== undefined).sort((a, b) => a - b);
594
+ const data: number[] = [];
595
+ for (const entry of series.y.data) {
596
+ if (entry !== undefined) { data.push(entry); }
597
+ }
598
+ data.sort((a, b) => a - b);
599
+
585
600
  const result = BoxStats(data);
586
601
  max_n = Math.max(max_n, result.n);
587
602
  return result;
@@ -48,6 +48,16 @@ export interface SerializedNamedExpression {
48
48
  }
49
49
 
50
50
  export interface SerializedModel {
51
+
52
+ // has this been superceded by TREBDocument?
53
+ // I can't tell why we're still using this.
54
+
55
+ // ...
56
+
57
+ // it seems like TREBDocument was a replacment,
58
+ // but it has some required fields that this type
59
+ // doesn't have.
60
+
51
61
  sheet_data: SerializedSheet[];
52
62
  active_sheet: number;
53
63
 
@@ -2846,7 +2846,7 @@ export class EmbeddedSpreadsheet<USER_DATA_TYPE = unknown> {
2846
2846
  *
2847
2847
  * @internal
2848
2848
  */
2849
- public async ExportBlob(): Promise<Blob> {
2849
+ public async ExportBlob(serialized?: SerializedModel): Promise<Blob> {
2850
2850
 
2851
2851
  // this is inlined to ensure the code will be tree-shaken properly
2852
2852
  if (!process.env.XLSX_SUPPORT) {
@@ -2870,19 +2870,19 @@ export class EmbeddedSpreadsheet<USER_DATA_TYPE = unknown> {
2870
2870
  reject(event);
2871
2871
  };
2872
2872
 
2873
- // FIXME: type
2874
-
2875
- const serialized: SerializedModel = this.Serialize({ // this.grid.Serialize({
2876
- rendered_values: true,
2877
- expand_arrays: true,
2878
- export_colors: true,
2879
- decorated_cells: true,
2880
- tables: true,
2881
- share_resources: false,
2882
- export_functions: true,
2883
- apply_row_pattern: true, // if there's a row pattern, set it on rows so they export properly
2884
- });
2885
-
2873
+ if (!serialized) {
2874
+ serialized = this.Serialize({
2875
+ rendered_values: true,
2876
+ expand_arrays: true,
2877
+ export_colors: true,
2878
+ decorated_cells: true,
2879
+ tables: true,
2880
+ share_resources: false,
2881
+ export_functions: true,
2882
+ apply_row_pattern: true, // if there's a row pattern, set it on rows so they export properly
2883
+ });
2884
+ }
2885
+
2886
2886
  // why do _we_ put this in, instead of the grid method?
2887
2887
  serialized.decimal_mark = Localization.decimal_separator;
2888
2888
 
@@ -97,7 +97,7 @@
97
97
  --treb-grid-background: rgb(30,30,30);
98
98
  --treb-grid-header-background: rgb(86,86,86);
99
99
  --treb-grid-header-color: rgb(221,221,221);
100
- --treb-tab-bar-active-tab-background: field;
100
+ --treb-tab-bar-active-tab-background: #444;
101
101
  --treb-tab-bar-tab-background: transparent;
102
102
  --treb-tab-bar-tab-border-color: #444;
103
103
  --treb-ui-border-color: #aaa;
@@ -2076,12 +2076,33 @@ export class Exporter {
2076
2076
  }
2077
2077
  }
2078
2078
 
2079
+ const color_series: {
2080
+ rgb?: string;
2081
+ tint?: string;
2082
+ theme?: string;
2083
+ } = {
2084
+ rgb: 'FF376092' // default
2085
+ };
2086
+
2087
+ if (sparkline.style?.text) {
2088
+ if (IsHTMLColor(sparkline.style.text)) {
2089
+ color_series.rgb = sparkline.style.text.text;
2090
+ }
2091
+ else if (IsThemeColor(sparkline.style.text)) {
2092
+ color_series.rgb = undefined;
2093
+ color_series.theme = sparkline.style.text.theme.toString();
2094
+ color_series.tint = typeof sparkline.style.text.tint === 'number' ?
2095
+ sparkline.style.text.tint.toString() : undefined;
2096
+ }
2097
+ }
2098
+
2079
2099
  return {
2080
2100
  a$: {
2081
2101
  displayEmptyCellsAs: 'gap',
2102
+ displayHidden: '1',
2082
2103
  type: /column/i.test(sparkline.formula) ? 'column' : undefined,
2083
2104
  },
2084
- 'x14:colorSeries': { a$: { rgb: 'FF376092' }},
2105
+ 'x14:colorSeries': { a$: { ...color_series }},
2085
2106
  'x14:sparklines': {
2086
2107
  'x14:sparkline': {
2087
2108
  'xm:f': source,
@@ -418,7 +418,7 @@ export class OverlayEditor extends Editor<ResetSelectionEvent> {
418
418
  case 'Down':
419
419
  case 'Left':
420
420
  case 'Right':
421
- return this.selecting ? undefined : 'handled';
421
+ return this.selecting ? undefined : 'commit';
422
422
 
423
423
  }
424
424
 
@@ -4014,7 +4014,15 @@ export class Grid extends GridBase {
4014
4014
  // unless we're selecting an argument, close the ICE
4015
4015
 
4016
4016
  if (this.overlay_editor?.editing && !this.overlay_editor?.selecting) {
4017
- this.DismissEditor();
4017
+
4018
+ // commit
4019
+
4020
+ if (this.overlay_editor?.selection) {
4021
+ const value = this.overlay_editor?.edit_node.textContent || undefined;
4022
+ this.SetInferredType(this.overlay_editor.selection, value, false);
4023
+ }
4024
+
4025
+ this.DismissEditor();
4018
4026
  }
4019
4027
 
4020
4028
  const offset_point = {