@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.
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 > 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,
@@ -11287,15 +11297,23 @@ function evenRange(start, end, count) {
11287
11297
  const step = (end - start) / (count - 1);
11288
11298
  return Array.from({ length: count }, (_, i) => start + step * i);
11289
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
+ }
11290
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) {
11291
11312
  if (n <= 0) return 0;
11292
- const padding = scaleConfig?.padding ?? 0.35;
11293
- const paddingInner = scaleConfig?.paddingInner ?? padding;
11294
- const paddingOuter = scaleConfig?.paddingOuter ?? padding;
11313
+ const { paddingInner, paddingOuter } = resolveBandPadding(scaleConfig);
11295
11314
  const denom = n - paddingInner + 2 * paddingOuter;
11296
11315
  if (denom <= 0) return 0;
11297
- const step = plotWidth / denom;
11298
- return step * (1 - paddingInner);
11316
+ return plotWidth / denom;
11299
11317
  }
11300
11318
  function buildBandScale(channel, data, rangeStart, rangeEnd) {
11301
11319
  const values = channel.scale?.domain ? channel.scale.domain : applyCategoricalSort(uniqueStrings(fieldValues(data, channel.field)), channel.sort);
@@ -11754,8 +11772,17 @@ function resolveLayoutPlan(chartSpec, renderSpec, options, theme, strategy, wate
11754
11772
  const rightMarginEst = hPad + (isRadial ? hPad : axisMargin);
11755
11773
  const plotWidth = Math.max(0, width - leftGutter - Math.max(rightMarginEst, hPad));
11756
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
+ }
11757
11784
  let maxXLabelWidth = 0;
11758
- for (const label of xLabels) {
11785
+ for (const label of drawnLabels) {
11759
11786
  const w = measure(label, theme.fonts.sizes.axisTick, theme.fonts.weights.normal);
11760
11787
  if (w > maxXLabelWidth) maxXLabelWidth = w;
11761
11788
  }
@@ -11763,7 +11790,9 @@ function resolveLayoutPlan(chartSpec, renderSpec, options, theme, strategy, wate
11763
11790
  void 0,
11764
11791
  maxXLabelWidth,
11765
11792
  bandwidth,
11766
- xLabels.length
11793
+ drawnLabels.length,
11794
+ anchorSpacing,
11795
+ theme.fonts.sizes.axisTick
11767
11796
  );
11768
11797
  }
11769
11798
  const xAxisExtent = isRadial || xAxisSuppressed ? 0 : computeXAxisExtentFromLabels2({
@@ -13834,7 +13863,7 @@ function generateLinkPath(link) {
13834
13863
  }
13835
13864
 
13836
13865
  // src/sankey/compile-sankey.ts
13837
- var LABEL_GAP = 6;
13866
+ var LABEL_GAP2 = 6;
13838
13867
  var LINK_OPACITY_LIGHT = 0.5;
13839
13868
  var LINK_OPACITY_DARK = 0.75;
13840
13869
  var NODE_CORNER_RADIUS = 2;
@@ -13906,16 +13935,16 @@ function computeNodeLabel(node, maxDepth, theme, nodeWidth, nodeLabelAlign = "au
13906
13935
  let maxWidth;
13907
13936
  if (containerWidth !== void 0) {
13908
13937
  if (placeLeft) {
13909
- maxWidth = x0 - LABEL_GAP - pad2;
13938
+ maxWidth = x0 - LABEL_GAP2 - pad2;
13910
13939
  } else {
13911
- maxWidth = containerWidth - pad2 - (x1 + LABEL_GAP);
13940
+ maxWidth = containerWidth - pad2 - (x1 + LABEL_GAP2);
13912
13941
  }
13913
13942
  if (maxWidth !== void 0 && maxWidth < 0) maxWidth = 0;
13914
13943
  }
13915
13944
  if (placeLeft) {
13916
13945
  return {
13917
13946
  text: node.label ?? node.id,
13918
- x: x0 - LABEL_GAP,
13947
+ x: x0 - LABEL_GAP2,
13919
13948
  y: midY,
13920
13949
  style: { ...style, textAnchor: "end", dominantBaseline: "central" },
13921
13950
  visible: true,
@@ -13924,7 +13953,7 @@ function computeNodeLabel(node, maxDepth, theme, nodeWidth, nodeLabelAlign = "au
13924
13953
  }
13925
13954
  return {
13926
13955
  text: node.label ?? node.id,
13927
- x: x1 + LABEL_GAP,
13956
+ x: x1 + LABEL_GAP2,
13928
13957
  y: midY,
13929
13958
  style: { ...style, textAnchor: "start", dominantBaseline: "central" },
13930
13959
  visible: true,
@@ -14036,7 +14065,7 @@ function compileSankey(spec, options) {
14036
14065
  const depth = node.depth ?? 0;
14037
14066
  const labelsLeft = nodeLabelAlign === "left" || nodeLabelAlign === "auto" && depth === maxDepthFirst;
14038
14067
  if (labelsLeft) continue;
14039
- const labelX = (node.x1 ?? nodeWidth) + LABEL_GAP;
14068
+ const labelX = (node.x1 ?? nodeWidth) + LABEL_GAP2;
14040
14069
  const labelText = node.label ?? node.id;
14041
14070
  const labelWidth = estimateTextWidth18(labelText, labelFontSize, labelFontWeight);
14042
14071
  const overflow = labelX + labelWidth - rightEdge;