@trebco/treb 29.8.4 → 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.
Files changed (38) hide show
  1. package/dist/treb-spreadsheet-light.mjs +11 -11
  2. package/dist/treb-spreadsheet.mjs +15 -15
  3. package/dist/treb.d.ts +11 -1
  4. package/eslint.config.js +9 -0
  5. package/package.json +1 -1
  6. package/treb-base-types/src/area-utils.ts +60 -0
  7. package/treb-base-types/src/area.ts +11 -0
  8. package/treb-base-types/src/cell.ts +6 -1
  9. package/treb-base-types/src/cells.ts +38 -7
  10. package/treb-base-types/src/index.ts +2 -0
  11. package/treb-calculator/src/calculator.ts +274 -4
  12. package/treb-calculator/src/dag/array-vertex.ts +0 -10
  13. package/treb-calculator/src/dag/graph.ts +118 -77
  14. package/treb-calculator/src/dag/spreadsheet_vertex.ts +38 -9
  15. package/treb-calculator/src/dag/spreadsheet_vertex_base.ts +1 -0
  16. package/treb-calculator/src/expression-calculator.ts +7 -2
  17. package/treb-calculator/src/function-error.ts +6 -0
  18. package/treb-charts/src/chart-functions.ts +39 -5
  19. package/treb-charts/src/chart-types.ts +23 -0
  20. package/treb-charts/src/chart-utils.ts +165 -2
  21. package/treb-charts/src/chart.ts +6 -1
  22. package/treb-charts/src/default-chart-renderer.ts +70 -1
  23. package/treb-charts/src/index.ts +1 -0
  24. package/treb-charts/src/renderer.ts +95 -2
  25. package/treb-charts/style/charts.scss +41 -0
  26. package/treb-embed/src/embedded-spreadsheet.ts +11 -4
  27. package/treb-embed/src/options.ts +8 -0
  28. package/treb-embed/style/dark-theme.scss +4 -0
  29. package/treb-embed/style/grid.scss +15 -0
  30. package/treb-embed/style/z-index.scss +3 -0
  31. package/treb-export/src/import2.ts +9 -0
  32. package/treb-export/src/workbook2.ts +67 -3
  33. package/treb-grid/src/editors/editor.ts +12 -5
  34. package/treb-grid/src/layout/base_layout.ts +41 -0
  35. package/treb-grid/src/types/grid.ts +61 -25
  36. package/treb-parser/src/parser-types.ts +3 -0
  37. package/treb-parser/src/parser.ts +21 -2
  38. 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 => {
@@ -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: string, union: ExtendedUnion) {
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 && chart_data.x_labels.length) {
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) {
@@ -21,5 +21,6 @@
21
21
 
22
22
  export { Chart } from './chart';
23
23
  export { ChartFunctions } from './chart-functions';
24
+ export type { ChartFunction } from './chart-functions';
24
25
  export { Util } from './util';
25
26
  export type { ChartRenderer } from './renderer-type';
@@ -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.parent.getBoundingClientRect();
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
@@ -31,6 +31,9 @@ $z-index-corner: 20;
31
31
  $z-index-header-tile-cover: 22;
32
32
  $z-index-cell-editor: 24;
33
33
  $z-index-scroller: 26; /* legacy only */
34
+
35
+ $z-index-spill-border: 35;
36
+
34
37
  $z-index-autocomplete: 39;
35
38
  $z-index-tooltip: 39;
36
39
  $z-index-dropdown-caret: 39;
@@ -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';