@opendata-ai/openchart-engine 7.9.1 → 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.
package/dist/index.js CHANGED
@@ -10506,12 +10506,41 @@ import {
10506
10506
  } from "@opendata-ai/openchart-core";
10507
10507
 
10508
10508
  // src/layout/axes/rotation.ts
10509
+ import { TICK_LINE_HEIGHT_FACTOR, X_AXIS_ROTATED_EXTENT_CAP } from "@opendata-ai/openchart-core";
10509
10510
  var BAND_FIT_FRACTION = 0.85;
10510
10511
  var AUTO_ROTATE_ANGLE = -45;
10511
- function resolveBandTickAngle(explicitAngle, maxLabelWidth, bandwidth, labelCount) {
10512
+ var STEEP_ROTATE_ANGLE = -90;
10513
+ var LABEL_GAP = 4;
10514
+ function ribbonSeparation(spacing, angleDeg) {
10515
+ return spacing * Math.abs(Math.sin(angleDeg * Math.PI / 180));
10516
+ }
10517
+ function alongTextClearance(spacing, angleDeg) {
10518
+ return spacing * Math.abs(Math.cos(angleDeg * Math.PI / 180));
10519
+ }
10520
+ function rotatedLabelsFit(spacing, angleDeg, fontSize, maxLabelWidth) {
10521
+ return ribbonSeparation(spacing, angleDeg) >= fontSize * TICK_LINE_HEIGHT_FACTOR + LABEL_GAP || alongTextClearance(spacing, angleDeg) >= maxLabelWidth + LABEL_GAP;
10522
+ }
10523
+ function resolveBandTickAngle(explicitAngle, maxLabelWidth, bandwidth, labelCount, spacing, fontSize) {
10512
10524
  if (explicitAngle !== void 0) return explicitAngle;
10513
10525
  if (labelCount < 2) return void 0;
10514
- return maxLabelWidth > bandwidth * BAND_FIT_FRACTION ? AUTO_ROTATE_ANGLE : void 0;
10526
+ if (maxLabelWidth <= bandwidth * BAND_FIT_FRACTION) return void 0;
10527
+ if (rotatedLabelsFit(spacing, AUTO_ROTATE_ANGLE, fontSize, maxLabelWidth)) {
10528
+ return AUTO_ROTATE_ANGLE;
10529
+ }
10530
+ return maxLabelWidth <= X_AXIS_ROTATED_EXTENT_CAP ? STEEP_ROTATE_ANGLE : AUTO_ROTATE_ANGLE;
10531
+ }
10532
+ function bandLabelStride(maxLabelWidth, angleDeg, fontSize, spacing) {
10533
+ if (spacing <= 0) return 1;
10534
+ if (angleDeg === void 0 || angleDeg === 0) {
10535
+ return Math.max(1, Math.ceil((maxLabelWidth + LABEL_GAP) / spacing));
10536
+ }
10537
+ const separation = ribbonSeparation(spacing, angleDeg);
10538
+ const clearance = alongTextClearance(spacing, angleDeg);
10539
+ const required = fontSize * TICK_LINE_HEIGHT_FACTOR + LABEL_GAP;
10540
+ const byRibbon = separation > 0 ? Math.ceil(required / separation) : Number.POSITIVE_INFINITY;
10541
+ const byLength = clearance > 0 ? Math.ceil((maxLabelWidth + LABEL_GAP) / clearance) : Number.POSITIVE_INFINITY;
10542
+ const stride = Math.min(byRibbon, byLength);
10543
+ return Number.isFinite(stride) ? Math.max(1, stride) : 1;
10515
10544
  }
10516
10545
 
10517
10546
  // src/layout/axes/thinning.ts
@@ -10821,44 +10850,10 @@ function fitContinuousTicks(scale, initialTicks, initialCount, fontSize, fontWei
10821
10850
  const fallback = ensureMinContinuousTicks(scale, rawFallback, floor);
10822
10851
  return thinTicksUntilFit(fallback, fontSize, fontWeight, measureText, orientation);
10823
10852
  }
10824
- function labelSpanAtAngle(tick, angleDeg, fontSize, fontWeight, measureText) {
10825
- const angleRad = Math.abs(angleDeg) * Math.PI / 180;
10826
- const cosA = angleRad > 0 ? Math.abs(Math.cos(angleRad)) : 1;
10827
- const projected = measureLabel(tick.label, fontSize, fontWeight, measureText) * cosA;
10828
- if (angleDeg === 0) {
10829
- return { left: tick.position - projected / 2, right: tick.position + projected / 2 };
10830
- }
10831
- if (angleDeg < 0) {
10832
- return { left: tick.position - projected, right: tick.position };
10833
- }
10834
- return { left: tick.position, right: tick.position + projected };
10835
- }
10836
- function thinBandTicksIfNeeded(ticks2, angleDeg, fontSize, fontWeight, measureText) {
10837
- if (ticks2.length < 3) return ticks2;
10838
- const minGap = fontSize * 0.5;
10839
- const spans = ticks2.map((t) => labelSpanAtAngle(t, angleDeg, fontSize, fontWeight, measureText));
10840
- const keep = new Array(ticks2.length).fill(false);
10841
- keep[0] = true;
10842
- let lastRight = spans[0].right;
10843
- for (let i = 1; i < ticks2.length; i++) {
10844
- if (spans[i].left >= lastRight + minGap) {
10845
- keep[i] = true;
10846
- lastRight = spans[i].right;
10847
- }
10848
- }
10853
+ function applyBandLabelStride(ticks2, stride) {
10854
+ if (stride <= 1 || ticks2.length < 3) return ticks2;
10849
10855
  const lastIdx = ticks2.length - 1;
10850
- if (!keep[lastIdx]) {
10851
- keep[lastIdx] = true;
10852
- for (let i = lastIdx - 1; i > 0; i--) {
10853
- if (!keep[i]) continue;
10854
- if (spans[i].right + minGap > spans[lastIdx].left) {
10855
- keep[i] = false;
10856
- continue;
10857
- }
10858
- break;
10859
- }
10860
- }
10861
- return ticks2.filter((_, i) => keep[i]);
10856
+ return ticks2.filter((_, i) => (lastIdx - i) % stride === 0);
10862
10857
  }
10863
10858
  function computeAxes(scales, chartArea, strategy, theme, measureText, dataContext) {
10864
10859
  const result = {};
@@ -10909,22 +10904,37 @@ function computeAxes(scales, chartArea, strategy, theme, measureText, dataContex
10909
10904
  major: true
10910
10905
  }));
10911
10906
  let tickAngle = axisConfig?.labelAngle;
10912
- if (tickAngle === void 0 && scales.x.type === "band" && allTicks.length > 1) {
10913
- const bandwidth = scales.x.scale.bandwidth();
10907
+ let bandStride = 1;
10908
+ if (scales.x.type === "band" && allTicks.length > 1) {
10909
+ const bandScale = scales.x.scale;
10910
+ const bandwidth = bandScale.bandwidth();
10911
+ let anchorSpacing = Number.POSITIVE_INFINITY;
10912
+ for (let i = 1; i < allTicks.length; i++) {
10913
+ const d = Math.abs(allTicks[i].position - allTicks[i - 1].position);
10914
+ if (d > 0 && d < anchorSpacing) anchorSpacing = d;
10915
+ }
10916
+ if (!Number.isFinite(anchorSpacing)) anchorSpacing = bandScale.step();
10914
10917
  let maxLabelWidth = 0;
10915
10918
  for (const t of allTicks) {
10916
10919
  const w = measureLabel(t.label, fontSize, fontWeight, measureText);
10917
10920
  if (w > maxLabelWidth) maxLabelWidth = w;
10918
10921
  }
10919
- tickAngle = resolveBandTickAngle(void 0, maxLabelWidth, bandwidth, allTicks.length);
10922
+ tickAngle = resolveBandTickAngle(
10923
+ axisConfig?.labelAngle,
10924
+ maxLabelWidth,
10925
+ bandwidth,
10926
+ allTicks.length,
10927
+ anchorSpacing,
10928
+ fontSize
10929
+ );
10930
+ bandStride = bandLabelStride(maxLabelWidth, tickAngle, fontSize, anchorSpacing);
10920
10931
  }
10921
10932
  const hasExplicitValues = !!axisConfig?.values;
10922
10933
  let ticks2;
10923
10934
  if (hasExplicitValues) {
10924
10935
  ticks2 = allTicks;
10925
10936
  } else if (scales.x.type === "band") {
10926
- const effectiveAngle = tickAngle ?? 0;
10927
- ticks2 = thinBandTicksIfNeeded(allTicks, effectiveAngle, fontSize, fontWeight, measureText);
10937
+ ticks2 = applyBandLabelStride(allTicks, bandStride);
10928
10938
  } else if (isContinuousX) {
10929
10939
  ticks2 = fitContinuousTicks(
10930
10940
  scales.x,
@@ -11076,7 +11086,12 @@ function computeAxes(scales, chartArea, strategy, theme, measureText, dataContex
11076
11086
  );
11077
11087
  return Math.max(max4, w);
11078
11088
  }, 0);
11079
- const titleOff = axisTitleOffset(maxTickLabelWidth, result.y.labelStyle.fontSize, totalWidth);
11089
+ const titleOff = axisTitleOffset(
11090
+ maxTickLabelWidth,
11091
+ result.y.labelStyle.fontSize,
11092
+ totalWidth,
11093
+ result.y.tickPosition === "inline"
11094
+ );
11080
11095
  result.y.titlePosition = {
11081
11096
  x: chartArea.x - titleOff,
11082
11097
  y: chartArea.y + chartArea.height / 2,
@@ -11282,15 +11297,23 @@ function evenRange(start, end, count) {
11282
11297
  const step = (end - start) / (count - 1);
11283
11298
  return Array.from({ length: count }, (_, i) => start + step * i);
11284
11299
  }
11300
+ function resolveBandPadding(scaleConfig) {
11301
+ const padding = scaleConfig?.padding ?? 0.35;
11302
+ return {
11303
+ paddingInner: scaleConfig?.paddingInner ?? padding,
11304
+ paddingOuter: scaleConfig?.paddingOuter ?? padding
11305
+ };
11306
+ }
11285
11307
  function estimateBandwidth(scaleConfig, plotWidth, n) {
11308
+ const { paddingInner } = resolveBandPadding(scaleConfig);
11309
+ return estimateBandStep(scaleConfig, plotWidth, n) * (1 - paddingInner);
11310
+ }
11311
+ function estimateBandStep(scaleConfig, plotWidth, n) {
11286
11312
  if (n <= 0) return 0;
11287
- const padding = scaleConfig?.padding ?? 0.35;
11288
- const paddingInner = scaleConfig?.paddingInner ?? padding;
11289
- const paddingOuter = scaleConfig?.paddingOuter ?? padding;
11313
+ const { paddingInner, paddingOuter } = resolveBandPadding(scaleConfig);
11290
11314
  const denom = n - paddingInner + 2 * paddingOuter;
11291
11315
  if (denom <= 0) return 0;
11292
- const step = plotWidth / denom;
11293
- return step * (1 - paddingInner);
11316
+ return plotWidth / denom;
11294
11317
  }
11295
11318
  function buildBandScale(channel, data, rangeStart, rangeEnd) {
11296
11319
  const values = channel.scale?.domain ? channel.scale.domain : applyCategoricalSort(uniqueStrings(fieldValues(data, channel.field)), channel.sort);
@@ -11749,8 +11772,17 @@ function resolveLayoutPlan(chartSpec, renderSpec, options, theme, strategy, wate
11749
11772
  const rightMarginEst = hPad + (isRadial ? hPad : axisMargin);
11750
11773
  const plotWidth = Math.max(0, width - leftGutter - Math.max(rightMarginEst, hPad));
11751
11774
  const bandwidth = estimateBandwidth(encoding.x?.scale, plotWidth, xLabels.length);
11775
+ const step = estimateBandStep(encoding.x?.scale, plotWidth, xLabels.length);
11776
+ const xTickCount = xAxis?.tickCount;
11777
+ let drawnLabels = xLabels;
11778
+ let anchorSpacing = step;
11779
+ if (xTickCount && xLabels.length > xTickCount) {
11780
+ const capStride = Math.ceil(xLabels.length / xTickCount);
11781
+ drawnLabels = xLabels.filter((_, i) => i % capStride === 0);
11782
+ anchorSpacing = step * capStride;
11783
+ }
11752
11784
  let maxXLabelWidth = 0;
11753
- for (const label of xLabels) {
11785
+ for (const label of drawnLabels) {
11754
11786
  const w = measure(label, theme.fonts.sizes.axisTick, theme.fonts.weights.normal);
11755
11787
  if (w > maxXLabelWidth) maxXLabelWidth = w;
11756
11788
  }
@@ -11758,7 +11790,9 @@ function resolveLayoutPlan(chartSpec, renderSpec, options, theme, strategy, wate
11758
11790
  void 0,
11759
11791
  maxXLabelWidth,
11760
11792
  bandwidth,
11761
- xLabels.length
11793
+ drawnLabels.length,
11794
+ anchorSpacing,
11795
+ theme.fonts.sizes.axisTick
11762
11796
  );
11763
11797
  }
11764
11798
  const xAxisExtent = isRadial || xAxisSuppressed ? 0 : computeXAxisExtentFromLabels2({
@@ -13829,7 +13863,7 @@ function generateLinkPath(link) {
13829
13863
  }
13830
13864
 
13831
13865
  // src/sankey/compile-sankey.ts
13832
- var LABEL_GAP = 6;
13866
+ var LABEL_GAP2 = 6;
13833
13867
  var LINK_OPACITY_LIGHT = 0.5;
13834
13868
  var LINK_OPACITY_DARK = 0.75;
13835
13869
  var NODE_CORNER_RADIUS = 2;
@@ -13901,16 +13935,16 @@ function computeNodeLabel(node, maxDepth, theme, nodeWidth, nodeLabelAlign = "au
13901
13935
  let maxWidth;
13902
13936
  if (containerWidth !== void 0) {
13903
13937
  if (placeLeft) {
13904
- maxWidth = x0 - LABEL_GAP - pad2;
13938
+ maxWidth = x0 - LABEL_GAP2 - pad2;
13905
13939
  } else {
13906
- maxWidth = containerWidth - pad2 - (x1 + LABEL_GAP);
13940
+ maxWidth = containerWidth - pad2 - (x1 + LABEL_GAP2);
13907
13941
  }
13908
13942
  if (maxWidth !== void 0 && maxWidth < 0) maxWidth = 0;
13909
13943
  }
13910
13944
  if (placeLeft) {
13911
13945
  return {
13912
13946
  text: node.label ?? node.id,
13913
- x: x0 - LABEL_GAP,
13947
+ x: x0 - LABEL_GAP2,
13914
13948
  y: midY,
13915
13949
  style: { ...style, textAnchor: "end", dominantBaseline: "central" },
13916
13950
  visible: true,
@@ -13919,7 +13953,7 @@ function computeNodeLabel(node, maxDepth, theme, nodeWidth, nodeLabelAlign = "au
13919
13953
  }
13920
13954
  return {
13921
13955
  text: node.label ?? node.id,
13922
- x: x1 + LABEL_GAP,
13956
+ x: x1 + LABEL_GAP2,
13923
13957
  y: midY,
13924
13958
  style: { ...style, textAnchor: "start", dominantBaseline: "central" },
13925
13959
  visible: true,
@@ -14031,7 +14065,7 @@ function compileSankey(spec, options) {
14031
14065
  const depth = node.depth ?? 0;
14032
14066
  const labelsLeft = nodeLabelAlign === "left" || nodeLabelAlign === "auto" && depth === maxDepthFirst;
14033
14067
  if (labelsLeft) continue;
14034
- const labelX = (node.x1 ?? nodeWidth) + LABEL_GAP;
14068
+ const labelX = (node.x1 ?? nodeWidth) + LABEL_GAP2;
14035
14069
  const labelText = node.label ?? node.id;
14036
14070
  const labelWidth = estimateTextWidth18(labelText, labelFontSize, labelFontWeight);
14037
14071
  const overflow = labelX + labelWidth - rightEdge;