@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.d.ts CHANGED
@@ -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.10.5",
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) {
@@ -285,13 +329,40 @@ 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
+
295
366
  if (typeof y_floor !== 'undefined') {
296
367
  y_min = Math.min(y_min, y_floor);
297
368
  }
@@ -366,6 +437,32 @@ const ApplyLabels = (series_list: SeriesType[], pattern: string, category_labels
366
437
 
367
438
  export const CreateBubbleChart = (args: UnionValue[]): ChartData => {
368
439
 
440
+ const series: SeriesType[] = TransformSeriesData(args[0]);
441
+ const common = CommonData(series);
442
+ const title = args[1]?.toString() || undefined;
443
+ const options = args[2]?.toString() || undefined;
444
+
445
+ // console.info({ series, common, title, options });
446
+
447
+ const chart_data: BubbleChartData = {
448
+
449
+ legend: common.legend,
450
+ legend_style: LegendStyle.bubble,
451
+ type: 'bubble',
452
+ series,
453
+ title,
454
+
455
+ x_scale: common.x.scale,
456
+ x_labels: common.x.labels,
457
+
458
+ y_scale: common.y.scale,
459
+ y_labels: common.y.labels,
460
+
461
+ };
462
+
463
+ return chart_data;
464
+
465
+ /*
369
466
  const [x, y, z] = [0,1,2].map(index => {
370
467
  const arg = args[index];
371
468
  if (arg.type === ValueType.array) {
@@ -450,6 +547,8 @@ export const CreateBubbleChart = (args: UnionValue[]): ChartData => {
450
547
 
451
548
  };
452
549
 
550
+ */
551
+
453
552
  };
454
553
 
455
554
  /**
@@ -208,18 +208,9 @@ export class DefaultChartRenderer implements ChartRendererType {
208
208
  chart_data.x_scale.count + 1, // (sigh)
209
209
  'chart-grid');
210
210
 
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
- );
211
+ for (const [index, series] of chart_data.series.entries()) {
212
+ const series_index = (typeof series.index === 'number') ? series.index : index + 1;
213
+ this.renderer.RenderBubbleSeries(area, series, chart_data.x_scale, chart_data.y_scale, `bubble-chart series-${series_index}`);
223
214
  }
224
215
 
225
216
  break;
@@ -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;
@@ -1193,23 +1201,24 @@ export class ChartRenderer {
1193
1201
 
1194
1202
  }
1195
1203
 
1196
- public RenderBubbleSeries(area: Area,
1197
- x: Array<number | undefined>,
1198
- y: Array<number | undefined>,
1199
- z: Array<number | undefined>,
1200
- c: any[] = [],
1204
+ public RenderBubbleSeries(
1205
+ area: Area,
1206
+ series: SeriesType,
1201
1207
  x_scale: RangeScale,
1202
1208
  y_scale: RangeScale,
1203
- min = 10,
1204
- max = 30,
1205
1209
  classes?: string | string[]): void {
1206
1210
 
1207
- const count = Math.max(x.length, y.length, z.length);
1208
1211
  const xrange = (x_scale.max - x_scale.min) || 1;
1209
1212
  const yrange = (y_scale.max - y_scale.min) || 1;
1210
1213
 
1211
1214
  // const marker_elements: string[] = [];
1212
- const points: Array<{x: number, y: number, z: number, series: number} | undefined> = [];
1215
+ const points: Array<{x: number, y: number, z: number} | undefined> = [];
1216
+ const labels: Array<{
1217
+ x: number,
1218
+ y: number,
1219
+ text: string,
1220
+ offset: number,
1221
+ }> = [];
1213
1222
 
1214
1223
  const d: string[] = [];
1215
1224
  const areas: string[] = [];
@@ -1219,6 +1228,43 @@ export class ChartRenderer {
1219
1228
  // if (title) node.setAttribute('title', title);
1220
1229
  this.group.appendChild(group);
1221
1230
 
1231
+ if (series.z) {
1232
+ for (const [index, z] of series.z.data.entries()) {
1233
+
1234
+ const x = series.x.data[index];
1235
+ const y = series.y.data[index];
1236
+
1237
+ if (typeof x !== 'undefined' && typeof y !== 'undefined' && typeof z !== 'undefined' && z > 0) {
1238
+
1239
+ const size_x = z / xrange * area.width;
1240
+ const size_y = z / yrange * area.height;
1241
+ const size = Math.max(size_x, size_y);
1242
+
1243
+ const point: Point & { z: number } = {
1244
+ x: area.left + ((x - x_scale.min) / xrange) * area.width,
1245
+ y: area.bottom - ((y - y_scale.min) / yrange) * area.height,
1246
+ z: size,
1247
+ };
1248
+
1249
+ points.push(point);
1250
+
1251
+ if (series.labels?.[index]) {
1252
+ const r = point.z/2;
1253
+ labels.push({
1254
+ x: point.x, // + Math.cos(Math.PI/4) * r,
1255
+ y: point.y, // + Math.sin(Math.PI/4) * r,
1256
+ text: series.labels?.[index] || '',
1257
+ offset: Math.cos(Math.PI/4) * r,
1258
+ });
1259
+ }
1260
+
1261
+ }
1262
+
1263
+ }
1264
+ }
1265
+
1266
+ /*
1267
+
1222
1268
  let z_min = z[0] || 0;
1223
1269
  let z_max = z[0] || 0;
1224
1270
 
@@ -1262,13 +1308,59 @@ export class ChartRenderer {
1262
1308
 
1263
1309
  }
1264
1310
 
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
- }
1311
+ */
1312
+
1313
+ for (const point of points) {
1314
+ if (point) {
1315
+ group.appendChild(SVGNode('circle', {
1316
+ cx: point.x,
1317
+ cy: point.y,
1318
+ r: point.z / 2,
1319
+ class: `point`,
1320
+ }));
1270
1321
  }
1322
+ }
1271
1323
 
1324
+ if (labels.length) {
1325
+ const container = this.label_group.getBoundingClientRect();
1326
+
1327
+ for (const entry of labels) {
1328
+ if (entry.text) {
1329
+
1330
+ const group = this.label_group.appendChild(SVGNode('g', {
1331
+ class: 'bubble-label',
1332
+ }));
1333
+
1334
+ const rect = group.appendChild(SVGNode('rect', {
1335
+ x: entry.x, // + entry.offset,
1336
+ y: entry.y, // + entry.offset,
1337
+ // rx: `3px`,
1338
+ // fill: 'Canvas',
1339
+ // 'fill-opacity': '60%',
1340
+ // stroke: `none`,
1341
+ // 'style': `--translate-offset: ${Math.round(entry.offset)}px`,
1342
+ class: 'label-background'
1343
+ }));
1344
+
1345
+ const label = group.appendChild(SVGNode('text', {
1346
+ x: entry.x, // + entry.offset,
1347
+ y: entry.y, // + entry.offset,
1348
+ offset: entry.offset,
1349
+ class: 'label-text',
1350
+ 'text-anchor': 'middle',
1351
+ 'alignment-baseline': 'middle',
1352
+ 'style': `--translate-offset: ${Math.round(entry.offset)}px`,
1353
+ }, entry.text));
1354
+
1355
+ const bounds = label.getBoundingClientRect();
1356
+
1357
+ rect.setAttribute('x', (bounds.left - container.left - 2).toString());
1358
+ rect.setAttribute('y', (bounds.top - container.top - 1).toString());
1359
+ rect.style.height = (bounds.height + 2) + `px`;
1360
+ rect.style.width = (bounds.width + 4) + `px`;
1361
+
1362
+ }
1363
+ }
1272
1364
  }
1273
1365
 
1274
1366
 
@@ -92,6 +92,13 @@
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 */
@@ -166,7 +173,31 @@
166
173
  stroke-width: 3;
167
174
  fill: color-mix(in srgb, currentColor 75%, transparent);
168
175
  stroke: currentColor;
169
-
176
+ }
177
+
178
+ .bubble-label {
179
+
180
+ .label-background {
181
+ stroke: none;
182
+ fill: none;
183
+
184
+ /*
185
+ fill: Canvas;
186
+ fill-opacity: .5;
187
+ rx: 2px;
188
+ */
189
+ }
190
+
191
+ .label-text {
192
+
193
+ /**
194
+ * default translate to lower-right. you can calc() to switch
195
+ * to a different position.
196
+ */
197
+ transform: translate(var(--translate-offset), var(--translate-offset));
198
+
199
+ }
200
+
170
201
  }
171
202
 
172
203
  /* scatter plot line (and marker -- change that class name) */