@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 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
- * and the resolved theme for theme-aware styling (e.g. label colors).
560
- * Returns an array of marks to render.
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
- function computeBarMarks(spec, scales, _chartArea, _strategy) {
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
- const subBandHeight = Math.max((bandwidth - gap * (groupCount - 1)) / groupCount, MIN_BAR_WIDTH);
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 anchorY = mark.y + mark.height / 2;
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 resolved = resolveCollisions(fittingCandidates);
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: candidates[i].anchorX,
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
- results.push(resolved[resolvedIdx++]);
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
- if (density === "all") {
2913
- return candidates.map((c) => ({
2914
- text: c.text,
2915
- x: c.anchorX,
2916
- y: c.anchorY,
2917
- style: c.style,
2918
- visible: true
2919
- }));
2920
- }
2921
- return resolveCollisions2(candidates);
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 = [...new Set(spec.data.map((d) => String(d[colorEnc.field])))];
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 < NARROW_VIEWPORT_MAX ? LABEL_GAP_COMPACT : LABEL_GAP_DEFAULT;
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: xTickAngle,
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 < NARROW_VIEWPORT_MAX ? padding + TOP_PAD_EXTRA_NARROW : padding;
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 = initialTicks.length > tolerance;
11800
- const overlaps = ticksOverlap(initialTicks, fontSize, fontWeight, measureText, orientation);
11801
- if (!overshoots && !overlaps) return initialTicks;
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 fallback = bestWithinFloor ?? buildContinuousTicks(scale, floor);
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
- if (maxLabelWidth > bandwidth * 0.85) {
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
- let rotatedHeight;
12030
- if (result.x.tickAngle && Math.abs(result.x.tickAngle) > 10) {
12031
- const angleRad = Math.abs(result.x.tickAngle) * (Math.PI / 180);
12032
- let maxLabelWidth = 40;
12033
- for (const tick of result.x.ticks) {
12034
- const w = estimateTextWidth14(
12035
- tick.label,
12036
- result.x.tickLabelStyle.fontSize,
12037
- result.x.tickLabelStyle.fontWeight
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 + (rotatedHeight != null ? rotatedHeight + 14 : 35)
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 NARROW_VIEWPORT_MAX2,
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 < NARROW_VIEWPORT_MAX2 ? padding + TOP_PAD_EXTRA_NARROW2 : padding;
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 < NARROW_VIEWPORT_MAX2 ? LABEL_GAP_COMPACT2 : LABEL_GAP_DEFAULT2;
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) {