@trebco/treb 28.10.0 → 28.11.1

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.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- /*! API v28.10. Copyright 2018-2024 trebco, llc. All rights reserved. LGPL: https://treb.app/license */
1
+ /*! API v28.11. Copyright 2018-2024 trebco, llc. All rights reserved. LGPL: https://treb.app/license */
2
2
 
3
3
  /**
4
4
  * add our tag to the map
@@ -438,6 +438,8 @@ export declare class EmbeddedSpreadsheet<USER_DATA_TYPE = unknown> {
438
438
  * Add a sheet, optionally named.
439
439
  */
440
440
  AddSheet(name?: string): number;
441
+ RemoveConnectedChart(id: number): void;
442
+ UpdateConnectedChart(id: number, formula: string): void;
441
443
 
442
444
  /**
443
445
  * Insert an annotation node. Usually this means inserting a chart. Regarding
@@ -0,0 +1,37 @@
1
+
2
+
3
+ "Connected elements" refers to a new API that's intended to link things
4
+ outside of the spreadsheet to the spreadsheet's graph and calculation events.
5
+
6
+ The canonical example is a chart that lives outside of the spreadsheet.
7
+ It should be updated any time the spreadsheet data changes. It should
8
+ also gracefully handle layout modifications (change sheet name, insert/delete
9
+ rows/columns) as if it were inside the spreadsheet.
10
+
11
+ One thing about these is that they're ephemeral; they have no persistent
12
+ representation in the data model. So you create them when you lay out your
13
+ web page, and they only exist for the lifetime of that page.
14
+
15
+ Still TODO:
16
+
17
+ - clean up junk (when?)
18
+ As long as we remove the leaf nodes from the graph, it should be
19
+ clean.
20
+
21
+ - deal with model rebuild (elements are getting orphaned here)
22
+ I think this is handled? still maybe an issue on Reset()
23
+
24
+ - API to update elements (e.g. change formula w/o removing)
25
+ that last one could be implemented as remove/add, possibly
26
+ reusing the generated ID. at least as a first cut.
27
+
28
+ Done:
29
+
30
+ - API to remove elements
31
+
32
+ - rewrite formula on layout changes
33
+
34
+ Open Qs:
35
+
36
+ - what happens if you completely flush the data model (reset, load new file?)
37
+
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trebco/treb",
3
- "version": "28.10.0",
3
+ "version": "28.11.1",
4
4
  "license": "LGPL-3.0-or-later",
5
5
  "homepage": "https://treb.app",
6
6
  "repository": {
@@ -55,6 +55,7 @@ import type { LeafVertex } from './dag/graph';
55
55
  import { ArgumentError, ReferenceError, UnknownError, ValueError, ExpressionError, NAError, DivideByZeroError } from './function-error';
56
56
  import { StateLeafVertex } from './dag/state_leaf_vertex';
57
57
  import { CalculationLeafVertex } from './dag/calculation_leaf_vertex';
58
+ import type { ConnectedElementType } from 'treb-grid';
58
59
 
59
60
  /**
60
61
  * breaking this out so we can use it for export (TODO)
@@ -1291,6 +1292,7 @@ export class Calculator extends Graph {
1291
1292
  subset = undefined;
1292
1293
  this.UpdateAnnotations();
1293
1294
  this.UpdateConditionals();
1295
+ this.UpdateConnectedElements();
1294
1296
  // this.UpdateNotifiers();
1295
1297
  this.full_rebuild_required = false; // unset
1296
1298
  }
@@ -1488,6 +1490,7 @@ export class Calculator extends Graph {
1488
1490
 
1489
1491
  this.UpdateAnnotations(); // all
1490
1492
  this.UpdateConditionals();
1493
+ this.UpdateConnectedElements();
1491
1494
 
1492
1495
  // and notifiers
1493
1496
 
@@ -1767,6 +1770,52 @@ export class Calculator extends Graph {
1767
1770
  }
1768
1771
  }
1769
1772
 
1773
+ public RemoveConnectedELement(element: ConnectedElementType) {
1774
+ let internal = element.internal as { vertex: StateLeafVertex };
1775
+ if (internal?.vertex) {
1776
+ this.RemoveLeafVertex(internal.vertex);
1777
+ return true;
1778
+ }
1779
+ return false;
1780
+ }
1781
+
1782
+ public UpdateConnectedElements(context?: Sheet, element?: ConnectedElementType) {
1783
+
1784
+ // we have a problem here in that these elements are not bound
1785
+ // to sheets, so we might have no context. for now we'll
1786
+ // just grab the first sheet, although that's not necessarily
1787
+ // what you want. we should enforce that these have hard sheet
1788
+ // references when created.
1789
+
1790
+ if (!context) {
1791
+ context = this.model.sheets.list[0];
1792
+ }
1793
+
1794
+ if (element) {
1795
+ let internal = element.internal as { vertex: StateLeafVertex };
1796
+ if (internal?.vertex) {
1797
+ this.RemoveLeafVertex(internal.vertex);
1798
+ }
1799
+ }
1800
+
1801
+ const elements = element ? [element] : this.model.connected_elements.values();
1802
+
1803
+ for (const element of elements) {
1804
+ let internal = element.internal as { vertex: StateLeafVertex };
1805
+ if (!internal) {
1806
+ internal = {
1807
+ vertex: new StateLeafVertex(),
1808
+ };
1809
+ element.internal = internal;
1810
+ }
1811
+
1812
+ const vertex = internal.vertex as LeafVertex;
1813
+ this.AddLeafVertex(vertex);
1814
+ this.UpdateLeafVertex(vertex, element.formula, context);
1815
+
1816
+ }
1817
+ }
1818
+
1770
1819
  public UpdateConditionals(list?: ConditionalFormat|ConditionalFormat[], context?: Sheet): void {
1771
1820
 
1772
1821
  // this method is (1) relying on the leaf vertex Set to avoid duplication,
@@ -31,6 +31,7 @@ import type { DataModel } from 'treb-grid';
31
31
  import { CalculationLeafVertex } from './calculation_leaf_vertex';
32
32
 
33
33
  export type LeafVertex = StateLeafVertex|CalculationLeafVertex;
34
+ export type { StateLeafVertex };
34
35
 
35
36
  // FIXME: this is a bad habit if you're testing on falsy for OK.
36
37
 
@@ -24,4 +24,4 @@ export * from './calculator';
24
24
 
25
25
  // for annotations that have dependencies
26
26
 
27
- export type { LeafVertex } from './dag/graph';
27
+ export type { LeafVertex, StateLeafVertex } from './dag/graph';
@@ -60,14 +60,20 @@ export const ChartFunctions: FunctionMap = {
60
60
  * more general "group" if you just want to group things.
61
61
  *
62
62
  * boxing properly as "extended" type
63
+ *
64
+ * this is getting too specific to bubble charts, which have a lot
65
+ * of requirements that other charts don't have. can we split?
66
+ *
63
67
  */
64
68
  Series: {
65
69
  arguments: [
66
70
  { name: 'Label' }, // , metadata: true, },
67
71
  { name: 'X', metadata: true, },
68
72
  { name: 'Y', metadata: true, },
73
+ { name: 'Z', metadata: true, },
69
74
  { name: 'index', },
70
75
  { name: 'subtype', },
76
+ { name: 'Labels', description: 'Labels for bubble charts only (atm)' },
71
77
  ],
72
78
  fn: (...args: any) => {
73
79
  return {
@@ -155,10 +161,7 @@ export const ChartFunctions: FunctionMap = {
155
161
 
156
162
  'Bubble.Chart': {
157
163
  arguments: [
158
- { name: 'X', metadata: true, },
159
- { name: 'Y', metadata: true, },
160
- { name: 'Z', metadata: true, },
161
- { name: 'Categories' },
164
+ { name: 'Data', metadata: true, },
162
165
  { name: 'Chart Title' },
163
166
  ],
164
167
  fn: Identity,
@@ -111,10 +111,14 @@ export interface BubbleChartData extends ChartDataBaseType {
111
111
 
112
112
  type: 'bubble';
113
113
 
114
+ /*
114
115
  x?: SubSeries;
115
116
  y?: SubSeries;
116
117
  z?: SubSeries;
117
118
  c?: any[];
119
+ */
120
+ series: SeriesType[];
121
+
118
122
 
119
123
  x_scale: RangeScale;
120
124
  y_scale: RangeScale;
@@ -219,7 +223,7 @@ export enum LegendPosition {
219
223
  }
220
224
 
221
225
  export enum LegendStyle {
222
- line, marker
226
+ line, marker, bubble
223
227
  }
224
228
 
225
229
  export interface LegendOptions {
@@ -243,6 +247,8 @@ export interface SeriesType {
243
247
  subtype?: string;
244
248
  x: SubSeries;
245
249
  y: SubSeries;
250
+ z?: SubSeries;
246
251
  index?: number;
252
+ labels?: string[];
247
253
  }
248
254
 
@@ -1,7 +1,7 @@
1
1
 
2
2
  import { type UnionValue, ValueType, type ArrayUnion } from 'treb-base-types';
3
3
  import { LegendStyle } from './chart-types';
4
- import type { SubSeries, SeriesType, BarData, ChartDataBaseType, ChartData, ScatterData2, LineData, DonutSlice } from './chart-types';
4
+ import type { SubSeries, SeriesType, BarData, ChartDataBaseType, ChartData, ScatterData2, LineData, DonutSlice, BubbleChartData } from './chart-types';
5
5
  import { NumberFormatCache } from 'treb-format';
6
6
  import { Util } from './util';
7
7
 
@@ -17,17 +17,33 @@ const DEFAULT_FORMAT = '#,##0.00'; // why not use "general", or whatever the usu
17
17
 
18
18
  export const ReadSeries = (data: Array<any>): SeriesType => {
19
19
 
20
+ // series type is (now)
21
+ //
22
+ // [0] label, string
23
+ // [1] X, array, metadata [* could be single value?]
24
+ // [2] Y, array, metadata [* could be single value?]
25
+ // [3] Z, array, metadata [* could be single value?]
26
+ // [4] index, number
27
+ // [5] subtype, string
28
+ //
29
+
20
30
  // in this case it's (label, X, Y)
21
31
  const series: SeriesType = {
22
32
  x: { data: [] },
23
33
  y: { data: [] },
24
34
  };
25
35
 
26
- if (data[3] && typeof data[3] === 'number') {
27
- series.index = data[3];
36
+ if (data[4] && typeof data[4] === 'number') {
37
+ series.index = data[4];
28
38
  }
29
- if (data[4]) {
30
- series.subtype = data[4].toString();
39
+
40
+ if (data[5]) {
41
+ series.subtype = data[5].toString();
42
+ }
43
+
44
+ if (data[6]) {
45
+ const labels = Util.Flatten(Array.isArray(data[6]) ? data[6] : [data[6]]);
46
+ series.labels = labels.map(value => (typeof value === 'undefined') ? '' : value.toString());
31
47
  }
32
48
 
33
49
  if (data[0]) {
@@ -47,6 +63,17 @@ export const ReadSeries = (data: Array<any>): SeriesType => {
47
63
  }
48
64
  }
49
65
 
66
+ // convert single value series to arrays so we can just use the old routine
67
+
68
+ for (let i = 1; i < 4; i++) {
69
+ if (data[i] && typeof data[i] === 'object' && data[i].key === 'metadata') {
70
+ data[i] = {
71
+ type: ValueType.array,
72
+ value: [data[i]],
73
+ }
74
+ }
75
+ }
76
+
50
77
  // read [2] first, so we can default for [1] if necessary
51
78
 
52
79
  if (!!data[2] && (typeof data[2] === 'object') && data[2].type === ValueType.array) {
@@ -67,7 +94,21 @@ export const ReadSeries = (data: Array<any>): SeriesType => {
67
94
  }
68
95
  }
69
96
 
70
- for (const subseries of [series.x, series.y]) {
97
+ const entries = [series.x, series.y]
98
+
99
+ // try reading [3]
100
+
101
+ if (!!data[3] && (typeof data[3] === 'object') && data[3].type === ValueType.array) {
102
+ const flat = Util.Flatten(data[3].value);
103
+ series.z = { data: [] };
104
+ series.z.data = flat.map(item => typeof item.value.value === 'number' ? item.value.value : undefined);
105
+ if (flat[0].value.format) {
106
+ series.z.format = flat[0].value.format;
107
+ }
108
+ }
109
+
110
+
111
+ for (const subseries of entries) {
71
112
 
72
113
  // in case of no values
73
114
  if (subseries.data.length) {
@@ -169,10 +210,13 @@ export const TransformSeriesData = (raw_data?: UnionValue, default_x?: UnionValu
169
210
  if (raw_data.type === ValueType.object) {
170
211
  if (raw_data.key === 'group') {
171
212
  if (Array.isArray(raw_data.value)) {
172
- for (const entry of raw_data.value) {
213
+ for (const [series_index, entry] of raw_data.value.entries()) {
173
214
  if (!!entry && (typeof entry === 'object')) {
174
215
  if (entry.key === 'series') {
175
216
  const series = ReadSeries(entry.value);
217
+ if (typeof series.index === 'undefined') {
218
+ series.index = series_index + 1;
219
+ }
176
220
  list.push(series);
177
221
  }
178
222
  else if (entry.type === ValueType.array) {
@@ -266,7 +310,7 @@ export const TransformSeriesData = (raw_data?: UnionValue, default_x?: UnionValu
266
310
  };
267
311
 
268
312
  /** get a unified scale, and formats */
269
- export const CommonData = (series: SeriesType[], y_floor?: number, y_ceiling?: number) => {
313
+ export const CommonData = (series: SeriesType[], y_floor?: number, y_ceiling?: number, x_floor?: number, x_ceiling?: number) => {
270
314
 
271
315
  let x_format = '';
272
316
  let y_format = '';
@@ -285,13 +329,47 @@ export const CommonData = (series: SeriesType[], y_floor?: number, y_ceiling?: n
285
329
  }
286
330
 
287
331
  const x = series.filter(test => test.x.range);
288
- const x_min = Math.min.apply(0, x.map(test => test.x.range?.min || 0));
289
- const x_max = Math.max.apply(0, x.map(test => test.x.range?.max || 0));
332
+ let x_min = Math.min.apply(0, x.map(test => test.x.range?.min || 0));
333
+ let x_max = Math.max.apply(0, x.map(test => test.x.range?.max || 0));
290
334
 
291
335
  const y = series.filter(test => test.y.range);
292
336
  let y_min = Math.min.apply(0, x.map(test => test.y.range?.min || 0));
293
337
  let y_max = Math.max.apply(0, x.map(test => test.y.range?.max || 0));
294
338
 
339
+ // if there's z data (used for bubble size), adjust x/y min/max to
340
+ // account for the z size so bubbles are contained within the grid
341
+
342
+ for (const subseries of series) {
343
+ if (subseries.z) {
344
+ for (const [index, z] of subseries.z.data.entries()) {
345
+ if (typeof z !== 'undefined') {
346
+ const x = subseries.x.data[index];
347
+
348
+ const half = Math.max(0, z/2); // accounting for negative values (which we don't use)
349
+
350
+ if (typeof x !== 'undefined') {
351
+ x_min = Math.min(x_min, x - half);
352
+ x_max = Math.max(x_max, x + half);
353
+ }
354
+
355
+ const y = subseries.y.data[index];
356
+ if (typeof y !== 'undefined') {
357
+ y_min = Math.min(y_min, y - half);
358
+ y_max = Math.max(y_max, y + half);
359
+ }
360
+
361
+ }
362
+ }
363
+ }
364
+ }
365
+
366
+ if (typeof x_floor !== 'undefined') {
367
+ x_min = Math.min(x_min, x_floor);
368
+ }
369
+ if (typeof x_ceiling !== 'undefined') {
370
+ x_min = Math.max(x_min, x_ceiling);
371
+ }
372
+
295
373
  if (typeof y_floor !== 'undefined') {
296
374
  y_min = Math.min(y_min, y_floor);
297
375
  }
@@ -366,6 +444,46 @@ const ApplyLabels = (series_list: SeriesType[], pattern: string, category_labels
366
444
 
367
445
  export const CreateBubbleChart = (args: UnionValue[]): ChartData => {
368
446
 
447
+ const series: SeriesType[] = TransformSeriesData(args[0]);
448
+
449
+ let y_floor: number|undefined = undefined;
450
+ let x_floor: number|undefined = undefined;
451
+
452
+ for (const entry of series) {
453
+
454
+ if (typeof entry.x.range?.min === 'number' && entry.x.range.min > 0 && entry.x.range.min < 50) {
455
+ x_floor = 0;
456
+ }
457
+ if (typeof entry.y.range?.min === 'number' && entry.y.range.min > 0 && entry.y.range.min < 50) {
458
+ y_floor = 0;
459
+ }
460
+ }
461
+
462
+ const common = CommonData(series, y_floor, undefined, x_floor);
463
+ const title = args[1]?.toString() || undefined;
464
+ const options = args[2]?.toString() || undefined;
465
+
466
+ // console.info({ series, common, title, options });
467
+
468
+ const chart_data: BubbleChartData = {
469
+
470
+ legend: common.legend,
471
+ legend_style: LegendStyle.bubble,
472
+ type: 'bubble',
473
+ series,
474
+ title,
475
+
476
+ x_scale: common.x.scale,
477
+ x_labels: common.x.labels,
478
+
479
+ y_scale: common.y.scale,
480
+ y_labels: common.y.labels,
481
+
482
+ };
483
+
484
+ return chart_data;
485
+
486
+ /*
369
487
  const [x, y, z] = [0,1,2].map(index => {
370
488
  const arg = args[index];
371
489
  if (arg.type === ValueType.array) {
@@ -450,6 +568,8 @@ export const CreateBubbleChart = (args: UnionValue[]): ChartData => {
450
568
 
451
569
  };
452
570
 
571
+ */
572
+
453
573
  };
454
574
 
455
575
  /**
@@ -196,6 +196,8 @@ export class DefaultChartRenderer implements ChartRendererType {
196
196
 
197
197
  // now do type-specific rendering
198
198
 
199
+ let zeros: number[]|undefined = [];
200
+
199
201
  switch (chart_data.type) {
200
202
  case 'scatter':
201
203
  this.renderer.RenderPoints(area, chart_data.x, chart_data.y, 'mc mc-correlation series-1');
@@ -203,23 +205,21 @@ export class DefaultChartRenderer implements ChartRendererType {
203
205
 
204
206
  case 'bubble':
205
207
 
208
+ if (chart_data.x_scale.min <= 0 && chart_data.x_scale.max >= 0) {
209
+ zeros[0] = Math.round(Math.abs(chart_data.x_scale.min / chart_data.x_scale.step));
210
+ }
211
+ if (chart_data.y_scale.min <= 0 && chart_data.y_scale.max >= 0) {
212
+ zeros[1] = Math.round(Math.abs(chart_data.y_scale.max / chart_data.y_scale.step));
213
+ }
214
+
206
215
  this.renderer.RenderGrid(area,
207
216
  chart_data.y_scale.count,
208
217
  chart_data.x_scale.count + 1, // (sigh)
209
- 'chart-grid');
218
+ 'chart-grid', zeros);
210
219
 
211
- if (chart_data.x && chart_data.y && chart_data.z) {
212
- this.renderer.RenderBubbleSeries(area,
213
- chart_data.x.data,
214
- chart_data.y.data,
215
- chart_data.z.data,
216
- chart_data.c || [],
217
- chart_data.x_scale,
218
- chart_data.y_scale,
219
- undefined,
220
- undefined,
221
- 'bubble-chart',
222
- );
220
+ for (const [index, series] of chart_data.series.entries()) {
221
+ const series_index = (typeof series.index === 'number') ? series.index : index + 1;
222
+ this.renderer.RenderBubbleSeries(area, series, chart_data.x_scale, chart_data.y_scale, `bubble-chart series-${series_index}`);
223
223
  }
224
224
 
225
225
  break;