@internetstiftelsen/charts 0.14.3 → 0.16.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 +7 -4
- package/dist/base-chart.d.ts +1 -2
- package/dist/base-chart.js +2 -21
- package/dist/chart-group.d.ts +1 -2
- package/dist/chart-group.js +1 -16
- package/dist/theme.js +6 -0
- package/dist/tooltip/dom.d.ts +56 -0
- package/dist/tooltip/dom.js +438 -0
- package/dist/tooltip/geometry.d.ts +18 -0
- package/dist/tooltip/geometry.js +395 -0
- package/dist/tooltip/types.d.ts +77 -0
- package/dist/tooltip/types.js +24 -0
- package/dist/tooltip/xy-interaction.d.ts +33 -0
- package/dist/tooltip/xy-interaction.js +608 -0
- package/dist/tooltip.d.ts +4 -74
- package/dist/tooltip.js +41 -1444
- package/dist/types.d.ts +3 -2
- package/dist/x-axis.d.ts +11 -1
- package/dist/x-axis.js +150 -10
- package/dist/xy-chart.d.ts +1 -0
- package/dist/xy-chart.js +10 -4
- package/docs/chart-group.md +0 -1
- package/docs/components.md +19 -25
- package/docs/xy-chart.md +3 -10
- package/package.json +17 -18
- package/dist/export-pdf.d.ts +0 -8
- package/dist/export-pdf.js +0 -67
package/dist/types.d.ts
CHANGED
|
@@ -9,7 +9,7 @@ export type GroupedDataGroup = {
|
|
|
9
9
|
data: DataItem[];
|
|
10
10
|
};
|
|
11
11
|
export type ChartData = DataItem[] | GroupedDataGroup[];
|
|
12
|
-
export type ExportFormat = 'svg' | 'json' | 'csv' | 'xlsx' | 'png' | 'jpg'
|
|
12
|
+
export type ExportFormat = 'svg' | 'json' | 'csv' | 'xlsx' | 'png' | 'jpg';
|
|
13
13
|
export type ExportOptions = {
|
|
14
14
|
download?: boolean;
|
|
15
15
|
filename?: string;
|
|
@@ -21,7 +21,6 @@ export type ExportOptions = {
|
|
|
21
21
|
pixelRatio?: number;
|
|
22
22
|
backgroundColor?: string;
|
|
23
23
|
sheetName?: string;
|
|
24
|
-
pdfMargin?: number;
|
|
25
24
|
};
|
|
26
25
|
export type ExportRenderContext = {
|
|
27
26
|
format: ExportFormat;
|
|
@@ -270,6 +269,8 @@ export type XAxisConfigBase = {
|
|
|
270
269
|
groupLabelKey?: string;
|
|
271
270
|
showGroupLabels?: boolean;
|
|
272
271
|
groupLabelGap?: number;
|
|
272
|
+
groupLabelMaxWidth?: number;
|
|
273
|
+
groupLabelOversizedBehavior?: LabelOversizedBehavior;
|
|
273
274
|
rotatedLabels?: boolean;
|
|
274
275
|
maxLabelWidth?: number;
|
|
275
276
|
oversizedBehavior?: LabelOversizedBehavior;
|
package/dist/x-axis.d.ts
CHANGED
|
@@ -9,6 +9,8 @@ export declare class XAxis implements LayoutAwareComponent<XAxisConfigBase> {
|
|
|
9
9
|
readonly groupLabelKey?: string;
|
|
10
10
|
readonly showGroupLabels: boolean;
|
|
11
11
|
readonly groupLabelGap: number;
|
|
12
|
+
private readonly groupLabelMaxWidth?;
|
|
13
|
+
private readonly groupLabelOversizedBehavior;
|
|
12
14
|
private readonly rotatedLabels;
|
|
13
15
|
private readonly tickPadding;
|
|
14
16
|
private fontSize;
|
|
@@ -16,6 +18,7 @@ export declare class XAxis implements LayoutAwareComponent<XAxisConfigBase> {
|
|
|
16
18
|
private readonly oversizedBehavior;
|
|
17
19
|
private readonly tickFormat;
|
|
18
20
|
private wrapLineCount;
|
|
21
|
+
private groupLabelWrapLineCount;
|
|
19
22
|
private estimatedHeight;
|
|
20
23
|
private estimatedTickLabelVerticalFootprint;
|
|
21
24
|
private readonly autoHideOverlapping;
|
|
@@ -31,7 +34,7 @@ export declare class XAxis implements LayoutAwareComponent<XAxisConfigBase> {
|
|
|
31
34
|
* Returns the space required by the x-axis
|
|
32
35
|
*/
|
|
33
36
|
getRequiredSpace(): ComponentSpace;
|
|
34
|
-
estimateLayoutSpace(labels: unknown[], theme: ChartTheme, svg: SVGSVGElement): void;
|
|
37
|
+
estimateLayoutSpace(labels: unknown[], theme: ChartTheme, svg: SVGSVGElement, data?: DataItem[], estimatedXAxisRangeWidth?: number): void;
|
|
35
38
|
clearEstimatedSpace(): void;
|
|
36
39
|
private getTickLabelVerticalFootprint;
|
|
37
40
|
render(svg: Selection<SVGSVGElement, undefined, null, undefined>, x: D3Scale, theme: ChartTheme, yPosition: number, data?: DataItem[]): void;
|
|
@@ -39,15 +42,22 @@ export declare class XAxis implements LayoutAwareComponent<XAxisConfigBase> {
|
|
|
39
42
|
private renderGroupLabels;
|
|
40
43
|
private buildGroupRanges;
|
|
41
44
|
private applyLabelConstraints;
|
|
45
|
+
private applyGroupLabelConstraints;
|
|
46
|
+
private resolveGroupLabelMaxWidth;
|
|
42
47
|
private wrapTextElement;
|
|
43
48
|
private addTitleTooltip;
|
|
44
49
|
private applyAutoHiding;
|
|
45
50
|
private measureLabel;
|
|
51
|
+
private estimateGroupLabelWrapLineCount;
|
|
52
|
+
private buildGroupLabelEstimates;
|
|
53
|
+
private resolveEstimatedGroupLabelMaxWidth;
|
|
54
|
+
private getLabelBlockHeight;
|
|
46
55
|
private setEstimatedDimensions;
|
|
47
56
|
private createAxisGenerator;
|
|
48
57
|
private applyAxisTextConstraints;
|
|
49
58
|
private applyLabelRotation;
|
|
50
59
|
private resolveGroupRangeInput;
|
|
60
|
+
private resolveGroupLabelKey;
|
|
51
61
|
private collectAutoHideLabels;
|
|
52
62
|
private measureMaxAutoHideLabelWidth;
|
|
53
63
|
private applyAutoHideVisibility;
|
package/dist/x-axis.js
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { axisBottom } from 'd3';
|
|
2
2
|
import { measureTextWidth, truncateText, wrapText, mergeDeep } from './utils.js';
|
|
3
3
|
import { GROUPED_CATEGORY_ID_KEY, GROUPED_CATEGORY_LABEL_KEY, GROUPED_GAP_TICK_PREFIX, GROUPED_GROUP_LABEL_KEY, } from './grouped-data.js';
|
|
4
|
+
const GROUP_LABEL_HORIZONTAL_PADDING = 4;
|
|
4
5
|
const DEFAULT_X_AXIS_CONFIG = {
|
|
5
6
|
display: true,
|
|
6
7
|
showGroupLabels: false,
|
|
7
8
|
groupLabelGap: 10,
|
|
9
|
+
groupLabelOversizedBehavior: 'truncate',
|
|
8
10
|
rotatedLabels: false,
|
|
9
11
|
oversizedBehavior: 'truncate',
|
|
10
12
|
tickFormat: null,
|
|
@@ -81,6 +83,18 @@ export class XAxis {
|
|
|
81
83
|
writable: true,
|
|
82
84
|
value: void 0
|
|
83
85
|
});
|
|
86
|
+
Object.defineProperty(this, "groupLabelMaxWidth", {
|
|
87
|
+
enumerable: true,
|
|
88
|
+
configurable: true,
|
|
89
|
+
writable: true,
|
|
90
|
+
value: void 0
|
|
91
|
+
});
|
|
92
|
+
Object.defineProperty(this, "groupLabelOversizedBehavior", {
|
|
93
|
+
enumerable: true,
|
|
94
|
+
configurable: true,
|
|
95
|
+
writable: true,
|
|
96
|
+
value: void 0
|
|
97
|
+
});
|
|
84
98
|
Object.defineProperty(this, "rotatedLabels", {
|
|
85
99
|
enumerable: true,
|
|
86
100
|
configurable: true,
|
|
@@ -123,6 +137,12 @@ export class XAxis {
|
|
|
123
137
|
writable: true,
|
|
124
138
|
value: 1
|
|
125
139
|
});
|
|
140
|
+
Object.defineProperty(this, "groupLabelWrapLineCount", {
|
|
141
|
+
enumerable: true,
|
|
142
|
+
configurable: true,
|
|
143
|
+
writable: true,
|
|
144
|
+
value: 1
|
|
145
|
+
});
|
|
126
146
|
Object.defineProperty(this, "estimatedHeight", {
|
|
127
147
|
enumerable: true,
|
|
128
148
|
configurable: true,
|
|
@@ -169,6 +189,9 @@ export class XAxis {
|
|
|
169
189
|
this.groupLabelKey = resolvedConfig.groupLabelKey;
|
|
170
190
|
this.showGroupLabels = resolvedConfig.showGroupLabels;
|
|
171
191
|
this.groupLabelGap = resolvedConfig.groupLabelGap;
|
|
192
|
+
this.groupLabelMaxWidth = resolvedConfig.groupLabelMaxWidth;
|
|
193
|
+
this.groupLabelOversizedBehavior =
|
|
194
|
+
resolvedConfig.groupLabelOversizedBehavior;
|
|
172
195
|
this.rotatedLabels = resolvedConfig.rotatedLabels;
|
|
173
196
|
this.maxLabelWidth = resolvedConfig.maxLabelWidth;
|
|
174
197
|
this.oversizedBehavior = resolvedConfig.oversizedBehavior;
|
|
@@ -186,6 +209,8 @@ export class XAxis {
|
|
|
186
209
|
groupLabelKey: this.groupLabelKey,
|
|
187
210
|
showGroupLabels: this.showGroupLabels,
|
|
188
211
|
groupLabelGap: this.groupLabelGap,
|
|
212
|
+
groupLabelMaxWidth: this.groupLabelMaxWidth,
|
|
213
|
+
groupLabelOversizedBehavior: this.groupLabelOversizedBehavior,
|
|
189
214
|
rotatedLabels: this.rotatedLabels,
|
|
190
215
|
maxLabelWidth: this.maxLabelWidth,
|
|
191
216
|
oversizedBehavior: this.oversizedBehavior,
|
|
@@ -231,7 +256,10 @@ export class XAxis {
|
|
|
231
256
|
height += (this.wrapLineCount - 1) * this.fontSize * 1.2;
|
|
232
257
|
}
|
|
233
258
|
if (this.showGroupLabels) {
|
|
234
|
-
height +=
|
|
259
|
+
height +=
|
|
260
|
+
this.groupLabelGap +
|
|
261
|
+
this.getLabelBlockHeight(this.fontSize, this.groupLabelWrapLineCount) +
|
|
262
|
+
5;
|
|
235
263
|
}
|
|
236
264
|
return {
|
|
237
265
|
width: 0, // X-axis spans full width
|
|
@@ -239,11 +267,12 @@ export class XAxis {
|
|
|
239
267
|
position: 'bottom',
|
|
240
268
|
};
|
|
241
269
|
}
|
|
242
|
-
estimateLayoutSpace(labels, theme, svg) {
|
|
270
|
+
estimateLayoutSpace(labels, theme, svg, data = [], estimatedXAxisRangeWidth) {
|
|
243
271
|
if (!labels.length) {
|
|
244
272
|
this.estimatedHeight = null;
|
|
245
273
|
this.estimatedTickLabelVerticalFootprint = null;
|
|
246
274
|
this.wrapLineCount = 1;
|
|
275
|
+
this.groupLabelWrapLineCount = 1;
|
|
247
276
|
return;
|
|
248
277
|
}
|
|
249
278
|
this.fontSize = this.resolveFontSizeValue(theme.axis.fontSize, this.fontSize);
|
|
@@ -261,12 +290,14 @@ export class XAxis {
|
|
|
261
290
|
maxLines = Math.max(maxLines, measurement.lines);
|
|
262
291
|
maxWidth = Math.max(maxWidth, measurement.width);
|
|
263
292
|
}
|
|
293
|
+
this.groupLabelWrapLineCount = this.estimateGroupLabelWrapLineCount(data, theme, svg, estimatedXAxisRangeWidth);
|
|
264
294
|
this.setEstimatedDimensions(maxWidth, maxLines, theme);
|
|
265
295
|
this.wrapLineCount = maxLines;
|
|
266
296
|
}
|
|
267
297
|
clearEstimatedSpace() {
|
|
268
298
|
this.estimatedHeight = null;
|
|
269
299
|
this.estimatedTickLabelVerticalFootprint = null;
|
|
300
|
+
this.groupLabelWrapLineCount = 1;
|
|
270
301
|
}
|
|
271
302
|
getTickLabelVerticalFootprint() {
|
|
272
303
|
if (this.estimatedTickLabelVerticalFootprint !== null) {
|
|
@@ -345,6 +376,7 @@ export class XAxis {
|
|
|
345
376
|
.attr('font-weight', groupLabelStyle.fontWeight)
|
|
346
377
|
.attr('fill', groupLabelStyle.color)
|
|
347
378
|
.text((range) => range.label);
|
|
379
|
+
this.applyGroupLabelConstraints(groupLayer.selectAll('text'), svg.node(), groupLabelStyle);
|
|
348
380
|
}
|
|
349
381
|
buildGroupRanges(scale, data) {
|
|
350
382
|
const input = this.resolveGroupRangeInput(scale, data);
|
|
@@ -422,12 +454,55 @@ export class XAxis {
|
|
|
422
454
|
}
|
|
423
455
|
});
|
|
424
456
|
}
|
|
425
|
-
|
|
457
|
+
applyGroupLabelConstraints(groupLabels, svg, groupLabelStyle) {
|
|
458
|
+
groupLabels.each((range, i, nodes) => {
|
|
459
|
+
const textEl = nodes[i];
|
|
460
|
+
const maxWidth = this.resolveGroupLabelMaxWidth(range);
|
|
461
|
+
if (maxWidth <= 0) {
|
|
462
|
+
textEl.style.visibility = 'hidden';
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
465
|
+
const originalText = textEl.textContent || '';
|
|
466
|
+
const textWidth = measureTextWidth(originalText, groupLabelStyle.fontSize, groupLabelStyle.fontFamily, groupLabelStyle.fontWeight, svg);
|
|
467
|
+
if (textWidth <= maxWidth) {
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
470
|
+
switch (this.groupLabelOversizedBehavior) {
|
|
471
|
+
case 'truncate': {
|
|
472
|
+
const result = truncateText(originalText, maxWidth, groupLabelStyle.fontSize, groupLabelStyle.fontFamily, groupLabelStyle.fontWeight, svg);
|
|
473
|
+
textEl.textContent = result.text;
|
|
474
|
+
if (result.truncated) {
|
|
475
|
+
this.addTitleTooltip(textEl, originalText);
|
|
476
|
+
}
|
|
477
|
+
break;
|
|
478
|
+
}
|
|
479
|
+
case 'wrap': {
|
|
480
|
+
const lines = wrapText(originalText, maxWidth, groupLabelStyle.fontSize, groupLabelStyle.fontFamily, groupLabelStyle.fontWeight, svg);
|
|
481
|
+
this.wrapTextElement(textEl, lines, originalText, groupLabelStyle.fontSize, false);
|
|
482
|
+
break;
|
|
483
|
+
}
|
|
484
|
+
case 'hide': {
|
|
485
|
+
textEl.style.visibility = 'hidden';
|
|
486
|
+
break;
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
});
|
|
490
|
+
}
|
|
491
|
+
resolveGroupLabelMaxWidth(range) {
|
|
492
|
+
if (this.groupLabelMaxWidth !== undefined) {
|
|
493
|
+
return this.groupLabelMaxWidth;
|
|
494
|
+
}
|
|
495
|
+
const rangeWidth = Math.abs(range.end - range.start);
|
|
496
|
+
return Math.max(0, rangeWidth - GROUP_LABEL_HORIZONTAL_PADDING * 2);
|
|
497
|
+
}
|
|
498
|
+
wrapTextElement(textEl, lines, originalText, fontSize = this.fontSize, updateWrapLineCount = true) {
|
|
426
499
|
// Clear existing content
|
|
427
500
|
textEl.textContent = '';
|
|
428
|
-
const lineHeight =
|
|
501
|
+
const lineHeight = fontSize * 1.2;
|
|
429
502
|
// Update wrap line count for height calculation
|
|
430
|
-
|
|
503
|
+
if (updateWrapLineCount) {
|
|
504
|
+
this.wrapLineCount = Math.max(this.wrapLineCount, lines.length);
|
|
505
|
+
}
|
|
431
506
|
lines.forEach((line, i) => {
|
|
432
507
|
const tspan = document.createElementNS('http://www.w3.org/2000/svg', 'tspan');
|
|
433
508
|
tspan.textContent = line;
|
|
@@ -482,6 +557,66 @@ export class XAxis {
|
|
|
482
557
|
lines: 1,
|
|
483
558
|
};
|
|
484
559
|
}
|
|
560
|
+
estimateGroupLabelWrapLineCount(data, theme, svg, estimatedXAxisRangeWidth) {
|
|
561
|
+
if (!this.showGroupLabels ||
|
|
562
|
+
this.groupLabelOversizedBehavior !== 'wrap') {
|
|
563
|
+
return 1;
|
|
564
|
+
}
|
|
565
|
+
const groupLabels = this.buildGroupLabelEstimates(data);
|
|
566
|
+
if (groupLabels.length === 0) {
|
|
567
|
+
return 1;
|
|
568
|
+
}
|
|
569
|
+
const groupLabelStyle = this.resolveGroupLabelStyle(theme);
|
|
570
|
+
const totalItemCount = data.length;
|
|
571
|
+
return groupLabels.reduce((maxLines, groupLabel) => {
|
|
572
|
+
const maxWidth = this.groupLabelMaxWidth ??
|
|
573
|
+
this.resolveEstimatedGroupLabelMaxWidth(groupLabel.itemCount, totalItemCount, estimatedXAxisRangeWidth);
|
|
574
|
+
if (maxWidth === undefined || maxWidth <= 0) {
|
|
575
|
+
return maxLines;
|
|
576
|
+
}
|
|
577
|
+
const lines = wrapText(groupLabel.label, maxWidth, groupLabelStyle.fontSize, groupLabelStyle.fontFamily, groupLabelStyle.fontWeight, svg);
|
|
578
|
+
return Math.max(maxLines, lines.length || 1);
|
|
579
|
+
}, 1);
|
|
580
|
+
}
|
|
581
|
+
buildGroupLabelEstimates(data) {
|
|
582
|
+
const groupLabelKey = this.resolveGroupLabelKey();
|
|
583
|
+
if (!this.dataKey || !groupLabelKey || data.length === 0) {
|
|
584
|
+
return [];
|
|
585
|
+
}
|
|
586
|
+
const labels = [];
|
|
587
|
+
let previousLabel = null;
|
|
588
|
+
let currentEstimate = null;
|
|
589
|
+
data.forEach((row) => {
|
|
590
|
+
const label = String(row[groupLabelKey] ?? '');
|
|
591
|
+
if (label === previousLabel) {
|
|
592
|
+
if (currentEstimate) {
|
|
593
|
+
currentEstimate.itemCount += 1;
|
|
594
|
+
}
|
|
595
|
+
return;
|
|
596
|
+
}
|
|
597
|
+
previousLabel = label;
|
|
598
|
+
if (label.trim() !== '') {
|
|
599
|
+
currentEstimate = { label, itemCount: 1 };
|
|
600
|
+
labels.push(currentEstimate);
|
|
601
|
+
return;
|
|
602
|
+
}
|
|
603
|
+
currentEstimate = null;
|
|
604
|
+
});
|
|
605
|
+
return labels;
|
|
606
|
+
}
|
|
607
|
+
resolveEstimatedGroupLabelMaxWidth(itemCount, totalItemCount, estimatedXAxisRangeWidth) {
|
|
608
|
+
if (estimatedXAxisRangeWidth === undefined ||
|
|
609
|
+
totalItemCount <= 0 ||
|
|
610
|
+
itemCount <= 0) {
|
|
611
|
+
return undefined;
|
|
612
|
+
}
|
|
613
|
+
const groupWidth = (estimatedXAxisRangeWidth * itemCount) / totalItemCount;
|
|
614
|
+
return Math.max(0, groupWidth - GROUP_LABEL_HORIZONTAL_PADDING * 2);
|
|
615
|
+
}
|
|
616
|
+
getLabelBlockHeight(fontSize, lineCount) {
|
|
617
|
+
const lineHeight = fontSize * 1.2;
|
|
618
|
+
return fontSize + Math.max(0, lineCount - 1) * lineHeight;
|
|
619
|
+
}
|
|
485
620
|
setEstimatedDimensions(maxWidth, maxLines, theme) {
|
|
486
621
|
const lineHeight = this.fontSize * 1.2;
|
|
487
622
|
const textHeight = lineHeight * maxLines;
|
|
@@ -500,7 +635,9 @@ export class XAxis {
|
|
|
500
635
|
if (this.showGroupLabels) {
|
|
501
636
|
const groupLabelStyle = this.resolveGroupLabelStyle(theme);
|
|
502
637
|
this.estimatedHeight +=
|
|
503
|
-
this.groupLabelGap +
|
|
638
|
+
this.groupLabelGap +
|
|
639
|
+
this.getLabelBlockHeight(groupLabelStyle.fontSize, this.groupLabelWrapLineCount) +
|
|
640
|
+
5;
|
|
504
641
|
}
|
|
505
642
|
}
|
|
506
643
|
createAxisGenerator(x, labelLookup) {
|
|
@@ -548,10 +685,7 @@ export class XAxis {
|
|
|
548
685
|
typeof scale.bandwidth !== 'function') {
|
|
549
686
|
return null;
|
|
550
687
|
}
|
|
551
|
-
const groupLabelKey = this.
|
|
552
|
-
(this.dataKey === GROUPED_CATEGORY_ID_KEY
|
|
553
|
-
? GROUPED_GROUP_LABEL_KEY
|
|
554
|
-
: undefined);
|
|
688
|
+
const groupLabelKey = this.resolveGroupLabelKey();
|
|
555
689
|
if (!groupLabelKey) {
|
|
556
690
|
return null;
|
|
557
691
|
}
|
|
@@ -570,6 +704,12 @@ export class XAxis {
|
|
|
570
704
|
groupLookup,
|
|
571
705
|
};
|
|
572
706
|
}
|
|
707
|
+
resolveGroupLabelKey() {
|
|
708
|
+
return (this.groupLabelKey ??
|
|
709
|
+
(this.dataKey === GROUPED_CATEGORY_ID_KEY
|
|
710
|
+
? GROUPED_GROUP_LABEL_KEY
|
|
711
|
+
: undefined));
|
|
712
|
+
}
|
|
573
713
|
collectAutoHideLabels(axisGroup) {
|
|
574
714
|
return axisGroup
|
|
575
715
|
.selectAll('.tick')
|
package/dist/xy-chart.d.ts
CHANGED
|
@@ -26,6 +26,7 @@ export declare class XYChart extends BaseChart {
|
|
|
26
26
|
update(data: ChartData): void;
|
|
27
27
|
protected applyComponentOverrides(overrides: Map<ChartComponentBase, ChartComponentBase>): () => void;
|
|
28
28
|
protected prepareLayout(context: BaseLayoutContext): void;
|
|
29
|
+
private getEstimatedXAxisRangeWidth;
|
|
29
30
|
private getYAxisEstimateLabels;
|
|
30
31
|
private createContinuousScaleForLayoutEstimate;
|
|
31
32
|
protected renderChart({ svg, plotGroup, plotArea, }: BaseRenderContext): void;
|
package/dist/xy-chart.js
CHANGED
|
@@ -155,6 +155,9 @@ export class XYChart extends BaseChart {
|
|
|
155
155
|
super.prepareLayout(context);
|
|
156
156
|
this.xAxis?.clearEstimatedSpace?.();
|
|
157
157
|
this.yAxis?.clearEstimatedSpace?.();
|
|
158
|
+
if (this.yAxis) {
|
|
159
|
+
this.yAxis.estimateLayoutSpace?.(this.getYAxisEstimateLabels(), this.renderTheme, context.svgNode);
|
|
160
|
+
}
|
|
158
161
|
if (this.xAxis) {
|
|
159
162
|
const xKey = this.getXKey();
|
|
160
163
|
const labelKey = this.xAxis.labelKey;
|
|
@@ -164,12 +167,15 @@ export class XYChart extends BaseChart {
|
|
|
164
167
|
}
|
|
165
168
|
return item[xKey];
|
|
166
169
|
});
|
|
167
|
-
this.xAxis.estimateLayoutSpace?.(labels, this.renderTheme, context.svgNode);
|
|
168
|
-
}
|
|
169
|
-
if (this.yAxis) {
|
|
170
|
-
this.yAxis.estimateLayoutSpace?.(this.getYAxisEstimateLabels(), this.renderTheme, context.svgNode);
|
|
170
|
+
this.xAxis.estimateLayoutSpace?.(labels, this.renderTheme, context.svgNode, this.data, this.getEstimatedXAxisRangeWidth());
|
|
171
171
|
}
|
|
172
172
|
}
|
|
173
|
+
getEstimatedXAxisRangeWidth() {
|
|
174
|
+
const yAxisWidth = this.yAxis?.getRequiredSpace().width ?? 0;
|
|
175
|
+
const { left, right } = this.renderTheme.margins;
|
|
176
|
+
const xScalePadding = 10;
|
|
177
|
+
return Math.max(0, this.width - left - right - yAxisWidth - xScalePadding);
|
|
178
|
+
}
|
|
173
179
|
getYAxisEstimateLabels() {
|
|
174
180
|
if (!this.yAxis) {
|
|
175
181
|
return [];
|
package/docs/chart-group.md
CHANGED
package/docs/components.md
CHANGED
|
@@ -14,6 +14,8 @@ new XAxis({
|
|
|
14
14
|
groupLabelKey?: string, // Optional key used for second-row grouped labels
|
|
15
15
|
showGroupLabels?: boolean, // Show second-row grouped labels (default: false)
|
|
16
16
|
groupLabelGap?: number, // Vertical gap between tick row and grouped row
|
|
17
|
+
groupLabelMaxWidth?: number, // Optional cap for grouped-label width
|
|
18
|
+
groupLabelOversizedBehavior?: 'truncate' | 'wrap' | 'hide', // Defaults to truncate
|
|
17
19
|
rotatedLabels?: boolean, // Rotate tick labels -45deg
|
|
18
20
|
maxLabelWidth?: number, // Optional cap for tick-label width
|
|
19
21
|
oversizedBehavior?: 'truncate' | 'wrap' | 'hide', // Only applies when maxLabelWidth is set
|
|
@@ -24,7 +26,11 @@ new XAxis({
|
|
|
24
26
|
})
|
|
25
27
|
```
|
|
26
28
|
|
|
27
|
-
Grouped label styles come from `theme.axis.groupLabel` and are bold by
|
|
29
|
+
Grouped label styles come from `theme.axis.groupLabel` and are bold by
|
|
30
|
+
default. When `groupLabelMaxWidth` is omitted, grouped labels are automatically
|
|
31
|
+
capped to their rendered group range so adjacent group labels do not collide.
|
|
32
|
+
Use `groupLabelOversizedBehavior` to truncate, wrap, or hide labels that exceed
|
|
33
|
+
that cap.
|
|
28
34
|
|
|
29
35
|
### Example
|
|
30
36
|
|
|
@@ -143,27 +149,18 @@ Tooltip modes:
|
|
|
143
149
|
- `shared` - One tooltip per hovered category/value group
|
|
144
150
|
- `split` - Default. One tooltip per visible series at the hovered category/value group
|
|
145
151
|
|
|
146
|
-
|
|
152
|
+
Shared tooltips omit series whose value is `null` or `undefined` for the hovered
|
|
153
|
+
data point. Shared `customFormatter` callbacks receive that filtered series list.
|
|
147
154
|
|
|
148
|
-
|
|
149
|
-
|
|
155
|
+
Use `position: 'side' | 'vertical'` for split tooltip placement. For bars,
|
|
156
|
+
`barAnchorPosition` controls whether connectors point to the `top` or `middle`
|
|
157
|
+
of each bar.
|
|
150
158
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
directional arrows and avoid overlapping on the same side of the chart when
|
|
154
|
-
possible.
|
|
159
|
+
Tooltips default to a `280px` max width. Set `transition.show: true` to fade and
|
|
160
|
+
slide tooltips between hovered positions. Tooltip colors use `theme.tooltip`.
|
|
155
161
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
Tooltips default to a `280px` max width so longer content wraps. Set `maxWidth`
|
|
160
|
-
to choose a different cap in pixels.
|
|
161
|
-
|
|
162
|
-
Set `transition.show: true` to fade tooltips in and out. Tooltip position and
|
|
163
|
-
connector geometry update immediately; only opacity and the small entrance
|
|
164
|
-
offset transition.
|
|
165
|
-
|
|
166
|
-
Tooltip box, text, connector, and arrow colors use `theme.tooltip`.
|
|
162
|
+
`defaultResponsiveConfig` switches tooltip components to `mode: 'shared'` at
|
|
163
|
+
the `sm` breakpoint so compact XY charts use one grouped tooltip by default.
|
|
167
164
|
|
|
168
165
|
Formatter, label formatter, and custom formatter output is inserted as HTML.
|
|
169
166
|
Only return trusted content or sanitize user-provided strings before returning
|
|
@@ -344,7 +341,7 @@ new Text({
|
|
|
344
341
|
|
|
345
342
|
Components can provide `exportHooks` to adjust export-only settings or mutate
|
|
346
343
|
the exported SVG right before serialization. Hooks run only for visual exports
|
|
347
|
-
(`svg`, `png`, `jpg
|
|
344
|
+
(`svg`, `png`, `jpg`) and never touch the live chart instance. Use
|
|
348
345
|
`beforeRender` to return config overrides and `before` to mutate the export
|
|
349
346
|
SVG.
|
|
350
347
|
|
|
@@ -403,24 +400,21 @@ breakpoints in chart constructor config (see
|
|
|
403
400
|
- `xlsx` (lazy-loads optional `write-excel-file`)
|
|
404
401
|
- `png`
|
|
405
402
|
- `jpg`
|
|
406
|
-
- `pdf` (lazy-loads optional `jspdf`)
|
|
407
403
|
|
|
408
404
|
```typescript
|
|
409
405
|
await chart.export('png', { download: true });
|
|
410
406
|
await chart.export('csv', { download: true, delimiter: ';' });
|
|
411
407
|
await chart.export('xlsx', { download: true, sheetName: 'Data' });
|
|
412
|
-
await chart.export('pdf', { download: true, pdfMargin: 16 });
|
|
413
408
|
```
|
|
414
409
|
|
|
415
410
|
Export option highlights:
|
|
416
411
|
|
|
417
412
|
- `columns`: choose specific columns for `csv` and `xlsx`
|
|
418
413
|
- `delimiter`: CSV delimiter (default `,`)
|
|
419
|
-
- `pixelRatio`: render scale for `png
|
|
420
|
-
- `backgroundColor`: defaults transparent for `png
|
|
414
|
+
- `pixelRatio`: render scale for `png` and `jpg`
|
|
415
|
+
- `backgroundColor`: defaults transparent for `png` and white for `jpg`
|
|
421
416
|
- `jpegQuality`: JPEG quality for `jpg` (default `0.92`)
|
|
422
417
|
- `sheetName`: sheet name for `xlsx` (default `Sheet1`)
|
|
423
|
-
- `pdfMargin`: page margin for `pdf` (default `0`)
|
|
424
418
|
|
|
425
419
|
`json` export returns only the chart data payload
|
|
426
420
|
|
package/docs/xy-chart.md
CHANGED
|
@@ -78,6 +78,7 @@ Rules for grouped datasets:
|
|
|
78
78
|
- Remaining keys are treated as metric columns.
|
|
79
79
|
- Each `group` must be a non-empty string.
|
|
80
80
|
- Group labels are rendered on a second x-axis row when `XAxis.showGroupLabels` is enabled.
|
|
81
|
+
- Group labels are capped to their rendered group range by default. Set `XAxis.groupLabelMaxWidth` and `XAxis.groupLabelOversizedBehavior` to override truncation, wrapping, or hiding.
|
|
81
82
|
- CSV/XLSX exports use spreadsheet-style grouped rows (blank first two headers, blank continuation group cells, no spacer rows).
|
|
82
83
|
|
|
83
84
|
### Scale Options
|
|
@@ -514,16 +515,8 @@ Bar charts support different stacking modes:
|
|
|
514
515
|
- `percent` - 100% stacked bars
|
|
515
516
|
- `layer` - Overlapping bars
|
|
516
517
|
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
`new Tooltip({ mode: 'shared' | 'split', position: 'side' | 'vertical' })`
|
|
520
|
-
to override the grouping and split-tooltip placement.
|
|
521
|
-
For bars, set `barAnchorPosition: 'top' | 'middle'` to choose whether split
|
|
522
|
-
tooltips point to the top or middle of each bar.
|
|
523
|
-
Tooltips default to a `280px` max width. Set `maxWidth` to choose a different
|
|
524
|
-
cap in pixels.
|
|
525
|
-
Use `transition: { show: true, duration: 120, easing: 'ease-out' }` to opt in
|
|
526
|
-
to softer tooltip opacity transitions without delaying position updates.
|
|
518
|
+
Add `new Tooltip()` for hover and focus tooltips. XY charts use split tooltips
|
|
519
|
+
by default; see the [Tooltip component](./components.md#tooltip) for options.
|
|
527
520
|
|
|
528
521
|
Use `barStack.reverseSeries: true` to reverse bar series display order for
|
|
529
522
|
rendering, legend entries, and split tooltip ordering without changing data
|
package/package.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "0.
|
|
2
|
+
"version": "0.16.0",
|
|
3
3
|
"name": "@internetstiftelsen/charts",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"sideEffects": false,
|
|
@@ -38,48 +38,47 @@
|
|
|
38
38
|
"d3-cloud": "^1.2.9"
|
|
39
39
|
},
|
|
40
40
|
"optionalDependencies": {
|
|
41
|
-
"
|
|
42
|
-
"write-excel-file": "^4.0.7"
|
|
41
|
+
"write-excel-file": "^4.1.1"
|
|
43
42
|
},
|
|
44
43
|
"devDependencies": {
|
|
45
44
|
"@eslint/js": "^10.0.1",
|
|
46
45
|
"@handsontable/react-wrapper": "^17.1.0",
|
|
47
46
|
"@internetstiftelsen/styleguide": "^5.1.27",
|
|
48
|
-
"@radix-ui/react-label": "^2.1.
|
|
49
|
-
"@radix-ui/react-select": "^2.
|
|
50
|
-
"@radix-ui/react-switch": "^1.
|
|
51
|
-
"@radix-ui/react-tabs": "^1.1.
|
|
52
|
-
"@speed-highlight/core": "^1.2.
|
|
53
|
-
"@tailwindcss/vite": "^4.3.
|
|
47
|
+
"@radix-ui/react-label": "^2.1.9",
|
|
48
|
+
"@radix-ui/react-select": "^2.3.0",
|
|
49
|
+
"@radix-ui/react-switch": "^1.3.0",
|
|
50
|
+
"@radix-ui/react-tabs": "^1.1.14",
|
|
51
|
+
"@speed-highlight/core": "^1.2.17",
|
|
52
|
+
"@tailwindcss/vite": "^4.3.1",
|
|
54
53
|
"@testing-library/dom": "^10.4.1",
|
|
55
54
|
"@testing-library/jest-dom": "^6.9.1",
|
|
56
55
|
"@testing-library/react": "^16.3.2",
|
|
57
56
|
"@types/d3": "^7.4.3",
|
|
58
57
|
"@types/d3-cloud": "^1.2.9",
|
|
59
|
-
"@types/node": "^25.9.
|
|
60
|
-
"@types/react": "^19.2.
|
|
58
|
+
"@types/node": "^25.9.3",
|
|
59
|
+
"@types/react": "^19.2.17",
|
|
61
60
|
"@types/react-dom": "^19.2.3",
|
|
62
61
|
"@vitejs/plugin-react-swc": "^4.3.1",
|
|
63
62
|
"class-variance-authority": "^0.7.1",
|
|
64
63
|
"clsx": "^2.1.1",
|
|
65
|
-
"eslint": "^10.
|
|
64
|
+
"eslint": "^10.5.0",
|
|
66
65
|
"eslint-plugin-react-hooks": "^7.1.1",
|
|
67
66
|
"eslint-plugin-react-refresh": "^0.5.2",
|
|
68
67
|
"globals": "^17.6.0",
|
|
69
68
|
"handsontable": "^17.1.0",
|
|
70
69
|
"jsdom": "^29.1.1",
|
|
71
|
-
"lucide-react": "^1.
|
|
72
|
-
"prettier": "3.8.
|
|
73
|
-
"radix-ui": "^1.
|
|
70
|
+
"lucide-react": "^1.18.0",
|
|
71
|
+
"prettier": "3.8.4",
|
|
72
|
+
"radix-ui": "^1.5.0",
|
|
74
73
|
"react": "^19.2.7",
|
|
75
74
|
"react-dom": "^19.2.7",
|
|
76
|
-
"sass": "^1.
|
|
75
|
+
"sass": "^1.101.0",
|
|
77
76
|
"tailwind-merge": "^3.6.0",
|
|
78
|
-
"tailwindcss": "^4.3.
|
|
77
|
+
"tailwindcss": "^4.3.1",
|
|
79
78
|
"tsc-alias": "^1.8.17",
|
|
80
79
|
"tw-animate-css": "^1.4.0",
|
|
81
80
|
"typescript": "~6.0.3",
|
|
82
|
-
"typescript-eslint": "^8.
|
|
81
|
+
"typescript-eslint": "^8.61.0",
|
|
83
82
|
"vite": "^8.0.16",
|
|
84
83
|
"vitest": "^4.1.8"
|
|
85
84
|
}
|
package/dist/export-pdf.d.ts
DELETED
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
type JsPdfLoader = () => Promise<unknown>;
|
|
2
|
-
export type PDFExportOptions = {
|
|
3
|
-
width: number;
|
|
4
|
-
height: number;
|
|
5
|
-
margin: number;
|
|
6
|
-
};
|
|
7
|
-
export declare function exportPDFBlob(imageBlob: Blob, options: PDFExportOptions, loader?: JsPdfLoader): Promise<Blob>;
|
|
8
|
-
export {};
|
package/dist/export-pdf.js
DELETED
|
@@ -1,67 +0,0 @@
|
|
|
1
|
-
function isObject(value) {
|
|
2
|
-
return typeof value === 'object' && value !== null;
|
|
3
|
-
}
|
|
4
|
-
function resolveJsPdfConstructor(moduleValue) {
|
|
5
|
-
if (!isObject(moduleValue)) {
|
|
6
|
-
throw new Error('Invalid "jspdf" module export');
|
|
7
|
-
}
|
|
8
|
-
const moduleCandidate = moduleValue;
|
|
9
|
-
const defaultCandidate = moduleCandidate.default;
|
|
10
|
-
let ctor = moduleCandidate.jsPDF;
|
|
11
|
-
if (!ctor && isObject(defaultCandidate)) {
|
|
12
|
-
ctor = defaultCandidate.jsPDF;
|
|
13
|
-
}
|
|
14
|
-
if (!ctor && typeof defaultCandidate === 'function') {
|
|
15
|
-
ctor = defaultCandidate;
|
|
16
|
-
}
|
|
17
|
-
if (typeof ctor !== 'function') {
|
|
18
|
-
throw new Error('Invalid "jspdf" module export');
|
|
19
|
-
}
|
|
20
|
-
return ctor;
|
|
21
|
-
}
|
|
22
|
-
async function defaultJsPdfLoader() {
|
|
23
|
-
return import('jspdf');
|
|
24
|
-
}
|
|
25
|
-
function blobToDataUrl(blob) {
|
|
26
|
-
return new Promise((resolve, reject) => {
|
|
27
|
-
const reader = new FileReader();
|
|
28
|
-
reader.onload = () => resolve(String(reader.result));
|
|
29
|
-
reader.onerror = () => reject(new Error('Failed to create PDF image data'));
|
|
30
|
-
reader.readAsDataURL(blob);
|
|
31
|
-
});
|
|
32
|
-
}
|
|
33
|
-
export async function exportPDFBlob(imageBlob, options, loader = defaultJsPdfLoader) {
|
|
34
|
-
let jsPdfModule;
|
|
35
|
-
try {
|
|
36
|
-
jsPdfModule = await loader();
|
|
37
|
-
}
|
|
38
|
-
catch {
|
|
39
|
-
throw new Error('PDF export requires optional dependency "jspdf". Install optional dependency "jspdf".');
|
|
40
|
-
}
|
|
41
|
-
const JsPdfCtor = resolveJsPdfConstructor(jsPdfModule);
|
|
42
|
-
const margin = Math.max(0, options.margin);
|
|
43
|
-
const pageWidth = Math.max(1, options.width + margin * 2);
|
|
44
|
-
const pageHeight = Math.max(1, options.height + margin * 2);
|
|
45
|
-
const orientation = pageWidth >= pageHeight ? 'landscape' : 'portrait';
|
|
46
|
-
const pdf = new JsPdfCtor({
|
|
47
|
-
unit: 'px',
|
|
48
|
-
format: [pageWidth, pageHeight],
|
|
49
|
-
orientation,
|
|
50
|
-
hotfixes: ['px_scaling'],
|
|
51
|
-
});
|
|
52
|
-
const actualPageWidth = pdf.internal.pageSize.getWidth();
|
|
53
|
-
const actualPageHeight = pdf.internal.pageSize.getHeight();
|
|
54
|
-
const availableWidth = actualPageWidth - margin * 2;
|
|
55
|
-
const availableHeight = actualPageHeight - margin * 2;
|
|
56
|
-
if (availableWidth <= 0 || availableHeight <= 0) {
|
|
57
|
-
throw new Error('Invalid PDF margin: no drawable area remains');
|
|
58
|
-
}
|
|
59
|
-
const imageDataUrl = await blobToDataUrl(imageBlob);
|
|
60
|
-
const scale = Math.min(availableWidth / options.width, availableHeight / options.height);
|
|
61
|
-
const drawWidth = options.width * scale;
|
|
62
|
-
const drawHeight = options.height * scale;
|
|
63
|
-
const x = (actualPageWidth - drawWidth) / 2;
|
|
64
|
-
const y = (actualPageHeight - drawHeight) / 2;
|
|
65
|
-
pdf.addImage(imageDataUrl, 'PNG', x, y, drawWidth, drawHeight);
|
|
66
|
-
return pdf.output('blob');
|
|
67
|
-
}
|