@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/bar.js
CHANGED
|
@@ -4,7 +4,6 @@ const LABEL_INSET_DEFAULT = 4;
|
|
|
4
4
|
const LABEL_INSET_STACKED = 6;
|
|
5
5
|
const LABEL_MIN_PADDING_DEFAULT = 8;
|
|
6
6
|
const LABEL_MIN_PADDING_STACKED = 16;
|
|
7
|
-
const LAYER_LABEL_GAP = 6;
|
|
8
7
|
function getLabelSpacing(mode) {
|
|
9
8
|
const stacked = mode !== 'none';
|
|
10
9
|
return {
|
|
@@ -14,6 +13,93 @@ function getLabelSpacing(mode) {
|
|
|
14
13
|
: LABEL_MIN_PADDING_DEFAULT,
|
|
15
14
|
};
|
|
16
15
|
}
|
|
16
|
+
function getBarSlotLayout(bandwidth, mode, maxBarSize, totalSeries, seriesIndex, gap) {
|
|
17
|
+
if (mode === 'none') {
|
|
18
|
+
const groupSize = maxBarSize
|
|
19
|
+
? Math.min(bandwidth, maxBarSize * totalSeries)
|
|
20
|
+
: bandwidth;
|
|
21
|
+
const totalGapSpace = groupSize * gap * (totalSeries - 1);
|
|
22
|
+
const availableSize = groupSize - totalGapSpace;
|
|
23
|
+
const thickness = availableSize / totalSeries;
|
|
24
|
+
const gapSize = totalSeries > 1 ? groupSize * gap : 0;
|
|
25
|
+
return {
|
|
26
|
+
thickness,
|
|
27
|
+
offset: (bandwidth - groupSize) / 2 +
|
|
28
|
+
seriesIndex * (thickness + gapSize),
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
if (mode === 'layer') {
|
|
32
|
+
const maxSize = maxBarSize
|
|
33
|
+
? Math.min(bandwidth, maxBarSize)
|
|
34
|
+
: bandwidth;
|
|
35
|
+
const scaleFactor = 1 - (seriesIndex / totalSeries) * 0.7;
|
|
36
|
+
const thickness = maxSize * scaleFactor;
|
|
37
|
+
return {
|
|
38
|
+
thickness,
|
|
39
|
+
offset: (bandwidth - thickness) / 2,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
const thickness = maxBarSize ? Math.min(bandwidth, maxBarSize) : bandwidth;
|
|
43
|
+
return {
|
|
44
|
+
thickness,
|
|
45
|
+
offset: (bandwidth - thickness) / 2,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
function getBarValueRange(categoryKey, value, stackingContext) {
|
|
49
|
+
const mode = stackingContext?.mode ?? 'normal';
|
|
50
|
+
if (mode === 'none' || mode === 'layer') {
|
|
51
|
+
return {
|
|
52
|
+
start: 0,
|
|
53
|
+
end: value,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
if (mode === 'percent') {
|
|
57
|
+
if (value >= 0) {
|
|
58
|
+
const cumulative = stackingContext?.positiveCumulativeData.get(categoryKey) ?? 0;
|
|
59
|
+
const total = stackingContext?.positiveTotalData.get(categoryKey) ?? 0;
|
|
60
|
+
if (total === 0) {
|
|
61
|
+
return { start: 0, end: 0 };
|
|
62
|
+
}
|
|
63
|
+
return {
|
|
64
|
+
start: (cumulative / total) * 100,
|
|
65
|
+
end: ((cumulative + value) / total) * 100,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
const cumulativeMagnitude = stackingContext?.negativeCumulativeData.get(categoryKey) ?? 0;
|
|
69
|
+
const totalMagnitude = stackingContext?.negativeTotalData.get(categoryKey) ?? 0;
|
|
70
|
+
if (totalMagnitude === 0) {
|
|
71
|
+
return { start: 0, end: 0 };
|
|
72
|
+
}
|
|
73
|
+
return {
|
|
74
|
+
start: -((cumulativeMagnitude / totalMagnitude) * 100),
|
|
75
|
+
end: -(((cumulativeMagnitude + Math.abs(value)) / totalMagnitude) *
|
|
76
|
+
100),
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
if (value >= 0) {
|
|
80
|
+
const cumulative = stackingContext?.positiveCumulativeData.get(categoryKey) ?? 0;
|
|
81
|
+
return {
|
|
82
|
+
start: cumulative,
|
|
83
|
+
end: cumulative + value,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
const cumulativeMagnitude = stackingContext?.negativeCumulativeData.get(categoryKey) ?? 0;
|
|
87
|
+
const start = -cumulativeMagnitude;
|
|
88
|
+
return {
|
|
89
|
+
start,
|
|
90
|
+
end: start + value,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
function getScaledValueRange(scale, startValue, endValue) {
|
|
94
|
+
const start = scale(startValue) ?? 0;
|
|
95
|
+
const end = scale(endValue) ?? 0;
|
|
96
|
+
return {
|
|
97
|
+
start,
|
|
98
|
+
end,
|
|
99
|
+
min: Math.min(start, end),
|
|
100
|
+
max: Math.max(start, end),
|
|
101
|
+
};
|
|
102
|
+
}
|
|
17
103
|
export class Bar {
|
|
18
104
|
constructor(config) {
|
|
19
105
|
Object.defineProperty(this, "type", {
|
|
@@ -46,6 +132,12 @@ export class Bar {
|
|
|
46
132
|
writable: true,
|
|
47
133
|
value: void 0
|
|
48
134
|
});
|
|
135
|
+
Object.defineProperty(this, "side", {
|
|
136
|
+
enumerable: true,
|
|
137
|
+
configurable: true,
|
|
138
|
+
writable: true,
|
|
139
|
+
value: void 0
|
|
140
|
+
});
|
|
49
141
|
Object.defineProperty(this, "valueLabel", {
|
|
50
142
|
enumerable: true,
|
|
51
143
|
configurable: true,
|
|
@@ -62,6 +154,7 @@ export class Bar {
|
|
|
62
154
|
this.fill = config.fill || '#8884d8';
|
|
63
155
|
this.colorAdapter = config.colorAdapter;
|
|
64
156
|
this.maxBarSize = config.maxBarSize;
|
|
157
|
+
this.side = config.side ?? 'right';
|
|
65
158
|
this.valueLabel = config.valueLabel;
|
|
66
159
|
this.exportHooks = config.exportHooks;
|
|
67
160
|
}
|
|
@@ -71,6 +164,7 @@ export class Bar {
|
|
|
71
164
|
fill: this.fill,
|
|
72
165
|
colorAdapter: this.colorAdapter,
|
|
73
166
|
maxBarSize: this.maxBarSize,
|
|
167
|
+
side: this.side,
|
|
74
168
|
valueLabel: this.valueLabel,
|
|
75
169
|
};
|
|
76
170
|
}
|
|
@@ -81,6 +175,12 @@ export class Bar {
|
|
|
81
175
|
exportHooks: this.exportHooks,
|
|
82
176
|
});
|
|
83
177
|
}
|
|
178
|
+
getRenderedValue(value, orientation = 'vertical') {
|
|
179
|
+
if (orientation === 'horizontal' && this.side === 'left') {
|
|
180
|
+
return -Math.abs(value);
|
|
181
|
+
}
|
|
182
|
+
return value;
|
|
183
|
+
}
|
|
84
184
|
render(plotGroup, data, xKey, x, y, parseValue, xScaleType = 'band', theme, stackingContext, orientation = 'vertical') {
|
|
85
185
|
if (orientation === 'vertical') {
|
|
86
186
|
this.renderVertical(plotGroup, data, xKey, x, y, parseValue, xScaleType, stackingContext);
|
|
@@ -101,49 +201,13 @@ export class Bar {
|
|
|
101
201
|
renderVertical(plotGroup, data, xKey, x, y, parseValue, xScaleType, stackingContext) {
|
|
102
202
|
const bandwidth = x.bandwidth ? x.bandwidth() : 20;
|
|
103
203
|
const mode = stackingContext?.mode ?? 'normal';
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
const gap = stackingContext?.gap ?? 0.1;
|
|
112
|
-
const groupWidth = this.maxBarSize
|
|
113
|
-
? Math.min(bandwidth, this.maxBarSize * totalSeries)
|
|
114
|
-
: bandwidth;
|
|
115
|
-
// Calculate total gap space and individual bar width
|
|
116
|
-
const totalGapSpace = groupWidth * gap * (totalSeries - 1);
|
|
117
|
-
const availableWidth = groupWidth - totalGapSpace;
|
|
118
|
-
barWidth = availableWidth / totalSeries;
|
|
119
|
-
const gapSize = totalSeries > 1 ? groupWidth * gap : 0;
|
|
120
|
-
barOffset =
|
|
121
|
-
(bandwidth - groupWidth) / 2 +
|
|
122
|
-
seriesIndex * (barWidth + gapSize);
|
|
123
|
-
}
|
|
124
|
-
else if (mode === 'layer') {
|
|
125
|
-
// Layer mode: each subsequent series has smaller bars
|
|
126
|
-
const totalSeries = stackingContext?.totalSeries ?? 1;
|
|
127
|
-
const seriesIndex = stackingContext?.seriesIndex ?? 0;
|
|
128
|
-
const maxWidth = this.maxBarSize
|
|
129
|
-
? Math.min(bandwidth, this.maxBarSize)
|
|
130
|
-
: bandwidth;
|
|
131
|
-
// Scale from 100% to a minimum (e.g., 30%) based on series position
|
|
132
|
-
const scaleFactor = 1 - (seriesIndex / totalSeries) * 0.7;
|
|
133
|
-
barWidth = maxWidth * scaleFactor;
|
|
134
|
-
barOffset = (bandwidth - barWidth) / 2;
|
|
135
|
-
}
|
|
136
|
-
else {
|
|
137
|
-
// Normal and Percent modes: full width stacked bars
|
|
138
|
-
barWidth = this.maxBarSize
|
|
139
|
-
? Math.min(bandwidth, this.maxBarSize)
|
|
140
|
-
: bandwidth;
|
|
141
|
-
barOffset = (bandwidth - barWidth) / 2;
|
|
142
|
-
}
|
|
143
|
-
// Get the baseline value from the Y scale's domain
|
|
144
|
-
const yDomain = y.domain();
|
|
145
|
-
const baselineValue = yDomain[0] >= 0 ? Math.max(0, yDomain[0]) : yDomain[0];
|
|
146
|
-
const yBaseline = y(baselineValue) || 0;
|
|
204
|
+
const { thickness: barWidth, offset: barOffset } = getBarSlotLayout(bandwidth, mode, this.maxBarSize, stackingContext?.totalSeries ?? 1, stackingContext?.seriesIndex ?? 0, stackingContext?.gap ?? 0.1);
|
|
205
|
+
const getVerticalBounds = (d) => {
|
|
206
|
+
const categoryKey = String(d[xKey]);
|
|
207
|
+
const value = this.getRenderedValue(parseValue(d[this.dataKey]), 'vertical');
|
|
208
|
+
const { start, end } = getBarValueRange(categoryKey, value, stackingContext);
|
|
209
|
+
return getScaledValueRange(y, start, end);
|
|
210
|
+
};
|
|
147
211
|
// Add bar rectangles
|
|
148
212
|
const sanitizedKey = sanitizeForCSS(this.dataKey);
|
|
149
213
|
plotGroup
|
|
@@ -158,96 +222,24 @@ export class Bar {
|
|
|
158
222
|
? xPos + barOffset
|
|
159
223
|
: xPos - barWidth / 2;
|
|
160
224
|
})
|
|
161
|
-
.attr('y', (d) =>
|
|
162
|
-
const categoryKey = String(d[xKey]);
|
|
163
|
-
const value = parseValue(d[this.dataKey]);
|
|
164
|
-
if (mode === 'none' || mode === 'layer') {
|
|
165
|
-
// No stacking - each bar starts from baseline
|
|
166
|
-
const yPos = y(value) || 0;
|
|
167
|
-
return Math.min(yBaseline, yPos);
|
|
168
|
-
}
|
|
169
|
-
else if (mode === 'percent') {
|
|
170
|
-
// Percent mode: calculate position based on cumulative percentage
|
|
171
|
-
const cumulative = stackingContext?.cumulativeData.get(categoryKey) ?? 0;
|
|
172
|
-
const total = stackingContext?.totalData.get(categoryKey) ?? 1;
|
|
173
|
-
const percentCumulative = (cumulative / total) * 100;
|
|
174
|
-
const percentValue = (value / total) * 100;
|
|
175
|
-
return y(percentCumulative + percentValue) || 0;
|
|
176
|
-
}
|
|
177
|
-
else {
|
|
178
|
-
// Normal stacking mode
|
|
179
|
-
const cumulative = stackingContext?.cumulativeData.get(categoryKey) ?? 0;
|
|
180
|
-
return y(cumulative + value) || 0;
|
|
181
|
-
}
|
|
182
|
-
})
|
|
225
|
+
.attr('y', (d) => getVerticalBounds(d).min)
|
|
183
226
|
.attr('width', barWidth)
|
|
184
227
|
.attr('height', (d) => {
|
|
185
|
-
const
|
|
186
|
-
|
|
187
|
-
if (mode === 'none' || mode === 'layer') {
|
|
188
|
-
const yPos = y(value) || 0;
|
|
189
|
-
return Math.abs(yBaseline - yPos);
|
|
190
|
-
}
|
|
191
|
-
else if (mode === 'percent') {
|
|
192
|
-
const total = stackingContext?.totalData.get(categoryKey) ?? 1;
|
|
193
|
-
const percentValue = (value / total) * 100;
|
|
194
|
-
const yTop = y(percentValue) || 0;
|
|
195
|
-
const yBottom = y(0) || 0;
|
|
196
|
-
return Math.abs(yBottom - yTop);
|
|
197
|
-
}
|
|
198
|
-
else {
|
|
199
|
-
// Normal stacking mode
|
|
200
|
-
const yTop = y(value) || 0;
|
|
201
|
-
return Math.abs(yBaseline - yTop);
|
|
202
|
-
}
|
|
228
|
+
const bounds = getVerticalBounds(d);
|
|
229
|
+
return Math.abs(bounds.max - bounds.min);
|
|
203
230
|
})
|
|
204
231
|
.attr('fill', (d, i) => this.colorAdapter ? this.colorAdapter(d, i) : this.fill);
|
|
205
232
|
}
|
|
206
233
|
renderHorizontal(plotGroup, data, xKey, x, y, parseValue, yScaleType, stackingContext) {
|
|
207
234
|
const bandwidth = y.bandwidth ? y.bandwidth() : 20;
|
|
208
235
|
const mode = stackingContext?.mode ?? 'normal';
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
const gap = stackingContext?.gap ?? 0.1;
|
|
217
|
-
const groupHeight = this.maxBarSize
|
|
218
|
-
? Math.min(bandwidth, this.maxBarSize * totalSeries)
|
|
219
|
-
: bandwidth;
|
|
220
|
-
// Calculate total gap space and individual bar height
|
|
221
|
-
const totalGapSpace = groupHeight * gap * (totalSeries - 1);
|
|
222
|
-
const availableHeight = groupHeight - totalGapSpace;
|
|
223
|
-
barHeight = availableHeight / totalSeries;
|
|
224
|
-
const gapSize = totalSeries > 1 ? groupHeight * gap : 0;
|
|
225
|
-
barOffset =
|
|
226
|
-
(bandwidth - groupHeight) / 2 +
|
|
227
|
-
seriesIndex * (barHeight + gapSize);
|
|
228
|
-
}
|
|
229
|
-
else if (mode === 'layer') {
|
|
230
|
-
// Layer mode: each subsequent series has smaller bars
|
|
231
|
-
const totalSeries = stackingContext?.totalSeries ?? 1;
|
|
232
|
-
const seriesIndex = stackingContext?.seriesIndex ?? 0;
|
|
233
|
-
const maxHeight = this.maxBarSize
|
|
234
|
-
? Math.min(bandwidth, this.maxBarSize)
|
|
235
|
-
: bandwidth;
|
|
236
|
-
const scaleFactor = 1 - (seriesIndex / totalSeries) * 0.7;
|
|
237
|
-
barHeight = maxHeight * scaleFactor;
|
|
238
|
-
barOffset = (bandwidth - barHeight) / 2;
|
|
239
|
-
}
|
|
240
|
-
else {
|
|
241
|
-
// Normal and Percent modes: full height stacked bars
|
|
242
|
-
barHeight = this.maxBarSize
|
|
243
|
-
? Math.min(bandwidth, this.maxBarSize)
|
|
244
|
-
: bandwidth;
|
|
245
|
-
barOffset = (bandwidth - barHeight) / 2;
|
|
246
|
-
}
|
|
247
|
-
// Get the baseline value from the scale's domain
|
|
248
|
-
const domain = x.domain();
|
|
249
|
-
const baselineValue = domain[0] >= 0 ? Math.max(0, domain[0]) : domain[0];
|
|
250
|
-
const xBaseline = x(baselineValue) || 0;
|
|
236
|
+
const { thickness: barHeight, offset: barOffset } = getBarSlotLayout(bandwidth, mode, this.maxBarSize, stackingContext?.totalSeries ?? 1, stackingContext?.seriesIndex ?? 0, stackingContext?.gap ?? 0.1);
|
|
237
|
+
const getHorizontalBounds = (d) => {
|
|
238
|
+
const categoryKey = String(d[xKey]);
|
|
239
|
+
const value = this.getRenderedValue(parseValue(d[this.dataKey]), 'horizontal');
|
|
240
|
+
const { start, end } = getBarValueRange(categoryKey, value, stackingContext);
|
|
241
|
+
return getScaledValueRange(x, start, end);
|
|
242
|
+
};
|
|
251
243
|
// Add bar rectangles (horizontal)
|
|
252
244
|
const sanitizedKey = sanitizeForCSS(this.dataKey);
|
|
253
245
|
plotGroup
|
|
@@ -256,25 +248,7 @@ export class Bar {
|
|
|
256
248
|
.join('rect')
|
|
257
249
|
.attr('class', `bar-${sanitizedKey}`)
|
|
258
250
|
.attr('data-index', (_, i) => i)
|
|
259
|
-
.attr('x', (d) =>
|
|
260
|
-
const categoryKey = String(d[xKey]);
|
|
261
|
-
const value = parseValue(d[this.dataKey]);
|
|
262
|
-
if (mode === 'none' || mode === 'layer') {
|
|
263
|
-
const xPos = x(value) || 0;
|
|
264
|
-
return Math.min(xBaseline, xPos);
|
|
265
|
-
}
|
|
266
|
-
else if (mode === 'percent') {
|
|
267
|
-
const cumulative = stackingContext?.cumulativeData.get(categoryKey) ?? 0;
|
|
268
|
-
const total = stackingContext?.totalData.get(categoryKey) ?? 1;
|
|
269
|
-
const percentCumulative = (cumulative / total) * 100;
|
|
270
|
-
return x(percentCumulative) || 0;
|
|
271
|
-
}
|
|
272
|
-
else {
|
|
273
|
-
// Normal stacking mode
|
|
274
|
-
const cumulative = stackingContext?.cumulativeData.get(categoryKey) ?? 0;
|
|
275
|
-
return x(cumulative) || 0;
|
|
276
|
-
}
|
|
277
|
-
})
|
|
251
|
+
.attr('x', (d) => getHorizontalBounds(d).min)
|
|
278
252
|
.attr('y', (d) => {
|
|
279
253
|
const yPos = getScalePosition(y, d[xKey], yScaleType);
|
|
280
254
|
return yScaleType === 'band'
|
|
@@ -282,25 +256,8 @@ export class Bar {
|
|
|
282
256
|
: yPos - barHeight / 2;
|
|
283
257
|
})
|
|
284
258
|
.attr('width', (d) => {
|
|
285
|
-
const
|
|
286
|
-
|
|
287
|
-
if (mode === 'none' || mode === 'layer') {
|
|
288
|
-
const xPos = x(value) || 0;
|
|
289
|
-
return Math.abs(xPos - xBaseline);
|
|
290
|
-
}
|
|
291
|
-
else if (mode === 'percent') {
|
|
292
|
-
const total = stackingContext?.totalData.get(categoryKey) ?? 1;
|
|
293
|
-
const percentValue = (value / total) * 100;
|
|
294
|
-
const xLeft = x(0) || 0;
|
|
295
|
-
const xRight = x(percentValue) || 0;
|
|
296
|
-
return Math.abs(xRight - xLeft);
|
|
297
|
-
}
|
|
298
|
-
else {
|
|
299
|
-
// Normal stacking mode
|
|
300
|
-
const xLeft = x(0) || 0;
|
|
301
|
-
const xRight = x(value) || 0;
|
|
302
|
-
return Math.abs(xRight - xLeft);
|
|
303
|
-
}
|
|
259
|
+
const bounds = getHorizontalBounds(d);
|
|
260
|
+
return Math.abs(bounds.max - bounds.min);
|
|
304
261
|
})
|
|
305
262
|
.attr('height', barHeight)
|
|
306
263
|
.attr('fill', (d, i) => this.colorAdapter ? this.colorAdapter(d, i) : this.fill);
|
|
@@ -308,43 +265,7 @@ export class Bar {
|
|
|
308
265
|
renderVerticalValueLabels(plotGroup, data, xKey, x, y, parseValue, xScaleType, theme, stackingContext) {
|
|
309
266
|
const bandwidth = x.bandwidth ? x.bandwidth() : 20;
|
|
310
267
|
const mode = stackingContext?.mode ?? 'normal';
|
|
311
|
-
|
|
312
|
-
let barWidth;
|
|
313
|
-
let barOffset;
|
|
314
|
-
if (mode === 'none') {
|
|
315
|
-
const totalSeries = stackingContext?.totalSeries ?? 1;
|
|
316
|
-
const seriesIndex = stackingContext?.seriesIndex ?? 0;
|
|
317
|
-
const gap = stackingContext?.gap ?? 0.1;
|
|
318
|
-
const groupWidth = this.maxBarSize
|
|
319
|
-
? Math.min(bandwidth, this.maxBarSize * totalSeries)
|
|
320
|
-
: bandwidth;
|
|
321
|
-
const totalGapSpace = groupWidth * gap * (totalSeries - 1);
|
|
322
|
-
const availableWidth = groupWidth - totalGapSpace;
|
|
323
|
-
barWidth = availableWidth / totalSeries;
|
|
324
|
-
const gapSize = totalSeries > 1 ? groupWidth * gap : 0;
|
|
325
|
-
barOffset =
|
|
326
|
-
(bandwidth - groupWidth) / 2 +
|
|
327
|
-
seriesIndex * (barWidth + gapSize);
|
|
328
|
-
}
|
|
329
|
-
else if (mode === 'layer') {
|
|
330
|
-
const totalSeries = stackingContext?.totalSeries ?? 1;
|
|
331
|
-
const seriesIndex = stackingContext?.seriesIndex ?? 0;
|
|
332
|
-
const maxWidth = this.maxBarSize
|
|
333
|
-
? Math.min(bandwidth, this.maxBarSize)
|
|
334
|
-
: bandwidth;
|
|
335
|
-
const scaleFactor = 1 - (seriesIndex / totalSeries) * 0.7;
|
|
336
|
-
barWidth = maxWidth * scaleFactor;
|
|
337
|
-
barOffset = (bandwidth - barWidth) / 2;
|
|
338
|
-
}
|
|
339
|
-
else {
|
|
340
|
-
barWidth = this.maxBarSize
|
|
341
|
-
? Math.min(bandwidth, this.maxBarSize)
|
|
342
|
-
: bandwidth;
|
|
343
|
-
barOffset = (bandwidth - barWidth) / 2;
|
|
344
|
-
}
|
|
345
|
-
const yDomain = y.domain();
|
|
346
|
-
const baselineValue = yDomain[0] >= 0 ? Math.max(0, yDomain[0]) : yDomain[0];
|
|
347
|
-
const yBaseline = y(baselineValue) || 0;
|
|
268
|
+
const { thickness: barWidth, offset: barOffset } = getBarSlotLayout(bandwidth, mode, this.maxBarSize, stackingContext?.totalSeries ?? 1, stackingContext?.seriesIndex ?? 0, stackingContext?.gap ?? 0.1);
|
|
348
269
|
const config = this.valueLabel;
|
|
349
270
|
const position = config.position || 'outside';
|
|
350
271
|
const insidePosition = config.insidePosition || 'top';
|
|
@@ -361,33 +282,18 @@ export class Bar {
|
|
|
361
282
|
.attr('class', `bar-value-labels-${sanitizeForCSS(this.dataKey)}`);
|
|
362
283
|
data.forEach((d, i) => {
|
|
363
284
|
const categoryKey = String(d[xKey]);
|
|
364
|
-
const
|
|
365
|
-
const
|
|
285
|
+
const rawValue = parseValue(d[this.dataKey]);
|
|
286
|
+
const renderedValue = this.getRenderedValue(rawValue, 'vertical');
|
|
287
|
+
const valueText = String(rawValue);
|
|
366
288
|
const xPos = getScalePosition(x, d[xKey], xScaleType);
|
|
367
289
|
const barColor = this.colorAdapter
|
|
368
290
|
? this.colorAdapter(d, i)
|
|
369
291
|
: this.fill;
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
barTop = Math.min(yBaseline, yPos);
|
|
376
|
-
barBottom = Math.max(yBaseline, yPos);
|
|
377
|
-
}
|
|
378
|
-
else if (mode === 'percent') {
|
|
379
|
-
const cumulative = stackingContext?.cumulativeData.get(categoryKey) ?? 0;
|
|
380
|
-
const total = stackingContext?.totalData.get(categoryKey) ?? 1;
|
|
381
|
-
const percentCumulative = (cumulative / total) * 100;
|
|
382
|
-
const percentValue = (value / total) * 100;
|
|
383
|
-
barTop = y(percentCumulative + percentValue) || 0;
|
|
384
|
-
barBottom = y(percentCumulative) || 0;
|
|
385
|
-
}
|
|
386
|
-
else {
|
|
387
|
-
const cumulative = stackingContext?.cumulativeData.get(categoryKey) ?? 0;
|
|
388
|
-
barTop = y(cumulative + value) || 0;
|
|
389
|
-
barBottom = y(cumulative) || 0;
|
|
390
|
-
}
|
|
292
|
+
const { start, end } = getBarValueRange(categoryKey, renderedValue, stackingContext);
|
|
293
|
+
const bounds = getScaledValueRange(y, start, end);
|
|
294
|
+
const barTop = bounds.min;
|
|
295
|
+
const barBottom = bounds.max;
|
|
296
|
+
const isNegative = renderedValue < 0;
|
|
391
297
|
const barHeight = Math.abs(barBottom - barTop);
|
|
392
298
|
const barCenterX = xPos +
|
|
393
299
|
(xScaleType === 'band' ? barOffset : -barWidth / 2) +
|
|
@@ -406,11 +312,13 @@ export class Bar {
|
|
|
406
312
|
let labelY = (barTop + barBottom) / 2; // Default to middle
|
|
407
313
|
let shouldRender = true;
|
|
408
314
|
if (position === 'outside') {
|
|
409
|
-
// Place above the bar
|
|
410
|
-
labelY = barTop - boxHeight / 2 - 4;
|
|
411
|
-
// Check if it fits (not going above plot area)
|
|
412
315
|
const plotTop = y.range()[1];
|
|
413
|
-
|
|
316
|
+
const plotBottom = y.range()[0];
|
|
317
|
+
labelY = isNegative
|
|
318
|
+
? barBottom + boxHeight / 2 + 4
|
|
319
|
+
: barTop - boxHeight / 2 - 4;
|
|
320
|
+
if ((!isNegative && labelY - boxHeight / 2 < plotTop) ||
|
|
321
|
+
(isNegative && labelY + boxHeight / 2 > plotBottom)) {
|
|
414
322
|
shouldRender = false;
|
|
415
323
|
}
|
|
416
324
|
}
|
|
@@ -432,25 +340,9 @@ export class Bar {
|
|
|
432
340
|
labelY = barBottom - boxHeight / 2 - inset;
|
|
433
341
|
break;
|
|
434
342
|
}
|
|
435
|
-
// Check if it fits inside the bar
|
|
436
343
|
if (boxHeight + minPadding > barHeight) {
|
|
437
344
|
shouldRender = false;
|
|
438
345
|
}
|
|
439
|
-
// In layer mode, check the label fits in the visible gap
|
|
440
|
-
// above the next layer's bar top
|
|
441
|
-
if (shouldRender &&
|
|
442
|
-
mode === 'layer' &&
|
|
443
|
-
insidePosition === 'top' &&
|
|
444
|
-
stackingContext?.nextLayerData) {
|
|
445
|
-
const nextValue = stackingContext.nextLayerData.get(categoryKey);
|
|
446
|
-
if (nextValue !== undefined) {
|
|
447
|
-
const nextBarTop = y(nextValue) || 0;
|
|
448
|
-
const labelBottom = labelY + boxHeight / 2;
|
|
449
|
-
if (labelBottom + LAYER_LABEL_GAP > nextBarTop) {
|
|
450
|
-
shouldRender = false;
|
|
451
|
-
}
|
|
452
|
-
}
|
|
453
|
-
}
|
|
454
346
|
}
|
|
455
347
|
}
|
|
456
348
|
tempText.remove();
|
|
@@ -492,43 +384,7 @@ export class Bar {
|
|
|
492
384
|
renderHorizontalValueLabels(plotGroup, data, xKey, x, y, parseValue, yScaleType, theme, stackingContext) {
|
|
493
385
|
const bandwidth = y.bandwidth ? y.bandwidth() : 20;
|
|
494
386
|
const mode = stackingContext?.mode ?? 'normal';
|
|
495
|
-
|
|
496
|
-
let barHeight;
|
|
497
|
-
let barOffset;
|
|
498
|
-
if (mode === 'none') {
|
|
499
|
-
const totalSeries = stackingContext?.totalSeries ?? 1;
|
|
500
|
-
const seriesIndex = stackingContext?.seriesIndex ?? 0;
|
|
501
|
-
const gap = stackingContext?.gap ?? 0.1;
|
|
502
|
-
const groupHeight = this.maxBarSize
|
|
503
|
-
? Math.min(bandwidth, this.maxBarSize * totalSeries)
|
|
504
|
-
: bandwidth;
|
|
505
|
-
const totalGapSpace = groupHeight * gap * (totalSeries - 1);
|
|
506
|
-
const availableHeight = groupHeight - totalGapSpace;
|
|
507
|
-
barHeight = availableHeight / totalSeries;
|
|
508
|
-
const gapSize = totalSeries > 1 ? groupHeight * gap : 0;
|
|
509
|
-
barOffset =
|
|
510
|
-
(bandwidth - groupHeight) / 2 +
|
|
511
|
-
seriesIndex * (barHeight + gapSize);
|
|
512
|
-
}
|
|
513
|
-
else if (mode === 'layer') {
|
|
514
|
-
const totalSeries = stackingContext?.totalSeries ?? 1;
|
|
515
|
-
const seriesIndex = stackingContext?.seriesIndex ?? 0;
|
|
516
|
-
const maxHeight = this.maxBarSize
|
|
517
|
-
? Math.min(bandwidth, this.maxBarSize)
|
|
518
|
-
: bandwidth;
|
|
519
|
-
const scaleFactor = 1 - (seriesIndex / totalSeries) * 0.7;
|
|
520
|
-
barHeight = maxHeight * scaleFactor;
|
|
521
|
-
barOffset = (bandwidth - barHeight) / 2;
|
|
522
|
-
}
|
|
523
|
-
else {
|
|
524
|
-
barHeight = this.maxBarSize
|
|
525
|
-
? Math.min(bandwidth, this.maxBarSize)
|
|
526
|
-
: bandwidth;
|
|
527
|
-
barOffset = (bandwidth - barHeight) / 2;
|
|
528
|
-
}
|
|
529
|
-
const domain = x.domain();
|
|
530
|
-
const baselineValue = domain[0] >= 0 ? Math.max(0, domain[0]) : domain[0];
|
|
531
|
-
const xBaseline = x(baselineValue) || 0;
|
|
387
|
+
const { thickness: barHeight, offset: barOffset } = getBarSlotLayout(bandwidth, mode, this.maxBarSize, stackingContext?.totalSeries ?? 1, stackingContext?.seriesIndex ?? 0, stackingContext?.gap ?? 0.1);
|
|
532
388
|
const config = this.valueLabel;
|
|
533
389
|
const position = config.position || 'outside';
|
|
534
390
|
const insidePosition = config.insidePosition || 'top';
|
|
@@ -545,33 +401,18 @@ export class Bar {
|
|
|
545
401
|
.attr('class', `bar-value-labels-${sanitizeForCSS(this.dataKey)}`);
|
|
546
402
|
data.forEach((d, i) => {
|
|
547
403
|
const categoryKey = String(d[xKey]);
|
|
548
|
-
const
|
|
549
|
-
const
|
|
404
|
+
const rawValue = parseValue(d[this.dataKey]);
|
|
405
|
+
const renderedValue = this.getRenderedValue(rawValue, 'horizontal');
|
|
406
|
+
const valueText = String(rawValue);
|
|
550
407
|
const yPos = getScalePosition(y, d[xKey], yScaleType);
|
|
551
408
|
const barColor = this.colorAdapter
|
|
552
409
|
? this.colorAdapter(d, i)
|
|
553
410
|
: this.fill;
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
barLeft = Math.min(xBaseline, xPos);
|
|
560
|
-
barRight = Math.max(xBaseline, xPos);
|
|
561
|
-
}
|
|
562
|
-
else if (mode === 'percent') {
|
|
563
|
-
const cumulative = stackingContext?.cumulativeData.get(categoryKey) ?? 0;
|
|
564
|
-
const total = stackingContext?.totalData.get(categoryKey) ?? 1;
|
|
565
|
-
const percentCumulative = (cumulative / total) * 100;
|
|
566
|
-
const percentValue = (value / total) * 100;
|
|
567
|
-
barLeft = x(percentCumulative) || 0;
|
|
568
|
-
barRight = x(percentCumulative + percentValue) || 0;
|
|
569
|
-
}
|
|
570
|
-
else {
|
|
571
|
-
const cumulative = stackingContext?.cumulativeData.get(categoryKey) ?? 0;
|
|
572
|
-
barLeft = x(cumulative) || 0;
|
|
573
|
-
barRight = x(cumulative + value) || 0;
|
|
574
|
-
}
|
|
411
|
+
const { start, end } = getBarValueRange(categoryKey, renderedValue, stackingContext);
|
|
412
|
+
const bounds = getScaledValueRange(x, start, end);
|
|
413
|
+
const barLeft = bounds.min;
|
|
414
|
+
const barRight = bounds.max;
|
|
415
|
+
const isNegative = renderedValue < 0;
|
|
575
416
|
const barWidth = Math.abs(barRight - barLeft);
|
|
576
417
|
const barCenterY = yPos +
|
|
577
418
|
(yScaleType === 'band' ? barOffset : -barHeight / 2) +
|
|
@@ -590,16 +431,17 @@ export class Bar {
|
|
|
590
431
|
const labelY = barCenterY;
|
|
591
432
|
let shouldRender = true;
|
|
592
433
|
if (position === 'outside') {
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
434
|
+
const plotLeft = Math.min(...x.range());
|
|
435
|
+
const plotRight = Math.max(...x.range());
|
|
436
|
+
labelX = isNegative
|
|
437
|
+
? barLeft - boxWidth / 2 - 4
|
|
438
|
+
: barRight + boxWidth / 2 + 4;
|
|
439
|
+
if ((!isNegative && labelX + boxWidth / 2 > plotRight) ||
|
|
440
|
+
(isNegative && labelX - boxWidth / 2 < plotLeft)) {
|
|
598
441
|
shouldRender = false;
|
|
599
442
|
}
|
|
600
443
|
}
|
|
601
444
|
else {
|
|
602
|
-
// Map top/middle/bottom to start/middle/end for horizontal
|
|
603
445
|
if (mode === 'layer' && insidePosition === 'bottom') {
|
|
604
446
|
// Bottom labels in layer mode are visually ambiguous and often hidden by overlap.
|
|
605
447
|
shouldRender = false;
|
|
@@ -607,33 +449,31 @@ export class Bar {
|
|
|
607
449
|
else {
|
|
608
450
|
const { inset, minPadding } = getLabelSpacing(mode);
|
|
609
451
|
switch (insidePosition) {
|
|
610
|
-
case 'top':
|
|
611
|
-
labelX =
|
|
452
|
+
case 'top':
|
|
453
|
+
labelX = isNegative
|
|
454
|
+
? barRight - boxWidth / 2 - inset
|
|
455
|
+
: barLeft + boxWidth / 2 + inset;
|
|
612
456
|
break;
|
|
613
457
|
case 'middle':
|
|
614
458
|
labelX = (barLeft + barRight) / 2;
|
|
615
459
|
break;
|
|
616
|
-
case 'bottom':
|
|
617
|
-
labelX =
|
|
460
|
+
case 'bottom':
|
|
461
|
+
labelX = isNegative
|
|
462
|
+
? barLeft + boxWidth / 2 + inset
|
|
463
|
+
: barRight - boxWidth / 2 - inset;
|
|
618
464
|
break;
|
|
619
465
|
}
|
|
620
|
-
// Check if it fits inside the bar
|
|
621
466
|
if (boxWidth + minPadding > barWidth) {
|
|
622
467
|
shouldRender = false;
|
|
623
468
|
}
|
|
624
|
-
// In layer mode, check the label fits in the visible gap
|
|
625
|
-
// before the next layer's bar end
|
|
626
469
|
if (shouldRender &&
|
|
627
|
-
|
|
628
|
-
insidePosition
|
|
629
|
-
|
|
630
|
-
const
|
|
631
|
-
if (
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
if (labelRight + LAYER_LABEL_GAP > nextBarRight) {
|
|
635
|
-
shouldRender = false;
|
|
636
|
-
}
|
|
470
|
+
position === 'inside' &&
|
|
471
|
+
insidePosition !== 'middle') {
|
|
472
|
+
const labelLeft = labelX - boxWidth / 2;
|
|
473
|
+
const labelRight = labelX + boxWidth / 2;
|
|
474
|
+
if (labelLeft < barLeft + 1 ||
|
|
475
|
+
labelRight > barRight - 1) {
|
|
476
|
+
shouldRender = false;
|
|
637
477
|
}
|
|
638
478
|
}
|
|
639
479
|
}
|