@opendata-ai/openchart-engine 7.0.3 → 7.1.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.js CHANGED
@@ -1243,7 +1243,6 @@ function computeEndpointLabels(spec, marks, theme, chartArea, strategy) {
1243
1243
  32
1244
1244
  );
1245
1245
  const columnX = chartArea.x + chartArea.width + ENDPOINT_COLUMN_GAP;
1246
- const markerX = chartArea.x + chartArea.width;
1247
1246
  const showLeader = config?.showLeader === true;
1248
1247
  const entries = provisional.map((p, i) => {
1249
1248
  const labelY = sweptTops[i];
@@ -1260,8 +1259,9 @@ function computeEndpointLabels(spec, marks, theme, chartArea, strategy) {
1260
1259
  };
1261
1260
  if (showMarker) {
1262
1261
  entry.marker = {
1263
- x: markerX,
1262
+ x: p.dataX + markerRadius,
1264
1263
  y: p.dataY,
1264
+ dataX: p.dataX,
1265
1265
  fill: markerFill,
1266
1266
  stroke: config?.markerStyle?.stroke ?? p.color,
1267
1267
  strokeWidth: markerStrokeWidth,
@@ -1493,8 +1493,8 @@ function computeBarMarks(spec, scales, _chartArea, _strategy) {
1493
1493
  const categoryGroups = groupByField(spec.data, yChannel.field);
1494
1494
  const needsStacking = Array.from(categoryGroups.values()).some((rows) => rows.length > 1);
1495
1495
  if (needsStacking) {
1496
- const stackDisabled = xChannel.stack === null || xChannel.stack === false;
1497
- if (stackDisabled) {
1496
+ const stackEnabled = xChannel.stack === true || xChannel.stack === "zero" || xChannel.stack === "normalize" || xChannel.stack === "center";
1497
+ if (!stackEnabled) {
1498
1498
  marks = computeGroupedBars(
1499
1499
  spec.data,
1500
1500
  xChannel.field,
@@ -1738,8 +1738,8 @@ function computeSimpleBars(data, valueField, categoryField, xScale, yScale, band
1738
1738
  import {
1739
1739
  buildD3Formatter,
1740
1740
  estimateTextWidth as estimateTextWidth4,
1741
- findAccessibleColor,
1742
1741
  getRepresentativeColor,
1742
+ pickLabelColor,
1743
1743
  resolveCollisions
1744
1744
  } from "@opendata-ai/openchart-core";
1745
1745
 
@@ -1806,7 +1806,7 @@ function computeBarLabels(marks, _chartArea, density = "auto", labelFormat, labe
1806
1806
  if (labelPrefix) valuePart = labelPrefix + valuePart;
1807
1807
  const textWidth = estimateTextWidth4(valuePart, LABEL_FONT_SIZE, LABEL_FONT_WEIGHT);
1808
1808
  const textHeight = LABEL_FONT_SIZE * 1.2;
1809
- const isStacked2 = mark.cornerRadius === 0;
1809
+ const isStacked2 = mark.stackGroup !== void 0;
1810
1810
  const isInside = mark.width >= MIN_WIDTH_FOR_INSIDE_LABEL;
1811
1811
  const isNegative = Number.isFinite(rawNum) ? rawNum < 0 : false;
1812
1812
  const bgColor = getRepresentativeColor(mark.fill);
@@ -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 = findAccessibleColor("#ffffff", bgColor, 4.5);
1818
+ fill = pickLabelColor(bgColor);
1819
1819
  textAnchor = "middle";
1820
1820
  } else if (isInside) {
1821
1821
  if (isNegative) {
1822
1822
  anchorX = mark.x + LABEL_PADDING;
1823
- fill = findAccessibleColor("#ffffff", bgColor, 4.5);
1823
+ fill = pickLabelColor(bgColor);
1824
1824
  textAnchor = "start";
1825
1825
  } else {
1826
1826
  anchorX = mark.x + mark.width - LABEL_PADDING;
1827
- fill = findAccessibleColor("#ffffff", bgColor, 4.5);
1827
+ fill = pickLabelColor(bgColor);
1828
1828
  textAnchor = "end";
1829
1829
  }
1830
1830
  } else {
@@ -1944,8 +1944,8 @@ function computeColumnMarks(spec, scales, _chartArea, _strategy) {
1944
1944
  const categoryGroups = groupByField(spec.data, xChannel.field);
1945
1945
  const needsStacking = Array.from(categoryGroups.values()).some((rows) => rows.length > 1);
1946
1946
  if (needsStacking) {
1947
- const stackDisabled = yChannel.stack === null || yChannel.stack === false;
1948
- if (stackDisabled) {
1947
+ const stackEnabled = yChannel.stack === true || yChannel.stack === "zero" || yChannel.stack === "normalize" || yChannel.stack === "center";
1948
+ if (!stackEnabled) {
1949
1949
  marks = computeGroupedColumns(
1950
1950
  spec.data,
1951
1951
  xChannel.field,
@@ -2212,7 +2212,7 @@ import {
2212
2212
  } from "@opendata-ai/openchart-core";
2213
2213
  var LABEL_FONT_SIZE2 = 10;
2214
2214
  var LABEL_FONT_WEIGHT2 = 600;
2215
- var LABEL_OFFSET_Y = 6;
2215
+ var LABEL_OFFSET_Y = 8;
2216
2216
  function computeColumnLabels(marks, _chartArea, density = "auto", labelFormat, labelPrefix, valueField, labelColor) {
2217
2217
  const targetMarks = filterByDensity(marks, density);
2218
2218
  const formatter = buildD3Formatter2(labelFormat);
@@ -2258,7 +2258,7 @@ function computeColumnLabels(marks, _chartArea, density = "auto", labelFormat, l
2258
2258
  fill: labelColor ?? getRepresentativeColor2(mark.fill),
2259
2259
  lineHeight: 1.2,
2260
2260
  textAnchor: "middle",
2261
- dominantBaseline: isNegative ? "hanging" : "auto"
2261
+ dominantBaseline: "hanging"
2262
2262
  }
2263
2263
  });
2264
2264
  }
@@ -3477,6 +3477,10 @@ var STACKED_GRADIENT_STOPS = [
3477
3477
  { offset: 0, opacity: 0.65 },
3478
3478
  { offset: 1, opacity: 0.35 }
3479
3479
  ];
3480
+ var STACKED_GRADIENT_STOPS_LIGHT = [
3481
+ { offset: 0, opacity: 0.65 },
3482
+ { offset: 1, opacity: 0 }
3483
+ ];
3480
3484
  function buildGradientFill(colorStr, stops) {
3481
3485
  return {
3482
3486
  gradient: "linear",
@@ -3569,7 +3573,7 @@ function computeSingleArea(spec, scales, _chartArea) {
3569
3573
  }
3570
3574
  return marks;
3571
3575
  }
3572
- function computeStackedArea(spec, scales, chartArea) {
3576
+ function computeStackedArea(spec, scales, chartArea, darkMode) {
3573
3577
  const encoding = spec.encoding;
3574
3578
  const xChannel = encoding.x;
3575
3579
  const yChannel = encoding.y;
@@ -3647,7 +3651,8 @@ function computeStackedArea(spec, scales, chartArea) {
3647
3651
  fillOpacity = isGradientDef3(markFill) ? 1 : spec.markDef.opacity ?? 0.7;
3648
3652
  } else {
3649
3653
  const colorStr = getRepresentativeColor4(color2);
3650
- fillValue = buildGradientFill(colorStr, STACKED_GRADIENT_STOPS);
3654
+ const stackedStops = darkMode ? STACKED_GRADIENT_STOPS : STACKED_GRADIENT_STOPS_LIGHT;
3655
+ fillValue = buildGradientFill(colorStr, stackedStops);
3651
3656
  fillOpacity = 1;
3652
3657
  }
3653
3658
  marks.push({
@@ -3675,11 +3680,11 @@ function computeStackedArea(spec, scales, chartArea) {
3675
3680
  }
3676
3681
  return marks;
3677
3682
  }
3678
- function computeAreaMarks(spec, scales, chartArea) {
3683
+ function computeAreaMarks(spec, scales, chartArea, darkMode) {
3679
3684
  const encoding = spec.encoding;
3680
3685
  const yChannel = encoding.y;
3681
3686
  if (yChannel && isStacked(yChannel.stack)) {
3682
- return computeStackedArea(spec, scales, chartArea);
3687
+ return computeStackedArea(spec, scales, chartArea, darkMode);
3683
3688
  }
3684
3689
  return computeSingleArea(spec, scales, chartArea);
3685
3690
  }
@@ -3938,8 +3943,8 @@ var lineRenderer = (spec, scales, chartArea, strategy, _theme) => {
3938
3943
  }
3939
3944
  return marks;
3940
3945
  };
3941
- var areaRenderer = (spec, scales, chartArea, strategy, _theme) => {
3942
- const areas = computeAreaMarks(spec, scales, chartArea);
3946
+ var areaRenderer = (spec, scales, chartArea, strategy, theme) => {
3947
+ const areas = computeAreaMarks(spec, scales, chartArea, theme.isDark);
3943
3948
  const encoding = spec.encoding;
3944
3949
  const hasColor = !!(encoding.color && "field" in encoding.color);
3945
3950
  const lines = hasColor ? linesFromAreas(areas) : computeLineMarks(spec, scales, chartArea, strategy);
@@ -9657,7 +9662,7 @@ var DEFAULT_COLLISION_PADDING = 5;
9657
9662
 
9658
9663
  // src/layout/axes/thinning.ts
9659
9664
  import { estimateTextWidth as estimateTextWidth11 } from "@opendata-ai/openchart-core";
9660
- var MIN_TICK_GAP_FACTOR = 1;
9665
+ var MIN_TICK_GAP_FACTOR = 0.5;
9661
9666
  var MIN_TICK_COUNT = 2;
9662
9667
  function measureLabel(text, fontSize, fontWeight, measureText) {
9663
9668
  return measureText ? measureText(text, fontSize, fontWeight).width : estimateTextWidth11(text, fontSize, fontWeight);
@@ -10159,6 +10164,7 @@ import {
10159
10164
  MAX_LEFT_LABEL_FRACTION_MEDIUM,
10160
10165
  MAX_LEFT_LABEL_FRACTION_MEDIUM_MAX,
10161
10166
  NARROW_VIEWPORT_MAX,
10167
+ TICK_LABEL_OFFSET as TICK_LABEL_OFFSET2,
10162
10168
  TOP_PAD_EXTRA_NARROW
10163
10169
  } from "@opendata-ai/openchart-core";
10164
10170
 
@@ -10316,6 +10322,9 @@ function chromeToInput(chrome) {
10316
10322
  brand: chrome.brand
10317
10323
  };
10318
10324
  }
10325
+ function bottomMargin(bottomHeight, padding, xAxisHeight) {
10326
+ return (bottomHeight > 0 ? bottomHeight : padding) + xAxisHeight;
10327
+ }
10319
10328
  function scalePadding(basePadding, width, height) {
10320
10329
  const minDim = Math.min(width, height);
10321
10330
  if (minDim >= 500) return basePadding;
@@ -10436,7 +10445,7 @@ function computeDimensions(spec, options, legendLayout, theme, strategy, waterma
10436
10445
  const margins = {
10437
10446
  top: topPad + chrome.topHeight + tentativeMetricsHeight,
10438
10447
  right: hPad + (isRadial ? hPad : axisMargin),
10439
- bottom: padding + chrome.bottomHeight + xAxisHeight,
10448
+ bottom: bottomMargin(chrome.bottomHeight, padding, xAxisHeight),
10440
10449
  left: hPad + (isRadial ? hPad : axisMargin)
10441
10450
  };
10442
10451
  const labelDensity = spec.labels.density;
@@ -10573,7 +10582,41 @@ function computeDimensions(spec, options, legendLayout, theme, strategy, waterma
10573
10582
  }
10574
10583
  const yAxis = encoding.y?.axis;
10575
10584
  if (yAxis && (yAxis.title || yAxis.label) && !isRadial) {
10576
- const axisTitleOffset = getAxisTitleOffset2(width);
10585
+ const yFieldForTitle = encoding.y?.field;
10586
+ const yAxisFormatForTitle = yAxis?.format;
10587
+ let estTickLabelWidth = 0;
10588
+ if (yFieldForTitle && (encoding.y?.type === "quantitative" || encoding.y?.type === "temporal")) {
10589
+ let maxAbsValForTitle = 0;
10590
+ for (const row of spec.data) {
10591
+ const v = Number(row[yFieldForTitle]);
10592
+ if (Number.isFinite(v) && Math.abs(v) > maxAbsValForTitle) maxAbsValForTitle = Math.abs(v);
10593
+ }
10594
+ let sampleLabelForTitle;
10595
+ if (yAxisFormatForTitle) {
10596
+ try {
10597
+ const fmt = format(yAxisFormatForTitle);
10598
+ sampleLabelForTitle = fmt(maxAbsValForTitle);
10599
+ } catch {
10600
+ sampleLabelForTitle = String(maxAbsValForTitle);
10601
+ }
10602
+ } else {
10603
+ if (maxAbsValForTitle >= 1e9) sampleLabelForTitle = "1.5B";
10604
+ else if (maxAbsValForTitle >= 1e6) sampleLabelForTitle = "1.5M";
10605
+ else if (maxAbsValForTitle >= 1e3) sampleLabelForTitle = "1.5K";
10606
+ else if (maxAbsValForTitle >= 100) sampleLabelForTitle = "100";
10607
+ else if (maxAbsValForTitle >= 10) sampleLabelForTitle = "10";
10608
+ else sampleLabelForTitle = "0.0";
10609
+ }
10610
+ const negPrefixForTitle = spec.data.some((r) => Number(r[yFieldForTitle]) < 0) ? "-" : "";
10611
+ estTickLabelWidth = estimateTextWidth16(
10612
+ negPrefixForTitle + sampleLabelForTitle,
10613
+ theme.fonts.sizes.axisTick,
10614
+ theme.fonts.weights.normal
10615
+ );
10616
+ }
10617
+ const AXIS_TITLE_GAP = 8;
10618
+ const dynamicTitleOffset = TICK_LABEL_OFFSET2 + estTickLabelWidth + AXIS_TITLE_GAP;
10619
+ const axisTitleOffset = Math.max(dynamicTitleOffset, getAxisTitleOffset2(width));
10577
10620
  const halfGlyph = Math.ceil(theme.fonts.sizes.body / 2);
10578
10621
  const rotatedLabelMargin = axisTitleOffset + halfGlyph + (width < BREAKPOINT_COMPACT_MAX2 ? 0 : AXIS_TITLE_TRAILING_PAD2);
10579
10622
  margins.left = Math.max(margins.left, hPad + rotatedLabelMargin);
@@ -10612,7 +10655,7 @@ function computeDimensions(spec, options, legendLayout, theme, strategy, waterma
10612
10655
  const fallbackTopAxisGap = isRadial && fallbackChrome.topHeight === 0 ? 0 : axisMargin + inlineTickOverhang;
10613
10656
  const newTop = topPad + fallbackChrome.topHeight + tentativeMetricsHeight;
10614
10657
  const topDelta = margins.top - newTop;
10615
- const newBottom = padding + fallbackChrome.bottomHeight + xAxisHeight;
10658
+ const newBottom = bottomMargin(fallbackChrome.bottomHeight, padding, xAxisHeight);
10616
10659
  const bottomDelta = margins.bottom - newBottom;
10617
10660
  if (topDelta > 0 || bottomDelta > 0) {
10618
10661
  const gap = legendGap(width);
@@ -11018,8 +11061,8 @@ function computeScales(spec, chartArea, data) {
11018
11061
  if (encoding.x) {
11019
11062
  let xData = data;
11020
11063
  let xChannel = encoding.x;
11021
- const xStackDisabled = encoding.x.stack === null || encoding.x.stack === false;
11022
- if (spec.markType === "bar" && encoding.color && encoding.x.type === "quantitative" && !xStackDisabled) {
11064
+ const xStackEnabled = encoding.x.stack === true || encoding.x.stack === "zero" || encoding.x.stack === "normalize" || encoding.x.stack === "center";
11065
+ if (spec.markType === "bar" && encoding.color && encoding.x.type === "quantitative" && xStackEnabled) {
11023
11066
  if (encoding.x.stack === "normalize") {
11024
11067
  xChannel = { ...encoding.x, scale: { ...encoding.x.scale, domain: [0, 1], nice: false } };
11025
11068
  } else if (encoding.x.stack === "center") {
@@ -11074,7 +11117,7 @@ function computeScales(spec, chartArea, data) {
11074
11117
  const stackProp = encoding.y.stack;
11075
11118
  const isExplicitlyStacked = stackProp === true || stackProp === "zero" || stackProp === "normalize" || stackProp === "center";
11076
11119
  const isAreaStacked = spec.markType === "area" && isExplicitlyStacked;
11077
- const isBarStacked = isVerticalBar && stackProp !== null && stackProp !== false;
11120
+ const isBarStacked = isVerticalBar && isExplicitlyStacked;
11078
11121
  const hasStackingGroup = isBarStacked && encoding.color !== void 0;
11079
11122
  const userRequestedStack = isExplicitlyStacked;
11080
11123
  const isLineOrArea2 = spec.markType === "line" || spec.markType === "area";