@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
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
|
@@ -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
|
|
|
@@ -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: '
|
|
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[
|
|
27
|
-
series.index = data[
|
|
36
|
+
if (data[4] && typeof data[4] === 'number') {
|
|
37
|
+
series.index = data[4];
|
|
28
38
|
}
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
|
|
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
|
-
|
|
289
|
-
|
|
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
|
-
|
|
212
|
-
|
|
213
|
-
|
|
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
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
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(
|
|
1197
|
-
|
|
1198
|
-
|
|
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
|
|
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
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
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) */
|