@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 +86 -57
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/axes.test.ts +106 -35
- 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 +37 -98
- 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 > 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,
|
|
@@ -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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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 -
|
|
13938
|
+
maxWidth = x0 - LABEL_GAP2 - pad2;
|
|
13910
13939
|
} else {
|
|
13911
|
-
maxWidth = containerWidth - pad2 - (x1 +
|
|
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 -
|
|
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 +
|
|
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) +
|
|
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;
|