@internetstiftelsen/charts 0.9.2 → 0.10.1
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 +137 -3
- package/dist/area.d.ts +2 -0
- package/dist/area.js +39 -31
- package/dist/bar.d.ts +20 -1
- package/dist/bar.js +395 -519
- package/dist/base-chart.d.ts +21 -1
- package/dist/base-chart.js +166 -93
- package/dist/chart-group.d.ts +137 -0
- package/dist/chart-group.js +1155 -0
- package/dist/chart-interface.d.ts +1 -1
- package/dist/donut-center-content.d.ts +1 -0
- package/dist/donut-center-content.js +21 -38
- package/dist/donut-chart.js +30 -15
- package/dist/gauge-chart.d.ts +20 -0
- package/dist/gauge-chart.js +229 -133
- 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 +45 -38
- package/dist/line.js +3 -1
- package/dist/pie-chart.d.ts +3 -0
- package/dist/pie-chart.js +45 -19
- package/dist/scatter.d.ts +16 -0
- package/dist/scatter.js +165 -0
- package/dist/tooltip.d.ts +2 -1
- package/dist/tooltip.js +21 -25
- package/dist/types.d.ts +19 -1
- package/dist/utils.js +11 -19
- package/dist/validation.d.ts +4 -0
- package/dist/validation.js +19 -0
- package/dist/x-axis.d.ts +10 -0
- package/dist/x-axis.js +190 -149
- package/dist/xy-chart.d.ts +40 -1
- package/dist/xy-chart.js +488 -165
- package/dist/y-axis.d.ts +7 -2
- package/dist/y-axis.js +99 -10
- package/docs/chart-group.md +213 -0
- package/docs/components.md +321 -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 +517 -0
- package/package.json +6 -4
package/dist/utils.js
CHANGED
|
@@ -161,27 +161,19 @@ export function wrapText(text, maxWidth, fontSize, fontFamily, fontWeight, svg)
|
|
|
161
161
|
const width = measureTextWidth(testLine, fontSize, fontFamily, fontWeight, svg);
|
|
162
162
|
if (width <= maxWidth) {
|
|
163
163
|
currentLine = testLine;
|
|
164
|
+
continue;
|
|
164
165
|
}
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
// Break the word into multiple lines by character
|
|
173
|
-
const wordSegments = breakWord(word, maxWidth, fontSize, fontFamily, fontWeight, svg);
|
|
174
|
-
// Add all segments except the last as complete lines
|
|
175
|
-
for (let i = 0; i < wordSegments.length - 1; i++) {
|
|
176
|
-
lines.push(wordSegments[i]);
|
|
177
|
-
}
|
|
178
|
-
// The last segment becomes the current line (may be combined with next word)
|
|
179
|
-
currentLine = wordSegments[wordSegments.length - 1];
|
|
180
|
-
}
|
|
181
|
-
else {
|
|
182
|
-
currentLine = word;
|
|
183
|
-
}
|
|
166
|
+
if (currentLine) {
|
|
167
|
+
lines.push(currentLine);
|
|
168
|
+
}
|
|
169
|
+
const wordWidth = measureTextWidth(word, fontSize, fontFamily, fontWeight, svg);
|
|
170
|
+
if (wordWidth <= maxWidth) {
|
|
171
|
+
currentLine = word;
|
|
172
|
+
continue;
|
|
184
173
|
}
|
|
174
|
+
const wordSegments = breakWord(word, maxWidth, fontSize, fontFamily, fontWeight, svg);
|
|
175
|
+
lines.push(...wordSegments.slice(0, -1));
|
|
176
|
+
currentLine = wordSegments[wordSegments.length - 1] ?? '';
|
|
185
177
|
}
|
|
186
178
|
if (currentLine) {
|
|
187
179
|
lines.push(currentLine);
|
package/dist/validation.d.ts
CHANGED
|
@@ -26,6 +26,10 @@ export declare class ChartValidator {
|
|
|
26
26
|
* Validates scale configuration
|
|
27
27
|
*/
|
|
28
28
|
static validateScaleConfig(scaleType: string, domain: ScaleDomainValue[] | undefined): void;
|
|
29
|
+
/**
|
|
30
|
+
* Validates that explicit numeric bar domains include zero so bars retain a truthful baseline
|
|
31
|
+
*/
|
|
32
|
+
static validateBarDomainIncludesZero(domain: ScaleDomainValue[] | undefined): void;
|
|
29
33
|
/**
|
|
30
34
|
* Warns about potential issues without throwing errors
|
|
31
35
|
*/
|
package/dist/validation.js
CHANGED
|
@@ -91,6 +91,25 @@ export class ChartValidator {
|
|
|
91
91
|
}
|
|
92
92
|
}
|
|
93
93
|
}
|
|
94
|
+
/**
|
|
95
|
+
* Validates that explicit numeric bar domains include zero so bars retain a truthful baseline
|
|
96
|
+
*/
|
|
97
|
+
static validateBarDomainIncludesZero(domain) {
|
|
98
|
+
if (!domain || domain.length < 2) {
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
const numericDomain = domain.filter((value) => {
|
|
102
|
+
return typeof value === 'number' && Number.isFinite(value);
|
|
103
|
+
});
|
|
104
|
+
if (numericDomain.length < 2) {
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
const minValue = Math.min(...numericDomain);
|
|
108
|
+
const maxValue = Math.max(...numericDomain);
|
|
109
|
+
if (minValue > 0 || maxValue < 0) {
|
|
110
|
+
throw new ChartValidationError('Bar charts require explicit numeric domains to include 0 so bars can render from a zero baseline');
|
|
111
|
+
}
|
|
112
|
+
}
|
|
94
113
|
/**
|
|
95
114
|
* Warns about potential issues without throwing errors
|
|
96
115
|
*/
|
package/dist/x-axis.d.ts
CHANGED
|
@@ -42,4 +42,14 @@ export declare class XAxis implements LayoutAwareComponent<XAxisConfigBase> {
|
|
|
42
42
|
private wrapTextElement;
|
|
43
43
|
private addTitleTooltip;
|
|
44
44
|
private applyAutoHiding;
|
|
45
|
+
private measureLabel;
|
|
46
|
+
private setEstimatedDimensions;
|
|
47
|
+
private createAxisGenerator;
|
|
48
|
+
private applyAxisTextConstraints;
|
|
49
|
+
private applyLabelRotation;
|
|
50
|
+
private resolveGroupRangeInput;
|
|
51
|
+
private collectAutoHideLabels;
|
|
52
|
+
private measureMaxAutoHideLabelWidth;
|
|
53
|
+
private applyAutoHideVisibility;
|
|
54
|
+
private hideLastOverlappingIntervalLabel;
|
|
45
55
|
}
|
package/dist/x-axis.js
CHANGED
|
@@ -1,6 +1,17 @@
|
|
|
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 DEFAULT_X_AXIS_CONFIG = {
|
|
5
|
+
display: true,
|
|
6
|
+
showGroupLabels: false,
|
|
7
|
+
groupLabelGap: 10,
|
|
8
|
+
rotatedLabels: false,
|
|
9
|
+
oversizedBehavior: 'truncate',
|
|
10
|
+
tickFormat: null,
|
|
11
|
+
autoHideOverlapping: false,
|
|
12
|
+
minLabelGap: 8,
|
|
13
|
+
preserveEndLabels: true,
|
|
14
|
+
};
|
|
4
15
|
export class XAxis {
|
|
5
16
|
resolveFontSizeValue(fontSize, fallback) {
|
|
6
17
|
if (typeof fontSize === 'number' && Number.isFinite(fontSize)) {
|
|
@@ -148,20 +159,24 @@ export class XAxis {
|
|
|
148
159
|
writable: true,
|
|
149
160
|
value: void 0
|
|
150
161
|
});
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
this.
|
|
156
|
-
this.
|
|
157
|
-
this.
|
|
158
|
-
this.
|
|
159
|
-
this.
|
|
160
|
-
this.
|
|
161
|
-
this.
|
|
162
|
-
this.
|
|
163
|
-
this.
|
|
164
|
-
this.
|
|
162
|
+
const resolvedConfig = {
|
|
163
|
+
...DEFAULT_X_AXIS_CONFIG,
|
|
164
|
+
...config,
|
|
165
|
+
};
|
|
166
|
+
this.display = resolvedConfig.display;
|
|
167
|
+
this.dataKey = resolvedConfig.dataKey;
|
|
168
|
+
this.labelKey = resolvedConfig.labelKey;
|
|
169
|
+
this.groupLabelKey = resolvedConfig.groupLabelKey;
|
|
170
|
+
this.showGroupLabels = resolvedConfig.showGroupLabels;
|
|
171
|
+
this.groupLabelGap = resolvedConfig.groupLabelGap;
|
|
172
|
+
this.rotatedLabels = resolvedConfig.rotatedLabels;
|
|
173
|
+
this.maxLabelWidth = resolvedConfig.maxLabelWidth;
|
|
174
|
+
this.oversizedBehavior = resolvedConfig.oversizedBehavior;
|
|
175
|
+
this.tickFormat = resolvedConfig.tickFormat;
|
|
176
|
+
this.autoHideOverlapping = resolvedConfig.autoHideOverlapping;
|
|
177
|
+
this.minLabelGap = resolvedConfig.minLabelGap;
|
|
178
|
+
this.preserveEndLabels = resolvedConfig.preserveEndLabels;
|
|
179
|
+
this.exportHooks = resolvedConfig.exportHooks;
|
|
165
180
|
}
|
|
166
181
|
getExportConfig() {
|
|
167
182
|
return {
|
|
@@ -239,39 +254,14 @@ export class XAxis {
|
|
|
239
254
|
let maxLines = 1;
|
|
240
255
|
for (const label of labels) {
|
|
241
256
|
const text = String(label ?? '');
|
|
242
|
-
if (!text)
|
|
243
|
-
continue;
|
|
244
|
-
const textWidth = measureTextWidth(text, fontSize, fontFamily, fontWeight, svg);
|
|
245
|
-
if (this.maxLabelWidth && this.oversizedBehavior === 'wrap') {
|
|
246
|
-
const lines = wrapText(text, this.maxLabelWidth, fontSize, fontFamily, fontWeight, svg);
|
|
247
|
-
maxLines = Math.max(maxLines, lines.length || 1);
|
|
248
|
-
maxWidth = Math.max(maxWidth, this.maxLabelWidth);
|
|
257
|
+
if (!text) {
|
|
249
258
|
continue;
|
|
250
259
|
}
|
|
251
|
-
const
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
maxWidth = Math.max(maxWidth, effectiveWidth);
|
|
255
|
-
}
|
|
256
|
-
const lineHeight = fontSize * 1.2;
|
|
257
|
-
const textHeight = lineHeight * maxLines;
|
|
258
|
-
if (this.rotatedLabels) {
|
|
259
|
-
const radians = Math.PI / 4;
|
|
260
|
-
const verticalFootprint = Math.sin(radians) * maxWidth + Math.cos(radians) * textHeight;
|
|
261
|
-
this.estimatedTickLabelVerticalFootprint = verticalFootprint;
|
|
262
|
-
this.estimatedHeight = this.tickPadding + verticalFootprint + 5;
|
|
263
|
-
}
|
|
264
|
-
else {
|
|
265
|
-
const wrappedExtraHeight = Math.max(0, maxLines - 1) * lineHeight;
|
|
266
|
-
this.estimatedTickLabelVerticalFootprint =
|
|
267
|
-
this.fontSize + wrappedExtraHeight;
|
|
268
|
-
this.estimatedHeight = this.tickPadding + textHeight + 5;
|
|
269
|
-
}
|
|
270
|
-
if (this.showGroupLabels) {
|
|
271
|
-
const groupLabelStyle = this.resolveGroupLabelStyle(theme);
|
|
272
|
-
this.estimatedHeight +=
|
|
273
|
-
this.groupLabelGap + groupLabelStyle.fontSize + 5;
|
|
260
|
+
const measurement = this.measureLabel(text, fontSize, fontFamily, fontWeight, svg);
|
|
261
|
+
maxLines = Math.max(maxLines, measurement.lines);
|
|
262
|
+
maxWidth = Math.max(maxWidth, measurement.width);
|
|
274
263
|
}
|
|
264
|
+
this.setEstimatedDimensions(maxWidth, maxLines, theme);
|
|
275
265
|
this.wrapLineCount = maxLines;
|
|
276
266
|
}
|
|
277
267
|
clearEstimatedSpace() {
|
|
@@ -295,50 +285,17 @@ export class XAxis {
|
|
|
295
285
|
if (!this.display) {
|
|
296
286
|
return;
|
|
297
287
|
}
|
|
298
|
-
const labelLookup = this.buildLabelLookup(data);
|
|
299
|
-
const axisGenerator = axisBottom(x)
|
|
300
|
-
.tickSizeOuter(0)
|
|
301
|
-
.tickSize(0)
|
|
302
|
-
.tickPadding(this.tickPadding);
|
|
303
|
-
if (labelLookup) {
|
|
304
|
-
axisGenerator.tickFormat((value) => {
|
|
305
|
-
const key = String(value);
|
|
306
|
-
if (!labelLookup.has(key)) {
|
|
307
|
-
return '';
|
|
308
|
-
}
|
|
309
|
-
return labelLookup.get(key) ?? '';
|
|
310
|
-
});
|
|
311
|
-
}
|
|
312
|
-
else if (this.tickFormat) {
|
|
313
|
-
// Apply tick formatting if specified
|
|
314
|
-
const tickFormat = this.tickFormat;
|
|
315
|
-
if (typeof tickFormat === 'function') {
|
|
316
|
-
axisGenerator.tickFormat((value) => tickFormat(value));
|
|
317
|
-
}
|
|
318
|
-
else {
|
|
319
|
-
axisGenerator.ticks(5, tickFormat);
|
|
320
|
-
}
|
|
321
|
-
}
|
|
322
288
|
const axis = svg
|
|
323
289
|
.append('g')
|
|
324
290
|
.attr('class', 'x-axis')
|
|
325
291
|
.attr('transform', `translate(0,${yPosition})`)
|
|
326
|
-
.call(
|
|
292
|
+
.call(this.createAxisGenerator(x, this.buildLabelLookup(data)))
|
|
327
293
|
.attr('font-size', theme.axis.fontSize)
|
|
328
294
|
.attr('font-family', theme.axis.fontFamily)
|
|
329
295
|
.attr('font-weight', theme.axis.fontWeight || 'normal')
|
|
330
296
|
.attr('stroke', 'none');
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
this.applyLabelConstraints(axis, svg.node(), theme.axis.fontSize, theme.axis.fontFamily, theme.axis.fontWeight || 'normal');
|
|
334
|
-
}
|
|
335
|
-
// Apply rotation to labels if enabled
|
|
336
|
-
if (this.rotatedLabels) {
|
|
337
|
-
axis.selectAll('text')
|
|
338
|
-
.style('text-anchor', 'end')
|
|
339
|
-
.attr('transform', 'rotate(-45)');
|
|
340
|
-
}
|
|
341
|
-
// Apply auto-hiding for overlapping labels
|
|
297
|
+
this.applyAxisTextConstraints(axis, svg.node(), theme);
|
|
298
|
+
this.applyLabelRotation(axis);
|
|
342
299
|
this.applyAutoHiding(axis, x);
|
|
343
300
|
axis.selectAll('.domain').remove();
|
|
344
301
|
if (this.showGroupLabels) {
|
|
@@ -390,28 +347,11 @@ export class XAxis {
|
|
|
390
347
|
.text((range) => range.label);
|
|
391
348
|
}
|
|
392
349
|
buildGroupRanges(scale, data) {
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
typeof scale.domain !== 'function' ||
|
|
396
|
-
typeof scale.bandwidth !== 'function') {
|
|
397
|
-
return [];
|
|
398
|
-
}
|
|
399
|
-
const groupLabelKey = this.groupLabelKey ??
|
|
400
|
-
(this.dataKey === GROUPED_CATEGORY_ID_KEY
|
|
401
|
-
? GROUPED_GROUP_LABEL_KEY
|
|
402
|
-
: undefined);
|
|
403
|
-
if (!groupLabelKey) {
|
|
404
|
-
return [];
|
|
405
|
-
}
|
|
406
|
-
const domain = scale.domain().map((value) => String(value));
|
|
407
|
-
const bandwidth = scale.bandwidth();
|
|
408
|
-
if (domain.length === 0 || bandwidth <= 0) {
|
|
350
|
+
const input = this.resolveGroupRangeInput(scale, data);
|
|
351
|
+
if (!input) {
|
|
409
352
|
return [];
|
|
410
353
|
}
|
|
411
|
-
const groupLookup =
|
|
412
|
-
data.forEach((row) => {
|
|
413
|
-
groupLookup.set(String(row[this.dataKey]), String(row[groupLabelKey] ?? ''));
|
|
414
|
-
});
|
|
354
|
+
const { domain, bandwidth, groupLookup } = input;
|
|
415
355
|
const ranges = [];
|
|
416
356
|
let currentLabel = null;
|
|
417
357
|
let startIndex = 0;
|
|
@@ -506,80 +446,181 @@ export class XAxis {
|
|
|
506
446
|
textEl.insertBefore(title, textEl.firstChild);
|
|
507
447
|
}
|
|
508
448
|
applyAutoHiding(axisGroup, scale) {
|
|
509
|
-
if (!this.autoHideOverlapping)
|
|
449
|
+
if (!this.autoHideOverlapping) {
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
const labelEntries = this.collectAutoHideLabels(axisGroup);
|
|
453
|
+
const labelCount = labelEntries.length;
|
|
454
|
+
if (labelCount <= 1) {
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
457
|
+
const maxLabelWidth = this.measureMaxAutoHideLabelWidth(labelEntries);
|
|
458
|
+
const availableSpace = (scale.range()[1] - scale.range()[0]) / labelCount;
|
|
459
|
+
const requiredSpace = maxLabelWidth + this.minLabelGap;
|
|
460
|
+
const skipInterval = Math.ceil(requiredSpace / availableSpace);
|
|
461
|
+
if (skipInterval <= 1) {
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
464
|
+
this.applyAutoHideVisibility(labelEntries, skipInterval);
|
|
465
|
+
if (this.preserveEndLabels && labelCount > 1) {
|
|
466
|
+
this.hideLastOverlappingIntervalLabel(labelEntries, skipInterval);
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
measureLabel(text, fontSize, fontFamily, fontWeight, svg) {
|
|
470
|
+
if (this.maxLabelWidth && this.oversizedBehavior === 'wrap') {
|
|
471
|
+
const lines = wrapText(text, this.maxLabelWidth, fontSize, fontFamily, fontWeight, svg);
|
|
472
|
+
return {
|
|
473
|
+
width: this.maxLabelWidth,
|
|
474
|
+
lines: lines.length || 1,
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
const textWidth = measureTextWidth(text, fontSize, fontFamily, fontWeight, svg);
|
|
478
|
+
return {
|
|
479
|
+
width: this.maxLabelWidth
|
|
480
|
+
? Math.min(textWidth, this.maxLabelWidth)
|
|
481
|
+
: textWidth,
|
|
482
|
+
lines: 1,
|
|
483
|
+
};
|
|
484
|
+
}
|
|
485
|
+
setEstimatedDimensions(maxWidth, maxLines, theme) {
|
|
486
|
+
const lineHeight = this.fontSize * 1.2;
|
|
487
|
+
const textHeight = lineHeight * maxLines;
|
|
488
|
+
if (this.rotatedLabels) {
|
|
489
|
+
const radians = Math.PI / 4;
|
|
490
|
+
const verticalFootprint = Math.sin(radians) * maxWidth + Math.cos(radians) * textHeight;
|
|
491
|
+
this.estimatedTickLabelVerticalFootprint = verticalFootprint;
|
|
492
|
+
this.estimatedHeight = this.tickPadding + verticalFootprint + 5;
|
|
493
|
+
}
|
|
494
|
+
else {
|
|
495
|
+
const wrappedExtraHeight = Math.max(0, maxLines - 1) * lineHeight;
|
|
496
|
+
this.estimatedTickLabelVerticalFootprint =
|
|
497
|
+
this.fontSize + wrappedExtraHeight;
|
|
498
|
+
this.estimatedHeight = this.tickPadding + textHeight + 5;
|
|
499
|
+
}
|
|
500
|
+
if (this.showGroupLabels) {
|
|
501
|
+
const groupLabelStyle = this.resolveGroupLabelStyle(theme);
|
|
502
|
+
this.estimatedHeight +=
|
|
503
|
+
this.groupLabelGap + groupLabelStyle.fontSize + 5;
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
createAxisGenerator(x, labelLookup) {
|
|
507
|
+
const axisGenerator = axisBottom(x)
|
|
508
|
+
.tickSizeOuter(0)
|
|
509
|
+
.tickSize(0)
|
|
510
|
+
.tickPadding(this.tickPadding);
|
|
511
|
+
if (labelLookup) {
|
|
512
|
+
axisGenerator.tickFormat((value) => {
|
|
513
|
+
return labelLookup.get(String(value)) ?? '';
|
|
514
|
+
});
|
|
515
|
+
return axisGenerator;
|
|
516
|
+
}
|
|
517
|
+
if (!this.tickFormat) {
|
|
518
|
+
return axisGenerator;
|
|
519
|
+
}
|
|
520
|
+
const tickFormat = this.tickFormat;
|
|
521
|
+
if (typeof tickFormat === 'function') {
|
|
522
|
+
axisGenerator.tickFormat((value) => {
|
|
523
|
+
return tickFormat(value);
|
|
524
|
+
});
|
|
525
|
+
return axisGenerator;
|
|
526
|
+
}
|
|
527
|
+
axisGenerator.ticks(5, tickFormat);
|
|
528
|
+
return axisGenerator;
|
|
529
|
+
}
|
|
530
|
+
applyAxisTextConstraints(axis, svg, theme) {
|
|
531
|
+
if (!this.maxLabelWidth) {
|
|
532
|
+
return;
|
|
533
|
+
}
|
|
534
|
+
this.applyLabelConstraints(axis, svg, theme.axis.fontSize, theme.axis.fontFamily, theme.axis.fontWeight || 'normal');
|
|
535
|
+
}
|
|
536
|
+
applyLabelRotation(axis) {
|
|
537
|
+
if (!this.rotatedLabels) {
|
|
510
538
|
return;
|
|
511
|
-
|
|
539
|
+
}
|
|
540
|
+
axis.selectAll('text')
|
|
541
|
+
.style('text-anchor', 'end')
|
|
542
|
+
.attr('transform', 'rotate(-45)');
|
|
543
|
+
}
|
|
544
|
+
resolveGroupRangeInput(scale, data) {
|
|
545
|
+
if (!this.dataKey ||
|
|
546
|
+
data.length === 0 ||
|
|
547
|
+
typeof scale.domain !== 'function' ||
|
|
548
|
+
typeof scale.bandwidth !== 'function') {
|
|
549
|
+
return null;
|
|
550
|
+
}
|
|
551
|
+
const groupLabelKey = this.groupLabelKey ??
|
|
552
|
+
(this.dataKey === GROUPED_CATEGORY_ID_KEY
|
|
553
|
+
? GROUPED_GROUP_LABEL_KEY
|
|
554
|
+
: undefined);
|
|
555
|
+
if (!groupLabelKey) {
|
|
556
|
+
return null;
|
|
557
|
+
}
|
|
558
|
+
const domain = scale.domain().map((value) => String(value));
|
|
559
|
+
const bandwidth = scale.bandwidth();
|
|
560
|
+
if (domain.length === 0 || bandwidth <= 0) {
|
|
561
|
+
return null;
|
|
562
|
+
}
|
|
563
|
+
const groupLookup = new Map();
|
|
564
|
+
data.forEach((row) => {
|
|
565
|
+
groupLookup.set(String(row[this.dataKey]), String(row[groupLabelKey] ?? ''));
|
|
566
|
+
});
|
|
567
|
+
return {
|
|
568
|
+
domain,
|
|
569
|
+
bandwidth,
|
|
570
|
+
groupLookup,
|
|
571
|
+
};
|
|
572
|
+
}
|
|
573
|
+
collectAutoHideLabels(axisGroup) {
|
|
574
|
+
return axisGroup
|
|
512
575
|
.selectAll('.tick')
|
|
513
|
-
.nodes()
|
|
514
|
-
const labelEntries = tickElements
|
|
576
|
+
.nodes()
|
|
515
577
|
.map((tickElement) => {
|
|
516
578
|
const textElement = tickElement.querySelector('text');
|
|
517
579
|
const tickValue = String(tickElement
|
|
518
580
|
.__data__ ?? '');
|
|
519
|
-
|
|
520
|
-
|
|
581
|
+
if (tickValue.startsWith(GROUPED_GAP_TICK_PREFIX) &&
|
|
582
|
+
textElement) {
|
|
521
583
|
textElement.style.visibility = 'hidden';
|
|
584
|
+
return null;
|
|
522
585
|
}
|
|
523
|
-
return
|
|
524
|
-
textElement,
|
|
525
|
-
isSyntheticGapTick,
|
|
526
|
-
};
|
|
586
|
+
return textElement;
|
|
527
587
|
})
|
|
528
|
-
.filter((
|
|
529
|
-
return
|
|
530
|
-
})
|
|
531
|
-
.map((entry) => {
|
|
532
|
-
return entry.textElement;
|
|
588
|
+
.filter((textElement) => {
|
|
589
|
+
return textElement !== null;
|
|
533
590
|
});
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
return;
|
|
537
|
-
}
|
|
538
|
-
// Measure all label widths
|
|
591
|
+
}
|
|
592
|
+
measureMaxAutoHideLabelWidth(labelEntries) {
|
|
539
593
|
let maxLabelWidth = 0;
|
|
540
594
|
for (const textEl of labelEntries) {
|
|
541
595
|
const bbox = textEl.getBBox();
|
|
542
|
-
// For rotated labels, use the horizontal footprint
|
|
543
596
|
const effectiveWidth = this.rotatedLabels
|
|
544
597
|
? bbox.width * Math.cos(Math.PI / 4)
|
|
545
598
|
: bbox.width;
|
|
546
599
|
maxLabelWidth = Math.max(maxLabelWidth, effectiveWidth);
|
|
547
600
|
}
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
// Calculate skip interval
|
|
552
|
-
const requiredSpace = maxLabelWidth + this.minLabelGap;
|
|
553
|
-
const skipInterval = Math.ceil(requiredSpace / availableSpace);
|
|
554
|
-
// If no skipping needed, show all labels
|
|
555
|
-
if (skipInterval <= 1)
|
|
556
|
-
return;
|
|
557
|
-
// Apply visibility
|
|
601
|
+
return maxLabelWidth;
|
|
602
|
+
}
|
|
603
|
+
applyAutoHideVisibility(labelEntries, skipInterval) {
|
|
558
604
|
labelEntries.forEach((textEl, index) => {
|
|
559
605
|
const isFirst = index === 0;
|
|
560
|
-
const isLast = index ===
|
|
606
|
+
const isLast = index === labelEntries.length - 1;
|
|
561
607
|
const isAtInterval = index % skipInterval === 0;
|
|
562
608
|
if (this.preserveEndLabels && (isFirst || isLast)) {
|
|
563
609
|
textEl.style.visibility = 'visible';
|
|
610
|
+
return;
|
|
564
611
|
}
|
|
565
|
-
|
|
566
|
-
textEl.style.visibility = 'visible';
|
|
567
|
-
}
|
|
568
|
-
else {
|
|
569
|
-
textEl.style.visibility = 'hidden';
|
|
570
|
-
}
|
|
612
|
+
textEl.style.visibility = isAtInterval ? 'visible' : 'hidden';
|
|
571
613
|
});
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
}
|
|
614
|
+
}
|
|
615
|
+
hideLastOverlappingIntervalLabel(labelEntries, skipInterval) {
|
|
616
|
+
const lastVisibleIntervalIndex = Math.floor((labelEntries.length - 2) / skipInterval) * skipInterval;
|
|
617
|
+
if (lastVisibleIntervalIndex === labelEntries.length - 1 ||
|
|
618
|
+
labelEntries.length - 1 - lastVisibleIntervalIndex >= skipInterval) {
|
|
619
|
+
return;
|
|
620
|
+
}
|
|
621
|
+
const textEl = labelEntries[lastVisibleIntervalIndex];
|
|
622
|
+
if (textEl && lastVisibleIntervalIndex !== 0) {
|
|
623
|
+
textEl.style.visibility = 'hidden';
|
|
583
624
|
}
|
|
584
625
|
}
|
|
585
626
|
}
|
package/dist/xy-chart.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { BaseChart, type BaseChartConfig, type BaseLayoutContext, type BaseRenderContext } from './base-chart.js';
|
|
2
2
|
import type { ChartComponentBase } from './chart-interface.js';
|
|
3
|
-
import { type AreaStackConfig, type BarStackConfig, type LegendSeries, type Orientation } from './types.js';
|
|
3
|
+
import { type AreaStackConfig, type AxisScaleConfig, type BarStackConfig, type LegendSeries, type Orientation, type ScaleType } from './types.js';
|
|
4
4
|
export type XYChartConfig = BaseChartConfig & {
|
|
5
5
|
orientation?: Orientation;
|
|
6
6
|
barStack?: BarStackConfig;
|
|
@@ -13,29 +13,68 @@ export declare class XYChart extends BaseChart {
|
|
|
13
13
|
private barStackReverseSeries;
|
|
14
14
|
private areaStackMode;
|
|
15
15
|
private readonly orientation;
|
|
16
|
+
private scaleConfigOverride;
|
|
16
17
|
constructor(config: XYChartConfig);
|
|
17
18
|
addChild(component: ChartComponentBase): this;
|
|
18
19
|
protected getExportComponents(): ChartComponentBase[];
|
|
19
20
|
protected createExportChart(): BaseChart;
|
|
20
21
|
protected applyComponentOverrides(overrides: Map<ChartComponentBase, ChartComponentBase>): () => void;
|
|
21
22
|
protected prepareLayout(context: BaseLayoutContext): void;
|
|
23
|
+
private getYAxisEstimateLabels;
|
|
24
|
+
private createContinuousScaleForLayoutEstimate;
|
|
22
25
|
protected renderChart({ svg, plotGroup, plotArea, }: BaseRenderContext): void;
|
|
23
26
|
private getXKey;
|
|
24
27
|
protected getLegendSeries(): LegendSeries[];
|
|
25
28
|
private getCategoryScaleType;
|
|
29
|
+
getOrientation(): Orientation;
|
|
30
|
+
getValueAxisScaleType(): ScaleType | null;
|
|
31
|
+
getValueAxisDomain(): [number, number] | null;
|
|
32
|
+
getBaseValueAxisDomain(): [number, number] | null;
|
|
33
|
+
setScaleConfigOverride(override: AxisScaleConfig | null, rerender?: boolean): this;
|
|
34
|
+
private resolveValueAxisDomain;
|
|
26
35
|
private getVisibleSeries;
|
|
27
36
|
private getDisplaySeries;
|
|
28
37
|
private resolveSeriesDefaults;
|
|
29
38
|
private shouldReplaceSeriesColor;
|
|
30
39
|
private cloneSeriesWithOverride;
|
|
31
40
|
private setupScales;
|
|
41
|
+
private get resolvedScaleConfig();
|
|
42
|
+
private getResolvedAxisConfigs;
|
|
43
|
+
private getAxisConfigsForScaleConfig;
|
|
32
44
|
private isHorizontalOrientation;
|
|
45
|
+
private getSeriesTypeName;
|
|
33
46
|
private validateSeriesOrientation;
|
|
34
47
|
private collectSeriesValues;
|
|
48
|
+
private getBarPercentDomain;
|
|
49
|
+
private getBarValueDomain;
|
|
35
50
|
private getStackedAreaGroups;
|
|
36
51
|
private buildBandDomainWithGroupGaps;
|
|
52
|
+
private getScaleRange;
|
|
37
53
|
private createScale;
|
|
54
|
+
private resolveScaleDomain;
|
|
38
55
|
private getSeriesTooltipValue;
|
|
56
|
+
private validateRenderState;
|
|
57
|
+
private validateSeriesData;
|
|
58
|
+
private validateValueScaleRequirements;
|
|
59
|
+
private validateXAxisDataKey;
|
|
60
|
+
private renderAxes;
|
|
61
|
+
private attachTooltip;
|
|
62
|
+
private validateExplicitBarValueDomain;
|
|
63
|
+
private validateScaleDomain;
|
|
64
|
+
private buildScale;
|
|
65
|
+
private getSeriesBuckets;
|
|
66
|
+
private isBarValueScale;
|
|
67
|
+
private resolveRawScaleDomain;
|
|
68
|
+
private resolveTimeDomain;
|
|
69
|
+
private resolveNumericDataKeyDomain;
|
|
70
|
+
private resolveSeriesValueDomain;
|
|
71
|
+
private hasPercentValueDomain;
|
|
72
|
+
private resolvePercentValueDomain;
|
|
73
|
+
private resolveStandardValueDomain;
|
|
74
|
+
private collectStackedAreaTotals;
|
|
75
|
+
private addAreaBaselines;
|
|
76
|
+
private applyNiceDomain;
|
|
77
|
+
private getAreaTooltipValue;
|
|
39
78
|
private renderSeries;
|
|
40
79
|
private computeStackingData;
|
|
41
80
|
private computeAreaStackingContexts;
|