@opendata-ai/openchart-engine 7.9.2 → 7.9.3

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.
@@ -36,7 +36,7 @@ import { legendGap } from '../legend/wrap';
36
36
  import { yTickPositionIsInline } from './axes';
37
37
  import { resolveBandTickAngle } from './axes/rotation';
38
38
  import { buildContinuousTicks, scaleSupportsTickCount, targetTickCount } from './axes/ticks';
39
- import { computeScales, estimateBandwidth } from './scales';
39
+ import { computeScales, estimateBandStep, estimateBandwidth } from './scales';
40
40
  import { bottomMargin, chromeToInput, INLINE_TICK_OVERHANG_PAD, scalePadding } from './shared';
41
41
 
42
42
  // ---------------------------------------------------------------------------
@@ -342,8 +342,21 @@ export function resolveLayoutPlan(
342
342
  // bandwidth = step * (1 - paddingInner). Uses the same override resolution
343
343
  // as buildBandScale so paddingInner/paddingOuter are honored here too.
344
344
  const bandwidth = estimateBandwidth(encoding.x?.scale, plotWidth, xLabels.length);
345
+ const step = estimateBandStep(encoding.x?.scale, plotWidth, xLabels.length);
346
+ // Mirror categoricalTicks' explicit tickCount cap: retained ticks sit a
347
+ // multiple of the band step apart, so the drawn labels and their anchor
348
+ // spacing — not the full category set — drive the angle here, matching
349
+ // what computeAxes measures from actual tick positions.
350
+ const xTickCount = xAxis?.tickCount as number | undefined;
351
+ let drawnLabels = xLabels;
352
+ let anchorSpacing = step;
353
+ if (xTickCount && xLabels.length > xTickCount) {
354
+ const capStride = Math.ceil(xLabels.length / xTickCount);
355
+ drawnLabels = xLabels.filter((_, i) => i % capStride === 0);
356
+ anchorSpacing = step * capStride;
357
+ }
345
358
  let maxXLabelWidth = 0;
346
- for (const label of xLabels) {
359
+ for (const label of drawnLabels) {
347
360
  const w = measure(label, theme.fonts.sizes.axisTick, theme.fonts.weights.normal);
348
361
  if (w > maxXLabelWidth) maxXLabelWidth = w;
349
362
  }
@@ -351,7 +364,9 @@ export function resolveLayoutPlan(
351
364
  undefined,
352
365
  maxXLabelWidth,
353
366
  bandwidth,
354
- xLabels.length,
367
+ drawnLabels.length,
368
+ anchorSpacing,
369
+ theme.fonts.sizes.axisTick,
355
370
  );
356
371
  }
357
372
 
@@ -470,19 +470,43 @@ function evenRange(start: number, end: number, count: number): number[] {
470
470
  * each independently. step = width / (n - paddingInner + 2*paddingOuter);
471
471
  * bandwidth = step * (1 - paddingInner).
472
472
  */
473
+ /** d3 scaleBand padding resolution: `padding` sets both, overrides win. */
474
+ function resolveBandPadding(scaleConfig: EncodingChannel['scale'] | undefined): {
475
+ paddingInner: number;
476
+ paddingOuter: number;
477
+ } {
478
+ const padding = scaleConfig?.padding ?? 0.35;
479
+ return {
480
+ paddingInner: scaleConfig?.paddingInner ?? padding,
481
+ paddingOuter: scaleConfig?.paddingOuter ?? padding,
482
+ };
483
+ }
484
+
473
485
  export function estimateBandwidth(
474
486
  scaleConfig: EncodingChannel['scale'] | undefined,
475
487
  plotWidth: number,
476
488
  n: number,
489
+ ): number {
490
+ const { paddingInner } = resolveBandPadding(scaleConfig);
491
+ return estimateBandStep(scaleConfig, plotWidth, n) * (1 - paddingInner);
492
+ }
493
+
494
+ /**
495
+ * Estimate d3 scaleBand step (center-to-center band distance) for a given
496
+ * plot width and category count, using the same padding resolution as
497
+ * estimateBandwidth. The step — not the bandwidth — is the anchor spacing
498
+ * that drives rotated-label collision checks.
499
+ */
500
+ export function estimateBandStep(
501
+ scaleConfig: EncodingChannel['scale'] | undefined,
502
+ plotWidth: number,
503
+ n: number,
477
504
  ): number {
478
505
  if (n <= 0) return 0;
479
- const padding = scaleConfig?.padding ?? 0.35;
480
- const paddingInner = scaleConfig?.paddingInner ?? padding;
481
- const paddingOuter = scaleConfig?.paddingOuter ?? padding;
506
+ const { paddingInner, paddingOuter } = resolveBandPadding(scaleConfig);
482
507
  const denom = n - paddingInner + 2 * paddingOuter;
483
508
  if (denom <= 0) return 0;
484
- const step = plotWidth / denom;
485
- return step * (1 - paddingInner);
509
+ return plotWidth / denom;
486
510
  }
487
511
 
488
512
  function buildBandScale(