@internetstiftelsen/charts 0.10.0 → 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 +1 -1
- package/dist/area.d.ts +2 -0
- package/dist/area.js +39 -31
- package/dist/bar.d.ts +17 -0
- package/dist/bar.js +295 -259
- package/dist/base-chart.d.ts +5 -0
- package/dist/base-chart.js +80 -66
- package/dist/chart-group.d.ts +16 -0
- package/dist/chart-group.js +201 -143
- package/dist/donut-center-content.d.ts +1 -0
- package/dist/donut-center-content.js +21 -38
- package/dist/donut-chart.js +29 -14
- package/dist/gauge-chart.d.ts +20 -0
- package/dist/gauge-chart.js +228 -132
- package/dist/legend.js +10 -9
- package/dist/line.js +3 -1
- package/dist/pie-chart.d.ts +3 -0
- package/dist/pie-chart.js +44 -18
- package/dist/scatter.js +3 -1
- package/dist/tooltip.js +18 -22
- package/dist/types.d.ts +3 -1
- package/dist/utils.js +11 -19
- package/dist/x-axis.d.ts +10 -0
- package/dist/x-axis.js +190 -149
- package/dist/xy-chart.d.ts +24 -0
- package/dist/xy-chart.js +251 -143
- package/dist/y-axis.d.ts +7 -2
- package/dist/y-axis.js +99 -10
- package/docs/components.md +14 -1
- package/docs/xy-chart.md +21 -6
- package/package.json +5 -4
package/dist/xy-chart.js
CHANGED
|
@@ -114,6 +114,7 @@ export class XYChart extends BaseChart {
|
|
|
114
114
|
prepareLayout(context) {
|
|
115
115
|
super.prepareLayout(context);
|
|
116
116
|
this.xAxis?.clearEstimatedSpace?.();
|
|
117
|
+
this.yAxis?.clearEstimatedSpace?.();
|
|
117
118
|
if (this.xAxis) {
|
|
118
119
|
const xKey = this.getXKey();
|
|
119
120
|
const labelKey = this.xAxis.labelKey;
|
|
@@ -125,24 +126,59 @@ export class XYChart extends BaseChart {
|
|
|
125
126
|
});
|
|
126
127
|
this.xAxis.estimateLayoutSpace?.(labels, this.renderTheme, context.svgNode);
|
|
127
128
|
}
|
|
129
|
+
if (this.yAxis) {
|
|
130
|
+
this.yAxis.estimateLayoutSpace?.(this.getYAxisEstimateLabels(), this.renderTheme, context.svgNode);
|
|
131
|
+
}
|
|
128
132
|
}
|
|
129
|
-
|
|
130
|
-
this.
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
133
|
+
getYAxisEstimateLabels() {
|
|
134
|
+
if (!this.yAxis) {
|
|
135
|
+
return [];
|
|
136
|
+
}
|
|
137
|
+
const yAxisConfig = this.yAxis.getExportConfig();
|
|
138
|
+
const { y: yConfig } = this.getResolvedAxisConfigs();
|
|
139
|
+
const tickFormat = yAxisConfig.tickFormat;
|
|
140
|
+
if (yConfig.type === 'band') {
|
|
141
|
+
const tickValues = this.resolveScaleDomain(yConfig, this.isHorizontalOrientation() ? this.getXKey() : null);
|
|
142
|
+
return tickValues.map((value) => {
|
|
143
|
+
if (typeof tickFormat === 'function') {
|
|
144
|
+
return tickFormat(value);
|
|
145
|
+
}
|
|
146
|
+
return String(value);
|
|
141
147
|
});
|
|
142
148
|
}
|
|
143
|
-
|
|
144
|
-
|
|
149
|
+
const scale = this.createContinuousScaleForLayoutEstimate(yConfig);
|
|
150
|
+
const tickValues = scale.ticks(5);
|
|
151
|
+
if (typeof tickFormat === 'function') {
|
|
152
|
+
return tickValues.map((value) => tickFormat(value));
|
|
145
153
|
}
|
|
154
|
+
const tickFormatter = scale.tickFormat(5, typeof tickFormat === 'string' ? tickFormat : undefined);
|
|
155
|
+
return tickValues.map((value) => tickFormatter(value));
|
|
156
|
+
}
|
|
157
|
+
createContinuousScaleForLayoutEstimate(config) {
|
|
158
|
+
const domain = this.resolveScaleDomain(config, null);
|
|
159
|
+
switch (config.type) {
|
|
160
|
+
case 'linear': {
|
|
161
|
+
const scale = scaleLinear()
|
|
162
|
+
.domain(domain)
|
|
163
|
+
.rangeRound([100, 0]);
|
|
164
|
+
return config.nice === false ? scale : scale.nice();
|
|
165
|
+
}
|
|
166
|
+
case 'time': {
|
|
167
|
+
const scale = scaleTime()
|
|
168
|
+
.domain(domain)
|
|
169
|
+
.range([100, 0]);
|
|
170
|
+
return config.nice === false ? scale : scale.nice();
|
|
171
|
+
}
|
|
172
|
+
case 'log': {
|
|
173
|
+
const scale = scaleLog()
|
|
174
|
+
.domain(domain)
|
|
175
|
+
.rangeRound([100, 0]);
|
|
176
|
+
return config.nice === false ? scale : scale.nice();
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
renderChart({ svg, plotGroup, plotArea, }) {
|
|
181
|
+
this.validateRenderState();
|
|
146
182
|
const xKey = this.getXKey();
|
|
147
183
|
const categoryScaleType = this.getCategoryScaleType();
|
|
148
184
|
const visibleSeries = this.getVisibleSeries();
|
|
@@ -152,20 +188,8 @@ export class XYChart extends BaseChart {
|
|
|
152
188
|
this.grid.render(plotGroup, this.x, this.y, this.renderTheme, this.isHorizontalOrientation());
|
|
153
189
|
}
|
|
154
190
|
this.renderSeries(visibleSeries);
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
this.xAxis.render(svg, this.x, this.renderTheme, plotArea.bottom, this.data);
|
|
158
|
-
}
|
|
159
|
-
if (this.yAxis) {
|
|
160
|
-
this.yAxis.render(svg, this.y, this.renderTheme, plotArea.left);
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
if (this.tooltip && this.x && this.y) {
|
|
164
|
-
const visibleAreaSeries = visibleSeries.filter((series) => series.type === 'area');
|
|
165
|
-
const areaStackingContextBySeries = this.computeAreaStackingContexts(this.data, xKey, visibleAreaSeries);
|
|
166
|
-
this.tooltip.initialize(this.renderTheme);
|
|
167
|
-
this.tooltip.attachToArea(svg, this.data, visibleSeries, xKey, this.x, this.y, this.renderTheme, plotArea, this.parseValue.bind(this), this.isHorizontalOrientation(), categoryScaleType, (series, dataPoint) => this.getSeriesTooltipValue(series, dataPoint, xKey, areaStackingContextBySeries));
|
|
168
|
-
}
|
|
191
|
+
this.renderAxes(svg, plotArea);
|
|
192
|
+
this.attachTooltip(svg, plotArea, visibleSeries, xKey, categoryScaleType);
|
|
169
193
|
this.renderInlineLegend(svg);
|
|
170
194
|
}
|
|
171
195
|
getXKey() {
|
|
@@ -487,6 +511,84 @@ export class XYChart extends BaseChart {
|
|
|
487
511
|
const scaleType = config.type;
|
|
488
512
|
const [rangeStart, rangeEnd] = this.getScaleRange(axis, scaleType, config.reverse ?? false);
|
|
489
513
|
const domain = this.resolveScaleDomain(config, dataKey);
|
|
514
|
+
this.validateExplicitBarValueDomain(config, dataKey, scaleType, domain);
|
|
515
|
+
this.validateScaleDomain(scaleType, domain);
|
|
516
|
+
return this.buildScale(scaleType, domain, [rangeStart, rangeEnd], config);
|
|
517
|
+
}
|
|
518
|
+
resolveScaleDomain(config, dataKey) {
|
|
519
|
+
const buckets = this.getSeriesBuckets();
|
|
520
|
+
const stackedAreaGroups = this.getStackedAreaGroups(buckets.areaSeries);
|
|
521
|
+
if (this.isBarValueScale(config.type, dataKey, buckets.barSeries) &&
|
|
522
|
+
config.type === 'log') {
|
|
523
|
+
throw new ChartValidationError('XYChart: bar series require a linear value axis because bars need a zero baseline');
|
|
524
|
+
}
|
|
525
|
+
const domain = this.resolveRawScaleDomain(config, dataKey, buckets, stackedAreaGroups);
|
|
526
|
+
return this.applyNiceDomain(config, domain);
|
|
527
|
+
}
|
|
528
|
+
getSeriesTooltipValue(series, dataPoint, xKey, areaStackingContextBySeries) {
|
|
529
|
+
const rawValue = dataPoint[series.dataKey];
|
|
530
|
+
if (rawValue === null || rawValue === undefined) {
|
|
531
|
+
return NaN;
|
|
532
|
+
}
|
|
533
|
+
const parsedValue = this.parseValue(rawValue);
|
|
534
|
+
if (!Number.isFinite(parsedValue)) {
|
|
535
|
+
return NaN;
|
|
536
|
+
}
|
|
537
|
+
if (series.type === 'bar') {
|
|
538
|
+
return series.getRenderedValue(parsedValue, this.orientation);
|
|
539
|
+
}
|
|
540
|
+
if (series.type !== 'area') {
|
|
541
|
+
return parsedValue;
|
|
542
|
+
}
|
|
543
|
+
return this.getAreaTooltipValue(dataPoint, xKey, parsedValue, areaStackingContextBySeries.get(series));
|
|
544
|
+
}
|
|
545
|
+
validateRenderState() {
|
|
546
|
+
this.validateSeriesOrientation();
|
|
547
|
+
this.validateSeriesData();
|
|
548
|
+
this.validateValueScaleRequirements();
|
|
549
|
+
this.validateXAxisDataKey();
|
|
550
|
+
}
|
|
551
|
+
validateSeriesData() {
|
|
552
|
+
this.series.forEach((series) => {
|
|
553
|
+
const typeName = this.getSeriesTypeName(series);
|
|
554
|
+
ChartValidator.validateDataKey(this.data, series.dataKey, typeName);
|
|
555
|
+
ChartValidator.validateNumericData(this.data, series.dataKey, typeName);
|
|
556
|
+
});
|
|
557
|
+
}
|
|
558
|
+
validateValueScaleRequirements() {
|
|
559
|
+
if ((this.resolvedScaleConfig.y?.type ?? 'linear') !== 'log') {
|
|
560
|
+
return;
|
|
561
|
+
}
|
|
562
|
+
this.series.forEach((series) => {
|
|
563
|
+
ChartValidator.validatePositiveData(this.data, series.dataKey, this.getSeriesTypeName(series));
|
|
564
|
+
});
|
|
565
|
+
}
|
|
566
|
+
validateXAxisDataKey() {
|
|
567
|
+
if (this.xAxis?.dataKey) {
|
|
568
|
+
ChartValidator.validateDataKey(this.data, this.xAxis.dataKey, 'XAxis');
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
renderAxes(svg, plotArea) {
|
|
572
|
+
if (!this.x || !this.y) {
|
|
573
|
+
return;
|
|
574
|
+
}
|
|
575
|
+
if (this.xAxis) {
|
|
576
|
+
this.xAxis.render(svg, this.x, this.renderTheme, plotArea.bottom, this.data);
|
|
577
|
+
}
|
|
578
|
+
if (this.yAxis) {
|
|
579
|
+
this.yAxis.render(svg, this.y, this.renderTheme, plotArea.left);
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
attachTooltip(svg, plotArea, visibleSeries, xKey, categoryScaleType) {
|
|
583
|
+
if (!this.tooltip || !this.x || !this.y) {
|
|
584
|
+
return;
|
|
585
|
+
}
|
|
586
|
+
const visibleAreaSeries = visibleSeries.filter((series) => series.type === 'area');
|
|
587
|
+
const areaStackingContextBySeries = this.computeAreaStackingContexts(this.data, xKey, visibleAreaSeries);
|
|
588
|
+
this.tooltip.initialize(this.renderTheme);
|
|
589
|
+
this.tooltip.attachToArea(svg, this.data, visibleSeries, xKey, this.x, this.y, this.renderTheme, plotArea, this.parseValue.bind(this), this.isHorizontalOrientation(), categoryScaleType, (series, dataPoint) => this.getSeriesTooltipValue(series, dataPoint, xKey, areaStackingContextBySeries));
|
|
590
|
+
}
|
|
591
|
+
validateExplicitBarValueDomain(config, dataKey, scaleType, domain) {
|
|
490
592
|
if (dataKey === null &&
|
|
491
593
|
this.series.some((series) => series.type === 'bar') &&
|
|
492
594
|
(scaleType === 'linear' || scaleType === 'log') &&
|
|
@@ -495,133 +597,154 @@ export class XYChart extends BaseChart {
|
|
|
495
597
|
config.max !== undefined)) {
|
|
496
598
|
ChartValidator.validateBarDomainIncludesZero(domain);
|
|
497
599
|
}
|
|
600
|
+
}
|
|
601
|
+
validateScaleDomain(scaleType, domain) {
|
|
498
602
|
if (scaleType === 'log') {
|
|
499
603
|
ChartValidator.validateScaleConfig(scaleType, domain);
|
|
500
604
|
}
|
|
605
|
+
}
|
|
606
|
+
buildScale(scaleType, domain, range, config) {
|
|
501
607
|
switch (scaleType) {
|
|
502
|
-
case 'band':
|
|
503
|
-
|
|
608
|
+
case 'band':
|
|
609
|
+
return scaleBand()
|
|
504
610
|
.domain(domain)
|
|
505
|
-
.rangeRound(
|
|
506
|
-
|
|
507
|
-
scale.paddingInner(config.padding);
|
|
508
|
-
}
|
|
509
|
-
else {
|
|
510
|
-
scale.paddingInner(0.1);
|
|
511
|
-
}
|
|
512
|
-
return scale;
|
|
513
|
-
}
|
|
611
|
+
.rangeRound(range)
|
|
612
|
+
.paddingInner(config.padding ?? 0.1);
|
|
514
613
|
case 'linear': {
|
|
515
614
|
const scale = scaleLinear()
|
|
516
615
|
.domain(domain)
|
|
517
|
-
.rangeRound(
|
|
616
|
+
.rangeRound(range);
|
|
518
617
|
return config.nice === false ? scale : scale.nice();
|
|
519
618
|
}
|
|
520
619
|
case 'time': {
|
|
521
620
|
const scale = scaleTime()
|
|
522
621
|
.domain(domain)
|
|
523
|
-
.range(
|
|
622
|
+
.range(range);
|
|
524
623
|
return config.nice === false ? scale : scale.nice();
|
|
525
624
|
}
|
|
526
625
|
case 'log': {
|
|
527
626
|
const scale = scaleLog()
|
|
528
627
|
.domain(domain)
|
|
529
|
-
.rangeRound(
|
|
628
|
+
.rangeRound(range);
|
|
530
629
|
return config.nice === false ? scale : scale.nice();
|
|
531
630
|
}
|
|
532
631
|
default:
|
|
533
632
|
throw new Error(`Unsupported scale type: ${scaleType}`);
|
|
534
633
|
}
|
|
535
634
|
}
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
635
|
+
getSeriesBuckets() {
|
|
636
|
+
return {
|
|
637
|
+
barSeries: this.series.filter((s) => s.type === 'bar'),
|
|
638
|
+
lineSeries: this.series.filter((s) => s.type === 'line'),
|
|
639
|
+
scatterSeries: this.series.filter((s) => s.type === 'scatter'),
|
|
640
|
+
areaSeries: this.series.filter((s) => s.type === 'area'),
|
|
641
|
+
};
|
|
642
|
+
}
|
|
643
|
+
isBarValueScale(scaleType, dataKey, barSeries) {
|
|
644
|
+
return (dataKey === null &&
|
|
543
645
|
barSeries.length > 0 &&
|
|
544
|
-
(
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
throw new ChartValidationError('XYChart: bar series require a linear value axis because bars need a zero baseline');
|
|
548
|
-
}
|
|
646
|
+
(scaleType === 'linear' || scaleType === 'log'));
|
|
647
|
+
}
|
|
648
|
+
resolveRawScaleDomain(config, dataKey, buckets, stackedAreaGroups) {
|
|
549
649
|
if (config.domain) {
|
|
550
|
-
|
|
551
|
-
}
|
|
552
|
-
else if (config.type === 'band' && dataKey) {
|
|
553
|
-
domain = this.buildBandDomainWithGroupGaps(dataKey, config.groupGap ?? 0);
|
|
554
|
-
}
|
|
555
|
-
else if (config.type === 'time' && dataKey) {
|
|
556
|
-
domain = [
|
|
557
|
-
min(this.data, (d) => resolveScaleValue(d[dataKey], 'time')),
|
|
558
|
-
max(this.data, (d) => resolveScaleValue(d[dataKey], 'time')),
|
|
559
|
-
];
|
|
560
|
-
}
|
|
561
|
-
else if ((config.type === 'linear' || config.type === 'log') &&
|
|
562
|
-
dataKey) {
|
|
563
|
-
const values = this.data
|
|
564
|
-
.map((d) => this.parseValue(d[dataKey]))
|
|
565
|
-
.filter((value) => Number.isFinite(value));
|
|
566
|
-
const minVal = config.min ?? min(values) ?? 0;
|
|
567
|
-
const maxVal = config.max ?? max(values) ?? 100;
|
|
568
|
-
domain = [minVal, maxVal];
|
|
650
|
+
return config.domain;
|
|
569
651
|
}
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
}
|
|
579
|
-
if (hasPercentAreas) {
|
|
580
|
-
minDomain = Math.min(minDomain, 0);
|
|
581
|
-
maxDomain = Math.max(maxDomain, 100);
|
|
582
|
-
}
|
|
583
|
-
domain = [minDomain, maxDomain];
|
|
584
|
-
}
|
|
585
|
-
else {
|
|
586
|
-
const values = this.collectSeriesValues([
|
|
587
|
-
...lineSeries,
|
|
588
|
-
...scatterSeries,
|
|
589
|
-
...areaSeries,
|
|
590
|
-
]);
|
|
591
|
-
const stackedValues = [];
|
|
592
|
-
const minCandidates = [...values];
|
|
593
|
-
const maxCandidates = [...values];
|
|
594
|
-
if (barSeries.length > 0) {
|
|
595
|
-
const [barMin, barMax] = this.getBarValueDomain(this.getXKey(), barSeries);
|
|
596
|
-
minCandidates.push(barMin);
|
|
597
|
-
maxCandidates.push(barMax);
|
|
598
|
-
}
|
|
599
|
-
const stackedAreaKeys = new Set();
|
|
600
|
-
stackedAreaGroups.forEach((stackSeries) => {
|
|
601
|
-
stackSeries.forEach((series) => {
|
|
602
|
-
stackedAreaKeys.add(series.dataKey);
|
|
603
|
-
});
|
|
604
|
-
this.data.forEach((dataPoint) => {
|
|
605
|
-
const total = stackSeries.reduce((sum, series) => {
|
|
606
|
-
const value = this.parseValue(dataPoint[series.dataKey]);
|
|
607
|
-
return Number.isFinite(value) ? sum + value : sum;
|
|
608
|
-
}, 0);
|
|
609
|
-
stackedValues.push(total);
|
|
610
|
-
});
|
|
611
|
-
minCandidates.push(0);
|
|
612
|
-
});
|
|
613
|
-
areaSeries
|
|
614
|
-
.filter((series) => !stackedAreaKeys.has(series.dataKey))
|
|
615
|
-
.forEach((series) => {
|
|
616
|
-
minCandidates.push(series.baseline);
|
|
617
|
-
});
|
|
618
|
-
const minVal = config.min ?? min(minCandidates) ?? 0;
|
|
619
|
-
const maxVal = config.max ??
|
|
620
|
-
max([...maxCandidates, ...stackedValues]) ??
|
|
621
|
-
100;
|
|
622
|
-
domain = [minVal, maxVal];
|
|
623
|
-
}
|
|
652
|
+
if (config.type === 'band' && dataKey) {
|
|
653
|
+
return this.buildBandDomainWithGroupGaps(dataKey, config.groupGap ?? 0);
|
|
654
|
+
}
|
|
655
|
+
if (config.type === 'time' && dataKey) {
|
|
656
|
+
return this.resolveTimeDomain(dataKey);
|
|
657
|
+
}
|
|
658
|
+
if ((config.type === 'linear' || config.type === 'log') && dataKey) {
|
|
659
|
+
return this.resolveNumericDataKeyDomain(config, dataKey);
|
|
624
660
|
}
|
|
661
|
+
return this.resolveSeriesValueDomain(config, buckets, stackedAreaGroups);
|
|
662
|
+
}
|
|
663
|
+
resolveTimeDomain(dataKey) {
|
|
664
|
+
return [
|
|
665
|
+
min(this.data, (d) => resolveScaleValue(d[dataKey], 'time')),
|
|
666
|
+
max(this.data, (d) => resolveScaleValue(d[dataKey], 'time')),
|
|
667
|
+
];
|
|
668
|
+
}
|
|
669
|
+
resolveNumericDataKeyDomain(config, dataKey) {
|
|
670
|
+
const values = this.data
|
|
671
|
+
.map((d) => this.parseValue(d[dataKey]))
|
|
672
|
+
.filter((value) => Number.isFinite(value));
|
|
673
|
+
return [
|
|
674
|
+
config.min ?? min(values) ?? 0,
|
|
675
|
+
config.max ?? max(values) ?? 100,
|
|
676
|
+
];
|
|
677
|
+
}
|
|
678
|
+
resolveSeriesValueDomain(config, buckets, stackedAreaGroups) {
|
|
679
|
+
if (this.hasPercentValueDomain(buckets.barSeries, stackedAreaGroups)) {
|
|
680
|
+
return this.resolvePercentValueDomain(buckets.barSeries, stackedAreaGroups);
|
|
681
|
+
}
|
|
682
|
+
return this.resolveStandardValueDomain(config, buckets, stackedAreaGroups);
|
|
683
|
+
}
|
|
684
|
+
hasPercentValueDomain(barSeries, stackedAreaGroups) {
|
|
685
|
+
return ((this.barStackMode === 'percent' && barSeries.length > 0) ||
|
|
686
|
+
(this.areaStackMode === 'percent' && stackedAreaGroups.size > 0));
|
|
687
|
+
}
|
|
688
|
+
resolvePercentValueDomain(barSeries, stackedAreaGroups) {
|
|
689
|
+
let minDomain = 0;
|
|
690
|
+
let maxDomain = 100;
|
|
691
|
+
if (this.barStackMode === 'percent' && barSeries.length > 0) {
|
|
692
|
+
[minDomain, maxDomain] = this.getBarValueDomain(this.getXKey(), barSeries);
|
|
693
|
+
}
|
|
694
|
+
if (this.areaStackMode === 'percent' && stackedAreaGroups.size > 0) {
|
|
695
|
+
minDomain = Math.min(minDomain, 0);
|
|
696
|
+
maxDomain = Math.max(maxDomain, 100);
|
|
697
|
+
}
|
|
698
|
+
return [minDomain, maxDomain];
|
|
699
|
+
}
|
|
700
|
+
resolveStandardValueDomain(config, buckets, stackedAreaGroups) {
|
|
701
|
+
const values = this.collectSeriesValues([
|
|
702
|
+
...buckets.lineSeries,
|
|
703
|
+
...buckets.scatterSeries,
|
|
704
|
+
...buckets.areaSeries,
|
|
705
|
+
]);
|
|
706
|
+
const minCandidates = [...values];
|
|
707
|
+
const maxCandidates = [...values];
|
|
708
|
+
const stackedValues = this.collectStackedAreaTotals(stackedAreaGroups);
|
|
709
|
+
if (buckets.barSeries.length > 0) {
|
|
710
|
+
const [barMin, barMax] = this.getBarValueDomain(this.getXKey(), buckets.barSeries);
|
|
711
|
+
minCandidates.push(barMin);
|
|
712
|
+
maxCandidates.push(barMax);
|
|
713
|
+
}
|
|
714
|
+
this.addAreaBaselines(minCandidates, buckets.areaSeries, stackedAreaGroups);
|
|
715
|
+
return [
|
|
716
|
+
config.min ?? min(minCandidates) ?? 0,
|
|
717
|
+
config.max ?? max([...maxCandidates, ...stackedValues]) ?? 100,
|
|
718
|
+
];
|
|
719
|
+
}
|
|
720
|
+
collectStackedAreaTotals(stackedAreaGroups) {
|
|
721
|
+
const stackedValues = [];
|
|
722
|
+
stackedAreaGroups.forEach((stackSeries) => {
|
|
723
|
+
this.data.forEach((dataPoint) => {
|
|
724
|
+
const total = stackSeries.reduce((sum, series) => {
|
|
725
|
+
const value = this.parseValue(dataPoint[series.dataKey]);
|
|
726
|
+
return Number.isFinite(value) ? sum + value : sum;
|
|
727
|
+
}, 0);
|
|
728
|
+
stackedValues.push(total);
|
|
729
|
+
});
|
|
730
|
+
});
|
|
731
|
+
return stackedValues;
|
|
732
|
+
}
|
|
733
|
+
addAreaBaselines(minCandidates, areaSeries, stackedAreaGroups) {
|
|
734
|
+
const stackedAreaKeys = new Set();
|
|
735
|
+
stackedAreaGroups.forEach((stackSeries) => {
|
|
736
|
+
stackSeries.forEach((series) => {
|
|
737
|
+
stackedAreaKeys.add(series.dataKey);
|
|
738
|
+
});
|
|
739
|
+
minCandidates.push(0);
|
|
740
|
+
});
|
|
741
|
+
areaSeries
|
|
742
|
+
.filter((series) => !stackedAreaKeys.has(series.dataKey))
|
|
743
|
+
.forEach((series) => {
|
|
744
|
+
minCandidates.push(series.baseline);
|
|
745
|
+
});
|
|
746
|
+
}
|
|
747
|
+
applyNiceDomain(config, domain) {
|
|
625
748
|
if (config.nice === false) {
|
|
626
749
|
return domain;
|
|
627
750
|
}
|
|
@@ -645,22 +768,7 @@ export class XYChart extends BaseChart {
|
|
|
645
768
|
}
|
|
646
769
|
return domain;
|
|
647
770
|
}
|
|
648
|
-
|
|
649
|
-
const rawValue = dataPoint[series.dataKey];
|
|
650
|
-
if (rawValue === null || rawValue === undefined) {
|
|
651
|
-
return NaN;
|
|
652
|
-
}
|
|
653
|
-
const parsedValue = this.parseValue(rawValue);
|
|
654
|
-
if (!Number.isFinite(parsedValue)) {
|
|
655
|
-
return NaN;
|
|
656
|
-
}
|
|
657
|
-
if (series.type !== 'area') {
|
|
658
|
-
if (series.type === 'bar') {
|
|
659
|
-
return series.getRenderedValue(parsedValue, this.orientation);
|
|
660
|
-
}
|
|
661
|
-
return parsedValue;
|
|
662
|
-
}
|
|
663
|
-
const stackingContext = areaStackingContextBySeries.get(series);
|
|
771
|
+
getAreaTooltipValue(dataPoint, xKey, parsedValue, stackingContext) {
|
|
664
772
|
if (!stackingContext || stackingContext.mode === 'none') {
|
|
665
773
|
return parsedValue;
|
|
666
774
|
}
|
package/dist/y-axis.d.ts
CHANGED
|
@@ -5,12 +5,14 @@ export declare class YAxis implements LayoutAwareComponent<YAxisConfigBase> {
|
|
|
5
5
|
readonly type: "yAxis";
|
|
6
6
|
readonly display: boolean;
|
|
7
7
|
private readonly tickPadding;
|
|
8
|
-
private
|
|
9
|
-
private readonly maxLabelWidth
|
|
8
|
+
private fontSize;
|
|
9
|
+
private readonly maxLabelWidth?;
|
|
10
10
|
private readonly tickFormat;
|
|
11
11
|
private readonly rotatedLabels;
|
|
12
12
|
private readonly oversizedBehavior;
|
|
13
|
+
private estimatedWidth;
|
|
13
14
|
readonly exportHooks?: ExportHooks<YAxisConfigBase>;
|
|
15
|
+
private resolveFontSizeValue;
|
|
14
16
|
constructor(config?: YAxisConfig);
|
|
15
17
|
getExportConfig(): YAxisConfigBase;
|
|
16
18
|
createExportComponent(override?: Partial<YAxisConfigBase>): LayoutAwareComponent<YAxisConfigBase>;
|
|
@@ -18,8 +20,11 @@ export declare class YAxis implements LayoutAwareComponent<YAxisConfigBase> {
|
|
|
18
20
|
* Returns the space required by the y-axis
|
|
19
21
|
*/
|
|
20
22
|
getRequiredSpace(): ComponentSpace;
|
|
23
|
+
estimateLayoutSpace(labels: unknown[], theme: ChartTheme, svg: SVGSVGElement): void;
|
|
24
|
+
clearEstimatedSpace(): void;
|
|
21
25
|
render(svg: Selection<SVGSVGElement, undefined, null, undefined>, y: D3Scale, theme: ChartTheme, xPosition: number): void;
|
|
22
26
|
private applyLabelConstraints;
|
|
27
|
+
private measureLabelDimensions;
|
|
23
28
|
private wrapTextElement;
|
|
24
29
|
private addTitleTooltip;
|
|
25
30
|
}
|
package/dist/y-axis.js
CHANGED
|
@@ -1,6 +1,18 @@
|
|
|
1
1
|
import { axisLeft } from 'd3';
|
|
2
2
|
import { measureTextWidth, truncateText, wrapText, mergeDeep } from './utils.js';
|
|
3
3
|
export class YAxis {
|
|
4
|
+
resolveFontSizeValue(fontSize, fallback) {
|
|
5
|
+
if (typeof fontSize === 'number' && Number.isFinite(fontSize)) {
|
|
6
|
+
return fontSize;
|
|
7
|
+
}
|
|
8
|
+
if (typeof fontSize === 'string') {
|
|
9
|
+
const parsedFontSize = parseFloat(fontSize);
|
|
10
|
+
if (Number.isFinite(parsedFontSize)) {
|
|
11
|
+
return parsedFontSize;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
return fallback;
|
|
15
|
+
}
|
|
4
16
|
constructor(config) {
|
|
5
17
|
Object.defineProperty(this, "type", {
|
|
6
18
|
enumerable: true,
|
|
@@ -50,18 +62,25 @@ export class YAxis {
|
|
|
50
62
|
writable: true,
|
|
51
63
|
value: void 0
|
|
52
64
|
});
|
|
65
|
+
Object.defineProperty(this, "estimatedWidth", {
|
|
66
|
+
enumerable: true,
|
|
67
|
+
configurable: true,
|
|
68
|
+
writable: true,
|
|
69
|
+
value: null
|
|
70
|
+
});
|
|
53
71
|
Object.defineProperty(this, "exportHooks", {
|
|
54
72
|
enumerable: true,
|
|
55
73
|
configurable: true,
|
|
56
74
|
writable: true,
|
|
57
75
|
value: void 0
|
|
58
76
|
});
|
|
59
|
-
|
|
60
|
-
this.
|
|
61
|
-
this.
|
|
62
|
-
this.
|
|
63
|
-
this.
|
|
64
|
-
this.
|
|
77
|
+
const { display = true, tickFormat = null, rotatedLabels = false, maxLabelWidth, oversizedBehavior = 'truncate', exportHooks, } = config ?? {};
|
|
78
|
+
this.display = display;
|
|
79
|
+
this.tickFormat = tickFormat;
|
|
80
|
+
this.rotatedLabels = rotatedLabels;
|
|
81
|
+
this.maxLabelWidth = maxLabelWidth;
|
|
82
|
+
this.oversizedBehavior = oversizedBehavior;
|
|
83
|
+
this.exportHooks = exportHooks;
|
|
65
84
|
}
|
|
66
85
|
getExportConfig() {
|
|
67
86
|
return {
|
|
@@ -90,20 +109,54 @@ export class YAxis {
|
|
|
90
109
|
position: 'left',
|
|
91
110
|
};
|
|
92
111
|
}
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
112
|
+
if (this.estimatedWidth !== null) {
|
|
113
|
+
return {
|
|
114
|
+
width: this.estimatedWidth,
|
|
115
|
+
height: 0,
|
|
116
|
+
position: 'left',
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
const fallbackLabelWidth = this.maxLabelWidth ?? this.fontSize;
|
|
120
|
+
const width = fallbackLabelWidth + this.tickPadding;
|
|
97
121
|
return {
|
|
98
122
|
width,
|
|
99
123
|
height: 0, // Y-axis spans full height
|
|
100
124
|
position: 'left',
|
|
101
125
|
};
|
|
102
126
|
}
|
|
127
|
+
estimateLayoutSpace(labels, theme, svg) {
|
|
128
|
+
if (!labels.length) {
|
|
129
|
+
this.estimatedWidth = 0;
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
this.fontSize = this.resolveFontSizeValue(theme.axis.fontSize, this.fontSize);
|
|
133
|
+
const fontSize = this.fontSize;
|
|
134
|
+
const fontFamily = theme.axis.fontFamily;
|
|
135
|
+
const fontWeight = theme.axis.fontWeight || 'normal';
|
|
136
|
+
let maxWidth = 0;
|
|
137
|
+
let maxHeight = 0;
|
|
138
|
+
for (const label of labels) {
|
|
139
|
+
const text = String(label ?? '');
|
|
140
|
+
if (!text)
|
|
141
|
+
continue;
|
|
142
|
+
const { width, height } = this.measureLabelDimensions(text, fontSize, fontFamily, fontWeight, svg);
|
|
143
|
+
maxWidth = Math.max(maxWidth, width);
|
|
144
|
+
maxHeight = Math.max(maxHeight, height);
|
|
145
|
+
}
|
|
146
|
+
const radians = Math.PI / 4;
|
|
147
|
+
const labelFootprint = this.rotatedLabels
|
|
148
|
+
? Math.cos(radians) * maxWidth + Math.sin(radians) * maxHeight
|
|
149
|
+
: maxWidth;
|
|
150
|
+
this.estimatedWidth = this.tickPadding + labelFootprint;
|
|
151
|
+
}
|
|
152
|
+
clearEstimatedSpace() {
|
|
153
|
+
this.estimatedWidth = null;
|
|
154
|
+
}
|
|
103
155
|
render(svg, y, theme, xPosition) {
|
|
104
156
|
if (!this.display) {
|
|
105
157
|
return;
|
|
106
158
|
}
|
|
159
|
+
this.fontSize = this.resolveFontSizeValue(theme.axis.fontSize, this.fontSize);
|
|
107
160
|
const axis = axisLeft(y).tickSize(0).tickPadding(this.tickPadding);
|
|
108
161
|
// Apply tick formatting if specified
|
|
109
162
|
if (this.tickFormat) {
|
|
@@ -138,6 +191,9 @@ export class YAxis {
|
|
|
138
191
|
axisGroup.selectAll('.domain').remove();
|
|
139
192
|
}
|
|
140
193
|
applyLabelConstraints(axisGroup, svg, fontSize, fontFamily, fontWeight) {
|
|
194
|
+
if (this.maxLabelWidth === undefined) {
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
141
197
|
const maxWidth = this.maxLabelWidth;
|
|
142
198
|
const behavior = this.oversizedBehavior;
|
|
143
199
|
axisGroup
|
|
@@ -170,6 +226,39 @@ export class YAxis {
|
|
|
170
226
|
}
|
|
171
227
|
});
|
|
172
228
|
}
|
|
229
|
+
measureLabelDimensions(text, fontSize, fontFamily, fontWeight, svg) {
|
|
230
|
+
const textWidth = measureTextWidth(text, fontSize, fontFamily, fontWeight, svg);
|
|
231
|
+
const lineHeight = fontSize * 1.2;
|
|
232
|
+
if (this.maxLabelWidth === undefined ||
|
|
233
|
+
textWidth <= this.maxLabelWidth) {
|
|
234
|
+
return {
|
|
235
|
+
width: textWidth,
|
|
236
|
+
height: fontSize,
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
switch (this.oversizedBehavior) {
|
|
240
|
+
case 'truncate':
|
|
241
|
+
return {
|
|
242
|
+
width: this.maxLabelWidth,
|
|
243
|
+
height: fontSize,
|
|
244
|
+
};
|
|
245
|
+
case 'wrap': {
|
|
246
|
+
const lines = wrapText(text, this.maxLabelWidth, fontSize, fontFamily, fontWeight, svg);
|
|
247
|
+
const widestLine = lines.reduce((widest, line) => {
|
|
248
|
+
return Math.max(widest, measureTextWidth(line, fontSize, fontFamily, fontWeight, svg));
|
|
249
|
+
}, 0);
|
|
250
|
+
return {
|
|
251
|
+
width: widestLine,
|
|
252
|
+
height: Math.max(lines.length, 1) * lineHeight,
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
case 'hide':
|
|
256
|
+
return {
|
|
257
|
+
width: 0,
|
|
258
|
+
height: 0,
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
}
|
|
173
262
|
wrapTextElement(textEl, lines, originalText) {
|
|
174
263
|
// Clear existing content
|
|
175
264
|
textEl.textContent = '';
|