@internetstiftelsen/charts 0.9.2 → 0.10.0
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/README.md +136 -2
- package/dist/bar.d.ts +3 -1
- package/dist/bar.js +167 -327
- package/dist/base-chart.d.ts +16 -1
- package/dist/base-chart.js +89 -30
- package/dist/chart-group.d.ts +121 -0
- package/dist/chart-group.js +1097 -0
- package/dist/chart-interface.d.ts +1 -1
- package/dist/donut-chart.js +1 -1
- package/dist/gauge-chart.js +1 -1
- package/dist/legend-state.d.ts +19 -0
- package/dist/legend-state.js +81 -0
- package/dist/legend.d.ts +5 -2
- package/dist/legend.js +35 -29
- package/dist/pie-chart.js +1 -1
- package/dist/scatter.d.ts +16 -0
- package/dist/scatter.js +163 -0
- package/dist/tooltip.d.ts +2 -1
- package/dist/tooltip.js +3 -3
- package/dist/types.d.ts +16 -0
- package/dist/validation.d.ts +4 -0
- package/dist/validation.js +19 -0
- package/dist/xy-chart.d.ts +16 -1
- package/dist/xy-chart.js +317 -102
- package/docs/chart-group.md +213 -0
- package/docs/components.md +308 -0
- package/docs/donut-chart.md +193 -0
- package/docs/gauge-chart.md +175 -0
- package/docs/getting-started.md +311 -0
- package/docs/pie-chart.md +123 -0
- package/docs/theming.md +162 -0
- package/docs/word-cloud-chart.md +98 -0
- package/docs/xy-chart.md +502 -0
- package/package.json +2 -1
package/dist/xy-chart.js
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import { max, min, scaleBand, scaleLinear, scaleLog, scaleTime, } from 'd3';
|
|
2
2
|
import { BaseChart, } from './base-chart.js';
|
|
3
|
-
import { ChartValidator } from './validation.js';
|
|
3
|
+
import { ChartValidationError, ChartValidator } from './validation.js';
|
|
4
4
|
import { GROUPED_GAP_TICK_PREFIX, GROUPED_GROUP_LABEL_KEY, } from './grouped-data.js';
|
|
5
5
|
import { resolveScaleValue } from './scale-utils.js';
|
|
6
|
+
import { mergeDeep } from './utils.js';
|
|
6
7
|
const DEFAULT_SERIES_COLOR = '#8884d8';
|
|
7
8
|
function isXYSeries(component) {
|
|
8
9
|
return (component.type === 'line' ||
|
|
10
|
+
component.type === 'scatter' ||
|
|
9
11
|
component.type === 'bar' ||
|
|
10
12
|
component.type === 'area');
|
|
11
13
|
}
|
|
@@ -48,6 +50,12 @@ export class XYChart extends BaseChart {
|
|
|
48
50
|
writable: true,
|
|
49
51
|
value: void 0
|
|
50
52
|
});
|
|
53
|
+
Object.defineProperty(this, "scaleConfigOverride", {
|
|
54
|
+
enumerable: true,
|
|
55
|
+
configurable: true,
|
|
56
|
+
writable: true,
|
|
57
|
+
value: null
|
|
58
|
+
});
|
|
51
59
|
this.orientation = config.orientation ?? 'vertical';
|
|
52
60
|
this.barStackMode = config.barStack?.mode ?? 'normal';
|
|
53
61
|
this.barStackGap = config.barStack?.gap ?? 0.1;
|
|
@@ -82,7 +90,7 @@ export class XYChart extends BaseChart {
|
|
|
82
90
|
width: this.configuredWidth,
|
|
83
91
|
height: this.configuredHeight,
|
|
84
92
|
theme: this.theme,
|
|
85
|
-
scales: this.
|
|
93
|
+
scales: this.resolvedScaleConfig,
|
|
86
94
|
responsive: this.responsiveConfig,
|
|
87
95
|
orientation: this.orientation,
|
|
88
96
|
barStack: {
|
|
@@ -121,22 +129,14 @@ export class XYChart extends BaseChart {
|
|
|
121
129
|
renderChart({ svg, plotGroup, plotArea, }) {
|
|
122
130
|
this.validateSeriesOrientation();
|
|
123
131
|
this.series.forEach((series) => {
|
|
124
|
-
const typeName = series
|
|
125
|
-
? 'Line'
|
|
126
|
-
: series.type === 'bar'
|
|
127
|
-
? 'Bar'
|
|
128
|
-
: 'Area';
|
|
132
|
+
const typeName = this.getSeriesTypeName(series);
|
|
129
133
|
ChartValidator.validateDataKey(this.data, series.dataKey, typeName);
|
|
130
134
|
ChartValidator.validateNumericData(this.data, series.dataKey, typeName);
|
|
131
135
|
});
|
|
132
|
-
const valueScaleType = this.
|
|
136
|
+
const valueScaleType = this.resolvedScaleConfig.y?.type ?? 'linear';
|
|
133
137
|
if (valueScaleType === 'log') {
|
|
134
138
|
this.series.forEach((series) => {
|
|
135
|
-
const typeName = series
|
|
136
|
-
? 'Line'
|
|
137
|
-
: series.type === 'bar'
|
|
138
|
-
? 'Bar'
|
|
139
|
-
: 'Area';
|
|
139
|
+
const typeName = this.getSeriesTypeName(series);
|
|
140
140
|
ChartValidator.validatePositiveData(this.data, series.dataKey, typeName);
|
|
141
141
|
});
|
|
142
142
|
}
|
|
@@ -177,7 +177,7 @@ export class XYChart extends BaseChart {
|
|
|
177
177
|
getLegendSeries() {
|
|
178
178
|
const displaySeries = this.getDisplaySeries();
|
|
179
179
|
return displaySeries.map((series) => {
|
|
180
|
-
if (series.type === 'line') {
|
|
180
|
+
if (series.type === 'line' || series.type === 'scatter') {
|
|
181
181
|
return {
|
|
182
182
|
dataKey: series.dataKey,
|
|
183
183
|
stroke: series.stroke,
|
|
@@ -190,7 +190,45 @@ export class XYChart extends BaseChart {
|
|
|
190
190
|
});
|
|
191
191
|
}
|
|
192
192
|
getCategoryScaleType() {
|
|
193
|
-
return this.
|
|
193
|
+
return this.resolvedScaleConfig.x?.type || 'band';
|
|
194
|
+
}
|
|
195
|
+
getOrientation() {
|
|
196
|
+
return this.orientation;
|
|
197
|
+
}
|
|
198
|
+
getValueAxisScaleType() {
|
|
199
|
+
if (this.orientation !== 'vertical') {
|
|
200
|
+
return null;
|
|
201
|
+
}
|
|
202
|
+
return this.resolvedScaleConfig.y?.type ?? 'linear';
|
|
203
|
+
}
|
|
204
|
+
getValueAxisDomain() {
|
|
205
|
+
return this.resolveValueAxisDomain(this.resolvedScaleConfig);
|
|
206
|
+
}
|
|
207
|
+
getBaseValueAxisDomain() {
|
|
208
|
+
return this.resolveValueAxisDomain(this.scaleConfig);
|
|
209
|
+
}
|
|
210
|
+
setScaleConfigOverride(override, rerender = true) {
|
|
211
|
+
this.scaleConfigOverride = override;
|
|
212
|
+
if (rerender) {
|
|
213
|
+
this.rerender();
|
|
214
|
+
}
|
|
215
|
+
return this;
|
|
216
|
+
}
|
|
217
|
+
resolveValueAxisDomain(scaleConfig) {
|
|
218
|
+
if (this.orientation !== 'vertical') {
|
|
219
|
+
return null;
|
|
220
|
+
}
|
|
221
|
+
const yConfig = this.getAxisConfigsForScaleConfig(scaleConfig).y;
|
|
222
|
+
if (yConfig.type !== 'linear' && yConfig.type !== 'log') {
|
|
223
|
+
return null;
|
|
224
|
+
}
|
|
225
|
+
const domain = this.resolveScaleDomain(yConfig, null);
|
|
226
|
+
if (domain.length < 2 ||
|
|
227
|
+
typeof domain[0] !== 'number' ||
|
|
228
|
+
typeof domain[1] !== 'number') {
|
|
229
|
+
return null;
|
|
230
|
+
}
|
|
231
|
+
return [domain[0], domain[1]];
|
|
194
232
|
}
|
|
195
233
|
getVisibleSeries() {
|
|
196
234
|
return this.filterVisibleItems(this.getDisplaySeries(), (series) => {
|
|
@@ -228,6 +266,13 @@ export class XYChart extends BaseChart {
|
|
|
228
266
|
: series.stroke,
|
|
229
267
|
});
|
|
230
268
|
}
|
|
269
|
+
if (series.type === 'scatter') {
|
|
270
|
+
return this.cloneSeriesWithOverride(series, {
|
|
271
|
+
stroke: this.shouldReplaceSeriesColor(series.stroke)
|
|
272
|
+
? paletteColor
|
|
273
|
+
: series.stroke,
|
|
274
|
+
});
|
|
275
|
+
}
|
|
231
276
|
if (series.type === 'bar') {
|
|
232
277
|
return this.cloneSeriesWithOverride(series, {
|
|
233
278
|
fill: this.shouldReplaceSeriesColor(series.fill)
|
|
@@ -253,23 +298,59 @@ export class XYChart extends BaseChart {
|
|
|
253
298
|
}
|
|
254
299
|
setupScales() {
|
|
255
300
|
const xKey = this.getXKey();
|
|
256
|
-
const isHorizontal = this.
|
|
257
|
-
|
|
258
|
-
let yConfig;
|
|
259
|
-
if (isHorizontal) {
|
|
260
|
-
xConfig = this.scaleConfig.y || { type: 'linear' };
|
|
261
|
-
yConfig = this.scaleConfig.x || { type: 'band' };
|
|
262
|
-
}
|
|
263
|
-
else {
|
|
264
|
-
xConfig = this.scaleConfig.x || { type: 'band' };
|
|
265
|
-
yConfig = this.scaleConfig.y || { type: 'linear' };
|
|
266
|
-
}
|
|
301
|
+
const isHorizontal = this.isHorizontalOrientation();
|
|
302
|
+
const { x: xConfig, y: yConfig } = this.getResolvedAxisConfigs();
|
|
267
303
|
this.x = this.createScale(xConfig, isHorizontal ? null : xKey, 'x');
|
|
268
304
|
this.y = this.createScale(yConfig, isHorizontal ? xKey : null, 'y');
|
|
269
305
|
}
|
|
306
|
+
get resolvedScaleConfig() {
|
|
307
|
+
if (!this.scaleConfigOverride) {
|
|
308
|
+
return this.scaleConfig;
|
|
309
|
+
}
|
|
310
|
+
return mergeDeep(this.scaleConfig, this.scaleConfigOverride);
|
|
311
|
+
}
|
|
312
|
+
getResolvedAxisConfigs() {
|
|
313
|
+
return this.getAxisConfigsForScaleConfig(this.resolvedScaleConfig);
|
|
314
|
+
}
|
|
315
|
+
getAxisConfigsForScaleConfig(scaleConfig) {
|
|
316
|
+
if (this.isHorizontalOrientation()) {
|
|
317
|
+
return {
|
|
318
|
+
x: {
|
|
319
|
+
type: 'linear',
|
|
320
|
+
...(scaleConfig.y ?? {}),
|
|
321
|
+
},
|
|
322
|
+
y: {
|
|
323
|
+
type: 'band',
|
|
324
|
+
...(scaleConfig.x ?? {}),
|
|
325
|
+
},
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
return {
|
|
329
|
+
x: {
|
|
330
|
+
type: 'band',
|
|
331
|
+
...(scaleConfig.x ?? {}),
|
|
332
|
+
},
|
|
333
|
+
y: {
|
|
334
|
+
type: 'linear',
|
|
335
|
+
...(scaleConfig.y ?? {}),
|
|
336
|
+
},
|
|
337
|
+
};
|
|
338
|
+
}
|
|
270
339
|
isHorizontalOrientation() {
|
|
271
340
|
return this.orientation === 'horizontal';
|
|
272
341
|
}
|
|
342
|
+
getSeriesTypeName(series) {
|
|
343
|
+
switch (series.type) {
|
|
344
|
+
case 'line':
|
|
345
|
+
return 'Line';
|
|
346
|
+
case 'scatter':
|
|
347
|
+
return 'Scatter';
|
|
348
|
+
case 'bar':
|
|
349
|
+
return 'Bar';
|
|
350
|
+
case 'area':
|
|
351
|
+
return 'Area';
|
|
352
|
+
}
|
|
353
|
+
}
|
|
273
354
|
validateSeriesOrientation() {
|
|
274
355
|
if (this.orientation !== 'horizontal') {
|
|
275
356
|
return;
|
|
@@ -277,10 +358,13 @@ export class XYChart extends BaseChart {
|
|
|
277
358
|
const hasLineSeries = this.series.some((series) => {
|
|
278
359
|
return series.type === 'line';
|
|
279
360
|
});
|
|
361
|
+
const hasScatterSeries = this.series.some((series) => {
|
|
362
|
+
return series.type === 'scatter';
|
|
363
|
+
});
|
|
280
364
|
const hasAreaSeries = this.series.some((series) => {
|
|
281
365
|
return series.type === 'area';
|
|
282
366
|
});
|
|
283
|
-
if (hasLineSeries || hasAreaSeries) {
|
|
367
|
+
if (hasLineSeries || hasScatterSeries || hasAreaSeries) {
|
|
284
368
|
throw new Error('XYChart: horizontal orientation currently supports Bar series only');
|
|
285
369
|
}
|
|
286
370
|
}
|
|
@@ -296,6 +380,35 @@ export class XYChart extends BaseChart {
|
|
|
296
380
|
});
|
|
297
381
|
return values;
|
|
298
382
|
}
|
|
383
|
+
getBarPercentDomain(positiveTotalData, negativeTotalData) {
|
|
384
|
+
const hasPositive = Array.from(positiveTotalData.values()).some((value) => value > 0);
|
|
385
|
+
const hasNegative = Array.from(negativeTotalData.values()).some((value) => value > 0);
|
|
386
|
+
if (hasPositive && hasNegative) {
|
|
387
|
+
return [-100, 100];
|
|
388
|
+
}
|
|
389
|
+
if (hasNegative) {
|
|
390
|
+
return [-100, 0];
|
|
391
|
+
}
|
|
392
|
+
return [0, 100];
|
|
393
|
+
}
|
|
394
|
+
getBarValueDomain(xKey, barSeries) {
|
|
395
|
+
if (barSeries.length === 0) {
|
|
396
|
+
return [0, 0];
|
|
397
|
+
}
|
|
398
|
+
const { positiveTotalData, negativeTotalData, rawValuesBySeriesIndex } = this.computeStackingData(this.data, xKey, barSeries);
|
|
399
|
+
if (this.barStackMode === 'percent') {
|
|
400
|
+
return this.getBarPercentDomain(positiveTotalData, negativeTotalData);
|
|
401
|
+
}
|
|
402
|
+
if (this.barStackMode === 'normal') {
|
|
403
|
+
const maxPositiveTotal = max(Array.from(positiveTotalData.values())) ?? 0;
|
|
404
|
+
const maxNegativeTotal = max(Array.from(negativeTotalData.values())) ?? 0;
|
|
405
|
+
return [-maxNegativeTotal, maxPositiveTotal];
|
|
406
|
+
}
|
|
407
|
+
const rawValues = Array.from(rawValuesBySeriesIndex.values()).flatMap((seriesValues) => Array.from(seriesValues.values()));
|
|
408
|
+
const minValue = min(rawValues) ?? 0;
|
|
409
|
+
const maxValue = max(rawValues) ?? 0;
|
|
410
|
+
return [Math.min(minValue, 0), Math.max(maxValue, 0)];
|
|
411
|
+
}
|
|
299
412
|
getStackedAreaGroups(areaSeries) {
|
|
300
413
|
const groups = new Map();
|
|
301
414
|
if (this.areaStackMode === 'none') {
|
|
@@ -346,31 +459,107 @@ export class XYChart extends BaseChart {
|
|
|
346
459
|
});
|
|
347
460
|
return domain;
|
|
348
461
|
}
|
|
349
|
-
|
|
462
|
+
getScaleRange(axis, scaleType, reverse) {
|
|
350
463
|
if (!this.plotArea) {
|
|
351
464
|
throw new Error('Plot area not calculated');
|
|
352
465
|
}
|
|
353
|
-
const scaleType = config.type || (axis === 'x' ? 'band' : 'linear');
|
|
354
|
-
const isXAxis = axis === 'x';
|
|
355
466
|
const plotPadding = 10;
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
467
|
+
let rangeStart;
|
|
468
|
+
let rangeEnd;
|
|
469
|
+
if (axis === 'x') {
|
|
470
|
+
rangeStart = this.plotArea.left + plotPadding;
|
|
471
|
+
rangeEnd = this.plotArea.right;
|
|
472
|
+
}
|
|
473
|
+
else if (scaleType === 'band') {
|
|
474
|
+
rangeStart = this.plotArea.top;
|
|
475
|
+
rangeEnd = this.plotArea.bottom - plotPadding;
|
|
476
|
+
}
|
|
477
|
+
else {
|
|
478
|
+
rangeStart = this.plotArea.bottom - plotPadding;
|
|
479
|
+
rangeEnd = this.plotArea.top;
|
|
480
|
+
}
|
|
481
|
+
return reverse ? [rangeEnd, rangeStart] : [rangeStart, rangeEnd];
|
|
482
|
+
}
|
|
483
|
+
createScale(config, dataKey, axis) {
|
|
484
|
+
if (!this.plotArea) {
|
|
485
|
+
throw new Error('Plot area not calculated');
|
|
486
|
+
}
|
|
487
|
+
const scaleType = config.type;
|
|
488
|
+
const [rangeStart, rangeEnd] = this.getScaleRange(axis, scaleType, config.reverse ?? false);
|
|
489
|
+
const domain = this.resolveScaleDomain(config, dataKey);
|
|
490
|
+
if (dataKey === null &&
|
|
491
|
+
this.series.some((series) => series.type === 'bar') &&
|
|
492
|
+
(scaleType === 'linear' || scaleType === 'log') &&
|
|
493
|
+
(config.domain !== undefined ||
|
|
494
|
+
config.min !== undefined ||
|
|
495
|
+
config.max !== undefined)) {
|
|
496
|
+
ChartValidator.validateBarDomainIncludesZero(domain);
|
|
497
|
+
}
|
|
498
|
+
if (scaleType === 'log') {
|
|
499
|
+
ChartValidator.validateScaleConfig(scaleType, domain);
|
|
500
|
+
}
|
|
501
|
+
switch (scaleType) {
|
|
502
|
+
case 'band': {
|
|
503
|
+
const scale = scaleBand()
|
|
504
|
+
.domain(domain)
|
|
505
|
+
.rangeRound([rangeStart, rangeEnd]);
|
|
506
|
+
if (config.padding !== undefined) {
|
|
507
|
+
scale.paddingInner(config.padding);
|
|
508
|
+
}
|
|
509
|
+
else {
|
|
510
|
+
scale.paddingInner(0.1);
|
|
511
|
+
}
|
|
512
|
+
return scale;
|
|
513
|
+
}
|
|
514
|
+
case 'linear': {
|
|
515
|
+
const scale = scaleLinear()
|
|
516
|
+
.domain(domain)
|
|
517
|
+
.rangeRound([rangeStart, rangeEnd]);
|
|
518
|
+
return config.nice === false ? scale : scale.nice();
|
|
519
|
+
}
|
|
520
|
+
case 'time': {
|
|
521
|
+
const scale = scaleTime()
|
|
522
|
+
.domain(domain)
|
|
523
|
+
.range([rangeStart, rangeEnd]);
|
|
524
|
+
return config.nice === false ? scale : scale.nice();
|
|
525
|
+
}
|
|
526
|
+
case 'log': {
|
|
527
|
+
const scale = scaleLog()
|
|
528
|
+
.domain(domain)
|
|
529
|
+
.rangeRound([rangeStart, rangeEnd]);
|
|
530
|
+
return config.nice === false ? scale : scale.nice();
|
|
531
|
+
}
|
|
532
|
+
default:
|
|
533
|
+
throw new Error(`Unsupported scale type: ${scaleType}`);
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
resolveScaleDomain(config, dataKey) {
|
|
537
|
+
const barSeries = this.series.filter((s) => s.type === 'bar');
|
|
538
|
+
const lineSeries = this.series.filter((s) => s.type === 'line');
|
|
539
|
+
const scatterSeries = this.series.filter((s) => s.type === 'scatter');
|
|
540
|
+
const areaSeries = this.series.filter((s) => s.type === 'area');
|
|
541
|
+
const stackedAreaGroups = this.getStackedAreaGroups(areaSeries);
|
|
542
|
+
const isBarValueScale = dataKey === null &&
|
|
543
|
+
barSeries.length > 0 &&
|
|
544
|
+
(config.type === 'linear' || config.type === 'log');
|
|
360
545
|
let domain;
|
|
546
|
+
if (isBarValueScale && config.type === 'log') {
|
|
547
|
+
throw new ChartValidationError('XYChart: bar series require a linear value axis because bars need a zero baseline');
|
|
548
|
+
}
|
|
361
549
|
if (config.domain) {
|
|
362
550
|
domain = config.domain;
|
|
363
551
|
}
|
|
364
|
-
else if (
|
|
552
|
+
else if (config.type === 'band' && dataKey) {
|
|
365
553
|
domain = this.buildBandDomainWithGroupGaps(dataKey, config.groupGap ?? 0);
|
|
366
554
|
}
|
|
367
|
-
else if (
|
|
555
|
+
else if (config.type === 'time' && dataKey) {
|
|
368
556
|
domain = [
|
|
369
557
|
min(this.data, (d) => resolveScaleValue(d[dataKey], 'time')),
|
|
370
558
|
max(this.data, (d) => resolveScaleValue(d[dataKey], 'time')),
|
|
371
559
|
];
|
|
372
560
|
}
|
|
373
|
-
else if ((
|
|
561
|
+
else if ((config.type === 'linear' || config.type === 'log') &&
|
|
562
|
+
dataKey) {
|
|
374
563
|
const values = this.data
|
|
375
564
|
.map((d) => this.parseValue(d[dataKey]))
|
|
376
565
|
.filter((value) => Number.isFinite(value));
|
|
@@ -379,31 +568,33 @@ export class XYChart extends BaseChart {
|
|
|
379
568
|
domain = [minVal, maxVal];
|
|
380
569
|
}
|
|
381
570
|
else {
|
|
382
|
-
const
|
|
383
|
-
const
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
(
|
|
388
|
-
|
|
571
|
+
const hasPercentBars = this.barStackMode === 'percent' && barSeries.length > 0;
|
|
572
|
+
const hasPercentAreas = this.areaStackMode === 'percent' && stackedAreaGroups.size > 0;
|
|
573
|
+
if (hasPercentBars || hasPercentAreas) {
|
|
574
|
+
let minDomain = 0;
|
|
575
|
+
let maxDomain = 100;
|
|
576
|
+
if (hasPercentBars) {
|
|
577
|
+
[minDomain, maxDomain] = this.getBarValueDomain(this.getXKey(), barSeries);
|
|
578
|
+
}
|
|
579
|
+
if (hasPercentAreas) {
|
|
580
|
+
minDomain = Math.min(minDomain, 0);
|
|
581
|
+
maxDomain = Math.max(maxDomain, 100);
|
|
582
|
+
}
|
|
583
|
+
domain = [minDomain, maxDomain];
|
|
389
584
|
}
|
|
390
585
|
else {
|
|
391
586
|
const values = this.collectSeriesValues([
|
|
392
587
|
...lineSeries,
|
|
393
|
-
...
|
|
588
|
+
...scatterSeries,
|
|
394
589
|
...areaSeries,
|
|
395
590
|
]);
|
|
396
591
|
const stackedValues = [];
|
|
397
592
|
const minCandidates = [...values];
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
}, 0);
|
|
404
|
-
stackedValues.push(total);
|
|
405
|
-
});
|
|
406
|
-
minCandidates.push(0);
|
|
593
|
+
const maxCandidates = [...values];
|
|
594
|
+
if (barSeries.length > 0) {
|
|
595
|
+
const [barMin, barMax] = this.getBarValueDomain(this.getXKey(), barSeries);
|
|
596
|
+
minCandidates.push(barMin);
|
|
597
|
+
maxCandidates.push(barMax);
|
|
407
598
|
}
|
|
408
599
|
const stackedAreaKeys = new Set();
|
|
409
600
|
stackedAreaGroups.forEach((stackSeries) => {
|
|
@@ -425,56 +616,34 @@ export class XYChart extends BaseChart {
|
|
|
425
616
|
minCandidates.push(series.baseline);
|
|
426
617
|
});
|
|
427
618
|
const minVal = config.min ?? min(minCandidates) ?? 0;
|
|
428
|
-
const maxVal = config.max ??
|
|
619
|
+
const maxVal = config.max ??
|
|
620
|
+
max([...maxCandidates, ...stackedValues]) ??
|
|
621
|
+
100;
|
|
429
622
|
domain = [minVal, maxVal];
|
|
430
623
|
}
|
|
431
624
|
}
|
|
432
|
-
if (
|
|
433
|
-
|
|
625
|
+
if (config.nice === false) {
|
|
626
|
+
return domain;
|
|
434
627
|
}
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
return scale;
|
|
447
|
-
}
|
|
448
|
-
case 'linear': {
|
|
449
|
-
const scale = scaleLinear()
|
|
450
|
-
.domain(domain)
|
|
451
|
-
.rangeRound([rangeStart, rangeEnd]);
|
|
452
|
-
if (config.nice !== false) {
|
|
453
|
-
scale.nice();
|
|
454
|
-
}
|
|
455
|
-
return scale;
|
|
456
|
-
}
|
|
457
|
-
case 'time': {
|
|
458
|
-
const scale = scaleTime()
|
|
459
|
-
.domain(domain)
|
|
460
|
-
.range([rangeStart, rangeEnd]);
|
|
461
|
-
if (config.nice !== false) {
|
|
462
|
-
scale.nice();
|
|
463
|
-
}
|
|
464
|
-
return scale;
|
|
465
|
-
}
|
|
466
|
-
case 'log': {
|
|
467
|
-
const scale = scaleLog()
|
|
468
|
-
.domain(domain)
|
|
469
|
-
.rangeRound([rangeStart, rangeEnd]);
|
|
470
|
-
if (config.nice !== false) {
|
|
471
|
-
scale.nice();
|
|
472
|
-
}
|
|
473
|
-
return scale;
|
|
474
|
-
}
|
|
475
|
-
default:
|
|
476
|
-
throw new Error(`Unsupported scale type: ${scaleType}`);
|
|
628
|
+
if (config.type === 'linear') {
|
|
629
|
+
return scaleLinear()
|
|
630
|
+
.domain(domain)
|
|
631
|
+
.nice()
|
|
632
|
+
.domain();
|
|
633
|
+
}
|
|
634
|
+
if (config.type === 'time') {
|
|
635
|
+
return scaleTime()
|
|
636
|
+
.domain(domain)
|
|
637
|
+
.nice()
|
|
638
|
+
.domain();
|
|
477
639
|
}
|
|
640
|
+
if (config.type === 'log') {
|
|
641
|
+
return scaleLog()
|
|
642
|
+
.domain(domain)
|
|
643
|
+
.nice()
|
|
644
|
+
.domain();
|
|
645
|
+
}
|
|
646
|
+
return domain;
|
|
478
647
|
}
|
|
479
648
|
getSeriesTooltipValue(series, dataPoint, xKey, areaStackingContextBySeries) {
|
|
480
649
|
const rawValue = dataPoint[series.dataKey];
|
|
@@ -486,6 +655,9 @@ export class XYChart extends BaseChart {
|
|
|
486
655
|
return NaN;
|
|
487
656
|
}
|
|
488
657
|
if (series.type !== 'area') {
|
|
658
|
+
if (series.type === 'bar') {
|
|
659
|
+
return series.getRenderedValue(parsedValue, this.orientation);
|
|
660
|
+
}
|
|
489
661
|
return parsedValue;
|
|
490
662
|
}
|
|
491
663
|
const stackingContext = areaStackingContextBySeries.get(series);
|
|
@@ -512,12 +684,13 @@ export class XYChart extends BaseChart {
|
|
|
512
684
|
const barSeries = visibleSeries.filter((s) => s.type === 'bar');
|
|
513
685
|
const areaSeries = visibleSeries.filter((s) => s.type === 'area');
|
|
514
686
|
const lineSeries = visibleSeries.filter((s) => s.type === 'line');
|
|
687
|
+
const scatterSeries = visibleSeries.filter((s) => s.type === 'scatter');
|
|
515
688
|
const areaValueLabelLayer = areaSeries.length > 0
|
|
516
689
|
? this.plotGroup
|
|
517
690
|
.append('g')
|
|
518
691
|
.attr('class', 'area-value-label-layer')
|
|
519
692
|
: null;
|
|
520
|
-
const { cumulativeDataBySeriesIndex, totalData, rawValuesBySeriesIndex, } = this.computeStackingData(this.data, xKey, barSeries);
|
|
693
|
+
const { cumulativeDataBySeriesIndex, positiveCumulativeDataBySeriesIndex, negativeCumulativeDataBySeriesIndex, totalData, positiveTotalData, negativeTotalData, rawValuesBySeriesIndex, } = this.computeStackingData(this.data, xKey, barSeries);
|
|
521
694
|
const areaStackingContextBySeries = this.computeAreaStackingContexts(this.data, xKey, areaSeries);
|
|
522
695
|
barSeries.forEach((series, barIndex) => {
|
|
523
696
|
const nextLayerData = this.barStackMode === 'layer'
|
|
@@ -529,6 +702,12 @@ export class XYChart extends BaseChart {
|
|
|
529
702
|
totalSeries: barSeries.length,
|
|
530
703
|
cumulativeData: cumulativeDataBySeriesIndex.get(barIndex) ?? new Map(),
|
|
531
704
|
totalData,
|
|
705
|
+
positiveCumulativeData: positiveCumulativeDataBySeriesIndex.get(barIndex) ??
|
|
706
|
+
new Map(),
|
|
707
|
+
negativeCumulativeData: negativeCumulativeDataBySeriesIndex.get(barIndex) ??
|
|
708
|
+
new Map(),
|
|
709
|
+
positiveTotalData,
|
|
710
|
+
negativeTotalData,
|
|
532
711
|
gap: this.barStackGap,
|
|
533
712
|
nextLayerData,
|
|
534
713
|
};
|
|
@@ -540,21 +719,37 @@ export class XYChart extends BaseChart {
|
|
|
540
719
|
lineSeries.forEach((series) => {
|
|
541
720
|
series.render(this.plotGroup, this.data, xKey, this.x, this.y, this.parseValue, categoryScaleType, this.renderTheme);
|
|
542
721
|
});
|
|
722
|
+
scatterSeries.forEach((series) => {
|
|
723
|
+
series.render(this.plotGroup, this.data, xKey, this.x, this.y, this.parseValue, categoryScaleType, this.renderTheme);
|
|
724
|
+
});
|
|
543
725
|
if (areaValueLabelLayer) {
|
|
544
726
|
areaValueLabelLayer.raise();
|
|
545
727
|
}
|
|
546
728
|
}
|
|
547
729
|
computeStackingData(data, xKey, barSeries) {
|
|
548
730
|
const cumulativeDataBySeriesIndex = new Map();
|
|
731
|
+
const positiveCumulativeDataBySeriesIndex = new Map();
|
|
732
|
+
const negativeCumulativeDataBySeriesIndex = new Map();
|
|
549
733
|
const rawValuesBySeriesIndex = new Map();
|
|
550
734
|
const totalData = new Map();
|
|
735
|
+
const positiveTotalData = new Map();
|
|
736
|
+
const negativeTotalData = new Map();
|
|
551
737
|
data.forEach((dataPoint) => {
|
|
552
738
|
const categoryKey = String(dataPoint[xKey]);
|
|
553
739
|
let total = 0;
|
|
740
|
+
let positiveTotal = 0;
|
|
741
|
+
let negativeTotal = 0;
|
|
554
742
|
barSeries.forEach((series, seriesIndex) => {
|
|
555
|
-
const
|
|
743
|
+
const rawValue = this.parseValue(dataPoint[series.dataKey]);
|
|
744
|
+
const value = series.getRenderedValue(rawValue, this.orientation);
|
|
556
745
|
if (Number.isFinite(value)) {
|
|
557
746
|
total += value;
|
|
747
|
+
if (value > 0) {
|
|
748
|
+
positiveTotal += value;
|
|
749
|
+
}
|
|
750
|
+
else if (value < 0) {
|
|
751
|
+
negativeTotal += Math.abs(value);
|
|
752
|
+
}
|
|
558
753
|
}
|
|
559
754
|
// Build per-series raw value maps (used for layer next-layer data)
|
|
560
755
|
let rawMap = rawValuesBySeriesIndex.get(seriesIndex);
|
|
@@ -567,25 +762,45 @@ export class XYChart extends BaseChart {
|
|
|
567
762
|
}
|
|
568
763
|
});
|
|
569
764
|
totalData.set(categoryKey, total);
|
|
765
|
+
positiveTotalData.set(categoryKey, positiveTotal);
|
|
766
|
+
negativeTotalData.set(categoryKey, negativeTotal);
|
|
570
767
|
});
|
|
571
768
|
barSeries.forEach((_, seriesIndex) => {
|
|
572
769
|
const cumulativeForSeries = new Map();
|
|
770
|
+
const positiveCumulativeForSeries = new Map();
|
|
771
|
+
const negativeCumulativeForSeries = new Map();
|
|
573
772
|
data.forEach((dataPoint) => {
|
|
574
773
|
const categoryKey = String(dataPoint[xKey]);
|
|
575
774
|
let cumulative = 0;
|
|
775
|
+
let positiveCumulative = 0;
|
|
776
|
+
let negativeCumulative = 0;
|
|
576
777
|
for (let i = 0; i < seriesIndex; i++) {
|
|
577
|
-
const value = this.parseValue(dataPoint[barSeries[i].dataKey]);
|
|
778
|
+
const value = barSeries[i].getRenderedValue(this.parseValue(dataPoint[barSeries[i].dataKey]), this.orientation);
|
|
578
779
|
if (Number.isFinite(value)) {
|
|
579
780
|
cumulative += value;
|
|
781
|
+
if (value > 0) {
|
|
782
|
+
positiveCumulative += value;
|
|
783
|
+
}
|
|
784
|
+
else if (value < 0) {
|
|
785
|
+
negativeCumulative += Math.abs(value);
|
|
786
|
+
}
|
|
580
787
|
}
|
|
581
788
|
}
|
|
582
789
|
cumulativeForSeries.set(categoryKey, cumulative);
|
|
790
|
+
positiveCumulativeForSeries.set(categoryKey, positiveCumulative);
|
|
791
|
+
negativeCumulativeForSeries.set(categoryKey, negativeCumulative);
|
|
583
792
|
});
|
|
584
793
|
cumulativeDataBySeriesIndex.set(seriesIndex, cumulativeForSeries);
|
|
794
|
+
positiveCumulativeDataBySeriesIndex.set(seriesIndex, positiveCumulativeForSeries);
|
|
795
|
+
negativeCumulativeDataBySeriesIndex.set(seriesIndex, negativeCumulativeForSeries);
|
|
585
796
|
});
|
|
586
797
|
return {
|
|
587
798
|
cumulativeDataBySeriesIndex,
|
|
799
|
+
positiveCumulativeDataBySeriesIndex,
|
|
800
|
+
negativeCumulativeDataBySeriesIndex,
|
|
588
801
|
totalData,
|
|
802
|
+
positiveTotalData,
|
|
803
|
+
negativeTotalData,
|
|
589
804
|
rawValuesBySeriesIndex,
|
|
590
805
|
};
|
|
591
806
|
}
|