@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.
@@ -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} from './chart-types';
24
+ import type { 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
 
@@ -196,14 +196,22 @@ export class ChartRenderer {
196
196
  group.appendChild(SVGNode('text', {
197
197
  'dominant-baseline': 'middle', x: x + marker_width, y, dy: (trident ? '.3em' : undefined) }, label.label));
198
198
 
199
- if (options.style === LegendStyle.marker) {
200
- group.appendChild(SVGNode('rect', {
201
- class: `series-${color}`, x, y: marker_y - 4, width: 8, height: 8 }));
202
- }
203
- else {
204
- group.appendChild(SVGNode('rect', {
205
- class: `series-${color}`, x, y: marker_y - 1, width: marker_width - 3, height: 2}));
206
- }
199
+ switch (options.style) {
200
+ case LegendStyle.marker:
201
+ group.appendChild(SVGNode('rect', {
202
+ class: `series-${color}`, x, y: marker_y - 4, width: 8, height: 8 }));
203
+ break;
204
+
205
+ case LegendStyle.bubble:
206
+ group.appendChild(SVGNode('circle', {
207
+ class: `series-${color}`, cx: x + marker_width - 11, cy: marker_y - 4 + 3, /* r: '0.25em' */ }));
208
+ break;
209
+
210
+ default:
211
+ group.appendChild(SVGNode('rect', {
212
+ class: `series-${color}`, x, y: marker_y - 1, width: marker_width - 3, height: 2}));
213
+ break;
214
+ }
207
215
 
208
216
  h = Math.max(h, text_metrrics.height);
209
217
  x += text_metrrics.width + marker_width + padding;
@@ -960,23 +968,54 @@ export class ChartRenderer {
960
968
 
961
969
  }
962
970
 
963
- public RenderGrid(area: Area, y_count: number, x_count = 0, classes?: string | string[]): void {
971
+ public RenderGrid(area: Area, y_count: number, x_count = 0, classes?: string | string[], zeros?: number[]): void {
964
972
 
965
973
  const d: string[] = [];
974
+ const d2: string[] = [];
966
975
 
967
976
  let step = area.height / y_count;
968
977
  for (let i = 0; i <= y_count; i++) {
978
+
969
979
  const y = Math.round(area.top + step * i) - 0.5;
970
- d.push(`M${area.left} ${y} L${area.right} ${y}`);
980
+
981
+ if (zeros && zeros[1] === i) {
982
+ d2.push(`M${area.left} ${y} L${area.right} ${y}`);
983
+ }
984
+ else {
985
+ d.push(`M${area.left} ${y} L${area.right} ${y}`);
986
+ }
971
987
  }
972
988
 
973
989
  step = area.width / (x_count - 1);
974
990
  for (let i = 0; i < x_count; i++) {
991
+
975
992
  const x = Math.round(area.left + step * i) - 0.5;
976
- d.push(`M${x} ${area.top} L${x} ${area.bottom}`);
993
+
994
+ if (zeros && zeros[0] === i) {
995
+ d2.push(`M${x} ${area.top} L${x} ${area.bottom}`);
996
+ }
997
+ else {
998
+ d.push(`M${x} ${area.top} L${x} ${area.bottom}`);
999
+ }
977
1000
  }
978
1001
 
979
1002
  this.group.appendChild(SVGNode('path', {d, class: classes}));
1003
+ if (d2.length) {
1004
+
1005
+ if (classes) {
1006
+ if (!Array.isArray(classes)) {
1007
+ classes = classes + ' zero';
1008
+ }
1009
+ else {
1010
+ classes.push('zero');
1011
+ }
1012
+ }
1013
+ else {
1014
+ classes = 'zero';
1015
+ }
1016
+
1017
+ this.group.appendChild(SVGNode('path', {d: d2, class: classes}));
1018
+ }
980
1019
 
981
1020
  }
982
1021
 
@@ -1193,23 +1232,24 @@ export class ChartRenderer {
1193
1232
 
1194
1233
  }
1195
1234
 
1196
- public RenderBubbleSeries(area: Area,
1197
- x: Array<number | undefined>,
1198
- y: Array<number | undefined>,
1199
- z: Array<number | undefined>,
1200
- c: any[] = [],
1235
+ public RenderBubbleSeries(
1236
+ area: Area,
1237
+ series: SeriesType,
1201
1238
  x_scale: RangeScale,
1202
1239
  y_scale: RangeScale,
1203
- min = 10,
1204
- max = 30,
1205
1240
  classes?: string | string[]): void {
1206
1241
 
1207
- const count = Math.max(x.length, y.length, z.length);
1208
1242
  const xrange = (x_scale.max - x_scale.min) || 1;
1209
1243
  const yrange = (y_scale.max - y_scale.min) || 1;
1210
1244
 
1211
1245
  // const marker_elements: string[] = [];
1212
- const points: Array<{x: number, y: number, z: number, series: number} | undefined> = [];
1246
+ const points: Array<{x: number, y: number, z: number} | undefined> = [];
1247
+ const labels: Array<{
1248
+ x: number,
1249
+ y: number,
1250
+ text: string,
1251
+ offset: number,
1252
+ }> = [];
1213
1253
 
1214
1254
  const d: string[] = [];
1215
1255
  const areas: string[] = [];
@@ -1219,6 +1259,43 @@ export class ChartRenderer {
1219
1259
  // if (title) node.setAttribute('title', title);
1220
1260
  this.group.appendChild(group);
1221
1261
 
1262
+ if (series.z) {
1263
+ for (const [index, z] of series.z.data.entries()) {
1264
+
1265
+ const x = series.x.data[index];
1266
+ const y = series.y.data[index];
1267
+
1268
+ if (typeof x !== 'undefined' && typeof y !== 'undefined' && typeof z !== 'undefined' && z > 0) {
1269
+
1270
+ const size_x = z / xrange * area.width;
1271
+ const size_y = z / yrange * area.height;
1272
+ const size = Math.max(size_x, size_y);
1273
+
1274
+ const point: Point & { z: number } = {
1275
+ x: area.left + ((x - x_scale.min) / xrange) * area.width,
1276
+ y: area.bottom - ((y - y_scale.min) / yrange) * area.height,
1277
+ z: size,
1278
+ };
1279
+
1280
+ points.push(point);
1281
+
1282
+ if (series.labels?.[index]) {
1283
+ const r = point.z/2;
1284
+ labels.push({
1285
+ x: point.x, // + Math.cos(Math.PI/4) * r,
1286
+ y: point.y, // + Math.sin(Math.PI/4) * r,
1287
+ text: series.labels?.[index] || '',
1288
+ offset: Math.cos(Math.PI/4) * r,
1289
+ });
1290
+ }
1291
+
1292
+ }
1293
+
1294
+ }
1295
+ }
1296
+
1297
+ /*
1298
+
1222
1299
  let z_min = z[0] || 0;
1223
1300
  let z_max = z[0] || 0;
1224
1301
 
@@ -1262,13 +1339,59 @@ export class ChartRenderer {
1262
1339
 
1263
1340
  }
1264
1341
 
1265
- {
1266
- for (const point of points) {
1267
- if (point) {
1268
- group.appendChild(SVGNode('circle', {cx: point.x, cy: point.y, r: point.z / 2, class: `point series-${point.series}`}));
1269
- }
1342
+ */
1343
+
1344
+ for (const point of points) {
1345
+ if (point) {
1346
+ group.appendChild(SVGNode('circle', {
1347
+ cx: point.x,
1348
+ cy: point.y,
1349
+ r: point.z / 2,
1350
+ class: `point`,
1351
+ }));
1270
1352
  }
1353
+ }
1271
1354
 
1355
+ if (labels.length) {
1356
+ const container = this.label_group.getBoundingClientRect();
1357
+
1358
+ for (const entry of labels) {
1359
+ if (entry.text) {
1360
+
1361
+ const group = this.label_group.appendChild(SVGNode('g', {
1362
+ class: 'bubble-label',
1363
+ }));
1364
+
1365
+ const rect = group.appendChild(SVGNode('rect', {
1366
+ x: entry.x, // + entry.offset,
1367
+ y: entry.y, // + entry.offset,
1368
+ // rx: `3px`,
1369
+ // fill: 'Canvas',
1370
+ // 'fill-opacity': '60%',
1371
+ // stroke: `none`,
1372
+ // 'style': `--translate-offset: ${Math.round(entry.offset)}px`,
1373
+ class: 'label-background'
1374
+ }));
1375
+
1376
+ const label = group.appendChild(SVGNode('text', {
1377
+ x: entry.x, // + entry.offset,
1378
+ y: entry.y, // + entry.offset,
1379
+ offset: entry.offset,
1380
+ class: 'label-text',
1381
+ 'text-anchor': 'middle',
1382
+ 'alignment-baseline': 'middle',
1383
+ 'style': `--translate-offset: ${Math.round(entry.offset)}px`,
1384
+ }, entry.text));
1385
+
1386
+ const bounds = label.getBoundingClientRect();
1387
+
1388
+ rect.setAttribute('x', (bounds.left - container.left - 2).toString());
1389
+ rect.setAttribute('y', (bounds.top - container.top - 1).toString());
1390
+ rect.style.height = (bounds.height + 2) + `px`;
1391
+ rect.style.width = (bounds.width + 4) + `px`;
1392
+
1393
+ }
1394
+ }
1272
1395
  }
1273
1396
 
1274
1397
 
@@ -92,11 +92,23 @@
92
92
  rect {
93
93
  fill: currentColor;
94
94
  }
95
+ circle {
96
+ fill: currentColor;
97
+ fill-opacity: .5;
98
+ stroke: currentColor;
99
+ stroke-width: 2px;
100
+ r: .25em;
101
+ }
95
102
  }
96
103
 
97
104
  /* grid */
98
105
  .chart-grid, .chart-ticks {
99
106
  stroke: var(--treb-chart-grid-color, #ddd);
107
+
108
+ &.zero {
109
+ stroke: var(--treb-chart-grid-zero-color, var(--treb-chart-grid-color, #999));
110
+ }
111
+
100
112
  }
101
113
 
102
114
  /* mouse elements */
@@ -166,7 +178,31 @@
166
178
  stroke-width: 3;
167
179
  fill: color-mix(in srgb, currentColor 75%, transparent);
168
180
  stroke: currentColor;
169
-
181
+ }
182
+
183
+ .bubble-label {
184
+
185
+ .label-background {
186
+ stroke: none;
187
+ fill: none;
188
+
189
+ /*
190
+ fill: Canvas;
191
+ fill-opacity: .5;
192
+ rx: 2px;
193
+ */
194
+ }
195
+
196
+ .label-text {
197
+
198
+ /**
199
+ * default translate to lower-right. you can calc() to switch
200
+ * to a different position.
201
+ */
202
+ transform: translate(var(--translate-offset), var(--translate-offset));
203
+
204
+ }
205
+
170
206
  }
171
207
 
172
208
  /* scatter plot line (and marker -- change that class name) */
@@ -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
- this.parser.flags.r1c1 = r1c1;
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
  /*