@opendata-ai/openchart-engine 7.3.0 → 7.4.0

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
 
@@ -9875,6 +9907,15 @@ function placeLegend(content, chartArea, containerWidth, _theme, xAxisHeight) {
9875
9907
  };
9876
9908
  }
9877
9909
 
9910
+ // src/layout/axes/rotation.ts
9911
+ var BAND_FIT_FRACTION = 0.85;
9912
+ var AUTO_ROTATE_ANGLE = -45;
9913
+ function resolveBandTickAngle(explicitAngle, maxLabelWidth, bandwidth, labelCount) {
9914
+ if (explicitAngle !== void 0) return explicitAngle;
9915
+ if (labelCount < 2) return void 0;
9916
+ return maxLabelWidth > bandwidth * BAND_FIT_FRACTION ? AUTO_ROTATE_ANGLE : void 0;
9917
+ }
9918
+
9878
9919
  // src/layout/axes/ticks.ts
9879
9920
  import {
9880
9921
  abbreviateNumber as abbreviateNumber2,
@@ -10259,6 +10300,16 @@ function evenRange(start, end, count) {
10259
10300
  const step = (end - start) / (count - 1);
10260
10301
  return Array.from({ length: count }, (_, i) => start + step * i);
10261
10302
  }
10303
+ function estimateBandwidth(scaleConfig, plotWidth, n) {
10304
+ if (n <= 0) return 0;
10305
+ const padding = scaleConfig?.padding ?? 0.35;
10306
+ const paddingInner = scaleConfig?.paddingInner ?? padding;
10307
+ const paddingOuter = scaleConfig?.paddingOuter ?? padding;
10308
+ const denom = n - paddingInner + 2 * paddingOuter;
10309
+ if (denom <= 0) return 0;
10310
+ const step = plotWidth / denom;
10311
+ return step * (1 - paddingInner);
10312
+ }
10262
10313
  function buildBandScale(channel, data, rangeStart, rangeEnd) {
10263
10314
  const values = channel.scale?.domain ? channel.scale.domain : applyCategoricalSort(uniqueStrings(fieldValues(data, channel.field)), channel.sort);
10264
10315
  const padding = channel.scale?.padding ?? 0.35;
@@ -10630,7 +10681,7 @@ function resolveLayoutPlan(chartSpec, renderSpec, options, theme, strategy, wate
10630
10681
  if (w > maxLabelWidth) maxLabelWidth = w;
10631
10682
  }
10632
10683
  if (maxLabelWidth > 0) {
10633
- const labelGap = width < NARROW_VIEWPORT_MAX ? LABEL_GAP_COMPACT : LABEL_GAP_DEFAULT;
10684
+ const labelGap = width < NARROW_VIEWPORT_MAX2 ? LABEL_GAP_COMPACT : LABEL_GAP_DEFAULT;
10634
10685
  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
10686
  const maxLeftReserved = Math.floor(width * maxLeftFraction);
10636
10687
  const reserved = Math.min(hPad + maxLabelWidth + labelGap, maxLeftReserved);
@@ -10705,16 +10756,33 @@ function resolveLayoutPlan(chartSpec, renderSpec, options, theme, strategy, wate
10705
10756
  watermark,
10706
10757
  bottomLegendReservation
10707
10758
  );
10759
+ let effectiveXTickAngle = xTickAngle;
10760
+ if (xTickAngle === void 0 && xLabels.length > 1) {
10761
+ const rightMarginEst = hPad + (isRadial ? hPad : axisMargin);
10762
+ const plotWidth = Math.max(0, width - leftGutter - Math.max(rightMarginEst, hPad));
10763
+ const bandwidth = estimateBandwidth(encoding.x?.scale, plotWidth, xLabels.length);
10764
+ let maxXLabelWidth = 0;
10765
+ for (const label of xLabels) {
10766
+ const w = measure(label, theme.fonts.sizes.axisTick, theme.fonts.weights.normal);
10767
+ if (w > maxXLabelWidth) maxXLabelWidth = w;
10768
+ }
10769
+ effectiveXTickAngle = resolveBandTickAngle(
10770
+ void 0,
10771
+ maxXLabelWidth,
10772
+ bandwidth,
10773
+ xLabels.length
10774
+ );
10775
+ }
10708
10776
  const xAxisExtent = isRadial || xAxisSuppressed ? 0 : computeXAxisExtentFromLabels({
10709
10777
  labels: xLabels,
10710
- tickAngle: xTickAngle,
10778
+ tickAngle: effectiveXTickAngle,
10711
10779
  hasTitle: hasXAxisTitle,
10712
10780
  tickFontSize: theme.fonts.sizes.axisTick,
10713
10781
  tickFontWeight: theme.fonts.weights.normal,
10714
10782
  xAxisHeight: theme.spacing.xAxisHeight,
10715
10783
  measure
10716
10784
  });
10717
- const topPad = width < NARROW_VIEWPORT_MAX ? padding + TOP_PAD_EXTRA_NARROW : padding;
10785
+ const topPad = width < NARROW_VIEWPORT_MAX2 ? padding + TOP_PAD_EXTRA_NARROW : padding;
10718
10786
  const hasTopLegend = legendContent.entries.length > 0 && legendContent.position === "top";
10719
10787
  const gap = legendGap(width);
10720
10788
  const inlineTickOverhang = yIsInline ? theme.fonts.sizes.axisTick + INLINE_TICK_OVERHANG_PAD : 0;
@@ -11793,12 +11861,25 @@ function effectiveDensity(baseDensity, axisLength, minimalThreshold, reducedThre
11793
11861
  }
11794
11862
  var CONTINUOUS_TICK_FLOOR = 4;
11795
11863
  var OVERSHOOT_TOLERANCE = 1.5;
11864
+ var MIN_QUANTITATIVE_TICKS = 2;
11865
+ function ensureMinContinuousTicks(scale, ticks2, requestedCount) {
11866
+ if (ticks2.length >= MIN_QUANTITATIVE_TICKS) return ticks2;
11867
+ if (!scale || !scaleSupportsTickCount(scale)) return ticks2;
11868
+ let best = ticks2;
11869
+ for (let n = Math.max(requestedCount, MIN_QUANTITATIVE_TICKS) + 1; n <= 10; n++) {
11870
+ const candidate = buildContinuousTicks(scale, n);
11871
+ if (candidate.length >= MIN_QUANTITATIVE_TICKS) return candidate;
11872
+ if (candidate.length > best.length) best = candidate;
11873
+ }
11874
+ return best;
11875
+ }
11796
11876
  function fitContinuousTicks(scale, initialTicks, initialCount, fontSize, fontWeight, axisLength, orientation, measureText) {
11797
11877
  if (!scale || !scaleSupportsTickCount(scale)) return initialTicks;
11878
+ const flooredTicks = ensureMinContinuousTicks(scale, initialTicks, initialCount);
11798
11879
  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;
11880
+ const overshoots = flooredTicks.length > tolerance;
11881
+ const overlaps = ticksOverlap(flooredTicks, fontSize, fontWeight, measureText, orientation);
11882
+ if (!overshoots && !overlaps) return flooredTicks;
11802
11883
  const minThreshold = orientation === "vertical" ? HEIGHT_MINIMAL_THRESHOLD : WIDTH_MINIMAL_THRESHOLD;
11803
11884
  const floor = axisLength >= minThreshold ? CONTINUOUS_TICK_FLOOR : 2;
11804
11885
  let bestWithinFloor;
@@ -11817,7 +11898,8 @@ function fitContinuousTicks(scale, initialTicks, initialCount, fontSize, fontWei
11817
11898
  }
11818
11899
  if (candidate.length >= floor) bestWithinFloor = candidate;
11819
11900
  }
11820
- const fallback = bestWithinFloor ?? buildContinuousTicks(scale, floor);
11901
+ const rawFallback = bestWithinFloor ?? buildContinuousTicks(scale, floor);
11902
+ const fallback = ensureMinContinuousTicks(scale, rawFallback, floor);
11821
11903
  return thinTicksUntilFit(fallback, fontSize, fontWeight, measureText, orientation);
11822
11904
  }
11823
11905
  function bandTicksOverlapAtAngle(ticks2, angleDeg, fontSize, fontWeight, measureText) {
@@ -11904,9 +11986,7 @@ function computeAxes(scales, chartArea, strategy, theme, measureText, dataContex
11904
11986
  const w = measureLabel(t.label, fontSize, fontWeight, measureText);
11905
11987
  if (w > maxLabelWidth) maxLabelWidth = w;
11906
11988
  }
11907
- if (maxLabelWidth > bandwidth * 0.85) {
11908
- tickAngle = -45;
11909
- }
11989
+ tickAngle = resolveBandTickAngle(void 0, maxLabelWidth, bandwidth, allTicks.length);
11910
11990
  }
11911
11991
  const hasExplicitValues = !!axisConfig?.values;
11912
11992
  let ticks2;
@@ -12026,36 +12106,21 @@ function computeAxes(scales, chartArea, strategy, theme, measureText, dataContex
12026
12106
  }
12027
12107
  const totalWidth = dataContext?.totalWidth ?? chartArea.x + chartArea.width;
12028
12108
  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
- }
12109
+ const isRotated = !!result.x.tickAngle && Math.abs(result.x.tickAngle) > 10;
12110
+ result.x.extent = computeXAxisExtentFromLabels2({
12111
+ labels: result.x.ticks.map((t) => t.label),
12112
+ tickAngle: result.x.tickAngle,
12113
+ hasTitle: !!result.x.label,
12114
+ tickFontSize: result.x.tickLabelStyle.fontSize,
12115
+ tickFontWeight: result.x.tickLabelStyle.fontWeight,
12116
+ xAxisHeight: theme.spacing.xAxisHeight,
12117
+ measure: (t, fs, fw) => measureLabel(t, fs, fw, measureText)
12118
+ });
12055
12119
  if (result.x.label) {
12120
+ const rotatedBand = isRotated ? result.x.extent - X_AXIS_TITLE_BAND_ROTATED + 14 : 35;
12056
12121
  result.x.titlePosition = {
12057
12122
  x: chartArea.x + chartArea.width / 2,
12058
- y: chartArea.y + chartArea.height + (rotatedHeight != null ? rotatedHeight + 14 : 35)
12123
+ y: chartArea.y + chartArea.height + rotatedBand
12059
12124
  };
12060
12125
  }
12061
12126
  }
@@ -12103,7 +12168,7 @@ import {
12103
12168
  MAX_LEFT_LABEL_FRACTION_DEFAULT as MAX_LEFT_LABEL_FRACTION_DEFAULT2,
12104
12169
  MAX_LEFT_LABEL_FRACTION_MEDIUM as MAX_LEFT_LABEL_FRACTION_MEDIUM2,
12105
12170
  MAX_LEFT_LABEL_FRACTION_MEDIUM_MAX as MAX_LEFT_LABEL_FRACTION_MEDIUM_MAX2,
12106
- NARROW_VIEWPORT_MAX as NARROW_VIEWPORT_MAX2,
12171
+ NARROW_VIEWPORT_MAX as NARROW_VIEWPORT_MAX3,
12107
12172
  TOP_PAD_EXTRA_NARROW as TOP_PAD_EXTRA_NARROW2
12108
12173
  } from "@opendata-ai/openchart-core";
12109
12174
 
@@ -12324,7 +12389,7 @@ function computeDimensions(spec, options, legendLayout, theme, strategy, waterma
12324
12389
  const yIsInlinePre = (yTickPositionExplicitPre ?? (yIsLineOrAreaPre && yIsContinuousPre && yAxisOrientPre !== "right" ? "inline" : "gutter")) === "inline";
12325
12390
  const inlineTickOverhang = yIsInlinePre ? theme.fonts.sizes.axisTick + INLINE_TICK_OVERHANG_PAD : 0;
12326
12391
  const topAxisGap = isRadial && chrome.topHeight === 0 ? 0 : axisMargin + inlineTickOverhang;
12327
- const topPad = width < NARROW_VIEWPORT_MAX2 ? padding + TOP_PAD_EXTRA_NARROW2 : padding;
12392
+ const topPad = width < NARROW_VIEWPORT_MAX3 ? padding + TOP_PAD_EXTRA_NARROW2 : padding;
12328
12393
  const wantsMetrics = !!spec.metrics && spec.metrics.length > 0 && chromeMode !== "hidden";
12329
12394
  const tentativeMetricsHeight = wantsMetrics ? metricBarHeight(metricFonts(theme)) : 0;
12330
12395
  const margins = {
@@ -12425,7 +12490,7 @@ function computeDimensions(spec, options, legendLayout, theme, strategy, waterma
12425
12490
  if (w > maxLabelWidth) maxLabelWidth = w;
12426
12491
  }
12427
12492
  if (maxLabelWidth > 0) {
12428
- const labelGap = width < NARROW_VIEWPORT_MAX2 ? LABEL_GAP_COMPACT2 : LABEL_GAP_DEFAULT2;
12493
+ const labelGap = width < NARROW_VIEWPORT_MAX3 ? LABEL_GAP_COMPACT2 : LABEL_GAP_DEFAULT2;
12429
12494
  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
12495
  const maxLeftReserved = Math.floor(width * maxLeftFraction);
12431
12496
  const reserved = Math.min(hPad + maxLabelWidth + labelGap, maxLeftReserved);
@@ -15533,7 +15598,7 @@ function compileChart(spec, options) {
15533
15598
  renderSpec.markDef
15534
15599
  );
15535
15600
  const renderer = getChartRenderer(rendererKey);
15536
- const marks = renderer ? renderer(renderSpec, scales, chartArea, strategy, theme) : [];
15601
+ const marks = renderer ? renderer(renderSpec, scales, chartArea, strategy, theme, options.width) : [];
15537
15602
  const endpointLabels = computeEndpointLabels(chartSpec, marks, theme, chartArea, strategy);
15538
15603
  const obstacles = [];
15539
15604
  if (finalLegend.bounds.width > 0) {