@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/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
- renderChart({ svg, plotGroup, plotArea, }) {
130
- this.validateSeriesOrientation();
131
- this.series.forEach((series) => {
132
- const typeName = this.getSeriesTypeName(series);
133
- ChartValidator.validateDataKey(this.data, series.dataKey, typeName);
134
- ChartValidator.validateNumericData(this.data, series.dataKey, typeName);
135
- });
136
- const valueScaleType = this.resolvedScaleConfig.y?.type ?? 'linear';
137
- if (valueScaleType === 'log') {
138
- this.series.forEach((series) => {
139
- const typeName = this.getSeriesTypeName(series);
140
- ChartValidator.validatePositiveData(this.data, series.dataKey, typeName);
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
- if (this.xAxis?.dataKey) {
144
- ChartValidator.validateDataKey(this.data, this.xAxis.dataKey, 'XAxis');
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
- if (this.x && this.y) {
156
- if (this.xAxis) {
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
- const scale = scaleBand()
608
+ case 'band':
609
+ return scaleBand()
504
610
  .domain(domain)
505
- .rangeRound([rangeStart, rangeEnd]);
506
- if (config.padding !== undefined) {
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([rangeStart, rangeEnd]);
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([rangeStart, rangeEnd]);
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([rangeStart, rangeEnd]);
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
- resolveScaleDomain(config, dataKey) {
537
- const barSeries = this.series.filter((s) => s.type === 'bar');
538
- const lineSeries = this.series.filter((s) => s.type === 'line');
539
- const scatterSeries = this.series.filter((s) => s.type === 'scatter');
540
- const areaSeries = this.series.filter((s) => s.type === 'area');
541
- const stackedAreaGroups = this.getStackedAreaGroups(areaSeries);
542
- const isBarValueScale = dataKey === null &&
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
- (config.type === 'linear' || config.type === 'log');
545
- let domain;
546
- if (isBarValueScale && config.type === 'log') {
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
- domain = config.domain;
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
- else {
571
- const hasPercentBars = this.barStackMode === 'percent' && barSeries.length > 0;
572
- const hasPercentAreas = this.areaStackMode === 'percent' && stackedAreaGroups.size > 0;
573
- if (hasPercentBars || hasPercentAreas) {
574
- let minDomain = 0;
575
- let maxDomain = 100;
576
- if (hasPercentBars) {
577
- [minDomain, maxDomain] = this.getBarValueDomain(this.getXKey(), barSeries);
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
- getSeriesTooltipValue(series, dataPoint, xKey, areaStackingContextBySeries) {
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 readonly fontSize;
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
- this.display = config?.display ?? true;
60
- this.tickFormat = config?.tickFormat ?? null;
61
- this.rotatedLabels = config?.rotatedLabels ?? false;
62
- this.maxLabelWidth = config?.maxLabelWidth ?? 40; // Default 40 for backward compatibility
63
- this.oversizedBehavior = config?.oversizedBehavior ?? 'truncate';
64
- this.exportHooks = config?.exportHooks;
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
- // Width = max label width + tick padding
94
- // Rotated labels need less width (cos(45°) ≈ 0.7 of horizontal width)
95
- const baseWidth = this.maxLabelWidth + this.tickPadding;
96
- const width = this.rotatedLabels ? baseWidth * 0.7 : baseWidth;
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 = '';