@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 +92 -58
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/axes.test.ts +131 -28
- package/src/__tests__/dimensions.test.ts +41 -0
- package/src/__tests__/rotation.test.ts +83 -0
- package/src/layout/axes/rotation.ts +130 -14
- package/src/layout/axes/ticks.ts +2 -2
- package/src/layout/axes.ts +48 -93
- package/src/layout/plan.ts +18 -3
- package/src/layout/scales.ts +29 -5
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
|
-
|
|
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
|
-
|
|
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
|
|
10825
|
-
|
|
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
|
-
|
|
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
|
-
|
|
10913
|
-
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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 -
|
|
13938
|
+
maxWidth = x0 - LABEL_GAP2 - pad2;
|
|
13905
13939
|
} else {
|
|
13906
|
-
maxWidth = containerWidth - pad2 - (x1 +
|
|
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 -
|
|
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 +
|
|
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) +
|
|
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;
|