@opendata-ai/openchart-engine 7.1.3 → 7.2.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
@@ -298,7 +298,7 @@ interface NormalizedChartSpec {
298
298
  metrics?: _opendata_ai_openchart_core.Metric[];
299
299
  annotations: Annotation[];
300
300
  /** Normalized label configuration with defaults applied. density, format, and prefix are always set; offsets and color stay optional. */
301
- labels: Required<Pick<LabelConfig, 'density' | 'format' | 'prefix'>> & Pick<LabelConfig, 'offsets' | 'color'>;
301
+ labels: Required<Pick<LabelConfig, 'density' | 'format' | 'prefix'>> & Pick<LabelConfig, 'offsets' | 'color' | 'fontSize' | 'suffix'>;
302
302
  /** Legend configuration (position override). */
303
303
  legend?: LegendConfig;
304
304
  /** Right-side endpoint labels column config (multi-series line/area only). */
package/dist/index.js CHANGED
@@ -1778,7 +1778,8 @@ 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, fontSize, labelSuffix) {
1782
+ const FONT_SIZE = fontSize ?? LABEL_FONT_SIZE;
1782
1783
  const targetMarks = filterByDensity(marks, density);
1783
1784
  const candidates = [];
1784
1785
  const fitsInSegment = [];
@@ -1804,8 +1805,9 @@ function computeBarLabels(marks, _chartArea, density = "auto", labelFormat, labe
1804
1805
  }
1805
1806
  }
1806
1807
  if (labelPrefix) valuePart = labelPrefix + valuePart;
1807
- const textWidth = estimateTextWidth4(valuePart, LABEL_FONT_SIZE, LABEL_FONT_WEIGHT);
1808
- const textHeight = LABEL_FONT_SIZE * 1.2;
1808
+ if (labelSuffix) valuePart = valuePart + labelSuffix;
1809
+ const textWidth = estimateTextWidth4(valuePart, FONT_SIZE, LABEL_FONT_WEIGHT);
1810
+ const textHeight = FONT_SIZE * 1.2;
1809
1811
  const isStacked2 = mark.stackGroup !== void 0;
1810
1812
  const isInside = mark.width >= MIN_WIDTH_FOR_INSIDE_LABEL;
1811
1813
  const isNegative = Number.isFinite(rawNum) ? rawNum < 0 : false;
@@ -1815,16 +1817,16 @@ function computeBarLabels(marks, _chartArea, density = "auto", labelFormat, labe
1815
1817
  let textAnchor;
1816
1818
  if (isStacked2 && isInside) {
1817
1819
  anchorX = mark.x + mark.width / 2;
1818
- fill = pickLabelColor(bgColor);
1820
+ fill = pickLabelColor(bgColor, darkMode);
1819
1821
  textAnchor = "middle";
1820
1822
  } else if (isInside) {
1821
1823
  if (isNegative) {
1822
1824
  anchorX = mark.x + LABEL_PADDING;
1823
- fill = pickLabelColor(bgColor);
1825
+ fill = pickLabelColor(bgColor, darkMode);
1824
1826
  textAnchor = "start";
1825
1827
  } else {
1826
1828
  anchorX = mark.x + mark.width - LABEL_PADDING;
1827
- fill = pickLabelColor(bgColor);
1829
+ fill = pickLabelColor(bgColor, darkMode);
1828
1830
  textAnchor = "end";
1829
1831
  }
1830
1832
  } else {
@@ -1850,7 +1852,7 @@ function computeBarLabels(marks, _chartArea, density = "auto", labelFormat, labe
1850
1852
  priority: "data",
1851
1853
  style: {
1852
1854
  fontFamily: "system-ui, -apple-system, sans-serif",
1853
- fontSize: LABEL_FONT_SIZE,
1855
+ fontSize: FONT_SIZE,
1854
1856
  fontWeight: LABEL_FONT_WEIGHT,
1855
1857
  fill,
1856
1858
  lineHeight: 1.2,
@@ -1898,7 +1900,7 @@ function computeBarLabels(marks, _chartArea, density = "auto", labelFormat, labe
1898
1900
  }
1899
1901
 
1900
1902
  // src/charts/bar/index.ts
1901
- var barRenderer = (spec, scales, chartArea, strategy, _theme) => {
1903
+ var barRenderer = (spec, scales, chartArea, strategy, theme) => {
1902
1904
  const marks = computeBarMarks(spec, scales, chartArea, strategy);
1903
1905
  const valueField = spec.encoding?.x && "field" in spec.encoding.x ? spec.encoding.x.field : void 0;
1904
1906
  const labels = computeBarLabels(
@@ -1908,7 +1910,10 @@ var barRenderer = (spec, scales, chartArea, strategy, _theme) => {
1908
1910
  spec.labels.format,
1909
1911
  spec.labels.prefix,
1910
1912
  valueField,
1911
- spec.labels.color
1913
+ spec.labels.color,
1914
+ theme.isDark,
1915
+ spec.labels.fontSize,
1916
+ spec.labels.suffix
1912
1917
  );
1913
1918
  for (let i = 0; i < marks.length && i < labels.length; i++) {
1914
1919
  marks[i].label = labels[i];
@@ -2213,7 +2218,8 @@ import {
2213
2218
  var LABEL_FONT_SIZE2 = 10;
2214
2219
  var LABEL_FONT_WEIGHT2 = 600;
2215
2220
  var LABEL_OFFSET_Y = 8;
2216
- function computeColumnLabels(marks, _chartArea, density = "auto", labelFormat, labelPrefix, valueField, labelColor) {
2221
+ function computeColumnLabels(marks, _chartArea, density = "auto", labelFormat, labelPrefix, valueField, labelColor, fontSize, labelSuffix) {
2222
+ const FONT_SIZE = fontSize ?? LABEL_FONT_SIZE2;
2217
2223
  const targetMarks = filterByDensity(marks, density);
2218
2224
  const formatter = buildD3Formatter2(labelFormat);
2219
2225
  const candidates = [];
@@ -2237,11 +2243,12 @@ function computeColumnLabels(marks, _chartArea, density = "auto", labelFormat, l
2237
2243
  valuePart = rawValue;
2238
2244
  }
2239
2245
  }
2240
- if (labelPrefix) valuePart = labelPrefix + valuePart;
2241
2246
  const numericValue = parseFloat(valuePart);
2242
2247
  const isNegative = Number.isFinite(numericValue) && numericValue < 0;
2243
- const textWidth = estimateTextWidth5(valuePart, LABEL_FONT_SIZE2, LABEL_FONT_WEIGHT2);
2244
- const textHeight = LABEL_FONT_SIZE2 * 1.2;
2248
+ if (labelPrefix) valuePart = labelPrefix + valuePart;
2249
+ if (labelSuffix) valuePart = valuePart + labelSuffix;
2250
+ const textWidth = estimateTextWidth5(valuePart, FONT_SIZE, LABEL_FONT_WEIGHT2);
2251
+ const textHeight = FONT_SIZE * 1.2;
2245
2252
  const anchorX = mark.x + mark.width / 2;
2246
2253
  const anchorY = isNegative ? mark.y + mark.height + LABEL_OFFSET_Y : mark.y - LABEL_OFFSET_Y - textHeight;
2247
2254
  candidates.push({
@@ -2253,7 +2260,7 @@ function computeColumnLabels(marks, _chartArea, density = "auto", labelFormat, l
2253
2260
  priority: "data",
2254
2261
  style: {
2255
2262
  fontFamily: "system-ui, -apple-system, sans-serif",
2256
- fontSize: LABEL_FONT_SIZE2,
2263
+ fontSize: FONT_SIZE,
2257
2264
  fontWeight: LABEL_FONT_WEIGHT2,
2258
2265
  fill: labelColor ?? getRepresentativeColor2(mark.fill),
2259
2266
  lineHeight: 1.2,
@@ -2286,7 +2293,9 @@ var columnRenderer = (spec, scales, chartArea, strategy, _theme) => {
2286
2293
  spec.labels.format,
2287
2294
  spec.labels.prefix,
2288
2295
  valueField,
2289
- spec.labels.color
2296
+ spec.labels.color,
2297
+ spec.labels.fontSize,
2298
+ spec.labels.suffix
2290
2299
  );
2291
2300
  for (let i = 0; i < marks.length && i < labels.length; i++) {
2292
2301
  marks[i].label = labels[i];
@@ -7286,8 +7295,10 @@ function normalizeLabels(labels) {
7286
7295
  density: labels.density ?? "auto",
7287
7296
  format: labels.format ?? "",
7288
7297
  prefix: labels.prefix ?? "",
7298
+ suffix: labels.suffix,
7289
7299
  offsets: labels.offsets,
7290
- color: labels.color
7300
+ color: labels.color,
7301
+ fontSize: labels.fontSize
7291
7302
  };
7292
7303
  }
7293
7304
  function normalizeChartSpec(spec, warnings) {
@@ -9757,22 +9768,23 @@ var TEMPORAL_SCALE_TYPES = /* @__PURE__ */ new Set(["time", "utc"]);
9757
9768
  function formatTickLabel(value2, resolvedScale) {
9758
9769
  const axisConfig = resolvedScale.channel.axis || void 0;
9759
9770
  const formatStr = axisConfig?.format;
9771
+ const suffix = axisConfig?.labelSuffix ?? "";
9760
9772
  if (TEMPORAL_SCALE_TYPES.has(resolvedScale.type)) {
9761
9773
  const temporalFmt = buildTemporalFormatter(formatStr);
9762
- if (temporalFmt) return temporalFmt(value2);
9774
+ if (temporalFmt) return temporalFmt(value2) + suffix;
9763
9775
  const useUtc = resolvedScale.type === "utc";
9764
- return formatDate(value2, void 0, void 0, useUtc);
9776
+ return formatDate(value2, void 0, void 0, useUtc) + suffix;
9765
9777
  }
9766
9778
  if (NUMERIC_SCALE_TYPES.has(resolvedScale.type)) {
9767
9779
  const num = value2;
9768
9780
  if (formatStr) {
9769
9781
  const fmt = buildD3Formatter5(formatStr);
9770
- if (fmt) return fmt(num);
9782
+ if (fmt) return fmt(num) + suffix;
9771
9783
  }
9772
- if (Math.abs(num) >= 1e3) return abbreviateNumber2(num);
9773
- return formatNumber3(num);
9784
+ if (Math.abs(num) >= 1e3) return abbreviateNumber2(num) + suffix;
9785
+ return formatNumber3(num) + suffix;
9774
9786
  }
9775
- return String(value2);
9787
+ return String(value2) + suffix;
9776
9788
  }
9777
9789
  function continuousTicks(resolvedScale, density, targetCount) {
9778
9790
  const scale = resolvedScale.scale;
@@ -10145,6 +10157,7 @@ function computeAxes(scales, chartArea, strategy, theme, measureText, dataContex
10145
10157
 
10146
10158
  // src/layout/dimensions.ts
10147
10159
  import {
10160
+ AXIS_TITLE_GAP,
10148
10161
  AXIS_TITLE_TRAILING_PAD as AXIS_TITLE_TRAILING_PAD2,
10149
10162
  BREAKPOINT_COMPACT_MAX as BREAKPOINT_COMPACT_MAX2,
10150
10163
  computeChrome as computeChrome3,
@@ -10424,7 +10437,8 @@ function computeDimensions(spec, options, legendLayout, theme, strategy, waterma
10424
10437
  const labelHeight = Math.min(rotatedHeight, 120);
10425
10438
  xAxisHeight = hasXAxisLabel ? labelHeight + 20 : labelHeight;
10426
10439
  } else {
10427
- xAxisHeight = hasXAxisLabel ? 48 : 26;
10440
+ const base = theme.spacing.xAxisHeight;
10441
+ xAxisHeight = hasXAxisLabel ? base + 22 : base;
10428
10442
  }
10429
10443
  const yAxisCfgPre = encoding.y?.axis ?? void 0;
10430
10444
  const yTickPositionExplicitPre = yAxisCfgPre?.tickPosition;
@@ -10609,7 +10623,6 @@ function computeDimensions(spec, options, legendLayout, theme, strategy, waterma
10609
10623
  theme.fonts.weights.normal
10610
10624
  );
10611
10625
  }
10612
- const AXIS_TITLE_GAP = 8;
10613
10626
  const dynamicTitleOffset = TICK_LABEL_OFFSET2 + estTickLabelWidth + AXIS_TITLE_GAP;
10614
10627
  const axisTitleOffset = Math.max(dynamicTitleOffset, getAxisTitleOffset2(width));
10615
10628
  const halfGlyph = Math.ceil(theme.fonts.sizes.body / 2);
@@ -10619,6 +10632,7 @@ function computeDimensions(spec, options, legendLayout, theme, strategy, waterma
10619
10632
  if (options.rightAxisReserve && options.rightAxisReserve > 0) {
10620
10633
  margins.right = Math.max(margins.right, hPad + options.rightAxisReserve);
10621
10634
  }
10635
+ const hasTopLegend = "entries" in legendLayout && legendLayout.entries.length > 0 && legendLayout.position === "top";
10622
10636
  if ("entries" in legendLayout && legendLayout.entries.length > 0) {
10623
10637
  const gap = legendGap(width);
10624
10638
  if (legendLayout.position === "right" || legendLayout.position === "bottom-right") {
@@ -10627,7 +10641,7 @@ function computeDimensions(spec, options, legendLayout, theme, strategy, waterma
10627
10641
  margins.top += legendLayout.bounds.height + gap;
10628
10642
  }
10629
10643
  }
10630
- margins.top += topAxisGap;
10644
+ margins.top += hasTopLegend ? inlineTickOverhang : topAxisGap;
10631
10645
  let chartArea = {
10632
10646
  x: margins.left,
10633
10647
  y: margins.top,
@@ -10648,13 +10662,14 @@ function computeDimensions(spec, options, legendLayout, theme, strategy, waterma
10648
10662
  bottomLegendReservation
10649
10663
  );
10650
10664
  const fallbackTopAxisGap = isRadial && fallbackChrome.topHeight === 0 ? 0 : axisMargin + inlineTickOverhang;
10665
+ const fallbackEffectiveAxisGap = hasTopLegend ? inlineTickOverhang : fallbackTopAxisGap;
10651
10666
  const newTop = topPad + fallbackChrome.topHeight + tentativeMetricsHeight;
10652
10667
  const topDelta = margins.top - newTop;
10653
10668
  const newBottom = bottomMargin(fallbackChrome.bottomHeight, padding, xAxisHeight);
10654
10669
  const bottomDelta = margins.bottom - newBottom;
10655
10670
  if (topDelta > 0 || bottomDelta > 0) {
10656
10671
  const gap = legendGap(width);
10657
- margins.top = newTop + ("entries" in legendLayout && legendLayout.entries.length > 0 && legendLayout.position === "top" ? legendLayout.bounds.height + gap : 0) + fallbackTopAxisGap;
10672
+ margins.top = newTop + ("entries" in legendLayout && legendLayout.entries.length > 0 && legendLayout.position === "top" ? legendLayout.bounds.height + gap : 0) + fallbackEffectiveAxisGap;
10658
10673
  margins.bottom = newBottom;
10659
10674
  chartArea = {
10660
10675
  x: margins.left,
@@ -11059,7 +11074,13 @@ function computeScales(spec, chartArea, data) {
11059
11074
  const xStackEnabled = encoding.x.stack === true || encoding.x.stack === "zero" || encoding.x.stack === "normalize" || encoding.x.stack === "center";
11060
11075
  if (spec.markType === "bar" && encoding.color && encoding.x.type === "quantitative" && xStackEnabled) {
11061
11076
  if (encoding.x.stack === "normalize") {
11062
- xChannel = { ...encoding.x, scale: { ...encoding.x.scale, domain: [0, 1], nice: false } };
11077
+ const existingAxis = encoding.x.axis;
11078
+ const axis = existingAxis === false || existingAxis?.format ? existingAxis : { ...typeof existingAxis === "object" ? existingAxis : {}, format: ".0%" };
11079
+ xChannel = {
11080
+ ...encoding.x,
11081
+ scale: { ...encoding.x.scale, domain: [0, 1], nice: false },
11082
+ axis
11083
+ };
11063
11084
  } else if (encoding.x.stack === "center") {
11064
11085
  const yField = encoding.y?.field;
11065
11086
  const xField = encoding.x.field;
@@ -11126,7 +11147,13 @@ function computeScales(spec, chartArea, data) {
11126
11147
  }
11127
11148
  if ((isBarStacked || isAreaStacked) && encoding.color && encoding.y.type === "quantitative") {
11128
11149
  if (encoding.y.stack === "normalize") {
11129
- yChannel = { ...encoding.y, scale: { ...encoding.y.scale, domain: [0, 1], nice: false } };
11150
+ const existingAxis = encoding.y.axis;
11151
+ const axis = existingAxis === false || existingAxis?.format ? existingAxis : { ...typeof existingAxis === "object" ? existingAxis : {}, format: ".0%" };
11152
+ yChannel = {
11153
+ ...encoding.y,
11154
+ scale: { ...encoding.y.scale, domain: [0, 1], nice: false },
11155
+ axis
11156
+ };
11130
11157
  } else if (encoding.y.stack === "center") {
11131
11158
  const xField = encoding.x?.field;
11132
11159
  const yField = encoding.y.field;