@opendata-ai/openchart-engine 7.3.0 → 7.4.1
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.d.ts +4 -3
- package/dist/index.js +150 -81
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/__snapshots__/compile-snapshot.test.ts.snap +4 -15
- package/src/__tests__/axes.test.ts +62 -0
- package/src/__tests__/dimensions.test.ts +63 -0
- package/src/__tests__/helpers/invariants.ts +4 -6
- package/src/__tests__/layout-fuzz.test.ts +24 -8
- package/src/__tests__/layout-invariants.test.ts +4 -2
- package/src/__tests__/legend.test.ts +54 -0
- package/src/charts/bar/__tests__/compute.test.ts +131 -45
- package/src/charts/bar/compute.ts +44 -4
- package/src/charts/bar/index.ts +9 -2
- package/src/charts/bar/labels.ts +77 -26
- package/src/charts/column/labels.ts +29 -16
- package/src/charts/registry.ts +4 -2
- package/src/compile.ts +3 -1
- package/src/layout/axes/rotation.ts +36 -0
- package/src/layout/axes.ts +65 -35
- package/src/layout/plan.ts +28 -2
- package/src/layout/scales.ts +22 -0
- package/src/legend/compute.ts +9 -1
package/dist/index.d.ts
CHANGED
|
@@ -556,10 +556,11 @@ declare function compile(spec: unknown): CompileResult;
|
|
|
556
556
|
* A chart renderer function.
|
|
557
557
|
*
|
|
558
558
|
* Takes a normalized spec, resolved scales, chart area, layout strategy,
|
|
559
|
-
*
|
|
560
|
-
*
|
|
559
|
+
* the resolved theme for theme-aware styling (e.g. label colors), and the
|
|
560
|
+
* full container width (used by geometry gates that must key off the
|
|
561
|
+
* container, not the derived plot width). Returns an array of marks to render.
|
|
561
562
|
*/
|
|
562
|
-
type ChartRenderer = (spec: NormalizedChartSpec, scales: ResolvedScales, chartArea: Rect, strategy: LayoutStrategy, theme: ResolvedTheme) => Mark[];
|
|
563
|
+
type ChartRenderer = (spec: NormalizedChartSpec, scales: ResolvedScales, chartArea: Rect, strategy: LayoutStrategy, theme: ResolvedTheme, containerWidth: number) => Mark[];
|
|
563
564
|
/**
|
|
564
565
|
* Register a chart renderer for a specific chart type.
|
|
565
566
|
*
|
package/dist/index.js
CHANGED
|
@@ -1925,7 +1925,7 @@ function computeEndpointLabels(spec, marks, theme, chartArea, strategy) {
|
|
|
1925
1925
|
}
|
|
1926
1926
|
|
|
1927
1927
|
// src/charts/bar/compute.ts
|
|
1928
|
-
import { isGradientDef } from "@opendata-ai/openchart-core";
|
|
1928
|
+
import { isGradientDef, NARROW_VIEWPORT_MAX } from "@opendata-ai/openchart-core";
|
|
1929
1929
|
|
|
1930
1930
|
// src/transforms/predicates.ts
|
|
1931
1931
|
function isFieldPredicate(pred) {
|
|
@@ -2094,7 +2094,8 @@ function orientGradientForHorizontalBar(grad) {
|
|
|
2094
2094
|
return { ...lg, x1: 0, y1: 0, x2: 1, y2: 0 };
|
|
2095
2095
|
}
|
|
2096
2096
|
var MIN_BAR_WIDTH = 1;
|
|
2097
|
-
|
|
2097
|
+
var MIN_GROUPED_BAR_THICKNESS = 8;
|
|
2098
|
+
function computeBarMarks(spec, scales, _chartArea, _strategy, containerWidth) {
|
|
2098
2099
|
const encoding = spec.encoding;
|
|
2099
2100
|
const xChannel = encoding.x;
|
|
2100
2101
|
const yChannel = encoding.y;
|
|
@@ -2143,7 +2144,8 @@ function computeBarMarks(spec, scales, _chartArea, _strategy) {
|
|
|
2143
2144
|
yScale,
|
|
2144
2145
|
bandwidth,
|
|
2145
2146
|
baseline,
|
|
2146
|
-
scales
|
|
2147
|
+
scales,
|
|
2148
|
+
containerWidth
|
|
2147
2149
|
);
|
|
2148
2150
|
} else {
|
|
2149
2151
|
const stackMode = xChannel.stack === "normalize" ? "normalize" : xChannel.stack === "center" ? "center" : "zero";
|
|
@@ -2218,7 +2220,7 @@ function computeStackedBars(data, valueField, categoryField, colorField, xScale,
|
|
|
2218
2220
|
}
|
|
2219
2221
|
return marks;
|
|
2220
2222
|
}
|
|
2221
|
-
function computeGroupedBars(data, valueField, categoryField, colorField, xScale, yScale, bandwidth, baseline, scales) {
|
|
2223
|
+
function computeGroupedBars(data, valueField, categoryField, colorField, xScale, yScale, bandwidth, baseline, scales, containerWidth) {
|
|
2222
2224
|
const marks = [];
|
|
2223
2225
|
const categoryGroups = groupByField(data, categoryField);
|
|
2224
2226
|
const groupIndexMap = /* @__PURE__ */ new Map();
|
|
@@ -2231,7 +2233,17 @@ function computeGroupedBars(data, valueField, categoryField, colorField, xScale,
|
|
|
2231
2233
|
const groupCount = groupIndexMap.size;
|
|
2232
2234
|
if (groupCount === 0) return marks;
|
|
2233
2235
|
const gap = Math.min(1, bandwidth * 0.05);
|
|
2234
|
-
|
|
2236
|
+
let groupHeight = bandwidth;
|
|
2237
|
+
let subBandHeight = (groupHeight - gap * (groupCount - 1)) / groupCount;
|
|
2238
|
+
const step = typeof yScale.step === "function" ? yScale.step() : bandwidth;
|
|
2239
|
+
const maxGroupHeight = Math.max(bandwidth, step - MIN_BAR_WIDTH);
|
|
2240
|
+
if (containerWidth < NARROW_VIEWPORT_MAX && subBandHeight < MIN_GROUPED_BAR_THICKNESS && maxGroupHeight > bandwidth) {
|
|
2241
|
+
const needed = MIN_GROUPED_BAR_THICKNESS * groupCount + gap * (groupCount - 1);
|
|
2242
|
+
groupHeight = Math.min(Math.max(groupHeight, needed), maxGroupHeight);
|
|
2243
|
+
subBandHeight = (groupHeight - gap * (groupCount - 1)) / groupCount;
|
|
2244
|
+
}
|
|
2245
|
+
subBandHeight = Math.max(subBandHeight, MIN_BAR_WIDTH);
|
|
2246
|
+
const groupOffset = (bandwidth - groupHeight) / 2;
|
|
2235
2247
|
for (const [category, rows] of categoryGroups) {
|
|
2236
2248
|
const bandY = yScale(category);
|
|
2237
2249
|
if (bandY === void 0) continue;
|
|
@@ -2243,7 +2255,7 @@ function computeGroupedBars(data, valueField, categoryField, colorField, xScale,
|
|
|
2243
2255
|
const color2 = getColor(scales, groupKey3);
|
|
2244
2256
|
const xPos = value2 >= 0 ? baseline : xScale(value2);
|
|
2245
2257
|
const barWidth = Math.max(Math.abs(xScale(value2) - baseline), MIN_BAR_WIDTH);
|
|
2246
|
-
const subY = bandY + groupIndex * (subBandHeight + gap);
|
|
2258
|
+
const subY = bandY + groupOffset + groupIndex * (subBandHeight + gap);
|
|
2247
2259
|
const aria = {
|
|
2248
2260
|
label: `${category}, ${groupKey3}: ${formatLabelValue(value2)}`
|
|
2249
2261
|
};
|
|
@@ -2422,6 +2434,7 @@ function computeBarLabels(marks, _chartArea, density = "auto", labelFormat, labe
|
|
|
2422
2434
|
const targetMarks = filterByDensity(marks, density);
|
|
2423
2435
|
const candidates = [];
|
|
2424
2436
|
const fitsInSegment = [];
|
|
2437
|
+
const displayAnchorX = [];
|
|
2425
2438
|
const formatter = buildD3Formatter(labelFormat);
|
|
2426
2439
|
for (const mark of targetMarks) {
|
|
2427
2440
|
let valuePart;
|
|
@@ -2479,12 +2492,15 @@ function computeBarLabels(marks, _chartArea, density = "auto", labelFormat, labe
|
|
|
2479
2492
|
textAnchor = "start";
|
|
2480
2493
|
}
|
|
2481
2494
|
}
|
|
2482
|
-
const
|
|
2495
|
+
const centerY = mark.y + mark.height / 2;
|
|
2496
|
+
const anchorY = centerY - textHeight / 2;
|
|
2497
|
+
const leftEdgeX = textAnchor === "end" ? anchorX - textWidth : textAnchor === "middle" ? anchorX - textWidth / 2 : anchorX;
|
|
2483
2498
|
const fits = !(isStacked2 && textWidth > mark.width - 2 * LABEL_PADDING);
|
|
2499
|
+
displayAnchorX.push(anchorX);
|
|
2484
2500
|
fitsInSegment.push(fits);
|
|
2485
2501
|
candidates.push({
|
|
2486
2502
|
text: valuePart,
|
|
2487
|
-
anchorX,
|
|
2503
|
+
anchorX: leftEdgeX,
|
|
2488
2504
|
anchorY,
|
|
2489
2505
|
width: textWidth,
|
|
2490
2506
|
height: textHeight,
|
|
@@ -2501,46 +2517,61 @@ function computeBarLabels(marks, _chartArea, density = "auto", labelFormat, labe
|
|
|
2501
2517
|
});
|
|
2502
2518
|
}
|
|
2503
2519
|
if (candidates.length === 0) return [];
|
|
2504
|
-
if (density === "all") {
|
|
2505
|
-
return candidates.map((c, i) => ({
|
|
2506
|
-
text: c.text,
|
|
2507
|
-
x: c.anchorX,
|
|
2508
|
-
y: c.anchorY,
|
|
2509
|
-
style: c.style,
|
|
2510
|
-
visible: fitsInSegment[i] !== false
|
|
2511
|
-
}));
|
|
2512
|
-
}
|
|
2513
2520
|
const fittingCandidates = [];
|
|
2514
2521
|
const unfittingIndices = /* @__PURE__ */ new Set();
|
|
2515
2522
|
for (let i = 0; i < candidates.length; i++) {
|
|
2516
2523
|
if (fitsInSegment[i] === false) {
|
|
2517
2524
|
unfittingIndices.add(i);
|
|
2518
2525
|
} else {
|
|
2519
|
-
fittingCandidates.push(candidates[i]);
|
|
2526
|
+
fittingCandidates.push({ ...candidates[i], index: i });
|
|
2520
2527
|
}
|
|
2521
2528
|
}
|
|
2522
|
-
const
|
|
2529
|
+
const resolvedByOrig = /* @__PURE__ */ new Map();
|
|
2530
|
+
for (const r of resolveCollisions(fittingCandidates)) {
|
|
2531
|
+
if (r.index !== void 0) resolvedByOrig.set(r.index, r);
|
|
2532
|
+
}
|
|
2533
|
+
const halfHeight = FONT_SIZE * 1.2 * 0.5;
|
|
2523
2534
|
const results = [];
|
|
2524
|
-
let resolvedIdx = 0;
|
|
2525
2535
|
for (let i = 0; i < candidates.length; i++) {
|
|
2526
2536
|
if (unfittingIndices.has(i)) {
|
|
2527
2537
|
results.push({
|
|
2528
2538
|
text: candidates[i].text,
|
|
2529
|
-
x:
|
|
2530
|
-
y: candidates[i].anchorY,
|
|
2539
|
+
x: displayAnchorX[i],
|
|
2540
|
+
y: candidates[i].anchorY + halfHeight,
|
|
2531
2541
|
style: candidates[i].style,
|
|
2532
2542
|
visible: false
|
|
2533
2543
|
});
|
|
2534
2544
|
} else {
|
|
2535
|
-
|
|
2545
|
+
const r = resolvedByOrig.get(i);
|
|
2546
|
+
if (!r) continue;
|
|
2547
|
+
const dx = displayAnchorX[i] - candidates[i].anchorX;
|
|
2548
|
+
const { index: _index, ...rest } = r;
|
|
2549
|
+
results.push({
|
|
2550
|
+
...rest,
|
|
2551
|
+
x: r.x + dx,
|
|
2552
|
+
y: r.y + halfHeight,
|
|
2553
|
+
connector: r.connector ? {
|
|
2554
|
+
...r.connector,
|
|
2555
|
+
from: {
|
|
2556
|
+
...r.connector.from,
|
|
2557
|
+
x: r.connector.from.x + dx,
|
|
2558
|
+
y: r.connector.from.y + halfHeight
|
|
2559
|
+
},
|
|
2560
|
+
to: {
|
|
2561
|
+
...r.connector.to,
|
|
2562
|
+
x: r.connector.to.x + dx,
|
|
2563
|
+
y: r.connector.to.y + halfHeight
|
|
2564
|
+
}
|
|
2565
|
+
} : void 0
|
|
2566
|
+
});
|
|
2536
2567
|
}
|
|
2537
2568
|
}
|
|
2538
2569
|
return results;
|
|
2539
2570
|
}
|
|
2540
2571
|
|
|
2541
2572
|
// src/charts/bar/index.ts
|
|
2542
|
-
var barRenderer = (spec, scales, chartArea, strategy, theme) => {
|
|
2543
|
-
const marks = computeBarMarks(spec, scales, chartArea, strategy);
|
|
2573
|
+
var barRenderer = (spec, scales, chartArea, strategy, theme, containerWidth) => {
|
|
2574
|
+
const marks = computeBarMarks(spec, scales, chartArea, strategy, containerWidth);
|
|
2544
2575
|
const valueField = spec.encoding?.x && "field" in spec.encoding.x ? spec.encoding.x.field : void 0;
|
|
2545
2576
|
const labels = computeBarLabels(
|
|
2546
2577
|
marks,
|
|
@@ -2852,7 +2883,8 @@ import {
|
|
|
2852
2883
|
buildD3Formatter as buildD3Formatter2,
|
|
2853
2884
|
estimateTextWidth as estimateTextWidth4,
|
|
2854
2885
|
getRepresentativeColor as getRepresentativeColor2,
|
|
2855
|
-
resolveCollisions as resolveCollisions2
|
|
2886
|
+
resolveCollisions as resolveCollisions2,
|
|
2887
|
+
textAscent
|
|
2856
2888
|
} from "@opendata-ai/openchart-core";
|
|
2857
2889
|
var LABEL_FONT_SIZE2 = 10;
|
|
2858
2890
|
var LABEL_FONT_WEIGHT2 = 600;
|
|
@@ -2903,22 +2935,22 @@ function computeColumnLabels(marks, _chartArea, density = "auto", labelFormat, l
|
|
|
2903
2935
|
fontWeight: LABEL_FONT_WEIGHT2,
|
|
2904
2936
|
fill: labelColor ?? getRepresentativeColor2(mark.fill),
|
|
2905
2937
|
lineHeight: 1.2,
|
|
2906
|
-
textAnchor: "middle"
|
|
2907
|
-
dominantBaseline: "hanging"
|
|
2938
|
+
textAnchor: "middle"
|
|
2908
2939
|
}
|
|
2909
2940
|
});
|
|
2910
2941
|
}
|
|
2911
2942
|
if (candidates.length === 0) return [];
|
|
2912
|
-
|
|
2913
|
-
|
|
2914
|
-
|
|
2915
|
-
|
|
2916
|
-
|
|
2917
|
-
|
|
2918
|
-
|
|
2919
|
-
|
|
2920
|
-
|
|
2921
|
-
|
|
2943
|
+
const ascent = textAscent(FONT_SIZE);
|
|
2944
|
+
return resolveCollisions2(candidates).map((label) => ({
|
|
2945
|
+
...label,
|
|
2946
|
+
y: label.y + ascent,
|
|
2947
|
+
// The connector originates at the label; shift its start to the baseline
|
|
2948
|
+
// too so the leader stays glued to the shifted text.
|
|
2949
|
+
connector: label.connector ? {
|
|
2950
|
+
...label.connector,
|
|
2951
|
+
from: { ...label.connector.from, y: label.connector.from.y + ascent }
|
|
2952
|
+
} : void 0
|
|
2953
|
+
}));
|
|
2922
2954
|
}
|
|
2923
2955
|
|
|
2924
2956
|
// src/charts/column/index.ts
|
|
@@ -9534,7 +9566,7 @@ import {
|
|
|
9534
9566
|
MAX_LEFT_LABEL_FRACTION_DEFAULT,
|
|
9535
9567
|
MAX_LEFT_LABEL_FRACTION_MEDIUM,
|
|
9536
9568
|
MAX_LEFT_LABEL_FRACTION_MEDIUM_MAX,
|
|
9537
|
-
NARROW_VIEWPORT_MAX,
|
|
9569
|
+
NARROW_VIEWPORT_MAX as NARROW_VIEWPORT_MAX2,
|
|
9538
9570
|
TOP_PAD_EXTRA_NARROW
|
|
9539
9571
|
} from "@opendata-ai/openchart-core";
|
|
9540
9572
|
|
|
@@ -9624,7 +9656,11 @@ function extractColorEntries(spec, theme) {
|
|
|
9624
9656
|
if (!colorEnc) return [];
|
|
9625
9657
|
if ("condition" in colorEnc) return [];
|
|
9626
9658
|
if (colorEnc.type === "quantitative") return [];
|
|
9627
|
-
const dataValues = [
|
|
9659
|
+
const dataValues = [
|
|
9660
|
+
...new Set(
|
|
9661
|
+
spec.data.filter((d) => d[colorEnc.field] != null).map((d) => String(d[colorEnc.field]))
|
|
9662
|
+
)
|
|
9663
|
+
];
|
|
9628
9664
|
const explicitDomain = colorEnc.scale?.domain;
|
|
9629
9665
|
const explicitRange = colorEnc.scale?.range;
|
|
9630
9666
|
const palette = explicitRange ?? theme.colors.categorical;
|
|
@@ -9875,6 +9911,15 @@ function placeLegend(content, chartArea, containerWidth, _theme, xAxisHeight) {
|
|
|
9875
9911
|
};
|
|
9876
9912
|
}
|
|
9877
9913
|
|
|
9914
|
+
// src/layout/axes/rotation.ts
|
|
9915
|
+
var BAND_FIT_FRACTION = 0.85;
|
|
9916
|
+
var AUTO_ROTATE_ANGLE = -45;
|
|
9917
|
+
function resolveBandTickAngle(explicitAngle, maxLabelWidth, bandwidth, labelCount) {
|
|
9918
|
+
if (explicitAngle !== void 0) return explicitAngle;
|
|
9919
|
+
if (labelCount < 2) return void 0;
|
|
9920
|
+
return maxLabelWidth > bandwidth * BAND_FIT_FRACTION ? AUTO_ROTATE_ANGLE : void 0;
|
|
9921
|
+
}
|
|
9922
|
+
|
|
9878
9923
|
// src/layout/axes/ticks.ts
|
|
9879
9924
|
import {
|
|
9880
9925
|
abbreviateNumber as abbreviateNumber2,
|
|
@@ -10259,6 +10304,16 @@ function evenRange(start, end, count) {
|
|
|
10259
10304
|
const step = (end - start) / (count - 1);
|
|
10260
10305
|
return Array.from({ length: count }, (_, i) => start + step * i);
|
|
10261
10306
|
}
|
|
10307
|
+
function estimateBandwidth(scaleConfig, plotWidth, n) {
|
|
10308
|
+
if (n <= 0) return 0;
|
|
10309
|
+
const padding = scaleConfig?.padding ?? 0.35;
|
|
10310
|
+
const paddingInner = scaleConfig?.paddingInner ?? padding;
|
|
10311
|
+
const paddingOuter = scaleConfig?.paddingOuter ?? padding;
|
|
10312
|
+
const denom = n - paddingInner + 2 * paddingOuter;
|
|
10313
|
+
if (denom <= 0) return 0;
|
|
10314
|
+
const step = plotWidth / denom;
|
|
10315
|
+
return step * (1 - paddingInner);
|
|
10316
|
+
}
|
|
10262
10317
|
function buildBandScale(channel, data, rangeStart, rangeEnd) {
|
|
10263
10318
|
const values = channel.scale?.domain ? channel.scale.domain : applyCategoricalSort(uniqueStrings(fieldValues(data, channel.field)), channel.sort);
|
|
10264
10319
|
const padding = channel.scale?.padding ?? 0.35;
|
|
@@ -10630,7 +10685,7 @@ function resolveLayoutPlan(chartSpec, renderSpec, options, theme, strategy, wate
|
|
|
10630
10685
|
if (w > maxLabelWidth) maxLabelWidth = w;
|
|
10631
10686
|
}
|
|
10632
10687
|
if (maxLabelWidth > 0) {
|
|
10633
|
-
const labelGap = width <
|
|
10688
|
+
const labelGap = width < NARROW_VIEWPORT_MAX2 ? LABEL_GAP_COMPACT : LABEL_GAP_DEFAULT;
|
|
10634
10689
|
const maxLeftFraction = width < BREAKPOINT_COMPACT_MAX ? MAX_LEFT_LABEL_FRACTION_COMPACT : width < MAX_LEFT_LABEL_FRACTION_MEDIUM_MAX ? MAX_LEFT_LABEL_FRACTION_MEDIUM : MAX_LEFT_LABEL_FRACTION_DEFAULT;
|
|
10635
10690
|
const maxLeftReserved = Math.floor(width * maxLeftFraction);
|
|
10636
10691
|
const reserved = Math.min(hPad + maxLabelWidth + labelGap, maxLeftReserved);
|
|
@@ -10705,16 +10760,33 @@ function resolveLayoutPlan(chartSpec, renderSpec, options, theme, strategy, wate
|
|
|
10705
10760
|
watermark,
|
|
10706
10761
|
bottomLegendReservation
|
|
10707
10762
|
);
|
|
10763
|
+
let effectiveXTickAngle = xTickAngle;
|
|
10764
|
+
if (xTickAngle === void 0 && xLabels.length > 1) {
|
|
10765
|
+
const rightMarginEst = hPad + (isRadial ? hPad : axisMargin);
|
|
10766
|
+
const plotWidth = Math.max(0, width - leftGutter - Math.max(rightMarginEst, hPad));
|
|
10767
|
+
const bandwidth = estimateBandwidth(encoding.x?.scale, plotWidth, xLabels.length);
|
|
10768
|
+
let maxXLabelWidth = 0;
|
|
10769
|
+
for (const label of xLabels) {
|
|
10770
|
+
const w = measure(label, theme.fonts.sizes.axisTick, theme.fonts.weights.normal);
|
|
10771
|
+
if (w > maxXLabelWidth) maxXLabelWidth = w;
|
|
10772
|
+
}
|
|
10773
|
+
effectiveXTickAngle = resolveBandTickAngle(
|
|
10774
|
+
void 0,
|
|
10775
|
+
maxXLabelWidth,
|
|
10776
|
+
bandwidth,
|
|
10777
|
+
xLabels.length
|
|
10778
|
+
);
|
|
10779
|
+
}
|
|
10708
10780
|
const xAxisExtent = isRadial || xAxisSuppressed ? 0 : computeXAxisExtentFromLabels({
|
|
10709
10781
|
labels: xLabels,
|
|
10710
|
-
tickAngle:
|
|
10782
|
+
tickAngle: effectiveXTickAngle,
|
|
10711
10783
|
hasTitle: hasXAxisTitle,
|
|
10712
10784
|
tickFontSize: theme.fonts.sizes.axisTick,
|
|
10713
10785
|
tickFontWeight: theme.fonts.weights.normal,
|
|
10714
10786
|
xAxisHeight: theme.spacing.xAxisHeight,
|
|
10715
10787
|
measure
|
|
10716
10788
|
});
|
|
10717
|
-
const topPad = width <
|
|
10789
|
+
const topPad = width < NARROW_VIEWPORT_MAX2 ? padding + TOP_PAD_EXTRA_NARROW : padding;
|
|
10718
10790
|
const hasTopLegend = legendContent.entries.length > 0 && legendContent.position === "top";
|
|
10719
10791
|
const gap = legendGap(width);
|
|
10720
10792
|
const inlineTickOverhang = yIsInline ? theme.fonts.sizes.axisTick + INLINE_TICK_OVERHANG_PAD : 0;
|
|
@@ -11793,12 +11865,25 @@ function effectiveDensity(baseDensity, axisLength, minimalThreshold, reducedThre
|
|
|
11793
11865
|
}
|
|
11794
11866
|
var CONTINUOUS_TICK_FLOOR = 4;
|
|
11795
11867
|
var OVERSHOOT_TOLERANCE = 1.5;
|
|
11868
|
+
var MIN_QUANTITATIVE_TICKS = 2;
|
|
11869
|
+
function ensureMinContinuousTicks(scale, ticks2, requestedCount) {
|
|
11870
|
+
if (ticks2.length >= MIN_QUANTITATIVE_TICKS) return ticks2;
|
|
11871
|
+
if (!scale || !scaleSupportsTickCount(scale)) return ticks2;
|
|
11872
|
+
let best = ticks2;
|
|
11873
|
+
for (let n = Math.max(requestedCount, MIN_QUANTITATIVE_TICKS) + 1; n <= 10; n++) {
|
|
11874
|
+
const candidate = buildContinuousTicks(scale, n);
|
|
11875
|
+
if (candidate.length >= MIN_QUANTITATIVE_TICKS) return candidate;
|
|
11876
|
+
if (candidate.length > best.length) best = candidate;
|
|
11877
|
+
}
|
|
11878
|
+
return best;
|
|
11879
|
+
}
|
|
11796
11880
|
function fitContinuousTicks(scale, initialTicks, initialCount, fontSize, fontWeight, axisLength, orientation, measureText) {
|
|
11797
11881
|
if (!scale || !scaleSupportsTickCount(scale)) return initialTicks;
|
|
11882
|
+
const flooredTicks = ensureMinContinuousTicks(scale, initialTicks, initialCount);
|
|
11798
11883
|
const tolerance = initialCount * OVERSHOOT_TOLERANCE;
|
|
11799
|
-
const overshoots =
|
|
11800
|
-
const overlaps = ticksOverlap(
|
|
11801
|
-
if (!overshoots && !overlaps) return
|
|
11884
|
+
const overshoots = flooredTicks.length > tolerance;
|
|
11885
|
+
const overlaps = ticksOverlap(flooredTicks, fontSize, fontWeight, measureText, orientation);
|
|
11886
|
+
if (!overshoots && !overlaps) return flooredTicks;
|
|
11802
11887
|
const minThreshold = orientation === "vertical" ? HEIGHT_MINIMAL_THRESHOLD : WIDTH_MINIMAL_THRESHOLD;
|
|
11803
11888
|
const floor = axisLength >= minThreshold ? CONTINUOUS_TICK_FLOOR : 2;
|
|
11804
11889
|
let bestWithinFloor;
|
|
@@ -11817,7 +11902,8 @@ function fitContinuousTicks(scale, initialTicks, initialCount, fontSize, fontWei
|
|
|
11817
11902
|
}
|
|
11818
11903
|
if (candidate.length >= floor) bestWithinFloor = candidate;
|
|
11819
11904
|
}
|
|
11820
|
-
const
|
|
11905
|
+
const rawFallback = bestWithinFloor ?? buildContinuousTicks(scale, floor);
|
|
11906
|
+
const fallback = ensureMinContinuousTicks(scale, rawFallback, floor);
|
|
11821
11907
|
return thinTicksUntilFit(fallback, fontSize, fontWeight, measureText, orientation);
|
|
11822
11908
|
}
|
|
11823
11909
|
function bandTicksOverlapAtAngle(ticks2, angleDeg, fontSize, fontWeight, measureText) {
|
|
@@ -11904,9 +11990,7 @@ function computeAxes(scales, chartArea, strategy, theme, measureText, dataContex
|
|
|
11904
11990
|
const w = measureLabel(t.label, fontSize, fontWeight, measureText);
|
|
11905
11991
|
if (w > maxLabelWidth) maxLabelWidth = w;
|
|
11906
11992
|
}
|
|
11907
|
-
|
|
11908
|
-
tickAngle = -45;
|
|
11909
|
-
}
|
|
11993
|
+
tickAngle = resolveBandTickAngle(void 0, maxLabelWidth, bandwidth, allTicks.length);
|
|
11910
11994
|
}
|
|
11911
11995
|
const hasExplicitValues = !!axisConfig?.values;
|
|
11912
11996
|
let ticks2;
|
|
@@ -12026,36 +12110,21 @@ function computeAxes(scales, chartArea, strategy, theme, measureText, dataContex
|
|
|
12026
12110
|
}
|
|
12027
12111
|
const totalWidth = dataContext?.totalWidth ?? chartArea.x + chartArea.width;
|
|
12028
12112
|
if (result.x) {
|
|
12029
|
-
|
|
12030
|
-
|
|
12031
|
-
|
|
12032
|
-
|
|
12033
|
-
|
|
12034
|
-
|
|
12035
|
-
|
|
12036
|
-
|
|
12037
|
-
|
|
12038
|
-
|
|
12039
|
-
if (w > maxLabelWidth) maxLabelWidth = w;
|
|
12040
|
-
}
|
|
12041
|
-
rotatedHeight = Math.min(maxLabelWidth * Math.sin(angleRad) + 6, 120);
|
|
12042
|
-
}
|
|
12043
|
-
if (rotatedHeight != null) {
|
|
12044
|
-
result.x.extent = result.x.label ? rotatedHeight + X_AXIS_TITLE_BAND_ROTATED : rotatedHeight;
|
|
12045
|
-
} else {
|
|
12046
|
-
result.x.extent = computeXAxisExtentFromLabels2({
|
|
12047
|
-
labels: result.x.ticks.map((t) => t.label),
|
|
12048
|
-
tickAngle: result.x.tickAngle,
|
|
12049
|
-
hasTitle: !!result.x.label,
|
|
12050
|
-
tickFontSize: result.x.tickLabelStyle.fontSize,
|
|
12051
|
-
tickFontWeight: result.x.tickLabelStyle.fontWeight,
|
|
12052
|
-
xAxisHeight: theme.spacing.xAxisHeight
|
|
12053
|
-
});
|
|
12054
|
-
}
|
|
12113
|
+
const isRotated = !!result.x.tickAngle && Math.abs(result.x.tickAngle) > 10;
|
|
12114
|
+
result.x.extent = computeXAxisExtentFromLabels2({
|
|
12115
|
+
labels: result.x.ticks.map((t) => t.label),
|
|
12116
|
+
tickAngle: result.x.tickAngle,
|
|
12117
|
+
hasTitle: !!result.x.label,
|
|
12118
|
+
tickFontSize: result.x.tickLabelStyle.fontSize,
|
|
12119
|
+
tickFontWeight: result.x.tickLabelStyle.fontWeight,
|
|
12120
|
+
xAxisHeight: theme.spacing.xAxisHeight,
|
|
12121
|
+
measure: (t, fs, fw) => measureLabel(t, fs, fw, measureText)
|
|
12122
|
+
});
|
|
12055
12123
|
if (result.x.label) {
|
|
12124
|
+
const rotatedBand = isRotated ? result.x.extent - X_AXIS_TITLE_BAND_ROTATED + 14 : 35;
|
|
12056
12125
|
result.x.titlePosition = {
|
|
12057
12126
|
x: chartArea.x + chartArea.width / 2,
|
|
12058
|
-
y: chartArea.y + chartArea.height +
|
|
12127
|
+
y: chartArea.y + chartArea.height + rotatedBand
|
|
12059
12128
|
};
|
|
12060
12129
|
}
|
|
12061
12130
|
}
|
|
@@ -12103,7 +12172,7 @@ import {
|
|
|
12103
12172
|
MAX_LEFT_LABEL_FRACTION_DEFAULT as MAX_LEFT_LABEL_FRACTION_DEFAULT2,
|
|
12104
12173
|
MAX_LEFT_LABEL_FRACTION_MEDIUM as MAX_LEFT_LABEL_FRACTION_MEDIUM2,
|
|
12105
12174
|
MAX_LEFT_LABEL_FRACTION_MEDIUM_MAX as MAX_LEFT_LABEL_FRACTION_MEDIUM_MAX2,
|
|
12106
|
-
NARROW_VIEWPORT_MAX as
|
|
12175
|
+
NARROW_VIEWPORT_MAX as NARROW_VIEWPORT_MAX3,
|
|
12107
12176
|
TOP_PAD_EXTRA_NARROW as TOP_PAD_EXTRA_NARROW2
|
|
12108
12177
|
} from "@opendata-ai/openchart-core";
|
|
12109
12178
|
|
|
@@ -12324,7 +12393,7 @@ function computeDimensions(spec, options, legendLayout, theme, strategy, waterma
|
|
|
12324
12393
|
const yIsInlinePre = (yTickPositionExplicitPre ?? (yIsLineOrAreaPre && yIsContinuousPre && yAxisOrientPre !== "right" ? "inline" : "gutter")) === "inline";
|
|
12325
12394
|
const inlineTickOverhang = yIsInlinePre ? theme.fonts.sizes.axisTick + INLINE_TICK_OVERHANG_PAD : 0;
|
|
12326
12395
|
const topAxisGap = isRadial && chrome.topHeight === 0 ? 0 : axisMargin + inlineTickOverhang;
|
|
12327
|
-
const topPad = width <
|
|
12396
|
+
const topPad = width < NARROW_VIEWPORT_MAX3 ? padding + TOP_PAD_EXTRA_NARROW2 : padding;
|
|
12328
12397
|
const wantsMetrics = !!spec.metrics && spec.metrics.length > 0 && chromeMode !== "hidden";
|
|
12329
12398
|
const tentativeMetricsHeight = wantsMetrics ? metricBarHeight(metricFonts(theme)) : 0;
|
|
12330
12399
|
const margins = {
|
|
@@ -12425,7 +12494,7 @@ function computeDimensions(spec, options, legendLayout, theme, strategy, waterma
|
|
|
12425
12494
|
if (w > maxLabelWidth) maxLabelWidth = w;
|
|
12426
12495
|
}
|
|
12427
12496
|
if (maxLabelWidth > 0) {
|
|
12428
|
-
const labelGap = width <
|
|
12497
|
+
const labelGap = width < NARROW_VIEWPORT_MAX3 ? LABEL_GAP_COMPACT2 : LABEL_GAP_DEFAULT2;
|
|
12429
12498
|
const maxLeftFraction = width < BREAKPOINT_COMPACT_MAX3 ? MAX_LEFT_LABEL_FRACTION_COMPACT2 : width < MAX_LEFT_LABEL_FRACTION_MEDIUM_MAX2 ? MAX_LEFT_LABEL_FRACTION_MEDIUM2 : MAX_LEFT_LABEL_FRACTION_DEFAULT2;
|
|
12430
12499
|
const maxLeftReserved = Math.floor(width * maxLeftFraction);
|
|
12431
12500
|
const reserved = Math.min(hPad + maxLabelWidth + labelGap, maxLeftReserved);
|
|
@@ -15533,7 +15602,7 @@ function compileChart(spec, options) {
|
|
|
15533
15602
|
renderSpec.markDef
|
|
15534
15603
|
);
|
|
15535
15604
|
const renderer = getChartRenderer(rendererKey);
|
|
15536
|
-
const marks = renderer ? renderer(renderSpec, scales, chartArea, strategy, theme) : [];
|
|
15605
|
+
const marks = renderer ? renderer(renderSpec, scales, chartArea, strategy, theme, options.width) : [];
|
|
15537
15606
|
const endpointLabels = computeEndpointLabels(chartSpec, marks, theme, chartArea, strategy);
|
|
15538
15607
|
const obstacles = [];
|
|
15539
15608
|
if (finalLegend.bounds.width > 0) {
|