@internetstiftelsen/charts 0.5.1 → 0.6.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 +4 -2
- package/area.d.ts +26 -0
- package/area.js +331 -0
- package/base-chart.d.ts +10 -3
- package/base-chart.js +33 -19
- package/chart-interface.d.ts +1 -1
- package/donut-center-content.d.ts +1 -1
- package/donut-center-content.js +7 -6
- package/donut-chart.d.ts +1 -0
- package/donut-chart.js +8 -1
- package/export-tabular.d.ts +4 -4
- package/export-tabular.js +8 -0
- package/export-xlsx.d.ts +2 -2
- package/gauge-chart.d.ts +138 -0
- package/gauge-chart.js +1041 -0
- package/grouped-data.d.ts +19 -0
- package/grouped-data.js +122 -0
- package/grouped-tabular.d.ts +26 -0
- package/grouped-tabular.js +149 -0
- package/package.json +1 -1
- package/pie-chart.d.ts +80 -0
- package/pie-chart.js +665 -0
- package/theme.d.ts +1 -0
- package/theme.js +25 -16
- package/tooltip.d.ts +3 -2
- package/tooltip.js +40 -39
- package/types.d.ts +46 -0
- package/validation.d.ts +4 -0
- package/validation.js +25 -0
- package/x-axis.d.ts +10 -2
- package/x-axis.js +205 -15
- package/xy-chart.d.ts +10 -1
- package/xy-chart.js +307 -90
package/x-axis.js
CHANGED
|
@@ -1,6 +1,23 @@
|
|
|
1
1
|
import { axisBottom } from 'd3';
|
|
2
2
|
import { measureTextWidth, truncateText, wrapText, mergeDeep } from './utils.js';
|
|
3
|
+
import { GROUPED_CATEGORY_ID_KEY, GROUPED_CATEGORY_LABEL_KEY, GROUPED_GAP_TICK_PREFIX, GROUPED_GROUP_LABEL_KEY, } from './grouped-data.js';
|
|
3
4
|
export class XAxis {
|
|
5
|
+
resolveGroupLabelStyle(theme) {
|
|
6
|
+
const axisGroupLabel = theme.axis.groupLabel;
|
|
7
|
+
const fontFamily = axisGroupLabel?.fontFamily ?? theme.axis.fontFamily;
|
|
8
|
+
const fontWeight = axisGroupLabel?.fontWeight ?? '700';
|
|
9
|
+
const parsedFontSize = parseFloat(axisGroupLabel?.fontSize ?? theme.axis.fontSize);
|
|
10
|
+
const fontSize = Number.isFinite(parsedFontSize)
|
|
11
|
+
? parsedFontSize
|
|
12
|
+
: this.fontSize;
|
|
13
|
+
const color = axisGroupLabel?.color ?? '#111827';
|
|
14
|
+
return {
|
|
15
|
+
fontFamily,
|
|
16
|
+
fontWeight,
|
|
17
|
+
fontSize,
|
|
18
|
+
color,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
4
21
|
constructor(config) {
|
|
5
22
|
Object.defineProperty(this, "type", {
|
|
6
23
|
enumerable: true,
|
|
@@ -14,6 +31,30 @@ export class XAxis {
|
|
|
14
31
|
writable: true,
|
|
15
32
|
value: void 0
|
|
16
33
|
});
|
|
34
|
+
Object.defineProperty(this, "labelKey", {
|
|
35
|
+
enumerable: true,
|
|
36
|
+
configurable: true,
|
|
37
|
+
writable: true,
|
|
38
|
+
value: void 0
|
|
39
|
+
});
|
|
40
|
+
Object.defineProperty(this, "groupLabelKey", {
|
|
41
|
+
enumerable: true,
|
|
42
|
+
configurable: true,
|
|
43
|
+
writable: true,
|
|
44
|
+
value: void 0
|
|
45
|
+
});
|
|
46
|
+
Object.defineProperty(this, "showGroupLabels", {
|
|
47
|
+
enumerable: true,
|
|
48
|
+
configurable: true,
|
|
49
|
+
writable: true,
|
|
50
|
+
value: void 0
|
|
51
|
+
});
|
|
52
|
+
Object.defineProperty(this, "groupLabelGap", {
|
|
53
|
+
enumerable: true,
|
|
54
|
+
configurable: true,
|
|
55
|
+
writable: true,
|
|
56
|
+
value: void 0
|
|
57
|
+
});
|
|
17
58
|
Object.defineProperty(this, "rotatedLabels", {
|
|
18
59
|
enumerable: true,
|
|
19
60
|
configurable: true,
|
|
@@ -88,6 +129,10 @@ export class XAxis {
|
|
|
88
129
|
value: void 0
|
|
89
130
|
});
|
|
90
131
|
this.dataKey = config?.dataKey;
|
|
132
|
+
this.labelKey = config?.labelKey;
|
|
133
|
+
this.groupLabelKey = config?.groupLabelKey;
|
|
134
|
+
this.showGroupLabels = config?.showGroupLabels ?? false;
|
|
135
|
+
this.groupLabelGap = config?.groupLabelGap ?? 10;
|
|
91
136
|
this.rotatedLabels = config?.rotatedLabels ?? false;
|
|
92
137
|
this.maxLabelWidth = config?.maxLabelWidth;
|
|
93
138
|
this.oversizedBehavior = config?.oversizedBehavior ?? 'truncate';
|
|
@@ -100,6 +145,10 @@ export class XAxis {
|
|
|
100
145
|
getExportConfig() {
|
|
101
146
|
return {
|
|
102
147
|
dataKey: this.dataKey,
|
|
148
|
+
labelKey: this.labelKey,
|
|
149
|
+
groupLabelKey: this.groupLabelKey,
|
|
150
|
+
showGroupLabels: this.showGroupLabels,
|
|
151
|
+
groupLabelGap: this.groupLabelGap,
|
|
103
152
|
rotatedLabels: this.rotatedLabels,
|
|
104
153
|
maxLabelWidth: this.maxLabelWidth,
|
|
105
154
|
oversizedBehavior: this.oversizedBehavior,
|
|
@@ -137,6 +186,9 @@ export class XAxis {
|
|
|
137
186
|
this.wrapLineCount > 1) {
|
|
138
187
|
height += (this.wrapLineCount - 1) * this.fontSize * 1.2;
|
|
139
188
|
}
|
|
189
|
+
if (this.showGroupLabels) {
|
|
190
|
+
height += this.groupLabelGap + this.fontSize + 5;
|
|
191
|
+
}
|
|
140
192
|
return {
|
|
141
193
|
width: 0, // X-axis spans full width
|
|
142
194
|
height,
|
|
@@ -185,18 +237,33 @@ export class XAxis {
|
|
|
185
237
|
else {
|
|
186
238
|
this.estimatedHeight = this.tickPadding + textHeight + 5;
|
|
187
239
|
}
|
|
240
|
+
if (this.showGroupLabels) {
|
|
241
|
+
const groupLabelStyle = this.resolveGroupLabelStyle(theme);
|
|
242
|
+
this.estimatedHeight +=
|
|
243
|
+
this.groupLabelGap + groupLabelStyle.fontSize + 5;
|
|
244
|
+
}
|
|
188
245
|
this.wrapLineCount = Math.max(this.wrapLineCount, maxLines);
|
|
189
246
|
}
|
|
190
247
|
clearEstimatedSpace() {
|
|
191
248
|
this.estimatedHeight = null;
|
|
192
249
|
}
|
|
193
|
-
render(svg, x, theme, yPosition) {
|
|
250
|
+
render(svg, x, theme, yPosition, data = []) {
|
|
251
|
+
const labelLookup = this.buildLabelLookup(data);
|
|
194
252
|
const axisGenerator = axisBottom(x)
|
|
195
253
|
.tickSizeOuter(0)
|
|
196
254
|
.tickSize(0)
|
|
197
255
|
.tickPadding(this.tickPadding);
|
|
198
|
-
|
|
199
|
-
|
|
256
|
+
if (labelLookup) {
|
|
257
|
+
axisGenerator.tickFormat((value) => {
|
|
258
|
+
const key = String(value);
|
|
259
|
+
if (!labelLookup.has(key)) {
|
|
260
|
+
return '';
|
|
261
|
+
}
|
|
262
|
+
return labelLookup.get(key) ?? '';
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
else if (this.tickFormat) {
|
|
266
|
+
// Apply tick formatting if specified
|
|
200
267
|
if (typeof this.tickFormat === 'function') {
|
|
201
268
|
axisGenerator.tickFormat(this.tickFormat);
|
|
202
269
|
}
|
|
@@ -226,6 +293,110 @@ export class XAxis {
|
|
|
226
293
|
// Apply auto-hiding for overlapping labels
|
|
227
294
|
this.applyAutoHiding(axis, x);
|
|
228
295
|
axis.selectAll('.domain').remove();
|
|
296
|
+
if (this.showGroupLabels) {
|
|
297
|
+
this.renderGroupLabels(svg, x, theme, yPosition, data);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
buildLabelLookup(data) {
|
|
301
|
+
if (!this.dataKey || data.length === 0) {
|
|
302
|
+
return null;
|
|
303
|
+
}
|
|
304
|
+
const labelKey = this.labelKey ??
|
|
305
|
+
(this.dataKey === GROUPED_CATEGORY_ID_KEY
|
|
306
|
+
? GROUPED_CATEGORY_LABEL_KEY
|
|
307
|
+
: undefined);
|
|
308
|
+
if (!labelKey) {
|
|
309
|
+
return null;
|
|
310
|
+
}
|
|
311
|
+
const lookup = new Map();
|
|
312
|
+
data.forEach((row) => {
|
|
313
|
+
const rawDomainValue = row[this.dataKey];
|
|
314
|
+
const labelValue = row[labelKey];
|
|
315
|
+
lookup.set(String(rawDomainValue), String(labelValue ?? ''));
|
|
316
|
+
});
|
|
317
|
+
return lookup;
|
|
318
|
+
}
|
|
319
|
+
renderGroupLabels(svg, x, theme, yPosition, data) {
|
|
320
|
+
const groupRanges = this.buildGroupRanges(x, data);
|
|
321
|
+
if (groupRanges.length === 0) {
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
const yOffset = this.tickPadding + this.fontSize + this.groupLabelGap;
|
|
325
|
+
const groupLabelStyle = this.resolveGroupLabelStyle(theme);
|
|
326
|
+
const groupLayer = svg
|
|
327
|
+
.append('g')
|
|
328
|
+
.attr('class', 'x-axis-group-labels')
|
|
329
|
+
.attr('transform', `translate(0,${yPosition})`);
|
|
330
|
+
groupLayer
|
|
331
|
+
.selectAll('text')
|
|
332
|
+
.data(groupRanges)
|
|
333
|
+
.join('text')
|
|
334
|
+
.attr('x', (range) => (range.start + range.end) / 2)
|
|
335
|
+
.attr('y', yOffset + groupLabelStyle.fontSize)
|
|
336
|
+
.attr('text-anchor', 'middle')
|
|
337
|
+
.attr('font-size', groupLabelStyle.fontSize)
|
|
338
|
+
.attr('font-family', groupLabelStyle.fontFamily)
|
|
339
|
+
.attr('font-weight', groupLabelStyle.fontWeight)
|
|
340
|
+
.attr('fill', groupLabelStyle.color)
|
|
341
|
+
.text((range) => range.label);
|
|
342
|
+
}
|
|
343
|
+
buildGroupRanges(scale, data) {
|
|
344
|
+
if (!this.dataKey ||
|
|
345
|
+
data.length === 0 ||
|
|
346
|
+
typeof scale.domain !== 'function' ||
|
|
347
|
+
typeof scale.bandwidth !== 'function') {
|
|
348
|
+
return [];
|
|
349
|
+
}
|
|
350
|
+
const groupLabelKey = this.groupLabelKey ??
|
|
351
|
+
(this.dataKey === GROUPED_CATEGORY_ID_KEY
|
|
352
|
+
? GROUPED_GROUP_LABEL_KEY
|
|
353
|
+
: undefined);
|
|
354
|
+
if (!groupLabelKey) {
|
|
355
|
+
return [];
|
|
356
|
+
}
|
|
357
|
+
const domain = scale.domain().map((value) => String(value));
|
|
358
|
+
const bandwidth = scale.bandwidth();
|
|
359
|
+
if (domain.length === 0 || bandwidth <= 0) {
|
|
360
|
+
return [];
|
|
361
|
+
}
|
|
362
|
+
const groupLookup = new Map();
|
|
363
|
+
data.forEach((row) => {
|
|
364
|
+
groupLookup.set(String(row[this.dataKey]), String(row[groupLabelKey] ?? ''));
|
|
365
|
+
});
|
|
366
|
+
const ranges = [];
|
|
367
|
+
let currentLabel = null;
|
|
368
|
+
let startIndex = 0;
|
|
369
|
+
const pushRange = (from, to, label) => {
|
|
370
|
+
const startValue = domain[from];
|
|
371
|
+
const endValue = domain[to];
|
|
372
|
+
const start = scale(startValue);
|
|
373
|
+
const end = scale(endValue);
|
|
374
|
+
if (start === undefined || end === undefined) {
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
ranges.push({
|
|
378
|
+
label,
|
|
379
|
+
start: start,
|
|
380
|
+
end: end + bandwidth,
|
|
381
|
+
});
|
|
382
|
+
};
|
|
383
|
+
domain.forEach((domainValue, index) => {
|
|
384
|
+
const groupLabel = groupLookup.get(domainValue) ?? '';
|
|
385
|
+
if (currentLabel === null) {
|
|
386
|
+
currentLabel = groupLabel;
|
|
387
|
+
startIndex = index;
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
if (groupLabel !== currentLabel) {
|
|
391
|
+
pushRange(startIndex, index - 1, currentLabel);
|
|
392
|
+
currentLabel = groupLabel;
|
|
393
|
+
startIndex = index;
|
|
394
|
+
}
|
|
395
|
+
});
|
|
396
|
+
if (currentLabel !== null) {
|
|
397
|
+
pushRange(startIndex, domain.length - 1, currentLabel);
|
|
398
|
+
}
|
|
399
|
+
return ranges.filter((range) => range.label.trim() !== '');
|
|
229
400
|
}
|
|
230
401
|
applyLabelConstraints(axisGroup, svg, fontSize, fontFamily, fontWeight) {
|
|
231
402
|
if (!this.maxLabelWidth)
|
|
@@ -288,15 +459,36 @@ export class XAxis {
|
|
|
288
459
|
applyAutoHiding(axisGroup, scale) {
|
|
289
460
|
if (!this.autoHideOverlapping)
|
|
290
461
|
return;
|
|
291
|
-
const
|
|
292
|
-
.selectAll('
|
|
462
|
+
const tickElements = axisGroup
|
|
463
|
+
.selectAll('.tick')
|
|
293
464
|
.nodes();
|
|
294
|
-
const
|
|
295
|
-
|
|
465
|
+
const labelEntries = tickElements
|
|
466
|
+
.map((tickElement) => {
|
|
467
|
+
const textElement = tickElement.querySelector('text');
|
|
468
|
+
const tickValue = String(tickElement
|
|
469
|
+
.__data__ ?? '');
|
|
470
|
+
const isSyntheticGapTick = tickValue.startsWith(GROUPED_GAP_TICK_PREFIX);
|
|
471
|
+
if (isSyntheticGapTick && textElement) {
|
|
472
|
+
textElement.style.visibility = 'hidden';
|
|
473
|
+
}
|
|
474
|
+
return {
|
|
475
|
+
textElement,
|
|
476
|
+
isSyntheticGapTick,
|
|
477
|
+
};
|
|
478
|
+
})
|
|
479
|
+
.filter((entry) => {
|
|
480
|
+
return !entry.isSyntheticGapTick && entry.textElement !== null;
|
|
481
|
+
})
|
|
482
|
+
.map((entry) => {
|
|
483
|
+
return entry.textElement;
|
|
484
|
+
});
|
|
485
|
+
const labelCount = labelEntries.length;
|
|
486
|
+
if (labelCount <= 1) {
|
|
296
487
|
return;
|
|
488
|
+
}
|
|
297
489
|
// Measure all label widths
|
|
298
490
|
let maxLabelWidth = 0;
|
|
299
|
-
for (const textEl of
|
|
491
|
+
for (const textEl of labelEntries) {
|
|
300
492
|
const bbox = textEl.getBBox();
|
|
301
493
|
// For rotated labels, use the horizontal footprint
|
|
302
494
|
const effectiveWidth = this.rotatedLabels
|
|
@@ -304,11 +496,9 @@ export class XAxis {
|
|
|
304
496
|
: bbox.width;
|
|
305
497
|
maxLabelWidth = Math.max(maxLabelWidth, effectiveWidth);
|
|
306
498
|
}
|
|
307
|
-
// Calculate available space per label
|
|
308
|
-
|
|
309
|
-
const availableSpace =
|
|
310
|
-
? bandwidth
|
|
311
|
-
: (scale.range()[1] - scale.range()[0]) / labelCount;
|
|
499
|
+
// Calculate available space per real label only.
|
|
500
|
+
// This deliberately ignores synthetic grouped-gap ticks.
|
|
501
|
+
const availableSpace = (scale.range()[1] - scale.range()[0]) / labelCount;
|
|
312
502
|
// Calculate skip interval
|
|
313
503
|
const requiredSpace = maxLabelWidth + this.minLabelGap;
|
|
314
504
|
const skipInterval = Math.ceil(requiredSpace / availableSpace);
|
|
@@ -316,7 +506,7 @@ export class XAxis {
|
|
|
316
506
|
if (skipInterval <= 1)
|
|
317
507
|
return;
|
|
318
508
|
// Apply visibility
|
|
319
|
-
|
|
509
|
+
labelEntries.forEach((textEl, index) => {
|
|
320
510
|
const isFirst = index === 0;
|
|
321
511
|
const isLast = index === labelCount - 1;
|
|
322
512
|
const isAtInterval = index % skipInterval === 0;
|
|
@@ -336,7 +526,7 @@ export class XAxis {
|
|
|
336
526
|
if (lastVisibleIntervalIndex !== labelCount - 1 &&
|
|
337
527
|
labelCount - 1 - lastVisibleIntervalIndex < skipInterval) {
|
|
338
528
|
// Hide the last interval label to avoid overlap with preserved last label
|
|
339
|
-
const textEl =
|
|
529
|
+
const textEl = labelEntries[lastVisibleIntervalIndex];
|
|
340
530
|
if (textEl && lastVisibleIntervalIndex !== 0) {
|
|
341
531
|
textEl.style.visibility = 'hidden';
|
|
342
532
|
}
|
package/xy-chart.d.ts
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
|
-
import type { BarStackConfig } from './types.js';
|
|
2
1
|
import { BaseChart, type BaseChartConfig } from './base-chart.js';
|
|
3
2
|
import type { ChartComponent } from './chart-interface.js';
|
|
3
|
+
import { type AreaStackConfig, type BarStackConfig } from './types.js';
|
|
4
4
|
export type XYChartConfig = BaseChartConfig & {
|
|
5
5
|
barStack?: BarStackConfig;
|
|
6
|
+
areaStack?: AreaStackConfig;
|
|
6
7
|
};
|
|
7
8
|
export declare class XYChart extends BaseChart {
|
|
8
9
|
private readonly series;
|
|
9
10
|
private barStackMode;
|
|
10
11
|
private barStackGap;
|
|
12
|
+
private areaStackMode;
|
|
11
13
|
constructor(config: XYChartConfig);
|
|
12
14
|
addChild(component: ChartComponent): this;
|
|
13
15
|
protected getExportComponents(): ChartComponent[];
|
|
@@ -16,9 +18,16 @@ export declare class XYChart extends BaseChart {
|
|
|
16
18
|
protected prepareLayout(): void;
|
|
17
19
|
protected renderChart(): void;
|
|
18
20
|
private getXKey;
|
|
21
|
+
private getCategoryScaleType;
|
|
22
|
+
private getVisibleSeries;
|
|
19
23
|
private setupScales;
|
|
20
24
|
private isHorizontalOrientation;
|
|
25
|
+
private collectSeriesValues;
|
|
26
|
+
private getStackedAreaGroups;
|
|
27
|
+
private buildBandDomainWithGroupGaps;
|
|
21
28
|
private createScale;
|
|
29
|
+
private getSeriesTooltipValue;
|
|
22
30
|
private renderSeries;
|
|
23
31
|
private computeStackingData;
|
|
32
|
+
private computeAreaStackingContexts;
|
|
24
33
|
}
|