@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.
Files changed (46) hide show
  1. package/README.md +137 -3
  2. package/dist/area.d.ts +2 -0
  3. package/dist/area.js +39 -31
  4. package/dist/bar.d.ts +20 -1
  5. package/dist/bar.js +395 -519
  6. package/dist/base-chart.d.ts +21 -1
  7. package/dist/base-chart.js +166 -93
  8. package/dist/chart-group.d.ts +137 -0
  9. package/dist/chart-group.js +1155 -0
  10. package/dist/chart-interface.d.ts +1 -1
  11. package/dist/donut-center-content.d.ts +1 -0
  12. package/dist/donut-center-content.js +21 -38
  13. package/dist/donut-chart.js +30 -15
  14. package/dist/gauge-chart.d.ts +20 -0
  15. package/dist/gauge-chart.js +229 -133
  16. package/dist/legend-state.d.ts +19 -0
  17. package/dist/legend-state.js +81 -0
  18. package/dist/legend.d.ts +5 -2
  19. package/dist/legend.js +45 -38
  20. package/dist/line.js +3 -1
  21. package/dist/pie-chart.d.ts +3 -0
  22. package/dist/pie-chart.js +45 -19
  23. package/dist/scatter.d.ts +16 -0
  24. package/dist/scatter.js +165 -0
  25. package/dist/tooltip.d.ts +2 -1
  26. package/dist/tooltip.js +21 -25
  27. package/dist/types.d.ts +19 -1
  28. package/dist/utils.js +11 -19
  29. package/dist/validation.d.ts +4 -0
  30. package/dist/validation.js +19 -0
  31. package/dist/x-axis.d.ts +10 -0
  32. package/dist/x-axis.js +190 -149
  33. package/dist/xy-chart.d.ts +40 -1
  34. package/dist/xy-chart.js +488 -165
  35. package/dist/y-axis.d.ts +7 -2
  36. package/dist/y-axis.js +99 -10
  37. package/docs/chart-group.md +213 -0
  38. package/docs/components.md +321 -0
  39. package/docs/donut-chart.md +193 -0
  40. package/docs/gauge-chart.md +175 -0
  41. package/docs/getting-started.md +311 -0
  42. package/docs/pie-chart.md +123 -0
  43. package/docs/theming.md +162 -0
  44. package/docs/word-cloud-chart.md +98 -0
  45. package/docs/xy-chart.md +517 -0
  46. 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
- else {
166
- if (currentLine) {
167
- lines.push(currentLine);
168
- }
169
- // Check if single word exceeds maxWidth
170
- const wordWidth = measureTextWidth(word, fontSize, fontFamily, fontWeight, svg);
171
- if (wordWidth > maxWidth) {
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);
@@ -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
  */
@@ -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
- this.display = config?.display ?? true;
152
- this.dataKey = config?.dataKey;
153
- this.labelKey = config?.labelKey;
154
- this.groupLabelKey = config?.groupLabelKey;
155
- this.showGroupLabels = config?.showGroupLabels ?? false;
156
- this.groupLabelGap = config?.groupLabelGap ?? 10;
157
- this.rotatedLabels = config?.rotatedLabels ?? false;
158
- this.maxLabelWidth = config?.maxLabelWidth;
159
- this.oversizedBehavior = config?.oversizedBehavior ?? 'truncate';
160
- this.tickFormat = config?.tickFormat ?? null;
161
- this.autoHideOverlapping = config?.autoHideOverlapping ?? false;
162
- this.minLabelGap = config?.minLabelGap ?? 8;
163
- this.preserveEndLabels = config?.preserveEndLabels ?? true;
164
- this.exportHooks = config?.exportHooks;
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 effectiveWidth = this.maxLabelWidth
252
- ? Math.min(textWidth, this.maxLabelWidth)
253
- : textWidth;
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(axisGenerator)
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
- // Apply label constraints before rotation
332
- if (this.maxLabelWidth) {
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
- if (!this.dataKey ||
394
- data.length === 0 ||
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 = new Map();
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
- const tickElements = axisGroup
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
- const isSyntheticGapTick = tickValue.startsWith(GROUPED_GAP_TICK_PREFIX);
520
- if (isSyntheticGapTick && textElement) {
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((entry) => {
529
- return !entry.isSyntheticGapTick && entry.textElement !== null;
530
- })
531
- .map((entry) => {
532
- return entry.textElement;
588
+ .filter((textElement) => {
589
+ return textElement !== null;
533
590
  });
534
- const labelCount = labelEntries.length;
535
- if (labelCount <= 1) {
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
- // Calculate available space per real label only.
549
- // This deliberately ignores synthetic grouped-gap ticks.
550
- const availableSpace = (scale.range()[1] - scale.range()[0]) / labelCount;
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 === labelCount - 1;
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
- else if (isAtInterval) {
566
- textEl.style.visibility = 'visible';
567
- }
568
- else {
569
- textEl.style.visibility = 'hidden';
570
- }
612
+ textEl.style.visibility = isAtInterval ? 'visible' : 'hidden';
571
613
  });
572
- // Handle edge case: if last label is preserved but would overlap with the last visible interval label
573
- if (this.preserveEndLabels && labelCount > 1) {
574
- const lastVisibleIntervalIndex = Math.floor((labelCount - 2) / skipInterval) * skipInterval;
575
- if (lastVisibleIntervalIndex !== labelCount - 1 &&
576
- labelCount - 1 - lastVisibleIntervalIndex < skipInterval) {
577
- // Hide the last interval label to avoid overlap with preserved last label
578
- const textEl = labelEntries[lastVisibleIntervalIndex];
579
- if (textEl && lastVisibleIntervalIndex !== 0) {
580
- textEl.style.visibility = 'hidden';
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
  }
@@ -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;