@internetstiftelsen/charts 0.7.1 → 0.9.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.
@@ -0,0 +1,199 @@
1
+ import cloud from 'd3-cloud';
2
+ import { scaleSqrt } from 'd3';
3
+ import { BaseChart, } from './base-chart.js';
4
+ import { isGroupedData } from './grouped-data.js';
5
+ const DEFAULT_OPTIONS = {
6
+ maxWords: 75,
7
+ minWordLength: 1,
8
+ minValue: 1,
9
+ minFontSize: 3,
10
+ maxFontSize: 20,
11
+ padding: 1,
12
+ rotation: undefined,
13
+ spiral: 'archimedean',
14
+ };
15
+ const GROUPED_DATA_ERROR = 'WordCloudChart: grouped datasets are not supported; provide a flat array of rows instead';
16
+ function createPreparedWords(data, plotArea, options, colors) {
17
+ const counts = new Map();
18
+ data.forEach((row) => {
19
+ const word = row.word.trim();
20
+ const count = row.count;
21
+ if (!word ||
22
+ word.length < options.minWordLength ||
23
+ !Number.isFinite(count) ||
24
+ count < options.minValue) {
25
+ return;
26
+ }
27
+ counts.set(word, (counts.get(word) ?? 0) + count);
28
+ });
29
+ if (counts.size === 0) {
30
+ throw new Error('WordCloudChart: no valid words remain after filtering; adjust minWordLength or minValue, or provide valid rows');
31
+ }
32
+ const words = Array.from(counts.entries())
33
+ .map(([text, value]) => ({ text, value }))
34
+ .sort((left, right) => right.value - left.value)
35
+ .slice(0, options.maxWords);
36
+ const baseDimension = Math.max(1, Math.min(plotArea.width, plotArea.height));
37
+ const minFontSize = (baseDimension * options.minFontSize) / 100;
38
+ const maxFontSize = (baseDimension * options.maxFontSize) / 100;
39
+ const maxValue = words[0].value;
40
+ const minValue = words[words.length - 1].value;
41
+ const fontScale = maxValue === minValue
42
+ ? null
43
+ : scaleSqrt()
44
+ .domain([minValue, maxValue])
45
+ .range([minFontSize, maxFontSize]);
46
+ return words.map((word, index) => {
47
+ return {
48
+ text: word.text,
49
+ value: word.value,
50
+ size: fontScale ? fontScale(word.value) : maxFontSize,
51
+ color: colors[index % colors.length] ?? '#000000',
52
+ };
53
+ });
54
+ }
55
+ export class WordCloudChart extends BaseChart {
56
+ constructor(config) {
57
+ super(config);
58
+ Object.defineProperty(this, "options", {
59
+ enumerable: true,
60
+ configurable: true,
61
+ writable: true,
62
+ value: void 0
63
+ });
64
+ Object.defineProperty(this, "layout", {
65
+ enumerable: true,
66
+ configurable: true,
67
+ writable: true,
68
+ value: null
69
+ });
70
+ Object.defineProperty(this, "layoutRunId", {
71
+ enumerable: true,
72
+ configurable: true,
73
+ writable: true,
74
+ value: 0
75
+ });
76
+ Object.defineProperty(this, "resolvePendingReady", {
77
+ enumerable: true,
78
+ configurable: true,
79
+ writable: true,
80
+ value: null
81
+ });
82
+ const wordCloud = config.wordCloud ?? {};
83
+ this.options = {
84
+ maxWords: wordCloud.maxWords ?? DEFAULT_OPTIONS.maxWords,
85
+ minWordLength: wordCloud.minWordLength ?? DEFAULT_OPTIONS.minWordLength,
86
+ minValue: wordCloud.minValue ?? DEFAULT_OPTIONS.minValue,
87
+ minFontSize: wordCloud.minFontSize ?? DEFAULT_OPTIONS.minFontSize,
88
+ maxFontSize: wordCloud.maxFontSize ?? DEFAULT_OPTIONS.maxFontSize,
89
+ padding: wordCloud.padding ?? DEFAULT_OPTIONS.padding,
90
+ rotation: wordCloud.rotation,
91
+ spiral: wordCloud.spiral ?? DEFAULT_OPTIONS.spiral,
92
+ };
93
+ this.initializeDataState();
94
+ }
95
+ destroy() {
96
+ this.layoutRunId += 1;
97
+ this.stopLayout();
98
+ this.setReadyPromise(Promise.resolve());
99
+ super.destroy();
100
+ }
101
+ validateSourceData(data) {
102
+ if (isGroupedData(data)) {
103
+ throw new Error(GROUPED_DATA_ERROR);
104
+ }
105
+ }
106
+ renderChart({ svg, plotArea }) {
107
+ this.stopLayout();
108
+ this.renderTitle(svg);
109
+ const words = createPreparedWords(this.data, plotArea, this.options, this.renderTheme.colorPalette);
110
+ this.setReadyPromise(new Promise((resolve) => {
111
+ this.resolvePendingReady = resolve;
112
+ this.startLayout(words, plotArea, ++this.layoutRunId, resolve);
113
+ }));
114
+ }
115
+ createExportChart() {
116
+ return new WordCloudChart({
117
+ data: this.sourceData,
118
+ theme: this.theme,
119
+ responsive: this.responsiveConfig,
120
+ wordCloud: this.options,
121
+ });
122
+ }
123
+ startLayout(words, plotArea, runId, resolve) {
124
+ const layout = cloud()
125
+ .words(words.map((word) => ({ ...word })))
126
+ .size([
127
+ Math.max(1, Math.floor(plotArea.width)),
128
+ Math.max(1, Math.floor(plotArea.height)),
129
+ ])
130
+ .padding(this.options.padding)
131
+ .spiral(this.options.spiral)
132
+ .font(this.renderTheme.fontFamily)
133
+ .fontWeight(this.renderTheme.valueLabel.fontWeight)
134
+ .fontSize((word) => word.size)
135
+ .text((word) => word.text)
136
+ .on('end', (placedWords) => {
137
+ this.layout = null;
138
+ if (runId !== this.layoutRunId ||
139
+ !this.plotGroup ||
140
+ !this.plotArea) {
141
+ this.finishReady(resolve);
142
+ return;
143
+ }
144
+ if (placedWords.length < words.length) {
145
+ console.warn(`[Chart Warning] WordCloudChart: rendered ${placedWords.length} of ${words.length} words within the available area; reduce maxWords or font sizes to fit more words`);
146
+ }
147
+ this.renderWords(this.plotGroup, this.plotArea, placedWords);
148
+ this.finishReady(resolve);
149
+ });
150
+ if (this.options.rotation === 'none') {
151
+ layout.rotate(0);
152
+ }
153
+ else if (this.options.rotation === 'right-angle') {
154
+ layout.rotate((_word, index) => (index % 2 === 0 ? 0 : 90));
155
+ }
156
+ this.layout = layout;
157
+ layout.start();
158
+ }
159
+ renderWords(plotGroup, plotArea, words) {
160
+ plotGroup.attr('transform', `translate(${plotArea.left}, ${plotArea.top})`);
161
+ plotGroup
162
+ .append('rect')
163
+ .attr('class', 'word-cloud-viewport')
164
+ .attr('x', 0)
165
+ .attr('y', 0)
166
+ .attr('width', plotArea.width)
167
+ .attr('height', plotArea.height)
168
+ .attr('fill', 'transparent')
169
+ .attr('stroke', 'none')
170
+ .attr('pointer-events', 'none');
171
+ plotGroup
172
+ .append('g')
173
+ .attr('class', 'word-cloud')
174
+ .attr('transform', `translate(${plotArea.width / 2}, ${plotArea.height / 2})`)
175
+ .selectAll('text')
176
+ .data(words)
177
+ .join('text')
178
+ .attr('class', 'word-cloud-word')
179
+ .attr('text-anchor', 'middle')
180
+ .style('font-family', this.renderTheme.fontFamily)
181
+ .style('font-weight', String(this.renderTheme.valueLabel.fontWeight))
182
+ .style('font-size', (word) => `${word.size}px`)
183
+ .style('fill', (word) => word.color)
184
+ .attr('transform', (word) => `translate(${word.x ?? 0}, ${word.y ?? 0}) rotate(${word.rotate ?? 0})`)
185
+ .text((word) => word.text);
186
+ }
187
+ stopLayout() {
188
+ if (this.layout) {
189
+ this.layout.stop();
190
+ this.layout = null;
191
+ }
192
+ this.resolvePendingReady?.();
193
+ this.resolvePendingReady = null;
194
+ }
195
+ finishReady(resolve) {
196
+ this.resolvePendingReady = null;
197
+ resolve();
198
+ }
199
+ }
package/xy-chart.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { BaseChart, type BaseChartConfig } from './base-chart.js';
1
+ import { BaseChart, type BaseChartConfig, type BaseLayoutContext, type BaseRenderContext } from './base-chart.js';
2
2
  import type { ChartComponent } from './chart-interface.js';
3
3
  import { type AreaStackConfig, type BarStackConfig, type LegendSeries } from './types.js';
4
4
  export type XYChartConfig = BaseChartConfig & {
@@ -9,19 +9,23 @@ export declare class XYChart extends BaseChart {
9
9
  private readonly series;
10
10
  private barStackMode;
11
11
  private barStackGap;
12
+ private barStackReverseSeries;
12
13
  private areaStackMode;
13
14
  constructor(config: XYChartConfig);
14
15
  addChild(component: ChartComponent): this;
15
16
  protected getExportComponents(): ChartComponent[];
16
17
  protected createExportChart(): BaseChart;
17
18
  protected applyComponentOverrides(overrides: Map<ChartComponent, ChartComponent>): () => void;
18
- private rerender;
19
- protected prepareLayout(): void;
20
- protected renderChart(): void;
19
+ protected prepareLayout(context: BaseLayoutContext): void;
20
+ protected renderChart({ svg, plotGroup, plotArea, }: BaseRenderContext): void;
21
21
  private getXKey;
22
22
  protected getLegendSeries(): LegendSeries[];
23
23
  private getCategoryScaleType;
24
24
  private getVisibleSeries;
25
+ private getDisplaySeries;
26
+ private resolveSeriesDefaults;
27
+ private shouldReplaceSeriesColor;
28
+ private cloneSeriesWithOverride;
25
29
  private setupScales;
26
30
  private isHorizontalOrientation;
27
31
  private collectSeriesValues;
package/xy-chart.js CHANGED
@@ -1,7 +1,13 @@
1
1
  import { max, min, scaleBand, scaleLinear, scaleLog, scaleTime, } from 'd3';
2
- import { BaseChart } from './base-chart.js';
2
+ import { BaseChart, } from './base-chart.js';
3
3
  import { ChartValidator } from './validation.js';
4
4
  import { GROUPED_GAP_TICK_PREFIX, GROUPED_GROUP_LABEL_KEY, } from './grouped-data.js';
5
+ const DEFAULT_SERIES_COLOR = '#8884d8';
6
+ function isXYSeries(component) {
7
+ return (component.type === 'line' ||
8
+ component.type === 'bar' ||
9
+ component.type === 'area');
10
+ }
5
11
  export class XYChart extends BaseChart {
6
12
  constructor(config) {
7
13
  super(config);
@@ -23,6 +29,12 @@ export class XYChart extends BaseChart {
23
29
  writable: true,
24
30
  value: void 0
25
31
  });
32
+ Object.defineProperty(this, "barStackReverseSeries", {
33
+ enumerable: true,
34
+ configurable: true,
35
+ writable: true,
36
+ value: void 0
37
+ });
26
38
  Object.defineProperty(this, "areaStackMode", {
27
39
  enumerable: true,
28
40
  configurable: true,
@@ -31,85 +43,30 @@ export class XYChart extends BaseChart {
31
43
  });
32
44
  this.barStackMode = config.barStack?.mode ?? 'normal';
33
45
  this.barStackGap = config.barStack?.gap ?? 0.1;
46
+ this.barStackReverseSeries = config.barStack?.reverseSeries ?? false;
34
47
  this.areaStackMode = config.areaStack?.mode ?? 'none';
35
48
  }
36
49
  addChild(component) {
37
- const type = component.type;
38
- if (type === 'line' || type === 'bar' || type === 'area') {
39
- const series = component;
40
- const defaultColor = '#8884d8';
41
- const colorIndex = this.series.length % this.theme.colorPalette.length;
42
- const newColor = this.theme.colorPalette[colorIndex];
43
- if (type === 'line') {
44
- const lineSeries = series;
45
- const currentColor = lineSeries.stroke;
46
- if (!currentColor || currentColor === defaultColor) {
47
- lineSeries.stroke = newColor;
48
- }
49
- }
50
- else if (type === 'bar') {
51
- const barSeries = series;
52
- const currentColor = barSeries.fill;
53
- if (!currentColor || currentColor === defaultColor) {
54
- barSeries.fill = newColor;
55
- }
56
- }
57
- else {
58
- const areaSeries = series;
59
- const isUsingDefaultColor = (!areaSeries.fill || areaSeries.fill === defaultColor) &&
60
- (!areaSeries.stroke || areaSeries.stroke === defaultColor);
61
- if (isUsingDefaultColor) {
62
- areaSeries.fill = newColor;
63
- areaSeries.stroke = newColor;
64
- }
65
- }
66
- this.series.push(series);
67
- }
68
- else if (type === 'xAxis') {
69
- this.xAxis = component;
70
- }
71
- else if (type === 'yAxis') {
72
- this.yAxis = component;
50
+ if (isXYSeries(component)) {
51
+ this.series.push(this.resolveSeriesDefaults(component));
52
+ return this;
73
53
  }
74
- else if (type === 'grid') {
75
- this.grid = component;
76
- }
77
- else if (type === 'tooltip') {
78
- this.tooltip = component;
79
- }
80
- else if (type === 'legend') {
81
- this.legend = component;
82
- this.legend.setToggleCallback(() => {
83
- this.rerender();
84
- });
85
- }
86
- else if (type === 'title') {
87
- this.title = component;
88
- }
89
- return this;
54
+ return super.addChild(component);
90
55
  }
91
56
  getExportComponents() {
92
- const components = [];
93
- if (this.title) {
94
- components.push(this.title);
95
- }
96
- if (this.grid) {
97
- components.push(this.grid);
98
- }
99
- components.push(...this.series);
100
- if (this.xAxis) {
101
- components.push(this.xAxis);
102
- }
103
- if (this.yAxis) {
104
- components.push(this.yAxis);
105
- }
106
- if (this.tooltip) {
107
- components.push(this.tooltip);
108
- }
109
- if (this.legend) {
110
- components.push(this.legend);
111
- }
112
- return components;
57
+ return [
58
+ ...this.getBaseExportComponents({
59
+ title: true,
60
+ grid: true,
61
+ }),
62
+ ...this.series,
63
+ ...this.getBaseExportComponents({
64
+ xAxis: true,
65
+ yAxis: true,
66
+ tooltip: true,
67
+ legend: true,
68
+ }),
69
+ ];
113
70
  }
114
71
  createExportChart() {
115
72
  return new XYChart({
@@ -120,6 +77,7 @@ export class XYChart extends BaseChart {
120
77
  barStack: {
121
78
  mode: this.barStackMode,
122
79
  gap: this.barStackGap,
80
+ reverseSeries: this.barStackReverseSeries,
123
81
  },
124
82
  areaStack: {
125
83
  mode: this.areaStackMode,
@@ -128,34 +86,16 @@ export class XYChart extends BaseChart {
128
86
  }
129
87
  applyComponentOverrides(overrides) {
130
88
  const restoreBase = super.applyComponentOverrides(overrides);
131
- if (overrides.size === 0) {
132
- return restoreBase;
133
- }
134
- const previousSeries = [...this.series];
135
- this.series.forEach((series, index) => {
136
- const override = overrides.get(series);
137
- if (override &&
138
- (override.type === 'line' ||
139
- override.type === 'bar' ||
140
- override.type === 'area')) {
141
- this.series[index] = override;
142
- }
143
- });
89
+ const restoreSeries = this.applyArrayComponentOverrides(this.series, overrides, isXYSeries);
144
90
  return () => {
145
- this.series.splice(0, this.series.length, ...previousSeries);
91
+ restoreSeries();
146
92
  restoreBase();
147
93
  };
148
94
  }
149
- rerender() {
150
- if (!this.container) {
151
- return;
152
- }
153
- this.update(this.sourceData);
154
- }
155
- prepareLayout() {
156
- const svgNode = this.svg?.node();
95
+ prepareLayout(context) {
96
+ super.prepareLayout(context);
157
97
  this.xAxis?.clearEstimatedSpace?.();
158
- if (svgNode && this.xAxis) {
98
+ if (this.xAxis) {
159
99
  const xKey = this.getXKey();
160
100
  const labelKey = this.xAxis.labelKey;
161
101
  const labels = this.data.map((item) => {
@@ -164,16 +104,10 @@ export class XYChart extends BaseChart {
164
104
  }
165
105
  return item[xKey];
166
106
  });
167
- this.xAxis.estimateLayoutSpace?.(labels, this.theme, svgNode);
168
- }
169
- if (svgNode && this.legend?.isInlineMode()) {
170
- this.legend.estimateLayoutSpace(this.getLegendSeries(), this.theme, this.width, svgNode);
107
+ this.xAxis.estimateLayoutSpace?.(labels, this.renderTheme, context.svgNode);
171
108
  }
172
109
  }
173
- renderChart() {
174
- if (!this.plotArea) {
175
- throw new Error('Plot area not calculated');
176
- }
110
+ renderChart({ svg, plotGroup, plotArea, }) {
177
111
  this.series.forEach((series) => {
178
112
  const typeName = series.type === 'line'
179
113
  ? 'Line'
@@ -201,32 +135,26 @@ export class XYChart extends BaseChart {
201
135
  const categoryScaleType = this.getCategoryScaleType();
202
136
  const visibleSeries = this.getVisibleSeries();
203
137
  this.setupScales();
204
- if (this.title) {
205
- const titlePos = this.layoutManager.getComponentPosition(this.title);
206
- this.title.render(this.svg, this.theme, this.width, titlePos.x, titlePos.y);
207
- }
138
+ this.renderTitle(svg);
208
139
  if (this.grid && this.x && this.y) {
209
- this.grid.render(this.plotGroup, this.x, this.y, this.theme);
140
+ this.grid.render(plotGroup, this.x, this.y, this.renderTheme);
210
141
  }
211
142
  this.renderSeries(visibleSeries);
212
143
  if (this.x && this.y) {
213
144
  if (this.xAxis) {
214
- this.xAxis.render(this.svg, this.x, this.theme, this.plotArea.bottom, this.data);
145
+ this.xAxis.render(svg, this.x, this.renderTheme, plotArea.bottom, this.data);
215
146
  }
216
147
  if (this.yAxis) {
217
- this.yAxis.render(this.svg, this.y, this.theme, this.plotArea.left);
148
+ this.yAxis.render(svg, this.y, this.renderTheme, plotArea.left);
218
149
  }
219
150
  }
220
151
  if (this.tooltip && this.x && this.y) {
221
152
  const visibleAreaSeries = visibleSeries.filter((series) => series.type === 'area');
222
153
  const areaStackingContextBySeries = this.computeAreaStackingContexts(this.data, xKey, visibleAreaSeries);
223
- this.tooltip.initialize(this.theme);
224
- this.tooltip.attachToArea(this.svg, this.data, visibleSeries, xKey, this.x, this.y, this.theme, this.plotArea, this.parseValue.bind(this), this.isHorizontalOrientation(), categoryScaleType, (series, dataPoint) => this.getSeriesTooltipValue(series, dataPoint, xKey, areaStackingContextBySeries));
225
- }
226
- if (this.legend?.isInlineMode()) {
227
- const legendPos = this.layoutManager.getComponentPosition(this.legend);
228
- this.legend.render(this.svg, this.getLegendSeries(), this.theme, this.width, legendPos.x, legendPos.y);
154
+ this.tooltip.initialize(this.renderTheme);
155
+ this.tooltip.attachToArea(svg, this.data, visibleSeries, xKey, this.x, this.y, this.renderTheme, plotArea, this.parseValue.bind(this), this.isHorizontalOrientation(), categoryScaleType, (series, dataPoint) => this.getSeriesTooltipValue(series, dataPoint, xKey, areaStackingContextBySeries));
229
156
  }
157
+ this.renderInlineLegend(svg);
230
158
  }
231
159
  getXKey() {
232
160
  if (this.xAxis?.dataKey) {
@@ -235,7 +163,8 @@ export class XYChart extends BaseChart {
235
163
  return (Object.keys(this.data[0]).find((key) => !this.series.some((s) => s.dataKey === key)) || 'column');
236
164
  }
237
165
  getLegendSeries() {
238
- return this.series.map((series) => {
166
+ const displaySeries = this.getDisplaySeries();
167
+ return displaySeries.map((series) => {
239
168
  if (series.type === 'line') {
240
169
  return {
241
170
  dataKey: series.dataKey,
@@ -252,10 +181,63 @@ export class XYChart extends BaseChart {
252
181
  return this.scaleConfig.x?.type || 'band';
253
182
  }
254
183
  getVisibleSeries() {
255
- if (!this.legend) {
184
+ return this.filterVisibleItems(this.getDisplaySeries(), (series) => {
185
+ return series.dataKey;
186
+ });
187
+ }
188
+ getDisplaySeries() {
189
+ if (!this.barStackReverseSeries) {
190
+ return this.series;
191
+ }
192
+ const barSeries = this.series.filter((entry) => {
193
+ return entry.type === 'bar';
194
+ });
195
+ if (barSeries.length < 2) {
256
196
  return this.series;
257
197
  }
258
- return this.series.filter((series) => this.legend.isSeriesVisible(series.dataKey));
198
+ const reversedBars = [...barSeries].reverse();
199
+ let reversedBarIndex = 0;
200
+ return this.series.map((entry) => {
201
+ if (entry.type !== 'bar') {
202
+ return entry;
203
+ }
204
+ const nextBar = reversedBars[reversedBarIndex];
205
+ reversedBarIndex += 1;
206
+ return nextBar;
207
+ });
208
+ }
209
+ resolveSeriesDefaults(series) {
210
+ const colorIndex = this.series.length % this.theme.colorPalette.length;
211
+ const paletteColor = this.theme.colorPalette[colorIndex];
212
+ if (series.type === 'line') {
213
+ return this.cloneSeriesWithOverride(series, {
214
+ stroke: this.shouldReplaceSeriesColor(series.stroke)
215
+ ? paletteColor
216
+ : series.stroke,
217
+ });
218
+ }
219
+ if (series.type === 'bar') {
220
+ return this.cloneSeriesWithOverride(series, {
221
+ fill: this.shouldReplaceSeriesColor(series.fill)
222
+ ? paletteColor
223
+ : series.fill,
224
+ });
225
+ }
226
+ const shouldUsePaletteColor = this.shouldReplaceSeriesColor(series.fill) &&
227
+ this.shouldReplaceSeriesColor(series.stroke);
228
+ return this.cloneSeriesWithOverride(series, shouldUsePaletteColor
229
+ ? {
230
+ fill: paletteColor,
231
+ stroke: paletteColor,
232
+ }
233
+ : {});
234
+ }
235
+ shouldReplaceSeriesColor(color) {
236
+ return !color || color === DEFAULT_SERIES_COLOR;
237
+ }
238
+ cloneSeriesWithOverride(series, override) {
239
+ const exportable = series;
240
+ return exportable.createExportComponent(override);
259
241
  }
260
242
  setupScales() {
261
243
  const xKey = this.getXKey();
@@ -509,9 +491,12 @@ export class XYChart extends BaseChart {
509
491
  .append('g')
510
492
  .attr('class', 'area-value-label-layer')
511
493
  : null;
512
- const { cumulativeDataBySeriesIndex, totalData } = this.computeStackingData(this.data, xKey, barSeries);
494
+ const { cumulativeDataBySeriesIndex, totalData, rawValuesBySeriesIndex, } = this.computeStackingData(this.data, xKey, barSeries);
513
495
  const areaStackingContextBySeries = this.computeAreaStackingContexts(this.data, xKey, areaSeries);
514
496
  barSeries.forEach((series, barIndex) => {
497
+ const nextLayerData = this.barStackMode === 'layer'
498
+ ? rawValuesBySeriesIndex.get(barIndex + 1)
499
+ : undefined;
515
500
  const stackingContext = {
516
501
  mode: this.barStackMode,
517
502
  seriesIndex: barIndex,
@@ -519,14 +504,15 @@ export class XYChart extends BaseChart {
519
504
  cumulativeData: cumulativeDataBySeriesIndex.get(barIndex) ?? new Map(),
520
505
  totalData,
521
506
  gap: this.barStackGap,
507
+ nextLayerData,
522
508
  };
523
- series.render(this.plotGroup, this.data, xKey, this.x, this.y, this.parseValue, categoryScaleType, this.theme, stackingContext);
509
+ series.render(this.plotGroup, this.data, xKey, this.x, this.y, this.parseValue, categoryScaleType, this.renderTheme, stackingContext);
524
510
  });
525
511
  areaSeries.forEach((series) => {
526
- series.render(this.plotGroup, this.data, xKey, this.x, this.y, this.parseValue, categoryScaleType, this.theme, areaStackingContextBySeries.get(series), areaValueLabelLayer ?? undefined);
512
+ series.render(this.plotGroup, this.data, xKey, this.x, this.y, this.parseValue, categoryScaleType, this.renderTheme, areaStackingContextBySeries.get(series), areaValueLabelLayer ?? undefined);
527
513
  });
528
514
  lineSeries.forEach((series) => {
529
- series.render(this.plotGroup, this.data, xKey, this.x, this.y, this.parseValue, categoryScaleType, this.theme);
515
+ series.render(this.plotGroup, this.data, xKey, this.x, this.y, this.parseValue, categoryScaleType, this.renderTheme);
530
516
  });
531
517
  if (areaValueLabelLayer) {
532
518
  areaValueLabelLayer.raise();
@@ -534,15 +520,25 @@ export class XYChart extends BaseChart {
534
520
  }
535
521
  computeStackingData(data, xKey, barSeries) {
536
522
  const cumulativeDataBySeriesIndex = new Map();
523
+ const rawValuesBySeriesIndex = new Map();
537
524
  const totalData = new Map();
538
525
  data.forEach((dataPoint) => {
539
526
  const categoryKey = String(dataPoint[xKey]);
540
527
  let total = 0;
541
- barSeries.forEach((series) => {
528
+ barSeries.forEach((series, seriesIndex) => {
542
529
  const value = this.parseValue(dataPoint[series.dataKey]);
543
530
  if (Number.isFinite(value)) {
544
531
  total += value;
545
532
  }
533
+ // Build per-series raw value maps (used for layer next-layer data)
534
+ let rawMap = rawValuesBySeriesIndex.get(seriesIndex);
535
+ if (!rawMap) {
536
+ rawMap = new Map();
537
+ rawValuesBySeriesIndex.set(seriesIndex, rawMap);
538
+ }
539
+ if (Number.isFinite(value)) {
540
+ rawMap.set(categoryKey, value);
541
+ }
546
542
  });
547
543
  totalData.set(categoryKey, total);
548
544
  });
@@ -561,7 +557,11 @@ export class XYChart extends BaseChart {
561
557
  });
562
558
  cumulativeDataBySeriesIndex.set(seriesIndex, cumulativeForSeries);
563
559
  });
564
- return { cumulativeDataBySeriesIndex, totalData };
560
+ return {
561
+ cumulativeDataBySeriesIndex,
562
+ totalData,
563
+ rawValuesBySeriesIndex,
564
+ };
565
565
  }
566
566
  computeAreaStackingContexts(data, xKey, areaSeries) {
567
567
  const contextMap = new Map();