@trebco/treb 28.10.0 → 28.10.5
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 +10 -10
- package/dist/treb-spreadsheet.mjs +9 -9
- package/dist/treb.d.ts +2 -0
- 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 +108 -9
- package/treb-charts/src/default-chart-renderer.ts +3 -12
- package/treb-charts/src/renderer.ts +115 -23
- package/treb-charts/style/charts.scss +32 -1
- package/treb-embed/src/embedded-spreadsheet.ts +152 -5
- 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
|
@@ -87,6 +87,10 @@ import type { BorderToolbarMessage, ToolbarMessage } from './toolbar-message';
|
|
|
87
87
|
import { Chart, ChartFunctions } from 'treb-charts';
|
|
88
88
|
import type { SetRangeOptions } from 'treb-grid';
|
|
89
89
|
|
|
90
|
+
import type { StateLeafVertex } from 'treb-calculator';
|
|
91
|
+
import type { ConnectedElementType } from 'treb-grid';
|
|
92
|
+
|
|
93
|
+
|
|
90
94
|
// --- worker ------------------------------------------------------------------
|
|
91
95
|
|
|
92
96
|
/**
|
|
@@ -95,7 +99,6 @@ import type { SetRangeOptions } from 'treb-grid';
|
|
|
95
99
|
* the script so we can run it as a worker.
|
|
96
100
|
*/
|
|
97
101
|
import * as export_worker_script from 'worker:../../treb-export/src/export-worker/index.worker';
|
|
98
|
-
import { StateLeafVertex } from 'treb-calculator/src/dag/state_leaf_vertex';
|
|
99
102
|
|
|
100
103
|
// --- types -------------------------------------------------------------------
|
|
101
104
|
|
|
@@ -2197,6 +2200,114 @@ export class EmbeddedSpreadsheet<USER_DATA_TYPE = unknown> {
|
|
|
2197
2200
|
|
|
2198
2201
|
}
|
|
2199
2202
|
|
|
2203
|
+
public RemoveConnectedChart(id: number) {
|
|
2204
|
+
const element = this.model.RemoveConnectedElement(id);
|
|
2205
|
+
if (element) {
|
|
2206
|
+
const removed = this.calculator.RemoveConnectedELement(element);
|
|
2207
|
+
if (removed) {
|
|
2208
|
+
// ... actually don't need to update here
|
|
2209
|
+
}
|
|
2210
|
+
}
|
|
2211
|
+
}
|
|
2212
|
+
|
|
2213
|
+
public UpdateConnectedChart(id: number, formula: string) {
|
|
2214
|
+
const element = this.model.connected_elements.get(id);
|
|
2215
|
+
if (element) {
|
|
2216
|
+
element.formula = formula;
|
|
2217
|
+
const internal = (element.internal) as { vertex: StateLeafVertex, state: any };
|
|
2218
|
+
|
|
2219
|
+
if (internal?.state) {
|
|
2220
|
+
internal.state = undefined;
|
|
2221
|
+
}
|
|
2222
|
+
this.calculator.UpdateConnectedElements(this.grid.active_sheet, element);
|
|
2223
|
+
|
|
2224
|
+
if (element.update) {
|
|
2225
|
+
element.update.call(0, element);
|
|
2226
|
+
}
|
|
2227
|
+
|
|
2228
|
+
}
|
|
2229
|
+
}
|
|
2230
|
+
|
|
2231
|
+
/**
|
|
2232
|
+
* @internal
|
|
2233
|
+
*
|
|
2234
|
+
* @returns an id that can be used to manage the reference
|
|
2235
|
+
*/
|
|
2236
|
+
public CreateConnectedChart(formula: string, target: HTMLElement, options: EvaluateOptions): number {
|
|
2237
|
+
|
|
2238
|
+
// FIXME: merge w/ insert annotation?
|
|
2239
|
+
|
|
2240
|
+
let r1c1 = options?.r1c1 || false;
|
|
2241
|
+
let argument_separator = options?.argument_separator || this.parser.argument_separator; // default to current
|
|
2242
|
+
|
|
2243
|
+
this.parser.Save();
|
|
2244
|
+
this.parser.flags.r1c1 = r1c1;
|
|
2245
|
+
|
|
2246
|
+
if (argument_separator === ',') {
|
|
2247
|
+
this.parser.argument_separator = ArgumentSeparatorType.Comma;
|
|
2248
|
+
this.parser.decimal_mark = DecimalMarkType.Period;
|
|
2249
|
+
}
|
|
2250
|
+
else {
|
|
2251
|
+
this.parser.argument_separator = ArgumentSeparatorType.Semicolon;
|
|
2252
|
+
this.parser.decimal_mark = DecimalMarkType.Comma;
|
|
2253
|
+
}
|
|
2254
|
+
|
|
2255
|
+
const result = this.parser.Parse(formula);
|
|
2256
|
+
|
|
2257
|
+
this.parser.Restore();
|
|
2258
|
+
|
|
2259
|
+
if (result.expression) {
|
|
2260
|
+
formula = '=' + this.parser.Render(result.expression, { missing: '' });
|
|
2261
|
+
}
|
|
2262
|
+
else {
|
|
2263
|
+
console.warn("invalid formula", result.error);
|
|
2264
|
+
}
|
|
2265
|
+
|
|
2266
|
+
const chart = this.CreateChart();
|
|
2267
|
+
chart.Initialize(target);
|
|
2268
|
+
|
|
2269
|
+
const id = this.model.AddConnectedElement({
|
|
2270
|
+
formula,
|
|
2271
|
+
|
|
2272
|
+
// this is circular, but I want to leave `this` bound to the sheet
|
|
2273
|
+
// instance in case we need it -- so what's a better approach? pass
|
|
2274
|
+
// in the formula explicitly, and update if we need to make changes?
|
|
2275
|
+
|
|
2276
|
+
update: (instance: ConnectedElementType) => {
|
|
2277
|
+
|
|
2278
|
+
const parse_result = this.parser.Parse(instance.formula);
|
|
2279
|
+
|
|
2280
|
+
if (parse_result &&
|
|
2281
|
+
parse_result.expression &&
|
|
2282
|
+
parse_result.expression.type === 'call') {
|
|
2283
|
+
|
|
2284
|
+
// FIXME: make a method for doing this
|
|
2285
|
+
|
|
2286
|
+
this.parser.Walk(parse_result.expression, (unit) => {
|
|
2287
|
+
if (unit.type === 'address' || unit.type === 'range') {
|
|
2288
|
+
this.model.ResolveSheetID(unit, undefined, this.grid.active_sheet);
|
|
2289
|
+
}
|
|
2290
|
+
return true;
|
|
2291
|
+
});
|
|
2292
|
+
|
|
2293
|
+
const expr_name = parse_result.expression.name.toLowerCase();
|
|
2294
|
+
const result = this.calculator.CalculateExpression(parse_result.expression);
|
|
2295
|
+
chart.Exec(expr_name, result as ExtendedUnion); // FIXME: type?
|
|
2296
|
+
}
|
|
2297
|
+
|
|
2298
|
+
chart.Update();
|
|
2299
|
+
|
|
2300
|
+
},
|
|
2301
|
+
|
|
2302
|
+
});
|
|
2303
|
+
|
|
2304
|
+
this.calculator.UpdateConnectedElements(this.grid.active_sheet);
|
|
2305
|
+
this.UpdateConnectedElements();
|
|
2306
|
+
|
|
2307
|
+
return id;
|
|
2308
|
+
|
|
2309
|
+
}
|
|
2310
|
+
|
|
2200
2311
|
/**
|
|
2201
2312
|
* Insert an annotation node. Usually this means inserting a chart. Regarding
|
|
2202
2313
|
* the argument separator, see the Evaluate function.
|
|
@@ -2226,11 +2337,21 @@ export class EmbeddedSpreadsheet<USER_DATA_TYPE = unknown> {
|
|
|
2226
2337
|
target = Rectangle.IsRectangle(rect) ? rect : this.model.ResolveArea(rect, this.grid.active_sheet);
|
|
2227
2338
|
}
|
|
2228
2339
|
|
|
2340
|
+
// FIXME: with the new parser save/restore semantics we should
|
|
2341
|
+
// just always do this. also I don't think the r1c1 logic works
|
|
2342
|
+
// properly here... unless we're assuming that the default state
|
|
2343
|
+
// is always off
|
|
2344
|
+
|
|
2229
2345
|
if (argument_separator && argument_separator !== this.parser.argument_separator || r1c1) {
|
|
2346
|
+
|
|
2347
|
+
this.parser.Save();
|
|
2348
|
+
|
|
2349
|
+
/*
|
|
2230
2350
|
const current = {
|
|
2231
2351
|
argument_separator: this.parser.argument_separator,
|
|
2232
2352
|
decimal_mark: this.parser.decimal_mark,
|
|
2233
2353
|
};
|
|
2354
|
+
*/
|
|
2234
2355
|
|
|
2235
2356
|
if (argument_separator === ',') {
|
|
2236
2357
|
this.parser.argument_separator = ArgumentSeparatorType.Comma;
|
|
@@ -2241,17 +2362,21 @@ export class EmbeddedSpreadsheet<USER_DATA_TYPE = unknown> {
|
|
|
2241
2362
|
this.parser.decimal_mark = DecimalMarkType.Comma;
|
|
2242
2363
|
}
|
|
2243
2364
|
|
|
2244
|
-
const r1c1_state = this.parser.flags.r1c1;
|
|
2245
|
-
if (r1c1)
|
|
2246
|
-
|
|
2247
|
-
|
|
2365
|
+
// const r1c1_state = this.parser.flags.r1c1;
|
|
2366
|
+
// if (r1c1)
|
|
2367
|
+
// {
|
|
2368
|
+
this.parser.flags.r1c1 = !!r1c1;
|
|
2369
|
+
// }
|
|
2248
2370
|
|
|
2249
2371
|
const result = this.parser.Parse(formula);
|
|
2250
2372
|
|
|
2373
|
+
/*
|
|
2251
2374
|
// reset
|
|
2252
2375
|
this.parser.argument_separator = current.argument_separator;
|
|
2253
2376
|
this.parser.decimal_mark = current.decimal_mark;
|
|
2254
2377
|
this.parser.flags.r1c1 = r1c1_state;
|
|
2378
|
+
*/
|
|
2379
|
+
this.parser.Restore();
|
|
2255
2380
|
|
|
2256
2381
|
if (result.expression) {
|
|
2257
2382
|
formula = '=' + this.parser.Render(result.expression, { missing: '' });
|
|
@@ -4856,6 +4981,7 @@ export class EmbeddedSpreadsheet<USER_DATA_TYPE = unknown> {
|
|
|
4856
4981
|
* (just sparklines atm) and update if necessary.
|
|
4857
4982
|
*/
|
|
4858
4983
|
protected UpdateAnnotations(): void {
|
|
4984
|
+
|
|
4859
4985
|
for (const annotation of this.grid.active_sheet.annotations) {
|
|
4860
4986
|
if (annotation.temp.vertex) {
|
|
4861
4987
|
const vertex = annotation.temp.vertex as StateLeafVertex;
|
|
@@ -4893,6 +5019,27 @@ export class EmbeddedSpreadsheet<USER_DATA_TYPE = unknown> {
|
|
|
4893
5019
|
}
|
|
4894
5020
|
|
|
4895
5021
|
}
|
|
5022
|
+
|
|
5023
|
+
this.UpdateConnectedElements();
|
|
5024
|
+
|
|
5025
|
+
}
|
|
5026
|
+
|
|
5027
|
+
protected UpdateConnectedElements() {
|
|
5028
|
+
for (const element of this.model.connected_elements.values()) {
|
|
5029
|
+
const internal = (element.internal) as { vertex: StateLeafVertex, state: any };
|
|
5030
|
+
if (internal?.vertex && internal.vertex.state_id !== internal.state ) {
|
|
5031
|
+
internal.state = internal.vertex.state_id;
|
|
5032
|
+
const fn = element.update;
|
|
5033
|
+
if (fn) {
|
|
5034
|
+
|
|
5035
|
+
// FIXME: what if there are multiple calls pending? some
|
|
5036
|
+
// kind of a dispatch system might be useful
|
|
5037
|
+
|
|
5038
|
+
Promise.resolve().then(() => fn.call(0, element));
|
|
5039
|
+
|
|
5040
|
+
}
|
|
5041
|
+
}
|
|
5042
|
+
}
|
|
4896
5043
|
}
|
|
4897
5044
|
|
|
4898
5045
|
/*
|
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
|
*
|