@trebco/treb 30.2.14 → 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.
- package/dist/treb-export-worker.mjs +2 -0
- package/dist/treb-spreadsheet.mjs +14 -14
- package/dist/treb.d.ts +1 -1
- package/{esbuild-custom-element.mjs → esbuild-composite.mjs} +6 -23
- package/esbuild-utils.mjs +28 -3
- package/eslint.config.js +2 -1
- package/package.json +7 -8
- package/treb-base-types/src/cells.ts +1 -1
- package/treb-calculator/src/calculator.ts +108 -69
- package/treb-calculator/src/complex-math.ts +48 -0
- package/treb-calculator/src/dag/graph.ts +4 -1
- package/treb-calculator/src/functions/base-functions.ts +106 -34
- package/treb-calculator/src/functions/date-utils.ts +64 -0
- package/treb-calculator/src/functions/finance-functions.ts +62 -0
- package/treb-calculator/src/functions/statistics-functions.ts +172 -0
- package/treb-calculator/src/functions/text-functions.ts +25 -0
- package/treb-calculator/src/utilities.ts +18 -4
- package/treb-charts/src/chart-utils.ts +42 -27
- package/treb-data-model/src/types.ts +10 -0
- package/treb-embed/modern.tsconfig.json +0 -1
- package/treb-embed/src/embedded-spreadsheet.ts +26 -51
- package/treb-embed/src/export-worker.ts +44 -0
- package/treb-embed/style/dark-theme.scss +1 -1
- package/treb-export/src/export.ts +22 -1
- package/treb-export/src/{export-worker/export-worker.ts → index.worker.ts} +2 -2
- package/treb-grid/src/editors/overlay_editor.ts +1 -1
- package/treb-grid/src/types/grid.ts +9 -1
- package/dist/treb-spreadsheet-light.mjs +0 -16
- package/treb-export/src/export-worker/index.worker.ts +0 -32
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* This file is part of TREB.
|
|
3
|
+
*
|
|
4
|
+
* TREB is free software: you can redistribute it and/or modify it under the
|
|
5
|
+
* terms of the GNU General Public License as published by the Free Software
|
|
6
|
+
* Foundation, either version 3 of the License, or (at your option) any
|
|
7
|
+
* later version.
|
|
8
|
+
*
|
|
9
|
+
* TREB is distributed in the hope that it will be useful, but WITHOUT ANY
|
|
10
|
+
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
|
11
|
+
* FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
|
|
12
|
+
* details.
|
|
13
|
+
*
|
|
14
|
+
* You should have received a copy of the GNU General Public License along
|
|
15
|
+
* with TREB. If not, see <https://www.gnu.org/licenses/>.
|
|
16
|
+
*
|
|
17
|
+
* Copyright 2022-2024 trebco, llc.
|
|
18
|
+
* info@treb.app
|
|
19
|
+
*
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { UnlotusDate } from 'treb-format';
|
|
23
|
+
|
|
24
|
+
export const DaysInYear = (year: number) => {
|
|
25
|
+
return (year % 4 === 0 && (year % 100 !== 0 || year === 1900)) ? 366 : 365;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export const DayOfYear = (year: number, month: number, day: number): number|false => {
|
|
29
|
+
const a = ConstructDate(year, 1, 1);
|
|
30
|
+
const b = ConstructDate(year, month, day);
|
|
31
|
+
if (a === false || b === false) { return false; }
|
|
32
|
+
return Math.round(b - a + 1);
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export const ConstructDate = (year: number, month: number, day: number): number|false => {
|
|
36
|
+
|
|
37
|
+
const date = new Date();
|
|
38
|
+
date.setMilliseconds(0);
|
|
39
|
+
date.setSeconds(0);
|
|
40
|
+
date.setMinutes(0);
|
|
41
|
+
date.setHours(0);
|
|
42
|
+
|
|
43
|
+
if (year < 0 || year > 10000) {
|
|
44
|
+
// return ArgumentError();
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
if (year < 1899) { year += 1900; }
|
|
48
|
+
date.setFullYear(year);
|
|
49
|
+
|
|
50
|
+
if (month < 1 || month > 12) {
|
|
51
|
+
// return ArgumentError();
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
date.setMonth(month - 1);
|
|
55
|
+
|
|
56
|
+
if (day < 1 || day > 31) {
|
|
57
|
+
// return ArgumentError();
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
date.setDate(day);
|
|
61
|
+
|
|
62
|
+
return UnlotusDate(date.getTime());
|
|
63
|
+
|
|
64
|
+
};
|
|
@@ -24,6 +24,8 @@ import { type CellValue, type UnionValue, ValueType } from 'treb-base-types';
|
|
|
24
24
|
import { FlattenCellValues } from '../utilities';
|
|
25
25
|
|
|
26
26
|
import { ArgumentError, ValueError } from '../function-error';
|
|
27
|
+
import { LotusDate, UnlotusDate } from 'treb-format';
|
|
28
|
+
import { ConstructDate, DaysInYear } from './date-utils';
|
|
27
29
|
|
|
28
30
|
// use a single, static object for base functions
|
|
29
31
|
|
|
@@ -84,8 +86,68 @@ const ppmt_function = (rate: number, period: number, periods: number, pv = 0, fv
|
|
|
84
86
|
ipmt_function(rate, period, periods, pv, fv, type);
|
|
85
87
|
};
|
|
86
88
|
|
|
89
|
+
|
|
90
|
+
|
|
87
91
|
export const FinanceFunctionLibrary: FunctionMap = {
|
|
88
92
|
|
|
93
|
+
/*
|
|
94
|
+
CoupNum: {
|
|
95
|
+
fn: (settlement: CellValue, maturity: CellValue, frequency: CellValue, basis: CellValue = 0) => {
|
|
96
|
+
|
|
97
|
+
if (typeof settlement !== 'number' || typeof maturity !== 'number' || settlement > maturity ) {
|
|
98
|
+
return ArgumentError();
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (frequency !== 1 && frequency !== 2 && frequency !== 4) {
|
|
102
|
+
return ArgumentError();
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (basis === 1) {
|
|
106
|
+
const settlement_date = LotusDate(settlement);
|
|
107
|
+
const maturity_date = LotusDate(maturity);
|
|
108
|
+
|
|
109
|
+
const comparison = ConstructDate(maturity_date.getUTCFullYear(), settlement_date.getUTCMonth() + 1, settlement_date.getUTCDay());
|
|
110
|
+
let years = Math.max(0, maturity_date.getUTCFullYear() - settlement_date.getUTCFullYear());
|
|
111
|
+
|
|
112
|
+
if (comparison && comparison < maturity) {
|
|
113
|
+
years += Math.round(maturity - comparison) / DaysInYear(maturity_date.getUTCFullYear());
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return {
|
|
117
|
+
type: ValueType.number,
|
|
118
|
+
value: Math.round(years * frequency),
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return {
|
|
123
|
+
type: ValueType.number,
|
|
124
|
+
value: 100,
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
}
|
|
128
|
+
},
|
|
129
|
+
*/
|
|
130
|
+
|
|
131
|
+
/*
|
|
132
|
+
Price: {
|
|
133
|
+
fn: (settlement: CellValue, maturity: CellValue, rate: CellValue, yld: CellValue, redemption: CellValue, frequency: CellValue, basis: CellValue = 0) => {
|
|
134
|
+
|
|
135
|
+
if (typeof settlement !== 'number' || typeof maturity !== 'number' || typeof rate !== 'number' || typeof yld !== 'number' || typeof redemption !== 'number') {
|
|
136
|
+
return ArgumentError();
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (frequency !== 1 && frequency !== 2 && frequency !== 4) {
|
|
140
|
+
return ArgumentError();
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return {
|
|
144
|
+
type: ValueType.number,
|
|
145
|
+
value: 100,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
},
|
|
149
|
+
*/
|
|
150
|
+
|
|
89
151
|
/**
|
|
90
152
|
* Excel's NPV function is somewhat broken because it assumes the first
|
|
91
153
|
* (usually negative) cashflow is in year 1, not year 0. so the thing to
|
|
@@ -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
|
-
|
|
515
|
-
|
|
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
|
-
|
|
529
|
-
quartiles[
|
|
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
|
|
533
|
-
quartiles[2] = median(data.
|
|
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
|
|
|
@@ -107,13 +107,6 @@ import type { StateLeafVertex } from 'treb-calculator';
|
|
|
107
107
|
*/
|
|
108
108
|
import './content-types.d.ts';
|
|
109
109
|
|
|
110
|
-
/**
|
|
111
|
-
* import the worker as a script file. tsc will read this on typecheck but
|
|
112
|
-
* that's actually to the good; when we build with esbuild we will inline
|
|
113
|
-
* the script so we can run it as a worker.
|
|
114
|
-
*/
|
|
115
|
-
import * as export_worker_script from 'worker:../../treb-export/src/export-worker/index.worker';
|
|
116
|
-
|
|
117
110
|
// --- types -------------------------------------------------------------------
|
|
118
111
|
|
|
119
112
|
/**
|
|
@@ -2853,7 +2846,7 @@ export class EmbeddedSpreadsheet<USER_DATA_TYPE = unknown> {
|
|
|
2853
2846
|
*
|
|
2854
2847
|
* @internal
|
|
2855
2848
|
*/
|
|
2856
|
-
public async ExportBlob(): Promise<Blob> {
|
|
2849
|
+
public async ExportBlob(serialized?: SerializedModel): Promise<Blob> {
|
|
2857
2850
|
|
|
2858
2851
|
// this is inlined to ensure the code will be tree-shaken properly
|
|
2859
2852
|
if (!process.env.XLSX_SUPPORT) {
|
|
@@ -2877,19 +2870,19 @@ export class EmbeddedSpreadsheet<USER_DATA_TYPE = unknown> {
|
|
|
2877
2870
|
reject(event);
|
|
2878
2871
|
};
|
|
2879
2872
|
|
|
2880
|
-
|
|
2881
|
-
|
|
2882
|
-
|
|
2883
|
-
|
|
2884
|
-
|
|
2885
|
-
|
|
2886
|
-
|
|
2887
|
-
|
|
2888
|
-
|
|
2889
|
-
|
|
2890
|
-
|
|
2891
|
-
}
|
|
2892
|
-
|
|
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
|
+
|
|
2893
2886
|
// why do _we_ put this in, instead of the grid method?
|
|
2894
2887
|
serialized.decimal_mark = Localization.decimal_separator;
|
|
2895
2888
|
|
|
@@ -6162,39 +6155,21 @@ export class EmbeddedSpreadsheet<USER_DATA_TYPE = unknown> {
|
|
|
6162
6155
|
}
|
|
6163
6156
|
|
|
6164
6157
|
/**
|
|
6165
|
-
*
|
|
6166
|
-
*
|
|
6158
|
+
* worker now uses a dynamic import with a module built separately.
|
|
6159
|
+
* see `export-worker.ts` (in this directory) for more detail.
|
|
6167
6160
|
*/
|
|
6168
6161
|
protected async LoadWorker(): Promise<Worker> {
|
|
6169
|
-
|
|
6170
|
-
|
|
6171
|
-
|
|
6172
|
-
|
|
6173
|
-
|
|
6174
|
-
|
|
6175
|
-
|
|
6176
|
-
|
|
6177
|
-
|
|
6178
|
-
|
|
6179
|
-
if (export_worker_script) {
|
|
6180
|
-
try {
|
|
6181
|
-
const worker = new Worker(
|
|
6182
|
-
URL.createObjectURL(new Blob([(export_worker_script as {default: string}).default], { type: 'application/javascript' })));
|
|
6183
|
-
return worker;
|
|
6184
|
-
}
|
|
6185
|
-
catch (err) {
|
|
6186
|
-
console.info('embedded worker failed');
|
|
6187
|
-
console.error(err);
|
|
6188
|
-
}
|
|
6189
|
-
}
|
|
6190
|
-
|
|
6191
|
-
}
|
|
6192
|
-
else {
|
|
6193
|
-
console.warn('this build does not include xlsx support.');
|
|
6162
|
+
try {
|
|
6163
|
+
const worker = `export`;
|
|
6164
|
+
const mod = await import(`esbuild-ignore-import:./treb-${worker}-worker.mjs`) as {
|
|
6165
|
+
CreateWorker: () => Promise<Worker>;
|
|
6166
|
+
};
|
|
6167
|
+
return await mod.CreateWorker();
|
|
6168
|
+
}
|
|
6169
|
+
catch (err) {
|
|
6170
|
+
console.error(err);
|
|
6171
|
+
throw(err);
|
|
6194
6172
|
}
|
|
6195
|
-
|
|
6196
|
-
throw new Error('creating worker failed');
|
|
6197
|
-
|
|
6198
6173
|
}
|
|
6199
6174
|
|
|
6200
6175
|
/**
|