@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-spreadsheet-light.mjs +11 -11
- package/dist/treb-spreadsheet.mjs +15 -15
- package/dist/treb.d.ts +3 -1
- package/notes/connected-elements.md +37 -0
- package/package.json +1 -1
- package/treb-calculator/src/calculator.ts +49 -0
- package/treb-calculator/src/dag/graph.ts +1 -0
- package/treb-calculator/src/index.ts +1 -1
- package/treb-charts/src/chart-functions.ts +7 -4
- package/treb-charts/src/chart-types.ts +7 -1
- package/treb-charts/src/chart-utils.ts +130 -10
- package/treb-charts/src/default-chart-renderer.ts +13 -13
- package/treb-charts/src/renderer.ts +149 -26
- package/treb-charts/style/charts.scss +37 -1
- package/treb-embed/src/embedded-spreadsheet.ts +152 -5
- package/treb-export/src/drawing2/bubble-chart-template.ts +553 -0
- package/treb-export/src/drawing2/chart2.ts +84 -1
- package/treb-export/src/export2.ts +49 -10
- package/treb-export/src/import2.ts +21 -0
- package/treb-export/src/workbook2.ts +27 -4
- package/treb-grid/src/index.ts +1 -1
- package/treb-grid/src/types/data_model.ts +32 -0
- package/treb-grid/src/types/grid_base.ts +34 -0
- package/treb-parser/src/parser-types.ts +6 -0
- package/treb-parser/src/parser.ts +48 -1
|
@@ -714,6 +714,9 @@ export class Exporter {
|
|
|
714
714
|
/** overload for return type */
|
|
715
715
|
public NormalizeAddress(unit: UnitRange, sheet: SerializedSheet): UnitRange;
|
|
716
716
|
|
|
717
|
+
/** extra overload */
|
|
718
|
+
public NormalizeAddress<UNIT = UnitAddress|UnitRange>(unit: UNIT, sheet: SerializedSheet): UNIT;
|
|
719
|
+
|
|
717
720
|
/**
|
|
718
721
|
* for charts we need addresses to be absolute ($) and ensure there's
|
|
719
722
|
* a sheet name -- use the active sheet if it's not explicitly referenced
|
|
@@ -738,6 +741,19 @@ export class Exporter {
|
|
|
738
741
|
|
|
739
742
|
}
|
|
740
743
|
|
|
744
|
+
public EnsureRange(unit: UnitAddress|UnitRange): UnitRange {
|
|
745
|
+
if (unit.type === 'range') {
|
|
746
|
+
return unit;
|
|
747
|
+
}
|
|
748
|
+
return {
|
|
749
|
+
type: 'range',
|
|
750
|
+
start: unit,
|
|
751
|
+
end: unit,
|
|
752
|
+
label: unit.label,
|
|
753
|
+
id: unit.id,
|
|
754
|
+
position: unit.position,
|
|
755
|
+
};
|
|
756
|
+
}
|
|
741
757
|
|
|
742
758
|
/**
|
|
743
759
|
* new-style annotation layout (kind of a two-cell anchor) to two-cell anchor
|
|
@@ -909,10 +925,13 @@ export class Exporter {
|
|
|
909
925
|
}
|
|
910
926
|
else if (/series/i.test(arg.name)) {
|
|
911
927
|
|
|
912
|
-
const [label, x, y] = arg.args; // y is required
|
|
928
|
+
const [label, x, y, z] = arg.args; // y is required
|
|
913
929
|
|
|
914
|
-
|
|
915
|
-
|
|
930
|
+
// FIXME: could be address also [x, y]
|
|
931
|
+
|
|
932
|
+
if (y && (y.type === 'range' || y.type === 'address')) {
|
|
933
|
+
|
|
934
|
+
options.data.push(this.EnsureRange(this.NormalizeAddress(y, sheet_source)));
|
|
916
935
|
|
|
917
936
|
if (label) {
|
|
918
937
|
|
|
@@ -933,9 +952,16 @@ export class Exporter {
|
|
|
933
952
|
}
|
|
934
953
|
|
|
935
954
|
if (!options.labels2) { options.labels2 = []; }
|
|
936
|
-
|
|
937
|
-
|
|
955
|
+
|
|
956
|
+
if (x && (x.type === 'range' || x.type === 'address')) {
|
|
957
|
+
options.labels2[options.data.length - 1] = this.EnsureRange(this.NormalizeAddress(x, sheet_source));
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
if (z && (z.type === 'range' || z.type === 'address')) {
|
|
961
|
+
if (!options.labels3) { options.labels3 = []; }
|
|
962
|
+
options.labels3[options.data.length - 1] = this.EnsureRange(this.NormalizeAddress(z, sheet_source));
|
|
938
963
|
}
|
|
964
|
+
|
|
939
965
|
}
|
|
940
966
|
else {
|
|
941
967
|
console.info('invalid series missing Y', {y, arg, ref});
|
|
@@ -951,30 +977,39 @@ export class Exporter {
|
|
|
951
977
|
const parse_result = this.parser.Parse(annotation.formula || '');
|
|
952
978
|
if (parse_result.expression && parse_result.expression.type === 'call') {
|
|
953
979
|
|
|
954
|
-
let type = '';
|
|
980
|
+
let type = ''; // FIXME
|
|
981
|
+
|
|
955
982
|
switch (parse_result.expression.name.toLowerCase()) {
|
|
956
983
|
case 'line.chart':
|
|
957
984
|
type = 'scatter';
|
|
958
985
|
break;
|
|
986
|
+
|
|
987
|
+
case 'bubble.chart':
|
|
988
|
+
type = 'bubble';
|
|
989
|
+
break;
|
|
990
|
+
|
|
959
991
|
case 'scatter.line':
|
|
960
992
|
type = 'scatter2';
|
|
961
993
|
break;
|
|
994
|
+
|
|
962
995
|
case 'donut.chart':
|
|
963
996
|
type = 'donut';
|
|
964
997
|
break;
|
|
998
|
+
|
|
965
999
|
case 'bar.chart':
|
|
966
1000
|
type = 'bar';
|
|
967
1001
|
break;
|
|
1002
|
+
|
|
968
1003
|
case 'column.chart':
|
|
969
1004
|
type = 'column';
|
|
970
1005
|
break;
|
|
971
1006
|
}
|
|
972
1007
|
|
|
973
|
-
if (type === 'column' || type === 'donut' || type === 'bar' || type === 'scatter' || type === 'scatter2') {
|
|
1008
|
+
if (type === 'column' || type === 'donut' || type === 'bar' || type === 'scatter' || type === 'scatter2' || type === 'bubble') {
|
|
974
1009
|
|
|
975
1010
|
const options: ChartOptions = { type, data: [] };
|
|
976
1011
|
|
|
977
|
-
const title_index = (type === 'scatter2') ? 1 : 2;
|
|
1012
|
+
const title_index = (type === 'scatter2' || type === 'bubble') ? 1 : 2;
|
|
978
1013
|
const title_arg = parse_result.expression.args[title_index];
|
|
979
1014
|
|
|
980
1015
|
if (title_arg && title_arg.type === 'literal') {
|
|
@@ -1000,7 +1035,7 @@ export class Exporter {
|
|
|
1000
1035
|
|
|
1001
1036
|
if (parse_result.expression.args[0]) {
|
|
1002
1037
|
const arg0 = parse_result.expression.args[0];
|
|
1003
|
-
if (type === 'scatter2' || type === 'bar' || type === 'column' || type === 'scatter') {
|
|
1038
|
+
if (type === 'scatter2' || type === 'bar' || type === 'column' || type === 'scatter' || type === 'bubble') {
|
|
1004
1039
|
parse_series(arg0, options, sheet_source.name);
|
|
1005
1040
|
}
|
|
1006
1041
|
else if (arg0.type === 'range') {
|
|
@@ -1045,7 +1080,7 @@ export class Exporter {
|
|
|
1045
1080
|
*/
|
|
1046
1081
|
}
|
|
1047
1082
|
|
|
1048
|
-
if (type !== 'scatter2') {
|
|
1083
|
+
if (type !== 'scatter2' && type !== 'bubble') {
|
|
1049
1084
|
if (parse_result.expression.args[1] && parse_result.expression.args[1].type === 'range') {
|
|
1050
1085
|
options.labels = this.NormalizeAddress(parse_result.expression.args[1], sheet_source);
|
|
1051
1086
|
}
|
|
@@ -1064,6 +1099,10 @@ export class Exporter {
|
|
|
1064
1099
|
options.smooth = true;
|
|
1065
1100
|
}
|
|
1066
1101
|
}
|
|
1102
|
+
else if (type === 'bubble') {
|
|
1103
|
+
// ...
|
|
1104
|
+
// console.info({parse_result});
|
|
1105
|
+
}
|
|
1067
1106
|
|
|
1068
1107
|
// FIXME: fix this type (this happened when we switched from annotation
|
|
1069
1108
|
// class to a data interface)
|
|
@@ -917,6 +917,27 @@ export class Importer {
|
|
|
917
917
|
const series = descriptor.chart?.series;
|
|
918
918
|
|
|
919
919
|
switch(descriptor.chart.type) {
|
|
920
|
+
|
|
921
|
+
case ChartType.Bubble:
|
|
922
|
+
type = 'treb-chart';
|
|
923
|
+
func = 'Bubble.Chart';
|
|
924
|
+
|
|
925
|
+
if (series && series.length) {
|
|
926
|
+
args[0] = `Group(${series.map(s => `Series(${
|
|
927
|
+
[
|
|
928
|
+
s.title || '',
|
|
929
|
+
s.values || '',
|
|
930
|
+
s.categories || '',
|
|
931
|
+
s.bubble_size || '',
|
|
932
|
+
|
|
933
|
+
].join(', ')
|
|
934
|
+
})`).join(', ')})`;
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
args[1] = descriptor.chart.title;
|
|
938
|
+
|
|
939
|
+
break;
|
|
940
|
+
|
|
920
941
|
case ChartType.Scatter:
|
|
921
942
|
type = 'treb-chart';
|
|
922
943
|
func = 'Scatter.Line';
|
|
@@ -69,12 +69,13 @@ export const ConditionalFormatOperators: Record<string, string> = {
|
|
|
69
69
|
};
|
|
70
70
|
|
|
71
71
|
export enum ChartType {
|
|
72
|
-
Unknown = 0, Column, Bar, Line, Scatter, Donut, Pie
|
|
72
|
+
Unknown = 0, Column, Bar, Line, Scatter, Donut, Pie, Bubble
|
|
73
73
|
}
|
|
74
74
|
|
|
75
75
|
export interface ChartSeries {
|
|
76
76
|
values?: string;
|
|
77
77
|
categories?: string;
|
|
78
|
+
bubble_size?: string;
|
|
78
79
|
title?: string;
|
|
79
80
|
}
|
|
80
81
|
|
|
@@ -411,7 +412,7 @@ export class Workbook {
|
|
|
411
412
|
|
|
412
413
|
}
|
|
413
414
|
|
|
414
|
-
const ParseSeries = (node: any,
|
|
415
|
+
const ParseSeries = (node: any, type?: ChartType): ChartSeries[] => {
|
|
415
416
|
|
|
416
417
|
const series: ChartSeries[] = [];
|
|
417
418
|
|
|
@@ -447,7 +448,7 @@ export class Workbook {
|
|
|
447
448
|
}
|
|
448
449
|
}
|
|
449
450
|
|
|
450
|
-
if (
|
|
451
|
+
if (type === ChartType.Scatter || type === ChartType.Bubble) {
|
|
451
452
|
const x = XMLUtils.FindChild(series_node, 'c:xVal/c:numRef/c:f');
|
|
452
453
|
if (x) {
|
|
453
454
|
series_data.categories = x; // .text?.toString();
|
|
@@ -456,6 +457,14 @@ export class Workbook {
|
|
|
456
457
|
if (y) {
|
|
457
458
|
series_data.values = y; // .text?.toString();
|
|
458
459
|
}
|
|
460
|
+
|
|
461
|
+
if (type === ChartType.Bubble) {
|
|
462
|
+
const z = XMLUtils.FindChild(series_node, 'c:bubbleSize/c:numRef/c:f');
|
|
463
|
+
if (z) {
|
|
464
|
+
series_data.bubble_size = z; // .text?.toString();
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
|
|
459
468
|
}
|
|
460
469
|
else {
|
|
461
470
|
const value_node = XMLUtils.FindChild(series_node, 'c:val/c:numRef/c:f');
|
|
@@ -522,9 +531,23 @@ export class Workbook {
|
|
|
522
531
|
node = XMLUtils.FindChild(xml, 'c:chartSpace/c:chart/c:plotArea/c:scatterChart');
|
|
523
532
|
if (node) {
|
|
524
533
|
result.type = ChartType.Scatter;
|
|
525
|
-
result.series = ParseSeries(node,
|
|
534
|
+
result.series = ParseSeries(node, ChartType.Scatter);
|
|
526
535
|
}
|
|
527
536
|
}
|
|
537
|
+
|
|
538
|
+
if (!node) {
|
|
539
|
+
node = XMLUtils.FindChild(xml, 'c:chartSpace/c:chart/c:plotArea/c:bubbleChart');
|
|
540
|
+
if (node) {
|
|
541
|
+
result.type = ChartType.Bubble;
|
|
542
|
+
result.series = ParseSeries(node, ChartType.Bubble);
|
|
543
|
+
console.info("Bubble series?", result.series);
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
if (!node) {
|
|
548
|
+
console.info("Chart type not handled");
|
|
549
|
+
}
|
|
550
|
+
|
|
528
551
|
// console.info("RX?", result);
|
|
529
552
|
|
|
530
553
|
return result;
|
package/treb-grid/src/index.ts
CHANGED
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
export { Grid } from './types/grid';
|
|
23
23
|
export { GridBase } from './types/grid_base';
|
|
24
24
|
export { Sheet } from './types/sheet';
|
|
25
|
-
export { DataModel, type MacroFunction } from './types/data_model';
|
|
25
|
+
export { DataModel, type MacroFunction, type ConnectedElementType } from './types/data_model';
|
|
26
26
|
export type { SerializedNamedExpression, SerializedModel } from './types/data_model';
|
|
27
27
|
export * from './types/grid_events';
|
|
28
28
|
export type { SerializedSheet, FreezePane } from './types/sheet_types';
|
|
@@ -26,6 +26,12 @@ import { NamedRangeCollection } from './named_range';
|
|
|
26
26
|
import { type ExpressionUnit, type UnitAddress, type UnitStructuredReference, type UnitRange, Parser, QuotedSheetNameRegex } from 'treb-parser';
|
|
27
27
|
import { Area, IsCellAddress, Style } from 'treb-base-types';
|
|
28
28
|
|
|
29
|
+
export interface ConnectedElementType {
|
|
30
|
+
formula: string;
|
|
31
|
+
update?: (instance: ConnectedElementType) => void;
|
|
32
|
+
internal?: unknown; // opaque type to prevent circular dependencies
|
|
33
|
+
}
|
|
34
|
+
|
|
29
35
|
export interface SerializedMacroFunction {
|
|
30
36
|
name: string;
|
|
31
37
|
function_def: string;
|
|
@@ -444,6 +450,32 @@ export class DataModel {
|
|
|
444
450
|
return address; // already range or address
|
|
445
451
|
|
|
446
452
|
}
|
|
453
|
+
|
|
454
|
+
public AddConnectedElement(connected_element: ConnectedElementType): number {
|
|
455
|
+
const id = this.connected_element_id++;
|
|
456
|
+
this.connected_elements.set(id, connected_element);
|
|
457
|
+
return id;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
public RemoveConnectedElement(id: number) {
|
|
461
|
+
const element = this.connected_elements.get(id);
|
|
462
|
+
this.connected_elements.delete(id);
|
|
463
|
+
return element;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
/**
|
|
467
|
+
* identifier for connected elements, used to manage. these need to be
|
|
468
|
+
* unique in the lifetime of a model instance, but no more than that.
|
|
469
|
+
*/
|
|
470
|
+
protected connected_element_id = 100;
|
|
471
|
+
|
|
472
|
+
/**
|
|
473
|
+
* these are intentionally NOT serialized. they're ephemeral, created
|
|
474
|
+
* at runtime and not persistent.
|
|
475
|
+
*
|
|
476
|
+
* @internal
|
|
477
|
+
*/
|
|
478
|
+
public connected_elements: Map<number, ConnectedElementType> = new Map();
|
|
447
479
|
|
|
448
480
|
}
|
|
449
481
|
|
|
@@ -2270,6 +2270,16 @@ export class GridBase {
|
|
|
2270
2270
|
}
|
|
2271
2271
|
}
|
|
2272
2272
|
|
|
2273
|
+
for (const element of this.model.connected_elements.values()) {
|
|
2274
|
+
if (element.formula) {
|
|
2275
|
+
const updated = this.PatchExpressionSheetName(element.formula, old_name, name);
|
|
2276
|
+
if (updated) {
|
|
2277
|
+
element.formula = updated;
|
|
2278
|
+
changes++;
|
|
2279
|
+
}
|
|
2280
|
+
}
|
|
2281
|
+
}
|
|
2282
|
+
|
|
2273
2283
|
return changes;
|
|
2274
2284
|
|
|
2275
2285
|
}
|
|
@@ -2850,6 +2860,18 @@ export class GridBase {
|
|
|
2850
2860
|
row_count: command.count
|
|
2851
2861
|
});
|
|
2852
2862
|
|
|
2863
|
+
// connected elements
|
|
2864
|
+
for (const external of this.model.connected_elements.values()) {
|
|
2865
|
+
if (external.formula) {
|
|
2866
|
+
const modified = this.PatchFormulasInternal(external.formula,
|
|
2867
|
+
command.before_row, command.count, 0, 0,
|
|
2868
|
+
target_sheet.name.toLowerCase(), false);
|
|
2869
|
+
if (modified) {
|
|
2870
|
+
external.formula = modified;
|
|
2871
|
+
}
|
|
2872
|
+
}
|
|
2873
|
+
}
|
|
2874
|
+
|
|
2853
2875
|
// see InsertColumnsInternal re: tables. rows are less complicated,
|
|
2854
2876
|
// except that if you delete the header row we want to remove the
|
|
2855
2877
|
// table entirely.
|
|
@@ -3172,6 +3194,18 @@ export class GridBase {
|
|
|
3172
3194
|
before_row: 0,
|
|
3173
3195
|
row_count: 0 });
|
|
3174
3196
|
|
|
3197
|
+
// connected elements
|
|
3198
|
+
for (const element of this.model.connected_elements.values()) {
|
|
3199
|
+
if (element.formula) {
|
|
3200
|
+
const modified = this.PatchFormulasInternal(element.formula,
|
|
3201
|
+
0, 0, command.before_column, command.count,
|
|
3202
|
+
target_sheet.name.toLowerCase(), false);
|
|
3203
|
+
if (modified) {
|
|
3204
|
+
element.formula = modified;
|
|
3205
|
+
}
|
|
3206
|
+
}
|
|
3207
|
+
}
|
|
3208
|
+
|
|
3175
3209
|
// patch tables. we removed this from the sheet routine entirely,
|
|
3176
3210
|
// we need to rebuild any affected tables now.
|
|
3177
3211
|
|
|
@@ -395,3 +395,9 @@ export interface RenderOptions {
|
|
|
395
395
|
long_structured_references: boolean;
|
|
396
396
|
table_name: string;
|
|
397
397
|
}
|
|
398
|
+
|
|
399
|
+
export interface PersistedParserConfig {
|
|
400
|
+
flags: Partial<ParserFlags>;
|
|
401
|
+
argument_separator: ArgumentSeparatorType;
|
|
402
|
+
decimal_mark: DecimalMarkType;
|
|
403
|
+
}
|
|
@@ -33,7 +33,8 @@ import type {
|
|
|
33
33
|
UnitLiteralNumber,
|
|
34
34
|
ParserFlags,
|
|
35
35
|
UnitStructuredReference,
|
|
36
|
-
RenderOptions
|
|
36
|
+
RenderOptions,
|
|
37
|
+
PersistedParserConfig} from './parser-types';
|
|
37
38
|
import {
|
|
38
39
|
ArgumentSeparatorType,
|
|
39
40
|
DecimalMarkType
|
|
@@ -248,6 +249,52 @@ export class Parser {
|
|
|
248
249
|
*/
|
|
249
250
|
protected full_reference_list: Array<UnitAddress | UnitRange | UnitIdentifier | UnitStructuredReference> = [];
|
|
250
251
|
|
|
252
|
+
protected parser_state: string[] = [];
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* save local configuration to a buffer, so it can be restored. we're doing
|
|
256
|
+
* this because in a lot of places we're caching parser flagss, changing
|
|
257
|
+
* them, and then restoring them. that's become repetitive, fragile to
|
|
258
|
+
* changes or new flags, and annoying.
|
|
259
|
+
*
|
|
260
|
+
* config is managed in a list with push/pop semantics. we store it as
|
|
261
|
+
* JSON so there's no possibility we'll accidentally mutate.
|
|
262
|
+
*
|
|
263
|
+
* FIXME: while we're at it why not migrate the separators -> flags, so
|
|
264
|
+
* there's a single location for this kind of state? (...TODO)
|
|
265
|
+
*
|
|
266
|
+
*/
|
|
267
|
+
public Save() {
|
|
268
|
+
const config: PersistedParserConfig = {
|
|
269
|
+
flags: this.flags,
|
|
270
|
+
argument_separator: this.argument_separator,
|
|
271
|
+
decimal_mark: this.decimal_mark,
|
|
272
|
+
}
|
|
273
|
+
this.parser_state.push(JSON.stringify(config));
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* restore persisted config
|
|
278
|
+
* @see Save
|
|
279
|
+
*/
|
|
280
|
+
public Restore() {
|
|
281
|
+
const json = this.parser_state.shift();
|
|
282
|
+
if (json) {
|
|
283
|
+
try {
|
|
284
|
+
const config = JSON.parse(json) as PersistedParserConfig;
|
|
285
|
+
this.flags = config.flags;
|
|
286
|
+
this.argument_separator = config.argument_separator;
|
|
287
|
+
this.decimal_mark = config.decimal_mark;
|
|
288
|
+
}
|
|
289
|
+
catch (err) {
|
|
290
|
+
console.error(err);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
else {
|
|
294
|
+
console.warn("No parser state to restore");
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
251
298
|
/**
|
|
252
299
|
* recursive tree walk.
|
|
253
300
|
*
|