@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.
- package/README.md +54 -8
- package/area.d.ts +0 -1
- package/area.js +2 -19
- package/bar.d.ts +0 -1
- package/bar.js +64 -136
- package/base-chart.d.ts +54 -5
- package/base-chart.js +260 -73
- package/donut-chart.d.ts +7 -9
- package/donut-chart.js +51 -131
- package/gauge-chart.d.ts +18 -7
- package/gauge-chart.js +315 -106
- package/line.js +3 -25
- package/package.json +3 -1
- package/pie-chart.d.ts +7 -11
- package/pie-chart.js +30 -153
- package/radial-chart-base.d.ts +25 -0
- package/radial-chart-base.js +77 -0
- package/scale-utils.d.ts +3 -0
- package/scale-utils.js +14 -0
- package/types.d.ts +2 -0
- package/utils.d.ts +7 -0
- package/utils.js +24 -0
- package/word-cloud-chart.d.ts +32 -0
- package/word-cloud-chart.js +199 -0
- package/xy-chart.d.ts +8 -4
- package/xy-chart.js +127 -127
|
@@ -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
|
-
|
|
19
|
-
protected
|
|
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
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
-
|
|
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
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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
|
-
|
|
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
|
-
|
|
91
|
+
restoreSeries();
|
|
146
92
|
restoreBase();
|
|
147
93
|
};
|
|
148
94
|
}
|
|
149
|
-
|
|
150
|
-
|
|
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 (
|
|
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.
|
|
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
|
-
|
|
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(
|
|
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(
|
|
145
|
+
this.xAxis.render(svg, this.x, this.renderTheme, plotArea.bottom, this.data);
|
|
215
146
|
}
|
|
216
147
|
if (this.yAxis) {
|
|
217
|
-
this.yAxis.render(
|
|
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.
|
|
224
|
-
this.tooltip.attachToArea(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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 {
|
|
560
|
+
return {
|
|
561
|
+
cumulativeDataBySeriesIndex,
|
|
562
|
+
totalData,
|
|
563
|
+
rawValuesBySeriesIndex,
|
|
564
|
+
};
|
|
565
565
|
}
|
|
566
566
|
computeAreaStackingContexts(data, xKey, areaSeries) {
|
|
567
567
|
const contextMap = new Map();
|