@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/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.scaleConfig,
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.type === 'line'
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.scaleConfig.y?.type ?? 'linear';
136
+ const valueScaleType = this.resolvedScaleConfig.y?.type ?? 'linear';
133
137
  if (valueScaleType === 'log') {
134
138
  this.series.forEach((series) => {
135
- const typeName = series.type === 'line'
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.scaleConfig.x?.type || 'band';
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.orientation === 'horizontal';
257
- let xConfig;
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
- createScale(config, dataKey, axis) {
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
- const rangeStart = isXAxis
357
- ? this.plotArea.left + plotPadding
358
- : this.plotArea.bottom - plotPadding;
359
- const rangeEnd = isXAxis ? this.plotArea.right : this.plotArea.top;
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 (scaleType === 'band' && dataKey) {
552
+ else if (config.type === 'band' && dataKey) {
365
553
  domain = this.buildBandDomainWithGroupGaps(dataKey, config.groupGap ?? 0);
366
554
  }
367
- else if (scaleType === 'time' && dataKey) {
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 ((scaleType === 'linear' || scaleType === 'log') && dataKey) {
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 barSeries = this.series.filter((s) => s.type === 'bar');
383
- const lineSeries = this.series.filter((s) => s.type === 'line');
384
- const areaSeries = this.series.filter((s) => s.type === 'area');
385
- const stackedAreaGroups = this.getStackedAreaGroups(areaSeries);
386
- if ((this.barStackMode === 'percent' && barSeries.length > 0) ||
387
- (this.areaStackMode === 'percent' && stackedAreaGroups.size > 0)) {
388
- domain = [0, 100];
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
- ...barSeries,
588
+ ...scatterSeries,
394
589
  ...areaSeries,
395
590
  ]);
396
591
  const stackedValues = [];
397
592
  const minCandidates = [...values];
398
- if (this.barStackMode === 'normal' && barSeries.length > 1) {
399
- this.data.forEach((dataPoint) => {
400
- const total = barSeries.reduce((sum, series) => {
401
- const value = this.parseValue(dataPoint[series.dataKey]);
402
- return Number.isFinite(value) ? sum + value : sum;
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 ?? max([...values, ...stackedValues]) ?? 100;
619
+ const maxVal = config.max ??
620
+ max([...maxCandidates, ...stackedValues]) ??
621
+ 100;
429
622
  domain = [minVal, maxVal];
430
623
  }
431
624
  }
432
- if (scaleType === 'log') {
433
- ChartValidator.validateScaleConfig(scaleType, domain);
625
+ if (config.nice === false) {
626
+ return domain;
434
627
  }
435
- switch (scaleType) {
436
- case 'band': {
437
- const scale = scaleBand()
438
- .domain(domain)
439
- .rangeRound([rangeStart, rangeEnd]);
440
- if (config.padding !== undefined) {
441
- scale.paddingInner(config.padding);
442
- }
443
- else {
444
- scale.paddingInner(0.1);
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 value = this.parseValue(dataPoint[series.dataKey]);
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
  }