@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
package/dist/treb.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! API v28.
|
|
1
|
+
/*! API v28.11. Copyright 2018-2024 trebco, llc. All rights reserved. LGPL: https://treb.app/license */
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* add our tag to the map
|
|
@@ -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) {
|
|
@@ -266,7 +310,7 @@ export const TransformSeriesData = (raw_data?: UnionValue, default_x?: UnionValu
|
|
|
266
310
|
};
|
|
267
311
|
|
|
268
312
|
/** get a unified scale, and formats */
|
|
269
|
-
export const CommonData = (series: SeriesType[], y_floor?: number, y_ceiling?: number) => {
|
|
313
|
+
export const CommonData = (series: SeriesType[], y_floor?: number, y_ceiling?: number, x_floor?: number, x_ceiling?: number) => {
|
|
270
314
|
|
|
271
315
|
let x_format = '';
|
|
272
316
|
let y_format = '';
|
|
@@ -285,13 +329,47 @@ 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
|
+
|
|
366
|
+
if (typeof x_floor !== 'undefined') {
|
|
367
|
+
x_min = Math.min(x_min, x_floor);
|
|
368
|
+
}
|
|
369
|
+
if (typeof x_ceiling !== 'undefined') {
|
|
370
|
+
x_min = Math.max(x_min, x_ceiling);
|
|
371
|
+
}
|
|
372
|
+
|
|
295
373
|
if (typeof y_floor !== 'undefined') {
|
|
296
374
|
y_min = Math.min(y_min, y_floor);
|
|
297
375
|
}
|
|
@@ -366,6 +444,46 @@ const ApplyLabels = (series_list: SeriesType[], pattern: string, category_labels
|
|
|
366
444
|
|
|
367
445
|
export const CreateBubbleChart = (args: UnionValue[]): ChartData => {
|
|
368
446
|
|
|
447
|
+
const series: SeriesType[] = TransformSeriesData(args[0]);
|
|
448
|
+
|
|
449
|
+
let y_floor: number|undefined = undefined;
|
|
450
|
+
let x_floor: number|undefined = undefined;
|
|
451
|
+
|
|
452
|
+
for (const entry of series) {
|
|
453
|
+
|
|
454
|
+
if (typeof entry.x.range?.min === 'number' && entry.x.range.min > 0 && entry.x.range.min < 50) {
|
|
455
|
+
x_floor = 0;
|
|
456
|
+
}
|
|
457
|
+
if (typeof entry.y.range?.min === 'number' && entry.y.range.min > 0 && entry.y.range.min < 50) {
|
|
458
|
+
y_floor = 0;
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
const common = CommonData(series, y_floor, undefined, x_floor);
|
|
463
|
+
const title = args[1]?.toString() || undefined;
|
|
464
|
+
const options = args[2]?.toString() || undefined;
|
|
465
|
+
|
|
466
|
+
// console.info({ series, common, title, options });
|
|
467
|
+
|
|
468
|
+
const chart_data: BubbleChartData = {
|
|
469
|
+
|
|
470
|
+
legend: common.legend,
|
|
471
|
+
legend_style: LegendStyle.bubble,
|
|
472
|
+
type: 'bubble',
|
|
473
|
+
series,
|
|
474
|
+
title,
|
|
475
|
+
|
|
476
|
+
x_scale: common.x.scale,
|
|
477
|
+
x_labels: common.x.labels,
|
|
478
|
+
|
|
479
|
+
y_scale: common.y.scale,
|
|
480
|
+
y_labels: common.y.labels,
|
|
481
|
+
|
|
482
|
+
};
|
|
483
|
+
|
|
484
|
+
return chart_data;
|
|
485
|
+
|
|
486
|
+
/*
|
|
369
487
|
const [x, y, z] = [0,1,2].map(index => {
|
|
370
488
|
const arg = args[index];
|
|
371
489
|
if (arg.type === ValueType.array) {
|
|
@@ -450,6 +568,8 @@ export const CreateBubbleChart = (args: UnionValue[]): ChartData => {
|
|
|
450
568
|
|
|
451
569
|
};
|
|
452
570
|
|
|
571
|
+
*/
|
|
572
|
+
|
|
453
573
|
};
|
|
454
574
|
|
|
455
575
|
/**
|
|
@@ -196,6 +196,8 @@ export class DefaultChartRenderer implements ChartRendererType {
|
|
|
196
196
|
|
|
197
197
|
// now do type-specific rendering
|
|
198
198
|
|
|
199
|
+
let zeros: number[]|undefined = [];
|
|
200
|
+
|
|
199
201
|
switch (chart_data.type) {
|
|
200
202
|
case 'scatter':
|
|
201
203
|
this.renderer.RenderPoints(area, chart_data.x, chart_data.y, 'mc mc-correlation series-1');
|
|
@@ -203,23 +205,21 @@ export class DefaultChartRenderer implements ChartRendererType {
|
|
|
203
205
|
|
|
204
206
|
case 'bubble':
|
|
205
207
|
|
|
208
|
+
if (chart_data.x_scale.min <= 0 && chart_data.x_scale.max >= 0) {
|
|
209
|
+
zeros[0] = Math.round(Math.abs(chart_data.x_scale.min / chart_data.x_scale.step));
|
|
210
|
+
}
|
|
211
|
+
if (chart_data.y_scale.min <= 0 && chart_data.y_scale.max >= 0) {
|
|
212
|
+
zeros[1] = Math.round(Math.abs(chart_data.y_scale.max / chart_data.y_scale.step));
|
|
213
|
+
}
|
|
214
|
+
|
|
206
215
|
this.renderer.RenderGrid(area,
|
|
207
216
|
chart_data.y_scale.count,
|
|
208
217
|
chart_data.x_scale.count + 1, // (sigh)
|
|
209
|
-
'chart-grid');
|
|
218
|
+
'chart-grid', zeros);
|
|
210
219
|
|
|
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
|
-
);
|
|
220
|
+
for (const [index, series] of chart_data.series.entries()) {
|
|
221
|
+
const series_index = (typeof series.index === 'number') ? series.index : index + 1;
|
|
222
|
+
this.renderer.RenderBubbleSeries(area, series, chart_data.x_scale, chart_data.y_scale, `bubble-chart series-${series_index}`);
|
|
223
223
|
}
|
|
224
224
|
|
|
225
225
|
break;
|