@opendata-ai/openchart-engine 6.23.1 → 6.24.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.js CHANGED
@@ -467,7 +467,9 @@ function resolveRefLineAnnotation(annotation, scales, chartArea, isDark) {
467
467
  return null;
468
468
  }
469
469
  let strokeDasharray;
470
- if (annotation.style === "dashed" || annotation.style === void 0) {
470
+ if (annotation.strokeDash && annotation.strokeDash.length > 0) {
471
+ strokeDasharray = annotation.strokeDash.join(" ");
472
+ } else if (annotation.style === "dashed" || annotation.style === void 0) {
471
473
  strokeDasharray = DEFAULT_REFLINE_DASH;
472
474
  } else if (annotation.style === "dotted") {
473
475
  strokeDasharray = "2 2";
@@ -549,11 +551,12 @@ function resolveRefLineAnnotation(annotation, scales, chartArea, isDark) {
549
551
 
550
552
  // src/annotations/compute.ts
551
553
  function computeAnnotations(spec, scales, chartArea, strategy, isDark = false, obstacles = [], svgDimensions) {
552
- if (strategy.annotationPosition === "tooltip-only") {
553
- return [];
554
- }
554
+ const isCompact = strategy.annotationPosition === "tooltip-only";
555
555
  const annotations = [];
556
556
  for (const annotation of spec.annotations) {
557
+ if (isCompact && annotation.responsive !== false) {
558
+ continue;
559
+ }
557
560
  let resolved = null;
558
561
  switch (annotation.type) {
559
562
  case "text":
@@ -979,6 +982,7 @@ function computeSimpleBars(data, valueField, categoryField, xScale, yScale, band
979
982
  import {
980
983
  buildD3Formatter,
981
984
  estimateTextWidth as estimateTextWidth2,
985
+ findAccessibleColor,
982
986
  getRepresentativeColor,
983
987
  resolveCollisions
984
988
  } from "@opendata-ai/openchart-core";
@@ -1047,21 +1051,35 @@ function computeBarLabels(marks, _chartArea, density = "auto", labelFormat, labe
1047
1051
  const textHeight = LABEL_FONT_SIZE * 1.2;
1048
1052
  const isStacked = mark.cornerRadius === 0;
1049
1053
  const isInside = mark.width >= MIN_WIDTH_FOR_INSIDE_LABEL;
1054
+ const isNegative = Number.isFinite(rawNum) ? rawNum < 0 : false;
1055
+ const bgColor = getRepresentativeColor(mark.fill);
1050
1056
  let anchorX;
1051
1057
  let fill;
1052
1058
  let textAnchor;
1053
1059
  if (isStacked && isInside) {
1054
1060
  anchorX = mark.x + mark.width / 2;
1055
- fill = "#ffffff";
1061
+ fill = findAccessibleColor("#ffffff", bgColor, 4.5);
1056
1062
  textAnchor = "middle";
1057
1063
  } else if (isInside) {
1058
- anchorX = mark.x + mark.width - LABEL_PADDING;
1059
- fill = "#ffffff";
1060
- textAnchor = "end";
1064
+ if (isNegative) {
1065
+ anchorX = mark.x + LABEL_PADDING;
1066
+ fill = findAccessibleColor("#ffffff", bgColor, 4.5);
1067
+ textAnchor = "start";
1068
+ } else {
1069
+ anchorX = mark.x + mark.width - LABEL_PADDING;
1070
+ fill = findAccessibleColor("#ffffff", bgColor, 4.5);
1071
+ textAnchor = "end";
1072
+ }
1061
1073
  } else {
1062
- anchorX = mark.x + mark.width + LABEL_PADDING;
1063
- fill = getRepresentativeColor(mark.fill);
1064
- textAnchor = "start";
1074
+ if (isNegative) {
1075
+ anchorX = mark.x - LABEL_PADDING;
1076
+ fill = getRepresentativeColor(mark.fill);
1077
+ textAnchor = "end";
1078
+ } else {
1079
+ anchorX = mark.x + mark.width + LABEL_PADDING;
1080
+ fill = getRepresentativeColor(mark.fill);
1081
+ textAnchor = "start";
1082
+ }
1065
1083
  }
1066
1084
  const anchorY = mark.y + mark.height / 2;
1067
1085
  const fits = !(isStacked && textWidth > mark.width - 2 * LABEL_PADDING);
@@ -1489,7 +1507,10 @@ function computeDotMarks(spec, scales, _chartArea, _strategy) {
1489
1507
  return [];
1490
1508
  }
1491
1509
  const bandwidth = yScale.bandwidth();
1492
- const baseline = xScale(0);
1510
+ const [rangeStart, rangeEnd] = xScale.range();
1511
+ const rangeMin = Math.min(rangeStart, rangeEnd);
1512
+ const rangeMax = Math.max(rangeStart, rangeEnd);
1513
+ const baseline = Math.max(rangeMin, Math.min(rangeMax, xScale(0)));
1493
1514
  const colorEnc = encoding.color && "field" in encoding.color ? encoding.color : void 0;
1494
1515
  const isSequentialColor = colorEnc?.type === "quantitative";
1495
1516
  const colorField = isSequentialColor ? void 0 : colorEnc?.field;
@@ -2659,14 +2680,16 @@ function computeSingleArea(spec, scales, _chartArea) {
2659
2680
  const color2 = getColor(scales, seriesKey);
2660
2681
  const sortedRows = xChannel.type === "nominal" || xChannel.type === "ordinal" ? rows : sortByField(rows, xChannel.field);
2661
2682
  const validPoints = [];
2683
+ const y2Channel = encoding.y2;
2662
2684
  for (const row of sortedRows) {
2663
2685
  const xVal = scaleValue(scales.x.scale, scales.x.type, row[xChannel.field]);
2664
2686
  const yVal = scaleValue(scales.y.scale, scales.y.type, row[yChannel.field]);
2665
2687
  if (xVal === null || yVal === null) continue;
2688
+ const yBottomVal = y2Channel && row[y2Channel.field] != null ? scaleValue(scales.y.scale, scales.y.type, row[y2Channel.field]) : null;
2666
2689
  validPoints.push({
2667
2690
  x: xVal,
2668
2691
  yTop: yVal,
2669
- yBottom: baselineY,
2692
+ yBottom: yBottomVal ?? baselineY,
2670
2693
  row
2671
2694
  });
2672
2695
  }
@@ -2680,6 +2703,7 @@ function computeSingleArea(spec, scales, _chartArea) {
2680
2703
  const bottomPoints = validPoints.map((p) => ({ x: p.x, y: p.yBottom }));
2681
2704
  const ariaLabel = seriesKey === "__default__" ? `Area with ${validPoints.length} data points` : `${seriesKey}: area with ${validPoints.length} data points`;
2682
2705
  const aria = { label: ariaLabel };
2706
+ const fillOpacity = y2Channel ? 0.25 : DEFAULT_FILL_OPACITY;
2683
2707
  marks.push({
2684
2708
  type: "area",
2685
2709
  topPoints,
@@ -2687,7 +2711,7 @@ function computeSingleArea(spec, scales, _chartArea) {
2687
2711
  path: pathStr,
2688
2712
  topPath: topPathStr,
2689
2713
  fill: color2,
2690
- fillOpacity: DEFAULT_FILL_OPACITY,
2714
+ fillOpacity,
2691
2715
  stroke: getRepresentativeColor4(color2),
2692
2716
  strokeWidth: 2,
2693
2717
  seriesKey: seriesKey === "__default__" ? void 0 : seriesKey,
@@ -6604,8 +6628,10 @@ function normalizeAnnotations(annotations) {
6604
6628
  fill: ann.fill ?? "#000000"
6605
6629
  };
6606
6630
  case "refline":
6631
+ case "rule":
6607
6632
  return {
6608
6633
  ...ann,
6634
+ type: "refline",
6609
6635
  style: ann.style ?? "dashed",
6610
6636
  strokeWidth: ann.strokeWidth ?? 1,
6611
6637
  stroke: ann.stroke ?? "#666666",
@@ -8117,7 +8143,8 @@ function computeAxes(scales, chartArea, strategy, theme, measureText) {
8117
8143
  position: t.position,
8118
8144
  major: true
8119
8145
  }));
8120
- const shouldThin = scales.x.type !== "band" && !axisConfig?.tickCount && !axisConfig?.values;
8146
+ const hasExplicitValues = !!axisConfig?.values;
8147
+ const shouldThin = scales.x.type !== "band" && !hasExplicitValues;
8121
8148
  let ticks2;
8122
8149
  if (!shouldThin) {
8123
8150
  ticks2 = allTicks;
@@ -8179,9 +8206,9 @@ function computeAxes(scales, chartArea, strategy, theme, measureText) {
8179
8206
  } else {
8180
8207
  allTicks = continuousTicks(scales.y, yDensity, yTargetCount);
8181
8208
  }
8182
- const shouldThin = scales.y.type !== "band" && !axisConfig?.tickCount && !axisConfig?.values;
8209
+ const shouldThinY = scales.y.type !== "band" && !axisConfig?.values;
8183
8210
  let ticks2;
8184
- if (!shouldThin) {
8211
+ if (!shouldThinY) {
8185
8212
  ticks2 = allTicks;
8186
8213
  } else if (isContinuousY) {
8187
8214
  ticks2 = fitContinuousTicks(
@@ -8227,7 +8254,51 @@ function computeAxes(scales, chartArea, strategy, theme, measureText) {
8227
8254
  }
8228
8255
 
8229
8256
  // src/layout/dimensions.ts
8230
- import { computeChrome as computeChrome2, estimateTextWidth as estimateTextWidth8 } from "@opendata-ai/openchart-core";
8257
+ import { computeChrome as computeChrome2, estimateTextWidth as estimateTextWidth9 } from "@opendata-ai/openchart-core";
8258
+
8259
+ // src/legend/wrap.ts
8260
+ import { COMPACT_WIDTH, estimateTextWidth as estimateTextWidth8 } from "@opendata-ai/openchart-core";
8261
+ var SWATCH_SIZE2 = 12;
8262
+ var SWATCH_GAP2 = 6;
8263
+ var ENTRY_GAP2 = 16;
8264
+ var ENTRY_GAP_COMPACT = 10;
8265
+ var LEGEND_GAP = 4;
8266
+ function legendGap(width) {
8267
+ return width < COMPACT_WIDTH ? 0 : LEGEND_GAP;
8268
+ }
8269
+ function measureLegendWrap(entries, maxWidth, labelStyle, maxRows, entryGap = ENTRY_GAP2) {
8270
+ if (entries.length === 0) {
8271
+ return { rowCount: 0, fittingCount: 0, rowWidths: [] };
8272
+ }
8273
+ let rowCount = 1;
8274
+ let rowWidth = 0;
8275
+ const rowWidths = [];
8276
+ let fittingCount = entries.length;
8277
+ let fittingCountLocked = false;
8278
+ for (let i = 0; i < entries.length; i++) {
8279
+ const labelWidth = estimateTextWidth8(
8280
+ entries[i].label,
8281
+ labelStyle.fontSize,
8282
+ labelStyle.fontWeight
8283
+ );
8284
+ const entryWidth = SWATCH_SIZE2 + SWATCH_GAP2 + labelWidth + entryGap;
8285
+ if (rowWidth + entryWidth > maxWidth && rowWidth > 0) {
8286
+ rowWidths.push(rowWidth);
8287
+ rowCount++;
8288
+ rowWidth = entryWidth;
8289
+ if (!fittingCountLocked && maxRows != null && rowCount > maxRows) {
8290
+ fittingCount = i;
8291
+ fittingCountLocked = true;
8292
+ }
8293
+ } else {
8294
+ rowWidth += entryWidth;
8295
+ }
8296
+ }
8297
+ rowWidths.push(rowWidth);
8298
+ return { rowCount, fittingCount, rowWidths };
8299
+ }
8300
+
8301
+ // src/layout/dimensions.ts
8231
8302
  function chromeToInput(chrome) {
8232
8303
  return {
8233
8304
  title: chrome.title,
@@ -8276,7 +8347,7 @@ function computeDimensions(spec, options, legendLayout, theme, strategy, waterma
8276
8347
  if (xField) {
8277
8348
  for (const row of spec.data) {
8278
8349
  const label = String(row[xField] ?? "");
8279
- const w = estimateTextWidth8(label, theme.fonts.sizes.axisTick, theme.fonts.weights.normal);
8350
+ const w = estimateTextWidth9(label, theme.fonts.sizes.axisTick, theme.fonts.weights.normal);
8280
8351
  if (w > maxLabelWidth) maxLabelWidth = w;
8281
8352
  }
8282
8353
  }
@@ -8294,7 +8365,8 @@ function computeDimensions(spec, options, legendLayout, theme, strategy, waterma
8294
8365
  left: padding + (isRadial ? padding : axisMargin)
8295
8366
  };
8296
8367
  const labelDensity = spec.labels.density;
8297
- if ((spec.markType === "line" || spec.markType === "area") && labelDensity !== "none") {
8368
+ const labelsHiddenByStrategy = strategy?.labelMode === "none";
8369
+ if ((spec.markType === "line" || spec.markType === "area") && labelDensity !== "none" && !labelsHiddenByStrategy) {
8298
8370
  const colorEnc = encoding.color;
8299
8371
  const colorField = colorEnc && "field" in colorEnc ? colorEnc.field : void 0;
8300
8372
  if (colorField) {
@@ -8304,7 +8376,7 @@ function computeDimensions(spec, options, legendLayout, theme, strategy, waterma
8304
8376
  const label = String(row[colorField] ?? "");
8305
8377
  if (!seen.has(label)) {
8306
8378
  seen.add(label);
8307
- const w = estimateTextWidth8(label, 11, 600);
8379
+ const w = estimateTextWidth9(label, 11, 600);
8308
8380
  if (w > maxLabelWidth) maxLabelWidth = w;
8309
8381
  }
8310
8382
  }
@@ -8324,7 +8396,7 @@ function computeDimensions(spec, options, legendLayout, theme, strategy, waterma
8324
8396
  const maxXStr = String(maxX);
8325
8397
  for (const ann of spec.annotations) {
8326
8398
  if (ann.type === "text" && String(ann.x) === maxXStr) {
8327
- const textWidth = estimateTextWidth8(ann.text, ann.fontSize ?? 11, ann.fontWeight ?? 600);
8399
+ const textWidth = estimateTextWidth9(ann.text, ann.fontSize ?? 11, ann.fontWeight ?? 600);
8328
8400
  const dx = ann.offset?.dx ?? 0;
8329
8401
  const anchor = ann.anchor ?? "auto";
8330
8402
  const baseRightExtent = anchor === "left" ? textWidth : (
@@ -8348,11 +8420,15 @@ function computeDimensions(spec, options, legendLayout, theme, strategy, waterma
8348
8420
  let maxLabelWidth = 0;
8349
8421
  for (const row of spec.data) {
8350
8422
  const label = String(row[yField] ?? "");
8351
- const w = estimateTextWidth8(label, theme.fonts.sizes.axisTick, theme.fonts.weights.normal);
8423
+ const w = estimateTextWidth9(label, theme.fonts.sizes.axisTick, theme.fonts.weights.normal);
8352
8424
  if (w > maxLabelWidth) maxLabelWidth = w;
8353
8425
  }
8354
8426
  if (maxLabelWidth > 0) {
8355
- margins.left = Math.max(margins.left, padding + maxLabelWidth + 12);
8427
+ const labelGap = width < 500 ? 8 : 12;
8428
+ const maxLeftFraction = width < 400 ? 0.45 : width < 600 ? 0.55 : 1;
8429
+ const maxLeftReserved = Math.floor(width * maxLeftFraction);
8430
+ const reserved = Math.min(padding + maxLabelWidth + labelGap, maxLeftReserved);
8431
+ margins.left = Math.max(margins.left, reserved);
8356
8432
  }
8357
8433
  } else if (encoding.y.type === "quantitative" || encoding.y.type === "temporal") {
8358
8434
  const yField = encoding.y.field;
@@ -8380,7 +8456,7 @@ function computeDimensions(spec, options, legendLayout, theme, strategy, waterma
8380
8456
  }
8381
8457
  const negPrefix = spec.data.some((r) => Number(r[yField]) < 0) ? "-" : "";
8382
8458
  const labelEst = negPrefix + sampleLabel;
8383
- const labelWidth = estimateTextWidth8(
8459
+ const labelWidth = estimateTextWidth9(
8384
8460
  labelEst,
8385
8461
  theme.fonts.sizes.axisTick,
8386
8462
  theme.fonts.weights.normal
@@ -8394,12 +8470,13 @@ function computeDimensions(spec, options, legendLayout, theme, strategy, waterma
8394
8470
  margins.left = Math.max(margins.left, padding + rotatedLabelMargin);
8395
8471
  }
8396
8472
  if (legendLayout.entries.length > 0) {
8473
+ const gap = legendGap(width);
8397
8474
  if (legendLayout.position === "right" || legendLayout.position === "bottom-right") {
8398
8475
  margins.right += legendLayout.bounds.width + 8;
8399
8476
  } else if (legendLayout.position === "top") {
8400
- margins.top += legendLayout.bounds.height + 4;
8477
+ margins.top += legendLayout.bounds.height + gap;
8401
8478
  } else if (legendLayout.position === "bottom") {
8402
- margins.bottom += legendLayout.bounds.height + 4;
8479
+ margins.bottom += legendLayout.bounds.height + gap;
8403
8480
  }
8404
8481
  }
8405
8482
  let chartArea = {
@@ -8425,7 +8502,8 @@ function computeDimensions(spec, options, legendLayout, theme, strategy, waterma
8425
8502
  const newBottom = padding + fallbackChrome.bottomHeight + xAxisHeight;
8426
8503
  const bottomDelta = margins.bottom - newBottom;
8427
8504
  if (topDelta > 0 || bottomDelta > 0) {
8428
- margins.top = newTop + (legendLayout.entries.length > 0 && legendLayout.position === "top" ? legendLayout.bounds.height + 4 : 0);
8505
+ const gap = legendGap(width);
8506
+ margins.top = newTop + (legendLayout.entries.length > 0 && legendLayout.position === "top" ? legendLayout.bounds.height + gap : 0);
8429
8507
  margins.bottom = newBottom;
8430
8508
  chartArea = {
8431
8509
  x: margins.left,
@@ -8894,46 +8972,7 @@ function computeScales(spec, chartArea, data) {
8894
8972
  }
8895
8973
 
8896
8974
  // src/legend/compute.ts
8897
- import { BRAND_RESERVE_WIDTH as BRAND_RESERVE_WIDTH2, estimateTextWidth as estimateTextWidth10 } from "@opendata-ai/openchart-core";
8898
-
8899
- // src/legend/wrap.ts
8900
- import { estimateTextWidth as estimateTextWidth9 } from "@opendata-ai/openchart-core";
8901
- var SWATCH_SIZE2 = 12;
8902
- var SWATCH_GAP2 = 6;
8903
- var ENTRY_GAP2 = 16;
8904
- function measureLegendWrap(entries, maxWidth, labelStyle, maxRows) {
8905
- if (entries.length === 0) {
8906
- return { rowCount: 0, fittingCount: 0, rowWidths: [] };
8907
- }
8908
- let rowCount = 1;
8909
- let rowWidth = 0;
8910
- const rowWidths = [];
8911
- let fittingCount = entries.length;
8912
- let fittingCountLocked = false;
8913
- for (let i = 0; i < entries.length; i++) {
8914
- const labelWidth = estimateTextWidth9(
8915
- entries[i].label,
8916
- labelStyle.fontSize,
8917
- labelStyle.fontWeight
8918
- );
8919
- const entryWidth = SWATCH_SIZE2 + SWATCH_GAP2 + labelWidth + ENTRY_GAP2;
8920
- if (rowWidth + entryWidth > maxWidth && rowWidth > 0) {
8921
- rowWidths.push(rowWidth);
8922
- rowCount++;
8923
- rowWidth = entryWidth;
8924
- if (!fittingCountLocked && maxRows != null && rowCount > maxRows) {
8925
- fittingCount = i;
8926
- fittingCountLocked = true;
8927
- }
8928
- } else {
8929
- rowWidth += entryWidth;
8930
- }
8931
- }
8932
- rowWidths.push(rowWidth);
8933
- return { rowCount, fittingCount, rowWidths };
8934
- }
8935
-
8936
- // src/legend/compute.ts
8975
+ import { BRAND_RESERVE_WIDTH as BRAND_RESERVE_WIDTH2, COMPACT_WIDTH as COMPACT_WIDTH2, estimateTextWidth as estimateTextWidth10 } from "@opendata-ai/openchart-core";
8937
8976
  var LEGEND_PADDING = 8;
8938
8977
  var LEGEND_RIGHT_WIDTH = 120;
8939
8978
  var RIGHT_LEGEND_MAX_HEIGHT_RATIO = 0.4;
@@ -8955,11 +8994,16 @@ function extractColorEntries(spec, theme) {
8955
8994
  if (!colorEnc) return [];
8956
8995
  if ("condition" in colorEnc) return [];
8957
8996
  if (colorEnc.type === "quantitative") return [];
8958
- const uniqueValues = [...new Set(spec.data.map((d) => String(d[colorEnc.field])))];
8997
+ const dataValues = [...new Set(spec.data.map((d) => String(d[colorEnc.field])))];
8959
8998
  const explicitDomain = colorEnc.scale?.domain;
8960
8999
  const explicitRange = colorEnc.scale?.range;
8961
9000
  const palette = explicitRange ?? theme.colors.categorical;
8962
9001
  const shape = swatchShapeForType(spec.markType);
9002
+ const uniqueValues = explicitDomain ? [
9003
+ ...explicitDomain.filter((v) => dataValues.includes(v)),
9004
+ ...dataValues.filter((v) => !explicitDomain.includes(v))
9005
+ ] : dataValues;
9006
+ const excludeSet = new Set(spec.legend?.exclude ?? []);
8963
9007
  return uniqueValues.map((value2, i) => {
8964
9008
  let colorIndex = i;
8965
9009
  if (explicitDomain && explicitRange) {
@@ -8972,7 +9016,7 @@ function extractColorEntries(spec, theme) {
8972
9016
  shape,
8973
9017
  active: true
8974
9018
  };
8975
- });
9019
+ }).filter((entry) => !excludeSet.has(entry.label));
8976
9020
  }
8977
9021
  function truncateEntries(entries, maxCount) {
8978
9022
  if (maxCount >= entries.length || maxCount <= 0) return entries;
@@ -9010,7 +9054,10 @@ function computeLegend(spec, strategy, theme, chartArea, watermark = true) {
9010
9054
  const hasLabels = spec.labels.density !== "none";
9011
9055
  const labelsWillRender = strategy.labelMode !== "none";
9012
9056
  const hasColorEncoding = spec.encoding.color != null;
9013
- const legendNotForced = spec.legend?.show !== true;
9057
+ const userConfiguredLegend = spec.legend != null && Object.keys(spec.legend).some(
9058
+ (k) => k !== "show" || spec.legend[k] !== false
9059
+ );
9060
+ const legendNotForced = !userConfiguredLegend;
9014
9061
  if (isLineOrArea && hasLabels && labelsWillRender && hasColorEncoding && legendNotForced) {
9015
9062
  const isArea = spec.markType === "area";
9016
9063
  const quantChannel = spec.encoding.y?.type === "quantitative" ? spec.encoding.y : spec.encoding.x;
@@ -9079,7 +9126,10 @@ function computeLegend(spec, strategy, theme, chartArea, watermark = true) {
9079
9126
  };
9080
9127
  }
9081
9128
  const reserveBrand = watermark && resolvedPosition === "bottom";
9082
- const availableWidth = chartArea.width - LEGEND_PADDING * 2 - (reserveBrand ? BRAND_RESERVE_WIDTH2 : 0);
9129
+ const isCompact = chartArea.width < COMPACT_WIDTH2;
9130
+ const effectivePadding = isCompact ? 2 : LEGEND_PADDING;
9131
+ const effectiveEntryGap = isCompact ? ENTRY_GAP_COMPACT : ENTRY_GAP2;
9132
+ const availableWidth = chartArea.width - effectivePadding * 2 - (reserveBrand ? BRAND_RESERVE_WIDTH2 : 0);
9083
9133
  if (spec.legend?.symbolLimit != null) {
9084
9134
  const limit = Math.max(1, spec.legend.symbolLimit);
9085
9135
  if (limit < entries.length) {
@@ -9087,17 +9137,29 @@ function computeLegend(spec, strategy, theme, chartArea, watermark = true) {
9087
9137
  }
9088
9138
  }
9089
9139
  const maxRows = spec.legend?.maxRows != null ? Math.max(1, spec.legend.maxRows) : spec.legend?.columns != null ? Math.ceil(entries.length / spec.legend.columns) : TOP_LEGEND_MAX_ROWS;
9090
- const { fittingCount } = measureLegendWrap(entries, availableWidth, labelStyle, maxRows);
9140
+ const { fittingCount } = measureLegendWrap(
9141
+ entries,
9142
+ availableWidth,
9143
+ labelStyle,
9144
+ maxRows,
9145
+ effectiveEntryGap
9146
+ );
9091
9147
  if (fittingCount < entries.length) {
9092
9148
  entries = truncateEntries(entries, fittingCount);
9093
9149
  }
9094
9150
  const totalWidth = entries.reduce((sum2, entry) => {
9095
9151
  const labelWidth = estimateTextWidth10(entry.label, labelStyle.fontSize, labelStyle.fontWeight);
9096
- return sum2 + SWATCH_SIZE2 + SWATCH_GAP2 + labelWidth + ENTRY_GAP2;
9152
+ return sum2 + SWATCH_SIZE2 + SWATCH_GAP2 + labelWidth + effectiveEntryGap;
9097
9153
  }, 0);
9098
- const { rowCount } = measureLegendWrap(entries, availableWidth, labelStyle);
9154
+ const { rowCount } = measureLegendWrap(
9155
+ entries,
9156
+ availableWidth,
9157
+ labelStyle,
9158
+ void 0,
9159
+ effectiveEntryGap
9160
+ );
9099
9161
  const rowHeight = SWATCH_SIZE2 + 4;
9100
- const legendHeight = rowCount * rowHeight + LEGEND_PADDING * 2;
9162
+ const legendHeight = rowCount * rowHeight + effectivePadding * 2;
9101
9163
  const offsetDx = spec.legend?.offset?.dx ?? 0;
9102
9164
  const offsetDy = spec.legend?.offset?.dy ?? 0;
9103
9165
  return {
@@ -9112,7 +9174,7 @@ function computeLegend(spec, strategy, theme, chartArea, watermark = true) {
9112
9174
  labelStyle,
9113
9175
  swatchSize: SWATCH_SIZE2,
9114
9176
  swatchGap: SWATCH_GAP2,
9115
- entryGap: ENTRY_GAP2
9177
+ entryGap: effectiveEntryGap
9116
9178
  };
9117
9179
  }
9118
9180
 
@@ -9766,12 +9828,12 @@ function compileSankey(spec, options) {
9766
9828
  theme,
9767
9829
  fullArea
9768
9830
  );
9769
- const legendGap = legend.entries.length > 0 ? 4 : 0;
9831
+ const legendGap2 = legend.entries.length > 0 ? 4 : 0;
9770
9832
  const area = {
9771
9833
  x: fullArea.x,
9772
- y: fullArea.y + legend.bounds.height + legendGap,
9834
+ y: fullArea.y + legend.bounds.height + legendGap2,
9773
9835
  width: fullArea.width,
9774
- height: fullArea.height - legend.bounds.height - legendGap
9836
+ height: fullArea.height - legend.bounds.height - legendGap2
9775
9837
  };
9776
9838
  if (area.height <= 0) {
9777
9839
  return emptyLayout(area, chrome, theme, options, watermark);
@@ -9929,7 +9991,8 @@ function compileSankey(spec, options) {
9929
9991
  height: options.height
9930
9992
  },
9931
9993
  animation: resolvedAnimation,
9932
- watermark
9994
+ watermark,
9995
+ measureText: options.measureText
9933
9996
  };
9934
9997
  }
9935
9998
  function buildSankeyLegend(nodeColorMap, colorField, data, sourceField, targetField, theme, area) {
@@ -11320,13 +11383,14 @@ function compileChart(spec, options) {
11320
11383
  const chartArea = dims.chartArea;
11321
11384
  const legendArea = { ...chartArea };
11322
11385
  if (legendLayout.entries.length > 0) {
11386
+ const gap = legendGap(options.width);
11323
11387
  switch (legendLayout.position) {
11324
11388
  case "top":
11325
- legendArea.y -= legendLayout.bounds.height + 4;
11326
- legendArea.height += legendLayout.bounds.height + 4;
11389
+ legendArea.y -= legendLayout.bounds.height + gap;
11390
+ legendArea.height += legendLayout.bounds.height + gap;
11327
11391
  break;
11328
11392
  case "bottom":
11329
- legendArea.height += legendLayout.bounds.height + 4;
11393
+ legendArea.height += legendLayout.bounds.height + gap;
11330
11394
  break;
11331
11395
  case "right":
11332
11396
  case "bottom-right":