@opendata-ai/openchart-engine 7.1.2 → 7.1.4

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 CHANGED
@@ -2,7 +2,7 @@
2
2
  import {
3
3
  adaptTheme as adaptTheme5,
4
4
  computeLabelBounds,
5
- estimateTextWidth as estimateTextWidth21,
5
+ estimateTextWidth as estimateTextWidth20,
6
6
  generateAltText,
7
7
  generateDataTable,
8
8
  getBreakpoint,
@@ -1778,7 +1778,7 @@ var LABEL_FONT_SIZE = 11;
1778
1778
  var LABEL_FONT_WEIGHT = 600;
1779
1779
  var LABEL_PADDING = 6;
1780
1780
  var MIN_WIDTH_FOR_INSIDE_LABEL = 40;
1781
- function computeBarLabels(marks, _chartArea, density = "auto", labelFormat, labelPrefix, valueField, labelColor) {
1781
+ function computeBarLabels(marks, _chartArea, density = "auto", labelFormat, labelPrefix, valueField, labelColor, darkMode = false) {
1782
1782
  const targetMarks = filterByDensity(marks, density);
1783
1783
  const candidates = [];
1784
1784
  const fitsInSegment = [];
@@ -1815,16 +1815,16 @@ function computeBarLabels(marks, _chartArea, density = "auto", labelFormat, labe
1815
1815
  let textAnchor;
1816
1816
  if (isStacked2 && isInside) {
1817
1817
  anchorX = mark.x + mark.width / 2;
1818
- fill = pickLabelColor(bgColor);
1818
+ fill = pickLabelColor(bgColor, darkMode);
1819
1819
  textAnchor = "middle";
1820
1820
  } else if (isInside) {
1821
1821
  if (isNegative) {
1822
1822
  anchorX = mark.x + LABEL_PADDING;
1823
- fill = pickLabelColor(bgColor);
1823
+ fill = pickLabelColor(bgColor, darkMode);
1824
1824
  textAnchor = "start";
1825
1825
  } else {
1826
1826
  anchorX = mark.x + mark.width - LABEL_PADDING;
1827
- fill = pickLabelColor(bgColor);
1827
+ fill = pickLabelColor(bgColor, darkMode);
1828
1828
  textAnchor = "end";
1829
1829
  }
1830
1830
  } else {
@@ -1898,7 +1898,7 @@ function computeBarLabels(marks, _chartArea, density = "auto", labelFormat, labe
1898
1898
  }
1899
1899
 
1900
1900
  // src/charts/bar/index.ts
1901
- var barRenderer = (spec, scales, chartArea, strategy, _theme) => {
1901
+ var barRenderer = (spec, scales, chartArea, strategy, theme) => {
1902
1902
  const marks = computeBarMarks(spec, scales, chartArea, strategy);
1903
1903
  const valueField = spec.encoding?.x && "field" in spec.encoding.x ? spec.encoding.x.field : void 0;
1904
1904
  const labels = computeBarLabels(
@@ -1908,7 +1908,8 @@ var barRenderer = (spec, scales, chartArea, strategy, _theme) => {
1908
1908
  spec.labels.format,
1909
1909
  spec.labels.prefix,
1910
1910
  valueField,
1911
- spec.labels.color
1911
+ spec.labels.color,
1912
+ theme.isDark
1912
1913
  );
1913
1914
  for (let i = 0; i < marks.length && i < labels.length; i++) {
1914
1915
  marks[i].label = labels[i];
@@ -9709,7 +9710,6 @@ import {
9709
9710
  abbreviateNumber as abbreviateNumber2,
9710
9711
  buildD3Formatter as buildD3Formatter5,
9711
9712
  buildTemporalFormatter,
9712
- estimateTextWidth as estimateTextWidth12,
9713
9713
  formatDate,
9714
9714
  formatNumber as formatNumber3
9715
9715
  } from "@opendata-ai/openchart-core";
@@ -9819,35 +9819,16 @@ function scaleSupportsTickCount(resolvedScale) {
9819
9819
  const scale = resolvedScale.scale;
9820
9820
  return "ticks" in scale && typeof scale.ticks === "function";
9821
9821
  }
9822
- function categoricalTicks(resolvedScale, density, orientation = "horizontal", bandwidth, labelAngle, fontSize, fontWeight, measureText, subtitleContext) {
9822
+ function categoricalTicks(resolvedScale, density, orientation = "horizontal", subtitleContext) {
9823
9823
  const scale = resolvedScale.scale;
9824
9824
  const domain = scale.domain();
9825
9825
  const catAxisCfg = resolvedScale.channel.axis || void 0;
9826
9826
  const explicitTickCount = catAxisCfg?.tickCount;
9827
9827
  let selectedValues = domain;
9828
9828
  if (resolvedScale.type === "band" && orientation === "horizontal") {
9829
- if (bandwidth !== void 0 && bandwidth > 0 && fontSize !== void 0) {
9830
- const maxLabelWidth = domain.reduce((max4, v) => {
9831
- const w = measureText ? measureText(v, fontSize, fontWeight ?? 400).width : estimateTextWidth12(v, fontSize, fontWeight ?? 400);
9832
- return Math.max(max4, w);
9833
- }, 0);
9834
- const angleRad = labelAngle !== void 0 ? Math.abs(labelAngle) * Math.PI / 180 : 0;
9835
- const footprint = angleRad > 0 ? maxLabelWidth * Math.abs(Math.cos(angleRad)) : maxLabelWidth;
9836
- const minGap = fontSize * 0.5;
9837
- if (footprint + minGap > bandwidth) {
9838
- const maxFitting = Math.max(1, Math.floor(bandwidth / (footprint + minGap)));
9839
- const cap = explicitTickCount ?? Math.min(domain.length, Math.max(maxFitting, TICK_COUNTS[density]));
9840
- if (domain.length > cap) {
9841
- const step = Math.ceil(domain.length / cap);
9842
- selectedValues = domain.filter((_, i) => i % step === 0);
9843
- }
9844
- }
9845
- } else {
9846
- const maxTicks = explicitTickCount ?? TICK_COUNTS[density];
9847
- if ((explicitTickCount || density !== "full") && domain.length > maxTicks) {
9848
- const step = Math.ceil(domain.length / maxTicks);
9849
- selectedValues = domain.filter((_, i) => i % step === 0);
9850
- }
9829
+ if (explicitTickCount && domain.length > explicitTickCount) {
9830
+ const step = Math.ceil(domain.length / explicitTickCount);
9831
+ selectedValues = domain.filter((_, i) => i % step === 0);
9851
9832
  }
9852
9833
  } else if (resolvedScale.type !== "band") {
9853
9834
  const maxTicks = explicitTickCount ?? TICK_COUNTS[density];
@@ -9958,6 +9939,34 @@ function fitContinuousTicks(scale, initialTicks, initialCount, fontSize, fontWei
9958
9939
  const fallback = bestWithinFloor ?? buildContinuousTicks(scale, floor);
9959
9940
  return thinTicksUntilFit(fallback, fontSize, fontWeight, measureText, orientation);
9960
9941
  }
9942
+ function bandTicksOverlapAtAngle(ticks2, angleDeg, fontSize, fontWeight, measureText) {
9943
+ if (ticks2.length < 2) return false;
9944
+ const angleRad = Math.abs(angleDeg) * Math.PI / 180;
9945
+ const cosA = angleRad > 0 ? Math.abs(Math.cos(angleRad)) : 1;
9946
+ const minGap = fontSize * 0.5;
9947
+ for (let i = 0; i < ticks2.length - 1; i++) {
9948
+ const aWidth = measureLabel(ticks2[i].label, fontSize, fontWeight, measureText) * cosA;
9949
+ const bWidth = measureLabel(ticks2[i + 1].label, fontSize, fontWeight, measureText) * cosA;
9950
+ const aRight = ticks2[i].position + aWidth / 2;
9951
+ const bLeft = ticks2[i + 1].position - bWidth / 2;
9952
+ if (aRight + minGap > bLeft) return true;
9953
+ }
9954
+ return false;
9955
+ }
9956
+ function thinBandTicksIfNeeded(ticks2, angleDeg, fontSize, fontWeight, measureText) {
9957
+ if (!bandTicksOverlapAtAngle(ticks2, angleDeg, fontSize, fontWeight, measureText)) return ticks2;
9958
+ let current = ticks2;
9959
+ while (current.length > 2) {
9960
+ const thinned = [current[0]];
9961
+ for (let i = 2; i < current.length - 1; i += 2) {
9962
+ thinned.push(current[i]);
9963
+ }
9964
+ if (current.length > 1) thinned.push(current[current.length - 1]);
9965
+ current = thinned;
9966
+ if (!bandTicksOverlapAtAngle(current, angleDeg, fontSize, fontWeight, measureText)) break;
9967
+ }
9968
+ return current;
9969
+ }
9961
9970
  function computeAxes(scales, chartArea, strategy, theme, measureText, dataContext) {
9962
9971
  const result = {};
9963
9972
  const baseDensity = strategy.axisLabelDensity;
@@ -9998,17 +10007,7 @@ function computeAxes(scales, chartArea, strategy, theme, measureText, dataContex
9998
10007
  if (axisConfig?.values) {
9999
10008
  allTicks = resolveExplicitTicks(axisConfig.values, scales.x);
10000
10009
  } else if (!isContinuousX) {
10001
- const xBandwidth = scales.x.type === "band" ? scales.x.scale.bandwidth() : void 0;
10002
- allTicks = categoricalTicks(
10003
- scales.x,
10004
- xDensity,
10005
- "horizontal",
10006
- xBandwidth,
10007
- axisConfig?.labelAngle,
10008
- fontSize,
10009
- fontWeight,
10010
- measureText
10011
- );
10010
+ allTicks = categoricalTicks(scales.x, xDensity, "horizontal");
10012
10011
  } else {
10013
10012
  allTicks = continuousTicks(scales.x, xDensity, xTargetCount);
10014
10013
  }
@@ -10016,11 +10015,25 @@ function computeAxes(scales, chartArea, strategy, theme, measureText, dataContex
10016
10015
  position: t.position,
10017
10016
  major: true
10018
10017
  }));
10018
+ let tickAngle = axisConfig?.labelAngle;
10019
+ if (tickAngle === void 0 && scales.x.type === "band" && allTicks.length > 1) {
10020
+ const bandwidth = scales.x.scale.bandwidth();
10021
+ let maxLabelWidth = 0;
10022
+ for (const t of allTicks) {
10023
+ const w = measureLabel(t.label, fontSize, fontWeight, measureText);
10024
+ if (w > maxLabelWidth) maxLabelWidth = w;
10025
+ }
10026
+ if (maxLabelWidth > bandwidth * 0.85) {
10027
+ tickAngle = -45;
10028
+ }
10029
+ }
10019
10030
  const hasExplicitValues = !!axisConfig?.values;
10020
- const shouldThin = scales.x.type !== "band" && !hasExplicitValues;
10021
10031
  let ticks2;
10022
- if (!shouldThin) {
10032
+ if (hasExplicitValues) {
10023
10033
  ticks2 = allTicks;
10034
+ } else if (scales.x.type === "band") {
10035
+ const effectiveAngle = tickAngle ?? 0;
10036
+ ticks2 = thinBandTicksIfNeeded(allTicks, effectiveAngle, fontSize, fontWeight, measureText);
10024
10037
  } else if (isContinuousX) {
10025
10038
  ticks2 = fitContinuousTicks(
10026
10039
  scales.x,
@@ -10035,18 +10048,6 @@ function computeAxes(scales, chartArea, strategy, theme, measureText, dataContex
10035
10048
  } else {
10036
10049
  ticks2 = thinTicksUntilFit(allTicks, fontSize, fontWeight, measureText);
10037
10050
  }
10038
- let tickAngle = axisConfig?.labelAngle;
10039
- if (tickAngle === void 0 && scales.x.type === "band" && ticks2.length > 1) {
10040
- const bandwidth = scales.x.scale.bandwidth();
10041
- let maxLabelWidth = 0;
10042
- for (const t of ticks2) {
10043
- const w = measureLabel(t.label, fontSize, fontWeight, measureText);
10044
- if (w > maxLabelWidth) maxLabelWidth = w;
10045
- }
10046
- if (maxLabelWidth > bandwidth * 0.85) {
10047
- tickAngle = -45;
10048
- }
10049
- }
10050
10051
  const axisTitle = axisConfig?.title;
10051
10052
  const xLabelColor = axisConfig?.labelColor;
10052
10053
  const xTickPosition = axisConfig?.tickPosition ?? "gutter";
@@ -10084,11 +10085,6 @@ function computeAxes(scales, chartArea, strategy, theme, measureText, dataContex
10084
10085
  scales.y,
10085
10086
  yDensity,
10086
10087
  "vertical",
10087
- void 0,
10088
- void 0,
10089
- void 0,
10090
- void 0,
10091
- void 0,
10092
10088
  yFieldName && yLabelField && dataContext ? { data: dataContext.data, fieldName: yFieldName, labelField: yLabelField } : void 0
10093
10089
  );
10094
10090
  } else {
@@ -10150,10 +10146,11 @@ function computeAxes(scales, chartArea, strategy, theme, measureText, dataContex
10150
10146
 
10151
10147
  // src/layout/dimensions.ts
10152
10148
  import {
10149
+ AXIS_TITLE_GAP,
10153
10150
  AXIS_TITLE_TRAILING_PAD as AXIS_TITLE_TRAILING_PAD2,
10154
10151
  BREAKPOINT_COMPACT_MAX as BREAKPOINT_COMPACT_MAX2,
10155
10152
  computeChrome as computeChrome3,
10156
- estimateTextWidth as estimateTextWidth16,
10153
+ estimateTextWidth as estimateTextWidth15,
10157
10154
  getAxisTitleOffset as getAxisTitleOffset2,
10158
10155
  HPAD_COMPACT_FRACTION,
10159
10156
  HPAD_COMPACT_MIN,
@@ -10169,7 +10166,7 @@ import {
10169
10166
  } from "@opendata-ai/openchart-core";
10170
10167
 
10171
10168
  // src/endpoint-labels/predict.ts
10172
- import { estimateTextWidth as estimateTextWidth13, wrapText as wrapText2 } from "@opendata-ai/openchart-core";
10169
+ import { estimateTextWidth as estimateTextWidth12, wrapText as wrapText2 } from "@opendata-ai/openchart-core";
10173
10170
  function predictEndpointLabelsWidth(spec, _theme) {
10174
10171
  if (spec.endpointLabels === false) return 0;
10175
10172
  if (spec.markType !== "line" && spec.markType !== "area") return 0;
@@ -10191,7 +10188,7 @@ function predictEndpointLabelsWidth(spec, _theme) {
10191
10188
  for (const name of seriesNames) {
10192
10189
  const lines = wrapText2(name, ENDPOINT_LABEL_FONT_SIZE, ENDPOINT_LABEL_FONT_WEIGHT, wrapWidth);
10193
10190
  for (const line of lines) {
10194
- const w = estimateTextWidth13(line, ENDPOINT_LABEL_FONT_SIZE, ENDPOINT_LABEL_FONT_WEIGHT);
10191
+ const w = estimateTextWidth12(line, ENDPOINT_LABEL_FONT_SIZE, ENDPOINT_LABEL_FONT_WEIGHT);
10195
10192
  if (w > maxLabelWidth) maxLabelWidth = w;
10196
10193
  }
10197
10194
  }
@@ -10211,14 +10208,14 @@ function predictEndpointLabelsWidth(spec, _theme) {
10211
10208
  else if (maxAbs >= 1e6) sample = "1.5M";
10212
10209
  else if (maxAbs >= 1e3) sample = "1.5K";
10213
10210
  else sample = String(Math.round(maxAbs * 100) / 100);
10214
- valueWidth = estimateTextWidth13(sample, ENDPOINT_VALUE_FONT_SIZE, ENDPOINT_VALUE_FONT_WEIGHT);
10211
+ valueWidth = estimateTextWidth12(sample, ENDPOINT_VALUE_FONT_SIZE, ENDPOINT_VALUE_FONT_WEIGHT);
10215
10212
  }
10216
10213
  const textColumn = Math.max(maxLabelWidth, valueWidth);
10217
10214
  return ENDPOINT_SWATCH_SIZE + ENDPOINT_GAP + textColumn + 4;
10218
10215
  }
10219
10216
 
10220
10217
  // src/legend/wrap.ts
10221
- import { COMPACT_WIDTH, estimateTextWidth as estimateTextWidth14 } from "@opendata-ai/openchart-core";
10218
+ import { COMPACT_WIDTH, estimateTextWidth as estimateTextWidth13 } from "@opendata-ai/openchart-core";
10222
10219
  var SWATCH_SIZE2 = 12;
10223
10220
  var SWATCH_GAP2 = 6;
10224
10221
  var ENTRY_GAP2 = 16;
@@ -10237,7 +10234,7 @@ function measureLegendWrap(entries, maxWidth, labelStyle, maxRows, entryGap = EN
10237
10234
  let fittingCount = entries.length;
10238
10235
  let fittingCountLocked = false;
10239
10236
  for (let i = 0; i < entries.length; i++) {
10240
- const labelWidth = estimateTextWidth14(
10237
+ const labelWidth = estimateTextWidth13(
10241
10238
  entries[i].label,
10242
10239
  labelStyle.fontSize,
10243
10240
  labelStyle.fontWeight
@@ -10260,7 +10257,7 @@ function measureLegendWrap(entries, maxWidth, labelStyle, maxRows, entryGap = EN
10260
10257
  }
10261
10258
 
10262
10259
  // src/layout/metrics.ts
10263
- import { estimateTextWidth as estimateTextWidth15 } from "@opendata-ai/openchart-core";
10260
+ import { estimateTextWidth as estimateTextWidth14 } from "@opendata-ai/openchart-core";
10264
10261
  var LABEL_FONT_SIZE7 = 10;
10265
10262
  var VALUE_FONT_SIZE2 = 22;
10266
10263
  var LABEL_LINE_HEIGHT_RATIO = 1.4;
@@ -10289,7 +10286,7 @@ function computeMetricBar(metrics, metricsTopY, metricsArea, remainingChartHeigh
10289
10286
  const cellWidth = metricsArea.width / metrics.length;
10290
10287
  for (const metric of metrics) {
10291
10288
  const text = valueRunText(metric);
10292
- const measured = measureText ? measureText(text, VALUE_FONT_SIZE2, 510).width : estimateTextWidth15(text, VALUE_FONT_SIZE2, 510);
10289
+ const measured = measureText ? measureText(text, VALUE_FONT_SIZE2, 510).width : estimateTextWidth14(text, VALUE_FONT_SIZE2, 510);
10293
10290
  if (measured > cellWidth - CELL_INNER_PAD) return void 0;
10294
10291
  }
10295
10292
  const labelLine = LABEL_FONT_SIZE7 * LABEL_LINE_HEIGHT_RATIO;
@@ -10421,7 +10418,7 @@ function computeDimensions(spec, options, legendLayout, theme, strategy, waterma
10421
10418
  if (xField) {
10422
10419
  for (const row of spec.data) {
10423
10420
  const label = String(row[xField] ?? "");
10424
- const w = estimateTextWidth16(label, theme.fonts.sizes.axisTick, theme.fonts.weights.normal);
10421
+ const w = estimateTextWidth15(label, theme.fonts.sizes.axisTick, theme.fonts.weights.normal);
10425
10422
  if (w > maxLabelWidth) maxLabelWidth = w;
10426
10423
  }
10427
10424
  }
@@ -10473,7 +10470,7 @@ function computeDimensions(spec, options, legendLayout, theme, strategy, waterma
10473
10470
  const label = String(row[colorField] ?? "");
10474
10471
  if (!seen.has(label)) {
10475
10472
  seen.add(label);
10476
- const w = estimateTextWidth16(label, 11, 600);
10473
+ const w = estimateTextWidth15(label, 11, 600);
10477
10474
  if (w > maxLabelWidth) maxLabelWidth = w;
10478
10475
  }
10479
10476
  }
@@ -10493,7 +10490,7 @@ function computeDimensions(spec, options, legendLayout, theme, strategy, waterma
10493
10490
  const maxXStr = String(maxX);
10494
10491
  for (const ann of spec.annotations) {
10495
10492
  if (ann.type === "text" && String(ann.x) === maxXStr) {
10496
- const textWidth = estimateTextWidth16(ann.text, ann.fontSize ?? 11, ann.fontWeight ?? 600);
10493
+ const textWidth = estimateTextWidth15(ann.text, ann.fontSize ?? 11, ann.fontWeight ?? 600);
10497
10494
  const dx = ann.offset?.dx ?? 0;
10498
10495
  const anchor = ann.anchor ?? "auto";
10499
10496
  const baseRightExtent = anchor === "left" ? textWidth : (
@@ -10524,12 +10521,12 @@ function computeDimensions(spec, options, legendLayout, theme, strategy, waterma
10524
10521
  let maxLabelWidth = 0;
10525
10522
  for (const row of spec.data) {
10526
10523
  const label = String(row[yField] ?? "");
10527
- let w = estimateTextWidth16(label, theme.fonts.sizes.axisTick, theme.fonts.weights.normal);
10524
+ let w = estimateTextWidth15(label, theme.fonts.sizes.axisTick, theme.fonts.weights.normal);
10528
10525
  if (yLabelField) {
10529
10526
  const subtitle = String(row[yLabelField] ?? "");
10530
10527
  if (subtitle) {
10531
10528
  const gap = theme.fonts.sizes.axisTick * 0.6;
10532
- const subtitleWidth = estimateTextWidth16(
10529
+ const subtitleWidth = estimateTextWidth15(
10533
10530
  subtitle,
10534
10531
  theme.fonts.sizes.axisTick,
10535
10532
  theme.fonts.weights.normal
@@ -10572,7 +10569,7 @@ function computeDimensions(spec, options, legendLayout, theme, strategy, waterma
10572
10569
  }
10573
10570
  const negPrefix = spec.data.some((r) => Number(r[yField]) < 0) ? "-" : "";
10574
10571
  const labelEst = negPrefix + sampleLabel;
10575
- const labelWidth = estimateTextWidth16(
10572
+ const labelWidth = estimateTextWidth15(
10576
10573
  labelEst,
10577
10574
  theme.fonts.sizes.axisTick,
10578
10575
  theme.fonts.weights.normal
@@ -10608,13 +10605,12 @@ function computeDimensions(spec, options, legendLayout, theme, strategy, waterma
10608
10605
  else sampleLabelForTitle = "0.0";
10609
10606
  }
10610
10607
  const negPrefixForTitle = spec.data.some((r) => Number(r[yFieldForTitle]) < 0) ? "-" : "";
10611
- estTickLabelWidth = estimateTextWidth16(
10608
+ estTickLabelWidth = estimateTextWidth15(
10612
10609
  negPrefixForTitle + sampleLabelForTitle,
10613
10610
  theme.fonts.sizes.axisTick,
10614
10611
  theme.fonts.weights.normal
10615
10612
  );
10616
10613
  }
10617
- const AXIS_TITLE_GAP = 8;
10618
10614
  const dynamicTitleOffset = TICK_LABEL_OFFSET2 + estTickLabelWidth + AXIS_TITLE_GAP;
10619
10615
  const axisTitleOffset = Math.max(dynamicTitleOffset, getAxisTitleOffset2(width));
10620
10616
  const halfGlyph = Math.ceil(theme.fonts.sizes.body / 2);
@@ -10624,6 +10620,7 @@ function computeDimensions(spec, options, legendLayout, theme, strategy, waterma
10624
10620
  if (options.rightAxisReserve && options.rightAxisReserve > 0) {
10625
10621
  margins.right = Math.max(margins.right, hPad + options.rightAxisReserve);
10626
10622
  }
10623
+ const hasTopLegend = "entries" in legendLayout && legendLayout.entries.length > 0 && legendLayout.position === "top";
10627
10624
  if ("entries" in legendLayout && legendLayout.entries.length > 0) {
10628
10625
  const gap = legendGap(width);
10629
10626
  if (legendLayout.position === "right" || legendLayout.position === "bottom-right") {
@@ -10632,7 +10629,7 @@ function computeDimensions(spec, options, legendLayout, theme, strategy, waterma
10632
10629
  margins.top += legendLayout.bounds.height + gap;
10633
10630
  }
10634
10631
  }
10635
- margins.top += topAxisGap;
10632
+ margins.top += hasTopLegend ? inlineTickOverhang : topAxisGap;
10636
10633
  let chartArea = {
10637
10634
  x: margins.left,
10638
10635
  y: margins.top,
@@ -10653,13 +10650,14 @@ function computeDimensions(spec, options, legendLayout, theme, strategy, waterma
10653
10650
  bottomLegendReservation
10654
10651
  );
10655
10652
  const fallbackTopAxisGap = isRadial && fallbackChrome.topHeight === 0 ? 0 : axisMargin + inlineTickOverhang;
10653
+ const fallbackEffectiveAxisGap = hasTopLegend ? inlineTickOverhang : fallbackTopAxisGap;
10656
10654
  const newTop = topPad + fallbackChrome.topHeight + tentativeMetricsHeight;
10657
10655
  const topDelta = margins.top - newTop;
10658
10656
  const newBottom = bottomMargin(fallbackChrome.bottomHeight, padding, xAxisHeight);
10659
10657
  const bottomDelta = margins.bottom - newBottom;
10660
10658
  if (topDelta > 0 || bottomDelta > 0) {
10661
10659
  const gap = legendGap(width);
10662
- margins.top = newTop + ("entries" in legendLayout && legendLayout.entries.length > 0 && legendLayout.position === "top" ? legendLayout.bounds.height + gap : 0) + fallbackTopAxisGap;
10660
+ margins.top = newTop + ("entries" in legendLayout && legendLayout.entries.length > 0 && legendLayout.position === "top" ? legendLayout.bounds.height + gap : 0) + fallbackEffectiveAxisGap;
10663
10661
  margins.bottom = newBottom;
10664
10662
  chartArea = {
10665
10663
  x: margins.left,
@@ -11064,7 +11062,13 @@ function computeScales(spec, chartArea, data) {
11064
11062
  const xStackEnabled = encoding.x.stack === true || encoding.x.stack === "zero" || encoding.x.stack === "normalize" || encoding.x.stack === "center";
11065
11063
  if (spec.markType === "bar" && encoding.color && encoding.x.type === "quantitative" && xStackEnabled) {
11066
11064
  if (encoding.x.stack === "normalize") {
11067
- xChannel = { ...encoding.x, scale: { ...encoding.x.scale, domain: [0, 1], nice: false } };
11065
+ const existingAxis = encoding.x.axis;
11066
+ const axis = existingAxis === false || existingAxis?.format ? existingAxis : { ...typeof existingAxis === "object" ? existingAxis : {}, format: ".0%" };
11067
+ xChannel = {
11068
+ ...encoding.x,
11069
+ scale: { ...encoding.x.scale, domain: [0, 1], nice: false },
11070
+ axis
11071
+ };
11068
11072
  } else if (encoding.x.stack === "center") {
11069
11073
  const yField = encoding.y?.field;
11070
11074
  const xField = encoding.x.field;
@@ -11131,7 +11135,13 @@ function computeScales(spec, chartArea, data) {
11131
11135
  }
11132
11136
  if ((isBarStacked || isAreaStacked) && encoding.color && encoding.y.type === "quantitative") {
11133
11137
  if (encoding.y.stack === "normalize") {
11134
- yChannel = { ...encoding.y, scale: { ...encoding.y.scale, domain: [0, 1], nice: false } };
11138
+ const existingAxis = encoding.y.axis;
11139
+ const axis = existingAxis === false || existingAxis?.format ? existingAxis : { ...typeof existingAxis === "object" ? existingAxis : {}, format: ".0%" };
11140
+ yChannel = {
11141
+ ...encoding.y,
11142
+ scale: { ...encoding.y.scale, domain: [0, 1], nice: false },
11143
+ axis
11144
+ };
11135
11145
  } else if (encoding.y.stack === "center") {
11136
11146
  const xField = encoding.x?.field;
11137
11147
  const yField = encoding.y.field;
@@ -11202,7 +11212,7 @@ function computeScales(spec, chartArea, data) {
11202
11212
  }
11203
11213
 
11204
11214
  // src/legend/compute.ts
11205
- import { BRAND_RESERVE_WIDTH as BRAND_RESERVE_WIDTH2, COMPACT_WIDTH as COMPACT_WIDTH2, estimateTextWidth as estimateTextWidth17 } from "@opendata-ai/openchart-core";
11215
+ import { BRAND_RESERVE_WIDTH as BRAND_RESERVE_WIDTH2, COMPACT_WIDTH as COMPACT_WIDTH2, estimateTextWidth as estimateTextWidth16 } from "@opendata-ai/openchart-core";
11206
11216
  var LEGEND_PADDING = 8;
11207
11217
  var LEGEND_RIGHT_WIDTH = 120;
11208
11218
  var RIGHT_LEGEND_MAX_HEIGHT_RATIO = 0.4;
@@ -11318,7 +11328,7 @@ function computeLegend(spec, strategy, theme, chartArea, watermark = true) {
11318
11328
  }
11319
11329
  if (resolvedPosition === "right" || resolvedPosition === "bottom-right") {
11320
11330
  const maxLabelWidth = Math.max(
11321
- ...entries.map((e) => estimateTextWidth17(e.label, labelStyle.fontSize, labelStyle.fontWeight))
11331
+ ...entries.map((e) => estimateTextWidth16(e.label, labelStyle.fontSize, labelStyle.fontWeight))
11322
11332
  );
11323
11333
  const legendWidth = Math.min(
11324
11334
  LEGEND_RIGHT_WIDTH,
@@ -11381,7 +11391,7 @@ function computeLegend(spec, strategy, theme, chartArea, watermark = true) {
11381
11391
  entries = truncateEntries(entries, fittingCount);
11382
11392
  }
11383
11393
  const totalWidth = entries.reduce((sum2, entry) => {
11384
- const labelWidth = estimateTextWidth17(entry.label, labelStyle.fontSize, labelStyle.fontWeight);
11394
+ const labelWidth = estimateTextWidth16(entry.label, labelStyle.fontSize, labelStyle.fontWeight);
11385
11395
  return sum2 + SWATCH_SIZE2 + SWATCH_GAP2 + labelWidth + effectiveEntryGap;
11386
11396
  }, 0);
11387
11397
  const { rowCount } = measureLegendWrap(
@@ -11418,7 +11428,7 @@ import {
11418
11428
  adaptTheme as adaptTheme3,
11419
11429
  buildD3Formatter as buildD3Formatter6,
11420
11430
  computeChrome as computeChrome4,
11421
- estimateTextWidth as estimateTextWidth18,
11431
+ estimateTextWidth as estimateTextWidth17,
11422
11432
  formatNumber as formatNumber4,
11423
11433
  resolveTheme as resolveTheme4
11424
11434
  } from "@opendata-ai/openchart-core";
@@ -12099,7 +12109,7 @@ function compileSankey(spec, options) {
12099
12109
  if (labelsLeft) continue;
12100
12110
  const labelX = (node.x1 ?? nodeWidth) + LABEL_GAP;
12101
12111
  const labelText = node.label ?? node.id;
12102
- const labelWidth = estimateTextWidth18(labelText, labelFontSize, labelFontWeight);
12112
+ const labelWidth = estimateTextWidth17(labelText, labelFontSize, labelFontWeight);
12103
12113
  const overflow = labelX + labelWidth - rightEdge;
12104
12114
  if (overflow > maxOverflow) maxOverflow = overflow;
12105
12115
  }
@@ -12363,7 +12373,7 @@ function emptyLayout3(area, chrome, theme, options, watermark) {
12363
12373
  }
12364
12374
 
12365
12375
  // src/tables/compile-table.ts
12366
- import { computeChrome as computeChrome5, estimateTextWidth as estimateTextWidth19 } from "@opendata-ai/openchart-core";
12376
+ import { computeChrome as computeChrome5, estimateTextWidth as estimateTextWidth18 } from "@opendata-ai/openchart-core";
12367
12377
 
12368
12378
  // src/tables/bar-column.ts
12369
12379
  var NEGATIVE_BAR_COLOR = "#c44e52";
@@ -12778,13 +12788,13 @@ function estimateColumnWidth(col, data, fontSize) {
12778
12788
  if (col.image) return (col.image.width ?? 24) + PADDING;
12779
12789
  if (col.flag) return 60;
12780
12790
  const label = col.label ?? col.key;
12781
- const headerWidth = estimateTextWidth19(label, fontSize, 600) + PADDING;
12791
+ const headerWidth = estimateTextWidth18(label, fontSize, 600) + PADDING;
12782
12792
  const sampleSize = Math.min(100, data.length);
12783
12793
  let maxDataWidth = 0;
12784
12794
  for (let i = 0; i < sampleSize; i++) {
12785
12795
  const val = data[i][col.key];
12786
12796
  const text = val == null ? "" : String(val);
12787
- const width = estimateTextWidth19(text, fontSize, 400) + PADDING;
12797
+ const width = estimateTextWidth18(text, fontSize, 400) + PADDING;
12788
12798
  if (width > maxDataWidth) maxDataWidth = width;
12789
12799
  }
12790
12800
  return Math.max(MIN_WIDTH, headerWidth, maxDataWidth);
@@ -13013,7 +13023,7 @@ import {
13013
13023
  adaptTheme as adaptTheme4,
13014
13024
  buildD3Formatter as buildD3Formatter8,
13015
13025
  computeChrome as computeChrome6,
13016
- estimateTextWidth as estimateTextWidth20,
13026
+ estimateTextWidth as estimateTextWidth19,
13017
13027
  formatNumber as formatNumber6,
13018
13028
  resolveTheme as resolveTheme5,
13019
13029
  SEQUENTIAL_PALETTES
@@ -13233,7 +13243,7 @@ function compileTileMap(spec, options) {
13233
13243
  height: contentHeight,
13234
13244
  animation: resolvedAnimation,
13235
13245
  watermark,
13236
- measureText: options.measureText ?? ((text, fontSize) => ({ width: estimateTextWidth20(text, fontSize), height: fontSize }))
13246
+ measureText: options.measureText ?? ((text, fontSize) => ({ width: estimateTextWidth19(text, fontSize), height: fontSize }))
13237
13247
  };
13238
13248
  }
13239
13249
  function emptyLayout4(chrome, theme, options, watermark) {
@@ -13268,7 +13278,7 @@ function emptyLayout4(chrome, theme, options, watermark) {
13268
13278
  height: options.height,
13269
13279
  watermark,
13270
13280
  animation: void 0,
13271
- measureText: options.measureText ?? ((text, fontSize) => ({ width: estimateTextWidth20(text, fontSize), height: fontSize }))
13281
+ measureText: options.measureText ?? ((text, fontSize) => ({ width: estimateTextWidth19(text, fontSize), height: fontSize }))
13272
13282
  };
13273
13283
  }
13274
13284
 
@@ -14287,7 +14297,7 @@ function compileChart(spec, options) {
14287
14297
  else sample = "0.0";
14288
14298
  const negPrefix = renderSpec.data.some((r) => Number(r[yField]) < 0) ? "-" : "";
14289
14299
  const labelEst = negPrefix + sample;
14290
- const labelWidth = estimateTextWidth21(
14300
+ const labelWidth = estimateTextWidth20(
14291
14301
  labelEst,
14292
14302
  theme.fonts.sizes.axisTick,
14293
14303
  theme.fonts.weights.normal