@trebco/treb 28.7.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 +14 -14
- package/dist/treb-spreadsheet.mjs +12 -12
- package/dist/treb.d.ts +39 -6
- package/notes/connected-elements.md +37 -0
- package/package.json +1 -1
- package/treb-base-types/src/gradient.ts +1 -1
- package/treb-base-types/src/localization.ts +6 -0
- package/treb-calculator/src/calculator.ts +72 -30
- package/treb-calculator/src/dag/calculation_leaf_vertex.ts +7 -0
- package/treb-calculator/src/dag/graph.ts +8 -0
- package/treb-calculator/src/functions/base-functions.ts +30 -1
- package/treb-calculator/src/index.ts +1 -1
- package/treb-charts/src/chart-functions.ts +14 -0
- package/treb-charts/src/chart-types.ts +25 -1
- package/treb-charts/src/chart-utils.ts +195 -9
- package/treb-charts/src/chart.ts +4 -0
- package/treb-charts/src/default-chart-renderer.ts +17 -1
- package/treb-charts/src/renderer.ts +182 -9
- package/treb-charts/style/charts.scss +39 -0
- package/treb-embed/markup/toolbar.html +35 -34
- package/treb-embed/src/custom-element/treb-global.ts +10 -2
- package/treb-embed/src/embedded-spreadsheet.ts +209 -106
- package/treb-embed/src/options.ts +7 -0
- package/treb-embed/style/layout.scss +4 -0
- package/treb-embed/style/toolbar.scss +37 -0
- package/treb-grid/src/index.ts +1 -1
- package/treb-grid/src/types/conditional_format.ts +1 -1
- package/treb-grid/src/types/data_model.ts +32 -0
- package/treb-grid/src/types/grid.ts +11 -1
- package/treb-grid/src/types/grid_base.ts +161 -5
- package/treb-grid/src/types/grid_command.ts +32 -0
- package/treb-grid/src/types/grid_events.ts +7 -0
- package/treb-grid/src/types/grid_options.ts +8 -0
- package/treb-grid/src/types/sheet.ts +0 -56
- package/treb-grid/src/types/update_flags.ts +1 -0
- package/treb-parser/src/parser-types.ts +6 -0
- package/treb-parser/src/parser.ts +48 -1
|
@@ -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
|
}
|
|
@@ -364,6 +435,121 @@ const ApplyLabels = (series_list: SeriesType[], pattern: string, category_labels
|
|
|
364
435
|
|
|
365
436
|
//------------------------------------------------------------------------------
|
|
366
437
|
|
|
438
|
+
export const CreateBubbleChart = (args: UnionValue[]): ChartData => {
|
|
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
|
+
/*
|
|
466
|
+
const [x, y, z] = [0,1,2].map(index => {
|
|
467
|
+
const arg = args[index];
|
|
468
|
+
if (arg.type === ValueType.array) {
|
|
469
|
+
return ArrayToSeries(arg).y;
|
|
470
|
+
}
|
|
471
|
+
return undefined;
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
let c: string[]|undefined = undefined;
|
|
475
|
+
if (Array.isArray(args[3])) {
|
|
476
|
+
c = Util.Flatten(args[3]).map(value => (value||'').toString());
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
const title = args[4]?.toString() || undefined;
|
|
480
|
+
|
|
481
|
+
// FIXME: need to pad out the axes by the values at the edges,
|
|
482
|
+
// so the whole circle is included in the chart area.
|
|
483
|
+
|
|
484
|
+
const [x_scale, y_scale] = [x, y].map(subseries => {
|
|
485
|
+
|
|
486
|
+
let series_min = 0;
|
|
487
|
+
let series_max = 1;
|
|
488
|
+
let first = false;
|
|
489
|
+
|
|
490
|
+
if (subseries?.data) {
|
|
491
|
+
|
|
492
|
+
for (const [index, value] of subseries.data.entries()) {
|
|
493
|
+
if (typeof value === 'number') {
|
|
494
|
+
|
|
495
|
+
if (!first) {
|
|
496
|
+
first = true;
|
|
497
|
+
series_min = value;
|
|
498
|
+
series_max = value;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
const size = (z?.data?.[index]) || 0;
|
|
502
|
+
series_min = Math.min(series_min, value - size / 2);
|
|
503
|
+
series_max = Math.max(series_max, value + size / 2);
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
return Util.Scale(series_min, series_max, 7);
|
|
509
|
+
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
let x_labels: string[] | undefined;
|
|
513
|
+
let y_labels: string[] | undefined;
|
|
514
|
+
|
|
515
|
+
if (x?.format) {
|
|
516
|
+
x_labels = [];
|
|
517
|
+
const format = NumberFormatCache.Get(x.format);
|
|
518
|
+
for (let i = 0; i <= x_scale.count; i++) {
|
|
519
|
+
x_labels.push(format.Format(x_scale.min + i * x_scale.step));
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
if (y?.format) {
|
|
524
|
+
y_labels = [];
|
|
525
|
+
const format = NumberFormatCache.Get(y.format);
|
|
526
|
+
for (let i = 0; i <= y_scale.count; i++) {
|
|
527
|
+
y_labels.push(format.Format(y_scale.min + i * y_scale.step));
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
return {
|
|
532
|
+
|
|
533
|
+
type: 'bubble',
|
|
534
|
+
|
|
535
|
+
title,
|
|
536
|
+
|
|
537
|
+
x,
|
|
538
|
+
y,
|
|
539
|
+
z,
|
|
540
|
+
c,
|
|
541
|
+
|
|
542
|
+
x_scale,
|
|
543
|
+
y_scale,
|
|
544
|
+
|
|
545
|
+
x_labels,
|
|
546
|
+
y_labels,
|
|
547
|
+
|
|
548
|
+
};
|
|
549
|
+
|
|
550
|
+
*/
|
|
551
|
+
|
|
552
|
+
};
|
|
367
553
|
|
|
368
554
|
/**
|
|
369
555
|
* args is [data, title, options]
|
package/treb-charts/src/chart.ts
CHANGED
|
@@ -95,6 +95,7 @@ export class DefaultChartRenderer implements ChartRendererType {
|
|
|
95
95
|
|| chart_data.type === 'histogram2'
|
|
96
96
|
|| chart_data.type === 'bar'
|
|
97
97
|
|| chart_data.type === 'scatter2'
|
|
98
|
+
|| chart_data.type === 'bubble'
|
|
98
99
|
) {
|
|
99
100
|
|
|
100
101
|
// we need to measure first, then lay out the other axis, then we
|
|
@@ -122,7 +123,7 @@ export class DefaultChartRenderer implements ChartRendererType {
|
|
|
122
123
|
let max_width = 0;
|
|
123
124
|
let max_height = 0;
|
|
124
125
|
|
|
125
|
-
const scale = (chart_data.type === 'scatter2') ? chart_data.y_scale : chart_data.scale;
|
|
126
|
+
const scale = (chart_data.type === 'scatter2' || chart_data.type === 'bubble') ? chart_data.y_scale : chart_data.scale;
|
|
126
127
|
|
|
127
128
|
const count = (chart_data.type === 'bar') ?
|
|
128
129
|
chart_data.y_labels.length :
|
|
@@ -163,6 +164,7 @@ export class DefaultChartRenderer implements ChartRendererType {
|
|
|
163
164
|
chart_data.type !== 'area' &&
|
|
164
165
|
chart_data.type !== 'bar' &&
|
|
165
166
|
chart_data.type !== 'scatter2' &&
|
|
167
|
+
chart_data.type !== 'bubble' &&
|
|
166
168
|
chart_data.type !== 'histogram2'
|
|
167
169
|
);
|
|
168
170
|
|
|
@@ -199,6 +201,20 @@ export class DefaultChartRenderer implements ChartRendererType {
|
|
|
199
201
|
this.renderer.RenderPoints(area, chart_data.x, chart_data.y, 'mc mc-correlation series-1');
|
|
200
202
|
break;
|
|
201
203
|
|
|
204
|
+
case 'bubble':
|
|
205
|
+
|
|
206
|
+
this.renderer.RenderGrid(area,
|
|
207
|
+
chart_data.y_scale.count,
|
|
208
|
+
chart_data.x_scale.count + 1, // (sigh)
|
|
209
|
+
'chart-grid');
|
|
210
|
+
|
|
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}`);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
break;
|
|
217
|
+
|
|
202
218
|
case 'scatter2':
|
|
203
219
|
|
|
204
220
|
this.renderer.RenderGrid(area,
|
|
@@ -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,6 +1201,171 @@ export class ChartRenderer {
|
|
|
1193
1201
|
|
|
1194
1202
|
}
|
|
1195
1203
|
|
|
1204
|
+
public RenderBubbleSeries(
|
|
1205
|
+
area: Area,
|
|
1206
|
+
series: SeriesType,
|
|
1207
|
+
x_scale: RangeScale,
|
|
1208
|
+
y_scale: RangeScale,
|
|
1209
|
+
classes?: string | string[]): void {
|
|
1210
|
+
|
|
1211
|
+
const xrange = (x_scale.max - x_scale.min) || 1;
|
|
1212
|
+
const yrange = (y_scale.max - y_scale.min) || 1;
|
|
1213
|
+
|
|
1214
|
+
// const marker_elements: string[] = [];
|
|
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
|
+
}> = [];
|
|
1222
|
+
|
|
1223
|
+
const d: string[] = [];
|
|
1224
|
+
const areas: string[] = [];
|
|
1225
|
+
|
|
1226
|
+
const group = SVGNode('g', {class: classes});
|
|
1227
|
+
|
|
1228
|
+
// if (title) node.setAttribute('title', title);
|
|
1229
|
+
this.group.appendChild(group);
|
|
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
|
+
|
|
1268
|
+
let z_min = z[0] || 0;
|
|
1269
|
+
let z_max = z[0] || 0;
|
|
1270
|
+
|
|
1271
|
+
const map: Map<string, number> = new Map();
|
|
1272
|
+
|
|
1273
|
+
for (let i = 0; i < count; i++) {
|
|
1274
|
+
|
|
1275
|
+
const a = x[i];
|
|
1276
|
+
const b = y[i];
|
|
1277
|
+
|
|
1278
|
+
if (typeof a === 'undefined' || typeof b === 'undefined') {
|
|
1279
|
+
points.push(undefined);
|
|
1280
|
+
}
|
|
1281
|
+
else {
|
|
1282
|
+
|
|
1283
|
+
const series_key = c[i] || '';
|
|
1284
|
+
let series = map.get(series_key);
|
|
1285
|
+
|
|
1286
|
+
if (typeof series === 'undefined') {
|
|
1287
|
+
|
|
1288
|
+
series = map.size + 1;
|
|
1289
|
+
|
|
1290
|
+
map.set(series_key, series);
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1293
|
+
let size = z[i] || 0;
|
|
1294
|
+
if (size) {
|
|
1295
|
+
const size_x = size / xrange * area.width;
|
|
1296
|
+
const size_y = size / yrange * area.height;
|
|
1297
|
+
size = Math.min(size_x, size_y);
|
|
1298
|
+
}
|
|
1299
|
+
|
|
1300
|
+
points.push({
|
|
1301
|
+
x: area.left + ((a - x_scale.min) / xrange) * area.width,
|
|
1302
|
+
y: area.bottom - ((b - y_scale.min) / yrange) * area.height,
|
|
1303
|
+
z: size,
|
|
1304
|
+
series,
|
|
1305
|
+
});
|
|
1306
|
+
|
|
1307
|
+
}
|
|
1308
|
+
|
|
1309
|
+
}
|
|
1310
|
+
|
|
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
|
+
}));
|
|
1321
|
+
}
|
|
1322
|
+
}
|
|
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
|
+
}
|
|
1364
|
+
}
|
|
1365
|
+
|
|
1366
|
+
|
|
1367
|
+
}
|
|
1368
|
+
|
|
1196
1369
|
public RenderScatterSeries(area: Area,
|
|
1197
1370
|
x: Array<number | undefined>,
|
|
1198
1371
|
y: Array<number | undefined>,
|
|
@@ -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 */
|
|
@@ -161,6 +168,38 @@
|
|
|
161
168
|
}
|
|
162
169
|
}
|
|
163
170
|
|
|
171
|
+
.bubble-chart {
|
|
172
|
+
|
|
173
|
+
stroke-width: 3;
|
|
174
|
+
fill: color-mix(in srgb, currentColor 75%, transparent);
|
|
175
|
+
stroke: currentColor;
|
|
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
|
+
|
|
201
|
+
}
|
|
202
|
+
|
|
164
203
|
/* scatter plot line (and marker -- change that class name) */
|
|
165
204
|
.scatter-plot {
|
|
166
205
|
|
|
@@ -65,6 +65,27 @@
|
|
|
65
65
|
</div>
|
|
66
66
|
</div>
|
|
67
67
|
|
|
68
|
+
<div composite>
|
|
69
|
+
<button data-command="border-bottom" data-target="border" title="Bottom border"></button>
|
|
70
|
+
<div class="treb-menu">
|
|
71
|
+
<button dropdown title="Border options"></button>
|
|
72
|
+
<div class="treb-icon-buttons" data-replace="border">
|
|
73
|
+
<button data-command="border-top" title="Top border"></button>
|
|
74
|
+
<button data-command="border-left" title="Left border"></button>
|
|
75
|
+
<button data-command="border-right" title="Right border"></button>
|
|
76
|
+
<button data-command="border-bottom" title="Bottom border"></button>
|
|
77
|
+
<button data-command="border-double-bottom" title="Double bottom border"></button>
|
|
78
|
+
<button data-command="border-outside" title="Outside borders"></button>
|
|
79
|
+
<button data-command="border-all" title="All borders"></button>
|
|
80
|
+
<button data-command="border-none" title="Clear borders"></button>
|
|
81
|
+
<div separator></div>
|
|
82
|
+
<div class="treb-menu treb-color-menu treb-submenu" data-color-command="border-color" data-replace-color="border" title="Border color" data-default-color-text="Default border color">
|
|
83
|
+
<button data-icon="palette" data-color-bar="border" data-color="{}"></button>
|
|
84
|
+
</div>
|
|
85
|
+
</div>
|
|
86
|
+
</div>
|
|
87
|
+
</div>
|
|
88
|
+
|
|
68
89
|
<div composite>
|
|
69
90
|
<button data-command="fill-color" data-color-bar="fill" data-color="{}" title="Fill color"></button>
|
|
70
91
|
<div class="treb-menu treb-color-menu" data-color-command="fill-color" data-replace-color="fill" data-default-color-text="No fill">
|
|
@@ -94,28 +115,8 @@
|
|
|
94
115
|
</div>
|
|
95
116
|
</div>
|
|
96
117
|
|
|
97
|
-
<div composite>
|
|
98
|
-
<button data-command="border-bottom" data-target="border" title="Bottom border"></button>
|
|
99
|
-
<div class="treb-menu">
|
|
100
|
-
<button dropdown title="Border options"></button>
|
|
101
|
-
<div class="treb-icon-buttons" data-replace="border">
|
|
102
|
-
<button data-command="border-top" title="Top border"></button>
|
|
103
|
-
<button data-command="border-left" title="Left border"></button>
|
|
104
|
-
<button data-command="border-right" title="Right border"></button>
|
|
105
|
-
<button data-command="border-bottom" title="Bottom border"></button>
|
|
106
|
-
<button data-command="border-double-bottom" title="Double bottom border"></button>
|
|
107
|
-
<button data-command="border-outside" title="Outside borders"></button>
|
|
108
|
-
<button data-command="border-all" title="All borders"></button>
|
|
109
|
-
<button data-command="border-none" title="Clear borders"></button>
|
|
110
|
-
<div separator></div>
|
|
111
|
-
<div class="treb-menu treb-color-menu treb-submenu" data-color-command="border-color" data-replace-color="border" title="Border color" data-default-color-text="Default border color">
|
|
112
|
-
<button data-icon="palette" data-color-bar="border" data-color="{}"></button>
|
|
113
|
-
</div>
|
|
114
|
-
</div>
|
|
115
|
-
</div>
|
|
116
|
-
</div>
|
|
117
|
-
|
|
118
118
|
<div composite font-scale>
|
|
119
|
+
<div class="treb-font-scale-icon"></div>
|
|
119
120
|
<input class="treb-font-scale" title="Font scale">
|
|
120
121
|
<div class="treb-menu">
|
|
121
122
|
<button dropdown title="Font scale options"></button>
|
|
@@ -131,19 +132,6 @@
|
|
|
131
132
|
</div>
|
|
132
133
|
</div>
|
|
133
134
|
|
|
134
|
-
<div class="treb-menu">
|
|
135
|
-
<button data-icon="layout" title="Rows & columns"></button>
|
|
136
|
-
<div>
|
|
137
|
-
<button data-command="insert-row">Insert row</button>
|
|
138
|
-
<button data-command="insert-column">Insert column</button>
|
|
139
|
-
<button data-command="delete-row">Delete row</button>
|
|
140
|
-
<button data-command="delete-column">Delete column</button>
|
|
141
|
-
<div separator add-remove-sheet></div>
|
|
142
|
-
<button data-command="insert-sheet" add-remove-sheet>Add sheet</button>
|
|
143
|
-
<button data-command="delete-sheet" add-remove-sheet>Delete sheet</button>
|
|
144
|
-
</div>
|
|
145
|
-
</div>
|
|
146
|
-
|
|
147
135
|
<div composite>
|
|
148
136
|
<input class="treb-number-format" title="Number format">
|
|
149
137
|
<div class="treb-menu">
|
|
@@ -157,6 +145,19 @@
|
|
|
157
145
|
<button data-command="increase-precision" title="Increase precision"></button>
|
|
158
146
|
</div>
|
|
159
147
|
|
|
148
|
+
<div class="treb-menu">
|
|
149
|
+
<button data-icon="layout" title="Rows & columns"></button>
|
|
150
|
+
<div>
|
|
151
|
+
<button data-command="insert-row">Insert row</button>
|
|
152
|
+
<button data-command="insert-column">Insert column</button>
|
|
153
|
+
<button data-command="delete-row">Delete row</button>
|
|
154
|
+
<button data-command="delete-column">Delete column</button>
|
|
155
|
+
<div separator add-remove-sheet></div>
|
|
156
|
+
<button data-command="insert-sheet" add-remove-sheet>Add sheet</button>
|
|
157
|
+
<button data-command="delete-sheet" add-remove-sheet>Delete sheet</button>
|
|
158
|
+
</div>
|
|
159
|
+
</div>
|
|
160
|
+
|
|
160
161
|
<div composite chart-menu>
|
|
161
162
|
<button data-command="insert-column-chart" data-target="annotation" title="Insert column chart"></button>
|
|
162
163
|
<div class="treb-menu">
|