@trebco/treb 29.8.3 → 30.1.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-spreadsheet-light.mjs +11 -11
- package/dist/treb-spreadsheet.mjs +15 -15
- package/dist/treb.d.ts +11 -1
- package/eslint.config.js +9 -0
- package/package.json +1 -1
- package/treb-base-types/src/area-utils.ts +60 -0
- package/treb-base-types/src/area.ts +11 -0
- package/treb-base-types/src/cell.ts +6 -1
- package/treb-base-types/src/cells.ts +38 -7
- package/treb-base-types/src/index.ts +2 -0
- package/treb-calculator/src/calculator.ts +274 -4
- package/treb-calculator/src/dag/array-vertex.ts +0 -10
- package/treb-calculator/src/dag/graph.ts +118 -77
- package/treb-calculator/src/dag/spreadsheet_vertex.ts +38 -9
- package/treb-calculator/src/dag/spreadsheet_vertex_base.ts +1 -0
- package/treb-calculator/src/expression-calculator.ts +7 -2
- package/treb-calculator/src/function-error.ts +6 -0
- package/treb-charts/src/chart-functions.ts +39 -5
- package/treb-charts/src/chart-types.ts +23 -0
- package/treb-charts/src/chart-utils.ts +165 -2
- package/treb-charts/src/chart.ts +6 -1
- package/treb-charts/src/default-chart-renderer.ts +70 -1
- package/treb-charts/src/index.ts +1 -0
- package/treb-charts/src/renderer.ts +95 -2
- package/treb-charts/style/charts.scss +41 -0
- package/treb-embed/src/embedded-spreadsheet.ts +11 -4
- package/treb-embed/src/options.ts +8 -0
- package/treb-embed/style/dark-theme.scss +4 -0
- package/treb-embed/style/grid.scss +15 -0
- package/treb-embed/style/z-index.scss +3 -0
- package/treb-export/src/import2.ts +9 -0
- package/treb-export/src/workbook2.ts +67 -3
- package/treb-grid/src/editors/editor.ts +12 -5
- package/treb-grid/src/layout/base_layout.ts +41 -0
- package/treb-grid/src/types/grid.ts +72 -28
- package/treb-parser/src/parser-types.ts +3 -0
- package/treb-parser/src/parser.ts +21 -2
- package/treb-utils/src/serialize_html.ts +35 -10
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
|
|
2
2
|
import { type UnionValue, ValueType, type ArrayUnion, IsComplex, type CellValue } from 'treb-base-types';
|
|
3
3
|
import { IsArrayUnion, IsMetadata, IsSeries, LegendStyle } from './chart-types';
|
|
4
|
-
import type { SubSeries, SeriesType, BarData, ChartDataBaseType, ChartData, ScatterData2, DonutSlice, BubbleChartData } from './chart-types';
|
|
4
|
+
import type { SubSeries, SeriesType, BarData, ChartDataBaseType, ChartData, ScatterData2, DonutSlice, BubbleChartData, BoxPlotData } from './chart-types';
|
|
5
5
|
import { NumberFormatCache } from 'treb-format';
|
|
6
6
|
import { Util } from './util';
|
|
7
7
|
import type { ReferenceSeries } from './chart-types';
|
|
8
|
+
import type { RangeScale } from 'treb-utils';
|
|
8
9
|
|
|
9
10
|
/**
|
|
10
11
|
* this file is the concrete translation from function arguments
|
|
@@ -320,8 +321,38 @@ export const TransformSeriesData = (raw_data?: UnionValue, default_x?: UnionValu
|
|
|
320
321
|
return list;
|
|
321
322
|
};
|
|
322
323
|
|
|
324
|
+
const AutoFormat = (scale: RangeScale): string => {
|
|
325
|
+
|
|
326
|
+
const zep = Math.abs(scale.step) % 1;
|
|
327
|
+
if (!zep) {
|
|
328
|
+
const log10 = Math.log10(Math.abs(scale.step));
|
|
329
|
+
if (log10 >= 5) {
|
|
330
|
+
return 'Scientific';
|
|
331
|
+
}
|
|
332
|
+
return '#,##0';
|
|
333
|
+
}
|
|
334
|
+
else {
|
|
335
|
+
const log10 = Math.log10(Math.abs(zep));
|
|
336
|
+
if (log10 < -4) {
|
|
337
|
+
return 'Scientific';
|
|
338
|
+
}
|
|
339
|
+
let count = 0;
|
|
340
|
+
for (let i = 0; i < scale.count; i++) {
|
|
341
|
+
const value = ((scale.min + scale.step * i) % 1).toFixed(6).replace(/0+$/, '');
|
|
342
|
+
count = Math.max(count, value.length - 2);
|
|
343
|
+
}
|
|
344
|
+
let format = '#,##0.';
|
|
345
|
+
for (let i = 0; i < count; i++) {
|
|
346
|
+
format += '0';
|
|
347
|
+
}
|
|
348
|
+
return format;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
};
|
|
353
|
+
|
|
323
354
|
/** get a unified scale, and formats */
|
|
324
|
-
export const CommonData = (series: SeriesType[], y_floor?: number, y_ceiling?: number, x_floor?: number, x_ceiling?: number) => {
|
|
355
|
+
export const CommonData = (series: SeriesType[], y_floor?: number, y_ceiling?: number, x_floor?: number, x_ceiling?: number, auto_number_format?: boolean) => {
|
|
325
356
|
|
|
326
357
|
let x_format = '';
|
|
327
358
|
let y_format = '';
|
|
@@ -402,6 +433,11 @@ export const CommonData = (series: SeriesType[], y_floor?: number, y_ceiling?: n
|
|
|
402
433
|
}
|
|
403
434
|
}
|
|
404
435
|
|
|
436
|
+
if (!y_format && auto_number_format) {
|
|
437
|
+
// y_format = default_number_format;
|
|
438
|
+
y_format = AutoFormat(y_scale);
|
|
439
|
+
}
|
|
440
|
+
|
|
405
441
|
if (y_format) {
|
|
406
442
|
y_labels = [];
|
|
407
443
|
const format = NumberFormatCache.Get(y_format);
|
|
@@ -451,6 +487,133 @@ const ApplyLabels = (series_list: SeriesType[], pattern: string, category_labels
|
|
|
451
487
|
|
|
452
488
|
};
|
|
453
489
|
|
|
490
|
+
/**
|
|
491
|
+
* return quartiles. we use the Tukey hinge-style. See
|
|
492
|
+
*
|
|
493
|
+
* https://en.wikipedia.org/wiki/Quartile
|
|
494
|
+
*
|
|
495
|
+
* Specifically,
|
|
496
|
+
*
|
|
497
|
+
* - Use the median to divide the ordered data set into two halves. The median
|
|
498
|
+
* becomes the second quartiles.
|
|
499
|
+
*
|
|
500
|
+
* - If there are an odd number of data points in the original ordered data
|
|
501
|
+
* set, include the median (the central value in the ordered list) in both
|
|
502
|
+
* halves.
|
|
503
|
+
*
|
|
504
|
+
* - If there are an even number of data points in the original ordered data
|
|
505
|
+
* set, split this data set exactly in half.
|
|
506
|
+
*
|
|
507
|
+
* - The lower quartile value is the median of the lower half of the data. The
|
|
508
|
+
* upper quartile value is the median of the upper half of the data.
|
|
509
|
+
*
|
|
510
|
+
* @param data - must be sorted with no holes
|
|
511
|
+
*/
|
|
512
|
+
export const BoxStats = (data: number[]) => {
|
|
513
|
+
|
|
514
|
+
const median = (data: number[]) => {
|
|
515
|
+
const n = data.length;
|
|
516
|
+
if (n % 2) {
|
|
517
|
+
return data[Math.floor(n/2)];
|
|
518
|
+
}
|
|
519
|
+
else {
|
|
520
|
+
return (data[n/2] + data[n/2 - 1])/2;
|
|
521
|
+
}
|
|
522
|
+
};
|
|
523
|
+
|
|
524
|
+
const n = data.length;
|
|
525
|
+
const quartiles: [number, number, number] = [0, median(data), 0];
|
|
526
|
+
|
|
527
|
+
if (n % 2) {
|
|
528
|
+
quartiles[0] = median(data.slice(0, Math.ceil(n/2)));
|
|
529
|
+
quartiles[2] = median(data.slice(Math.floor(n/2)));
|
|
530
|
+
}
|
|
531
|
+
else {
|
|
532
|
+
quartiles[0] = median(data.slice(0, n/2));
|
|
533
|
+
quartiles[2] = median(data.slice(n/2));
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
const iqr = quartiles[2] - quartiles[0];
|
|
537
|
+
const whiskers: [number, number] = [0, 0];
|
|
538
|
+
|
|
539
|
+
let sum = 0;
|
|
540
|
+
for (let i = 0; i < n; i++) {
|
|
541
|
+
sum += data[i];
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
for (let i = 0; i < n; i++) {
|
|
545
|
+
const pt = data[i];
|
|
546
|
+
if (pt >= quartiles[0] - iqr * 1.5) {
|
|
547
|
+
whiskers[0] = pt;
|
|
548
|
+
break;
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
for (let i = n-1; i >= 0; i--) {
|
|
553
|
+
const pt = data[i];
|
|
554
|
+
if (pt <= quartiles[2] + iqr * 1.5) {
|
|
555
|
+
whiskers[1] = pt;
|
|
556
|
+
break;
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
return {
|
|
561
|
+
data,
|
|
562
|
+
quartiles,
|
|
563
|
+
whiskers,
|
|
564
|
+
iqr,
|
|
565
|
+
n,
|
|
566
|
+
mean: n ? sum/n : 0,
|
|
567
|
+
min: data[0],
|
|
568
|
+
max: data[n-1],
|
|
569
|
+
};
|
|
570
|
+
|
|
571
|
+
};
|
|
572
|
+
|
|
573
|
+
//------------------------------------------------------------------------------
|
|
574
|
+
|
|
575
|
+
export const CreateBoxPlot = (args: UnionValue[]): ChartData => {
|
|
576
|
+
|
|
577
|
+
const series: SeriesType[] = TransformSeriesData(args[0]);
|
|
578
|
+
|
|
579
|
+
const common = CommonData(series, undefined, undefined, undefined, undefined, true);
|
|
580
|
+
|
|
581
|
+
let max_n = 0;
|
|
582
|
+
|
|
583
|
+
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);
|
|
585
|
+
const result = BoxStats(data);
|
|
586
|
+
max_n = Math.max(max_n, result.n);
|
|
587
|
+
return result;
|
|
588
|
+
});
|
|
589
|
+
|
|
590
|
+
const title = args[1]?.toString() || undefined;
|
|
591
|
+
const x_labels: string[] = [];
|
|
592
|
+
const series_names: string[] = [];
|
|
593
|
+
const format = NumberFormatCache.Get('#,##0');
|
|
594
|
+
|
|
595
|
+
for (const [index, entry] of stats.entries()) {
|
|
596
|
+
x_labels.push(format.Format(entry.n));
|
|
597
|
+
const s = series[index];
|
|
598
|
+
series_names.push(s.label || `Series ${index + 1}`);
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
const chart_data: BoxPlotData = {
|
|
602
|
+
type: 'box',
|
|
603
|
+
series,
|
|
604
|
+
title,
|
|
605
|
+
max_n,
|
|
606
|
+
data: stats,
|
|
607
|
+
x_labels,
|
|
608
|
+
series_names: series.some(test => !!test.label) ? series_names : undefined,
|
|
609
|
+
scale: common.y.scale,
|
|
610
|
+
y_labels: common.y.labels,
|
|
611
|
+
};
|
|
612
|
+
|
|
613
|
+
return chart_data;
|
|
614
|
+
|
|
615
|
+
};
|
|
616
|
+
|
|
454
617
|
//------------------------------------------------------------------------------
|
|
455
618
|
|
|
456
619
|
export const CreateBubbleChart = (args: UnionValue[]): ChartData => {
|
package/treb-charts/src/chart.ts
CHANGED
|
@@ -4,6 +4,7 @@ import type { ChartData } from './chart-types';
|
|
|
4
4
|
import type { ExtendedUnion, UnionValue } from 'treb-base-types';
|
|
5
5
|
import * as ChartUtils from './chart-utils';
|
|
6
6
|
import { DefaultChartRenderer } from './default-chart-renderer';
|
|
7
|
+
import type { ChartFunction } from './chart-functions';
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* transitioning to new structure, this should mirror the old chart
|
|
@@ -29,7 +30,7 @@ export class Chart {
|
|
|
29
30
|
this.renderer.Initialize(node);
|
|
30
31
|
}
|
|
31
32
|
|
|
32
|
-
public Exec(func:
|
|
33
|
+
public Exec(func: ChartFunction, union: ExtendedUnion) {
|
|
33
34
|
|
|
34
35
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
35
36
|
const args: any[] = (union?.value as any[]) || [];
|
|
@@ -69,6 +70,10 @@ export class Chart {
|
|
|
69
70
|
this.chart_data = ChartUtils.CreateBubbleChart(args);
|
|
70
71
|
break;
|
|
71
72
|
|
|
73
|
+
case 'box.plot':
|
|
74
|
+
this.chart_data = ChartUtils.CreateBoxPlot(args);
|
|
75
|
+
break;
|
|
76
|
+
|
|
72
77
|
default:
|
|
73
78
|
this.Clear();
|
|
74
79
|
break;
|
|
@@ -97,6 +97,7 @@ export class DefaultChartRenderer implements ChartRendererType {
|
|
|
97
97
|
|| chart_data.type === 'bar'
|
|
98
98
|
|| chart_data.type === 'scatter2'
|
|
99
99
|
|| chart_data.type === 'bubble'
|
|
100
|
+
|| chart_data.type === 'box'
|
|
100
101
|
) {
|
|
101
102
|
|
|
102
103
|
// we need to measure first, then lay out the other axis, then we
|
|
@@ -116,6 +117,19 @@ export class DefaultChartRenderer implements ChartRendererType {
|
|
|
116
117
|
});
|
|
117
118
|
}
|
|
118
119
|
|
|
120
|
+
let x_metrics2: Metrics[] = [];
|
|
121
|
+
let max_x_height2 = 0;
|
|
122
|
+
|
|
123
|
+
if (chart_data.type === 'box' && chart_data.series_names?.length) {
|
|
124
|
+
x_metrics2 = chart_data.series_names.map((text) => {
|
|
125
|
+
const metrics = this.renderer.MeasureText(text, ['axis-label', 'x-axis-label', 'series-name'], true);
|
|
126
|
+
max_x_height2 = Math.max(max_x_height2, metrics.height);
|
|
127
|
+
return metrics;
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const extra_padding = max_x_height && max_x_height2 ? 4 : 0;
|
|
132
|
+
|
|
119
133
|
// measure & render y axis
|
|
120
134
|
|
|
121
135
|
if (chart_data.y_labels && chart_data.y_labels.length) {
|
|
@@ -144,6 +158,12 @@ export class DefaultChartRenderer implements ChartRendererType {
|
|
|
144
158
|
if (x_metrics.length) {
|
|
145
159
|
area.bottom -= (max_x_height + chart_margin.bottom);
|
|
146
160
|
}
|
|
161
|
+
if (x_metrics2.length) {
|
|
162
|
+
area.bottom -= (max_x_height2 + chart_margin.bottom);
|
|
163
|
+
}
|
|
164
|
+
if (extra_padding) {
|
|
165
|
+
area.bottom -= extra_padding;
|
|
166
|
+
}
|
|
147
167
|
|
|
148
168
|
if (chart_data.type === 'bar') {
|
|
149
169
|
this.renderer.RenderYAxisBar(area, area.left + max_width, y_labels, ['axis-label', 'y-axis-label']);
|
|
@@ -157,7 +177,7 @@ export class DefaultChartRenderer implements ChartRendererType {
|
|
|
157
177
|
|
|
158
178
|
// now render x axis
|
|
159
179
|
|
|
160
|
-
if (x_metrics.length && chart_data.x_labels
|
|
180
|
+
if (x_metrics.length && chart_data.x_labels?.length) {
|
|
161
181
|
|
|
162
182
|
const tick = (chart_data.type === 'histogram2');
|
|
163
183
|
const offset_tick = (
|
|
@@ -193,6 +213,31 @@ export class DefaultChartRenderer implements ChartRendererType {
|
|
|
193
213
|
|
|
194
214
|
}
|
|
195
215
|
|
|
216
|
+
if (extra_padding) {
|
|
217
|
+
area.bottom += extra_padding;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (chart_data.type === 'box' && x_metrics2.length && chart_data.series_names?.length) {
|
|
221
|
+
|
|
222
|
+
// console.info({chart_data, x_metrics2});
|
|
223
|
+
|
|
224
|
+
if (chart_data.y_labels) {
|
|
225
|
+
// undo, temp
|
|
226
|
+
area.bottom += (max_x_height + max_x_height2 + extra_padding + chart_margin.bottom);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// render
|
|
230
|
+
this.renderer.RenderXAxis(area,
|
|
231
|
+
true, // offset_tick,
|
|
232
|
+
chart_data.series_names,
|
|
233
|
+
x_metrics2,
|
|
234
|
+
['axis-label', 'x-axis-label', 'series-name']);
|
|
235
|
+
|
|
236
|
+
// update bottom (either we unwound for labels, or we need to do it the first time)
|
|
237
|
+
area.bottom -= (max_x_height + max_x_height2 + chart_margin.bottom + chart_margin.bottom);
|
|
238
|
+
|
|
239
|
+
}
|
|
240
|
+
|
|
196
241
|
}
|
|
197
242
|
|
|
198
243
|
// now do type-specific rendering
|
|
@@ -204,6 +249,30 @@ export class DefaultChartRenderer implements ChartRendererType {
|
|
|
204
249
|
this.renderer.RenderPoints(area, chart_data.x, chart_data.y, 'mc mc-correlation series-1');
|
|
205
250
|
break;
|
|
206
251
|
|
|
252
|
+
case 'box':
|
|
253
|
+
this.renderer.RenderGrid(area,
|
|
254
|
+
chart_data.scale.count,
|
|
255
|
+
undefined, // chart_data.x_scale.count + 1, // (sigh)
|
|
256
|
+
'chart-grid', zeros);
|
|
257
|
+
|
|
258
|
+
// FIXME: override index for coloring
|
|
259
|
+
|
|
260
|
+
for (const [index, data] of chart_data.data.entries()) {
|
|
261
|
+
|
|
262
|
+
if (data.iqr > 0) {
|
|
263
|
+
this.renderer.RenderBoxAndWhisker(
|
|
264
|
+
area,
|
|
265
|
+
data,
|
|
266
|
+
index,
|
|
267
|
+
chart_data.max_n,
|
|
268
|
+
chart_data.scale,
|
|
269
|
+
chart_data.data.length,
|
|
270
|
+
`box-plot series-${index}`);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
break;
|
|
275
|
+
|
|
207
276
|
case 'bubble':
|
|
208
277
|
|
|
209
278
|
if (chart_data.x_scale.min <= 0 && chart_data.x_scale.max >= 0) {
|
package/treb-charts/src/index.ts
CHANGED
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
|
|
22
22
|
import type { Size, Point } from './rectangle';
|
|
23
23
|
import { Area } from './rectangle';
|
|
24
|
-
import type { DonutSlice, LegendOptions, SeriesType} from './chart-types';
|
|
24
|
+
import type { BoxPlotData, DonutSlice, LegendOptions, SeriesType} from './chart-types';
|
|
25
25
|
import { LegendLayout, LegendPosition, LegendStyle } from './chart-types';
|
|
26
26
|
import type { RangeScale } from 'treb-utils';
|
|
27
27
|
|
|
@@ -268,7 +268,7 @@ export class ChartRenderer {
|
|
|
268
268
|
}
|
|
269
269
|
|
|
270
270
|
public Resize(): void {
|
|
271
|
-
const bounds = this.
|
|
271
|
+
const bounds = this.svg_node.getBoundingClientRect();
|
|
272
272
|
this.svg_node.setAttribute('width', bounds.width.toString());
|
|
273
273
|
this.svg_node.setAttribute('height', bounds.height.toString());
|
|
274
274
|
this.size = {
|
|
@@ -1232,6 +1232,99 @@ export class ChartRenderer {
|
|
|
1232
1232
|
|
|
1233
1233
|
}
|
|
1234
1234
|
|
|
1235
|
+
public RenderBoxAndWhisker(
|
|
1236
|
+
area: Area,
|
|
1237
|
+
data: BoxPlotData['data'][0],
|
|
1238
|
+
index: number,
|
|
1239
|
+
max_n: number,
|
|
1240
|
+
scale: RangeScale,
|
|
1241
|
+
n: number,
|
|
1242
|
+
classes?: string|string[]){
|
|
1243
|
+
|
|
1244
|
+
const group = SVGNode('g', {class: classes});
|
|
1245
|
+
this.group.appendChild(group);
|
|
1246
|
+
|
|
1247
|
+
// space for each box is 1/n of the chart area. we'll allocate margin on a box basis.
|
|
1248
|
+
const width = area.width / n;
|
|
1249
|
+
const margin = width * .20; // ??
|
|
1250
|
+
const box_default_width = Math.min(width - 2 * margin, 90);
|
|
1251
|
+
|
|
1252
|
+
let box_width = box_default_width;
|
|
1253
|
+
|
|
1254
|
+
if (max_n > 0) {
|
|
1255
|
+
box_width = box_width * Math.sqrt(data.n) / Math.sqrt(max_n);
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
const Y = (value: number) => area.top + area.height - (value - scale.min) / (scale.max - scale.min) * area.height; // wtf?
|
|
1259
|
+
|
|
1260
|
+
const center = area.left + index * width + width / 2;
|
|
1261
|
+
|
|
1262
|
+
const q1 = Y(data.quartiles[0]);
|
|
1263
|
+
const q3 = Y(data.quartiles[2]);
|
|
1264
|
+
|
|
1265
|
+
group.appendChild(SVGNode('rect', {
|
|
1266
|
+
class: `iqr`,
|
|
1267
|
+
x: area.left + index * width + (width - box_width) / 2,
|
|
1268
|
+
y: q3,
|
|
1269
|
+
width: box_width,
|
|
1270
|
+
height: q1 - q3,
|
|
1271
|
+
}));
|
|
1272
|
+
|
|
1273
|
+
group.appendChild(SVGNode('line', {
|
|
1274
|
+
class: `median`,
|
|
1275
|
+
x1: center - box_width * .5,
|
|
1276
|
+
x2: center + box_width * .5,
|
|
1277
|
+
y1: Y(data.quartiles[1]),
|
|
1278
|
+
y2: Y(data.quartiles[1]),
|
|
1279
|
+
}));
|
|
1280
|
+
|
|
1281
|
+
group.appendChild(SVGNode('line', {
|
|
1282
|
+
class: `whisker`,
|
|
1283
|
+
x1: center - box_width * .25,
|
|
1284
|
+
x2: center + box_width * .25,
|
|
1285
|
+
y1: Y(data.whiskers[0]),
|
|
1286
|
+
y2: Y(data.whiskers[0]),
|
|
1287
|
+
}));
|
|
1288
|
+
|
|
1289
|
+
group.appendChild(SVGNode('line', {
|
|
1290
|
+
class: `whisker-extent`,
|
|
1291
|
+
x1: center,
|
|
1292
|
+
x2: center,
|
|
1293
|
+
y1: Y(data.whiskers[0]) - 2,
|
|
1294
|
+
y2: Y(data.quartiles[0]) + 2,
|
|
1295
|
+
}));
|
|
1296
|
+
|
|
1297
|
+
group.appendChild(SVGNode('line', {
|
|
1298
|
+
class: `whisker`,
|
|
1299
|
+
x1: center - box_width * .25,
|
|
1300
|
+
x2: center + box_width * .25,
|
|
1301
|
+
y1: Y(data.whiskers[1]),
|
|
1302
|
+
y2: Y(data.whiskers[1]),
|
|
1303
|
+
}));
|
|
1304
|
+
|
|
1305
|
+
group.appendChild(SVGNode('line', {
|
|
1306
|
+
class: `whisker-extent`,
|
|
1307
|
+
x1: center,
|
|
1308
|
+
x2: center,
|
|
1309
|
+
y1: Y(data.whiskers[1]) + 2,
|
|
1310
|
+
y2: Y(data.quartiles[2]) - 2,
|
|
1311
|
+
}));
|
|
1312
|
+
|
|
1313
|
+
for (const point of data.data) {
|
|
1314
|
+
if (point < data.whiskers[0] || point > data.whiskers[1]) {
|
|
1315
|
+
group.appendChild(SVGNode('circle', {
|
|
1316
|
+
class: `outlier`,
|
|
1317
|
+
cx: center,
|
|
1318
|
+
cy: Y(point),
|
|
1319
|
+
r: 3, // default; we can override in css
|
|
1320
|
+
}));
|
|
1321
|
+
}
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
|
|
1325
|
+
}
|
|
1326
|
+
|
|
1327
|
+
|
|
1235
1328
|
public RenderBubbleSeries(
|
|
1236
1329
|
area: Area,
|
|
1237
1330
|
series: SeriesType,
|
|
@@ -78,6 +78,11 @@
|
|
|
78
78
|
/* axis labels */
|
|
79
79
|
.axis-group {
|
|
80
80
|
font-size: .9em;
|
|
81
|
+
|
|
82
|
+
.series-name {
|
|
83
|
+
font-size: 1.3em;
|
|
84
|
+
}
|
|
85
|
+
|
|
81
86
|
}
|
|
82
87
|
|
|
83
88
|
/* default text color */
|
|
@@ -173,6 +178,42 @@
|
|
|
173
178
|
}
|
|
174
179
|
}
|
|
175
180
|
|
|
181
|
+
.box-plot {
|
|
182
|
+
|
|
183
|
+
.iqr {
|
|
184
|
+
fill: none;
|
|
185
|
+
stroke: CanvasText;
|
|
186
|
+
stroke-width: 1px;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
.median {
|
|
190
|
+
stroke-width: 3px;
|
|
191
|
+
stroke: CanvasText;
|
|
192
|
+
fill: none;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
.outlier {
|
|
196
|
+
stroke-width: 1px;
|
|
197
|
+
stroke: CanvasText;
|
|
198
|
+
fill: none;
|
|
199
|
+
r: 3;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
.whisker {
|
|
203
|
+
stroke-width: 1px;
|
|
204
|
+
stroke: CanvasText;
|
|
205
|
+
fill: none;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
.whisker-extent {
|
|
209
|
+
stroke-width: 1px;
|
|
210
|
+
stroke: CanvasText;
|
|
211
|
+
fill: none;
|
|
212
|
+
stroke-dasharray: 3 3;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
}
|
|
216
|
+
|
|
176
217
|
.bubble-chart {
|
|
177
218
|
|
|
178
219
|
stroke-width: 3;
|
|
@@ -92,7 +92,7 @@ import type { LanguageModel, TranslatedFunctionDescriptor } from './language-mod
|
|
|
92
92
|
import type { SelectionState } from './selection-state';
|
|
93
93
|
import type { BorderToolbarMessage, ToolbarMessage } from './toolbar-message';
|
|
94
94
|
|
|
95
|
-
import { Chart, ChartFunctions } from 'treb-charts';
|
|
95
|
+
import { Chart, ChartFunctions, type ChartFunction } from 'treb-charts';
|
|
96
96
|
import type { SetRangeOptions, ClipboardData, PasteOptions } from 'treb-grid';
|
|
97
97
|
|
|
98
98
|
import type { StateLeafVertex } from 'treb-calculator';
|
|
@@ -1203,7 +1203,8 @@ export class EmbeddedSpreadsheet<USER_DATA_TYPE = unknown> {
|
|
|
1203
1203
|
*/
|
|
1204
1204
|
protected CreateCalculator(model: DataModel, options: EmbeddedSpreadsheetOptions) {
|
|
1205
1205
|
return new Calculator(model, {
|
|
1206
|
-
complex_numbers: options.complex
|
|
1206
|
+
complex_numbers: options.complex,
|
|
1207
|
+
spill: options.spill,
|
|
1207
1208
|
});
|
|
1208
1209
|
}
|
|
1209
1210
|
|
|
@@ -2356,7 +2357,7 @@ export class EmbeddedSpreadsheet<USER_DATA_TYPE = unknown> {
|
|
|
2356
2357
|
return true;
|
|
2357
2358
|
});
|
|
2358
2359
|
|
|
2359
|
-
const expr_name = parse_result.expression.name.toLowerCase();
|
|
2360
|
+
const expr_name = parse_result.expression.name.toLowerCase() as ChartFunction;
|
|
2360
2361
|
const result = this.calculator.CalculateExpression(parse_result.expression);
|
|
2361
2362
|
chart.Exec(expr_name, result as ExtendedUnion); // FIXME: type?
|
|
2362
2363
|
}
|
|
@@ -3752,6 +3753,12 @@ export class EmbeddedSpreadsheet<USER_DATA_TYPE = unknown> {
|
|
|
3752
3753
|
}
|
|
3753
3754
|
|
|
3754
3755
|
this.calculator.Calculate(area);
|
|
3756
|
+
|
|
3757
|
+
if (this.calculator.grid_expanded) {
|
|
3758
|
+
// console.info("GRID EXPANDED");
|
|
3759
|
+
this.grid.UpdateLayout();
|
|
3760
|
+
}
|
|
3761
|
+
|
|
3755
3762
|
this.ApplyConditionalFormats(this.grid.active_sheet, false);
|
|
3756
3763
|
|
|
3757
3764
|
this.grid.Update(true); // , area);
|
|
@@ -5380,7 +5387,7 @@ export class EmbeddedSpreadsheet<USER_DATA_TYPE = unknown> {
|
|
|
5380
5387
|
return true;
|
|
5381
5388
|
});
|
|
5382
5389
|
|
|
5383
|
-
const expr_name = parse_result.expression.name.toLowerCase();
|
|
5390
|
+
const expr_name = parse_result.expression.name.toLowerCase() as ChartFunction;
|
|
5384
5391
|
|
|
5385
5392
|
const result = this.calculator.CalculateExpression(parse_result.expression);
|
|
5386
5393
|
|
|
@@ -321,6 +321,12 @@ export interface EmbeddedSpreadsheetOptions {
|
|
|
321
321
|
*/
|
|
322
322
|
indent_buttons?: boolean;
|
|
323
323
|
|
|
324
|
+
/**
|
|
325
|
+
* enable spill arrays and spill references. this is on by default
|
|
326
|
+
* starting in 30.1.0. set to false to disable.
|
|
327
|
+
*/
|
|
328
|
+
spill?: boolean;
|
|
329
|
+
|
|
324
330
|
}
|
|
325
331
|
|
|
326
332
|
/**
|
|
@@ -363,4 +369,6 @@ export const DefaultOptions: EmbeddedSpreadsheetOptions = {
|
|
|
363
369
|
spinner: false,
|
|
364
370
|
complex: 'off',
|
|
365
371
|
|
|
372
|
+
spill: true,
|
|
373
|
+
|
|
366
374
|
};
|
|
@@ -110,6 +110,10 @@
|
|
|
110
110
|
|
|
111
111
|
--treb-toolbar-color: rgb(221,221,221);
|
|
112
112
|
|
|
113
|
+
--treb-spill-border-color: pink;
|
|
114
|
+
--treb-spill-border-filter: drop-shadow( 3px 3px 2px rgba(255, 255, 255, .95));
|
|
115
|
+
// --treb-spill-border-dasharray: 4 2;
|
|
116
|
+
// --treb-spill-border-width: 2px;
|
|
113
117
|
|
|
114
118
|
}
|
|
115
119
|
|
|
@@ -38,6 +38,21 @@
|
|
|
38
38
|
|
|
39
39
|
.treb-main.treb-main {
|
|
40
40
|
|
|
41
|
+
.treb-spill-border {
|
|
42
|
+
|
|
43
|
+
position: absolute;
|
|
44
|
+
pointer-events: none;
|
|
45
|
+
|
|
46
|
+
rect {
|
|
47
|
+
stroke: var(--treb-spill-border-color, rgb(92, 92, 224));
|
|
48
|
+
stroke-dasharray: var(--treb-spill-border-dasharray, 0);
|
|
49
|
+
fill: none;
|
|
50
|
+
stroke-width: var(--treb-spill-border-width, 1px);
|
|
51
|
+
z-index: $z-index-spill-border;
|
|
52
|
+
filter: var(--treb-spill-border-filter, drop-shadow( 3px 3px 2px rgba(0, 0, 0, .5)));
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
41
56
|
|
|
42
57
|
/*
|
|
43
58
|
* new: we need the rendering buffer to inherit font size. so
|
|
@@ -1047,6 +1047,15 @@ export class Importer {
|
|
|
1047
1047
|
|
|
1048
1048
|
break;
|
|
1049
1049
|
|
|
1050
|
+
case ChartType.Box:
|
|
1051
|
+
type = 'treb-chart';
|
|
1052
|
+
func = 'Box.Plot';
|
|
1053
|
+
if (series?.length) {
|
|
1054
|
+
args[0] = `Group(${series.map(s => `Series(${s.title || ''},,${s.values||''})` || '').join(', ')})`;
|
|
1055
|
+
}
|
|
1056
|
+
args[1] = descriptor.chart.title;
|
|
1057
|
+
break;
|
|
1058
|
+
|
|
1050
1059
|
case ChartType.Scatter:
|
|
1051
1060
|
type = 'treb-chart';
|
|
1052
1061
|
func = 'Scatter.Line';
|