@opendata-ai/openchart-engine 6.23.1 → 6.24.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
@@ -1489,7 +1489,10 @@ function computeDotMarks(spec, scales, _chartArea, _strategy) {
1489
1489
  return [];
1490
1490
  }
1491
1491
  const bandwidth = yScale.bandwidth();
1492
- const baseline = xScale(0);
1492
+ const [rangeStart, rangeEnd] = xScale.range();
1493
+ const rangeMin = Math.min(rangeStart, rangeEnd);
1494
+ const rangeMax = Math.max(rangeStart, rangeEnd);
1495
+ const baseline = Math.max(rangeMin, Math.min(rangeMax, xScale(0)));
1493
1496
  const colorEnc = encoding.color && "field" in encoding.color ? encoding.color : void 0;
1494
1497
  const isSequentialColor = colorEnc?.type === "quantitative";
1495
1498
  const colorField = isSequentialColor ? void 0 : colorEnc?.field;
@@ -8117,7 +8120,8 @@ function computeAxes(scales, chartArea, strategy, theme, measureText) {
8117
8120
  position: t.position,
8118
8121
  major: true
8119
8122
  }));
8120
- const shouldThin = scales.x.type !== "band" && !axisConfig?.tickCount && !axisConfig?.values;
8123
+ const hasExplicitValues = !!axisConfig?.values;
8124
+ const shouldThin = scales.x.type !== "band" && !hasExplicitValues;
8121
8125
  let ticks2;
8122
8126
  if (!shouldThin) {
8123
8127
  ticks2 = allTicks;
@@ -8179,9 +8183,9 @@ function computeAxes(scales, chartArea, strategy, theme, measureText) {
8179
8183
  } else {
8180
8184
  allTicks = continuousTicks(scales.y, yDensity, yTargetCount);
8181
8185
  }
8182
- const shouldThin = scales.y.type !== "band" && !axisConfig?.tickCount && !axisConfig?.values;
8186
+ const shouldThinY = scales.y.type !== "band" && !axisConfig?.values;
8183
8187
  let ticks2;
8184
- if (!shouldThin) {
8188
+ if (!shouldThinY) {
8185
8189
  ticks2 = allTicks;
8186
8190
  } else if (isContinuousY) {
8187
8191
  ticks2 = fitContinuousTicks(
@@ -8227,7 +8231,51 @@ function computeAxes(scales, chartArea, strategy, theme, measureText) {
8227
8231
  }
8228
8232
 
8229
8233
  // src/layout/dimensions.ts
8230
- import { computeChrome as computeChrome2, estimateTextWidth as estimateTextWidth8 } from "@opendata-ai/openchart-core";
8234
+ import { computeChrome as computeChrome2, estimateTextWidth as estimateTextWidth9 } from "@opendata-ai/openchart-core";
8235
+
8236
+ // src/legend/wrap.ts
8237
+ import { COMPACT_WIDTH, estimateTextWidth as estimateTextWidth8 } from "@opendata-ai/openchart-core";
8238
+ var SWATCH_SIZE2 = 12;
8239
+ var SWATCH_GAP2 = 6;
8240
+ var ENTRY_GAP2 = 16;
8241
+ var ENTRY_GAP_COMPACT = 10;
8242
+ var LEGEND_GAP = 4;
8243
+ function legendGap(width) {
8244
+ return width < COMPACT_WIDTH ? 0 : LEGEND_GAP;
8245
+ }
8246
+ function measureLegendWrap(entries, maxWidth, labelStyle, maxRows, entryGap = ENTRY_GAP2) {
8247
+ if (entries.length === 0) {
8248
+ return { rowCount: 0, fittingCount: 0, rowWidths: [] };
8249
+ }
8250
+ let rowCount = 1;
8251
+ let rowWidth = 0;
8252
+ const rowWidths = [];
8253
+ let fittingCount = entries.length;
8254
+ let fittingCountLocked = false;
8255
+ for (let i = 0; i < entries.length; i++) {
8256
+ const labelWidth = estimateTextWidth8(
8257
+ entries[i].label,
8258
+ labelStyle.fontSize,
8259
+ labelStyle.fontWeight
8260
+ );
8261
+ const entryWidth = SWATCH_SIZE2 + SWATCH_GAP2 + labelWidth + entryGap;
8262
+ if (rowWidth + entryWidth > maxWidth && rowWidth > 0) {
8263
+ rowWidths.push(rowWidth);
8264
+ rowCount++;
8265
+ rowWidth = entryWidth;
8266
+ if (!fittingCountLocked && maxRows != null && rowCount > maxRows) {
8267
+ fittingCount = i;
8268
+ fittingCountLocked = true;
8269
+ }
8270
+ } else {
8271
+ rowWidth += entryWidth;
8272
+ }
8273
+ }
8274
+ rowWidths.push(rowWidth);
8275
+ return { rowCount, fittingCount, rowWidths };
8276
+ }
8277
+
8278
+ // src/layout/dimensions.ts
8231
8279
  function chromeToInput(chrome) {
8232
8280
  return {
8233
8281
  title: chrome.title,
@@ -8276,7 +8324,7 @@ function computeDimensions(spec, options, legendLayout, theme, strategy, waterma
8276
8324
  if (xField) {
8277
8325
  for (const row of spec.data) {
8278
8326
  const label = String(row[xField] ?? "");
8279
- const w = estimateTextWidth8(label, theme.fonts.sizes.axisTick, theme.fonts.weights.normal);
8327
+ const w = estimateTextWidth9(label, theme.fonts.sizes.axisTick, theme.fonts.weights.normal);
8280
8328
  if (w > maxLabelWidth) maxLabelWidth = w;
8281
8329
  }
8282
8330
  }
@@ -8304,7 +8352,7 @@ function computeDimensions(spec, options, legendLayout, theme, strategy, waterma
8304
8352
  const label = String(row[colorField] ?? "");
8305
8353
  if (!seen.has(label)) {
8306
8354
  seen.add(label);
8307
- const w = estimateTextWidth8(label, 11, 600);
8355
+ const w = estimateTextWidth9(label, 11, 600);
8308
8356
  if (w > maxLabelWidth) maxLabelWidth = w;
8309
8357
  }
8310
8358
  }
@@ -8324,7 +8372,7 @@ function computeDimensions(spec, options, legendLayout, theme, strategy, waterma
8324
8372
  const maxXStr = String(maxX);
8325
8373
  for (const ann of spec.annotations) {
8326
8374
  if (ann.type === "text" && String(ann.x) === maxXStr) {
8327
- const textWidth = estimateTextWidth8(ann.text, ann.fontSize ?? 11, ann.fontWeight ?? 600);
8375
+ const textWidth = estimateTextWidth9(ann.text, ann.fontSize ?? 11, ann.fontWeight ?? 600);
8328
8376
  const dx = ann.offset?.dx ?? 0;
8329
8377
  const anchor = ann.anchor ?? "auto";
8330
8378
  const baseRightExtent = anchor === "left" ? textWidth : (
@@ -8348,11 +8396,15 @@ function computeDimensions(spec, options, legendLayout, theme, strategy, waterma
8348
8396
  let maxLabelWidth = 0;
8349
8397
  for (const row of spec.data) {
8350
8398
  const label = String(row[yField] ?? "");
8351
- const w = estimateTextWidth8(label, theme.fonts.sizes.axisTick, theme.fonts.weights.normal);
8399
+ const w = estimateTextWidth9(label, theme.fonts.sizes.axisTick, theme.fonts.weights.normal);
8352
8400
  if (w > maxLabelWidth) maxLabelWidth = w;
8353
8401
  }
8354
8402
  if (maxLabelWidth > 0) {
8355
- margins.left = Math.max(margins.left, padding + maxLabelWidth + 12);
8403
+ const labelGap = width < 500 ? 8 : 12;
8404
+ const maxLeftFraction = width < 400 ? 0.45 : width < 600 ? 0.55 : 1;
8405
+ const maxLeftReserved = Math.floor(width * maxLeftFraction);
8406
+ const reserved = Math.min(padding + maxLabelWidth + labelGap, maxLeftReserved);
8407
+ margins.left = Math.max(margins.left, reserved);
8356
8408
  }
8357
8409
  } else if (encoding.y.type === "quantitative" || encoding.y.type === "temporal") {
8358
8410
  const yField = encoding.y.field;
@@ -8380,7 +8432,7 @@ function computeDimensions(spec, options, legendLayout, theme, strategy, waterma
8380
8432
  }
8381
8433
  const negPrefix = spec.data.some((r) => Number(r[yField]) < 0) ? "-" : "";
8382
8434
  const labelEst = negPrefix + sampleLabel;
8383
- const labelWidth = estimateTextWidth8(
8435
+ const labelWidth = estimateTextWidth9(
8384
8436
  labelEst,
8385
8437
  theme.fonts.sizes.axisTick,
8386
8438
  theme.fonts.weights.normal
@@ -8394,12 +8446,13 @@ function computeDimensions(spec, options, legendLayout, theme, strategy, waterma
8394
8446
  margins.left = Math.max(margins.left, padding + rotatedLabelMargin);
8395
8447
  }
8396
8448
  if (legendLayout.entries.length > 0) {
8449
+ const gap = legendGap(width);
8397
8450
  if (legendLayout.position === "right" || legendLayout.position === "bottom-right") {
8398
8451
  margins.right += legendLayout.bounds.width + 8;
8399
8452
  } else if (legendLayout.position === "top") {
8400
- margins.top += legendLayout.bounds.height + 4;
8453
+ margins.top += legendLayout.bounds.height + gap;
8401
8454
  } else if (legendLayout.position === "bottom") {
8402
- margins.bottom += legendLayout.bounds.height + 4;
8455
+ margins.bottom += legendLayout.bounds.height + gap;
8403
8456
  }
8404
8457
  }
8405
8458
  let chartArea = {
@@ -8425,7 +8478,8 @@ function computeDimensions(spec, options, legendLayout, theme, strategy, waterma
8425
8478
  const newBottom = padding + fallbackChrome.bottomHeight + xAxisHeight;
8426
8479
  const bottomDelta = margins.bottom - newBottom;
8427
8480
  if (topDelta > 0 || bottomDelta > 0) {
8428
- margins.top = newTop + (legendLayout.entries.length > 0 && legendLayout.position === "top" ? legendLayout.bounds.height + 4 : 0);
8481
+ const gap = legendGap(width);
8482
+ margins.top = newTop + (legendLayout.entries.length > 0 && legendLayout.position === "top" ? legendLayout.bounds.height + gap : 0);
8429
8483
  margins.bottom = newBottom;
8430
8484
  chartArea = {
8431
8485
  x: margins.left,
@@ -8894,46 +8948,7 @@ function computeScales(spec, chartArea, data) {
8894
8948
  }
8895
8949
 
8896
8950
  // 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
8951
+ import { BRAND_RESERVE_WIDTH as BRAND_RESERVE_WIDTH2, COMPACT_WIDTH as COMPACT_WIDTH2, estimateTextWidth as estimateTextWidth10 } from "@opendata-ai/openchart-core";
8937
8952
  var LEGEND_PADDING = 8;
8938
8953
  var LEGEND_RIGHT_WIDTH = 120;
8939
8954
  var RIGHT_LEGEND_MAX_HEIGHT_RATIO = 0.4;
@@ -8955,11 +8970,15 @@ function extractColorEntries(spec, theme) {
8955
8970
  if (!colorEnc) return [];
8956
8971
  if ("condition" in colorEnc) return [];
8957
8972
  if (colorEnc.type === "quantitative") return [];
8958
- const uniqueValues = [...new Set(spec.data.map((d) => String(d[colorEnc.field])))];
8973
+ const dataValues = [...new Set(spec.data.map((d) => String(d[colorEnc.field])))];
8959
8974
  const explicitDomain = colorEnc.scale?.domain;
8960
8975
  const explicitRange = colorEnc.scale?.range;
8961
8976
  const palette = explicitRange ?? theme.colors.categorical;
8962
8977
  const shape = swatchShapeForType(spec.markType);
8978
+ const uniqueValues = explicitDomain ? [
8979
+ ...explicitDomain.filter((v) => dataValues.includes(v)),
8980
+ ...dataValues.filter((v) => !explicitDomain.includes(v))
8981
+ ] : dataValues;
8963
8982
  return uniqueValues.map((value2, i) => {
8964
8983
  let colorIndex = i;
8965
8984
  if (explicitDomain && explicitRange) {
@@ -9079,7 +9098,10 @@ function computeLegend(spec, strategy, theme, chartArea, watermark = true) {
9079
9098
  };
9080
9099
  }
9081
9100
  const reserveBrand = watermark && resolvedPosition === "bottom";
9082
- const availableWidth = chartArea.width - LEGEND_PADDING * 2 - (reserveBrand ? BRAND_RESERVE_WIDTH2 : 0);
9101
+ const isCompact = chartArea.width < COMPACT_WIDTH2;
9102
+ const effectivePadding = isCompact ? 2 : LEGEND_PADDING;
9103
+ const effectiveEntryGap = isCompact ? ENTRY_GAP_COMPACT : ENTRY_GAP2;
9104
+ const availableWidth = chartArea.width - effectivePadding * 2 - (reserveBrand ? BRAND_RESERVE_WIDTH2 : 0);
9083
9105
  if (spec.legend?.symbolLimit != null) {
9084
9106
  const limit = Math.max(1, spec.legend.symbolLimit);
9085
9107
  if (limit < entries.length) {
@@ -9087,17 +9109,29 @@ function computeLegend(spec, strategy, theme, chartArea, watermark = true) {
9087
9109
  }
9088
9110
  }
9089
9111
  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);
9112
+ const { fittingCount } = measureLegendWrap(
9113
+ entries,
9114
+ availableWidth,
9115
+ labelStyle,
9116
+ maxRows,
9117
+ effectiveEntryGap
9118
+ );
9091
9119
  if (fittingCount < entries.length) {
9092
9120
  entries = truncateEntries(entries, fittingCount);
9093
9121
  }
9094
9122
  const totalWidth = entries.reduce((sum2, entry) => {
9095
9123
  const labelWidth = estimateTextWidth10(entry.label, labelStyle.fontSize, labelStyle.fontWeight);
9096
- return sum2 + SWATCH_SIZE2 + SWATCH_GAP2 + labelWidth + ENTRY_GAP2;
9124
+ return sum2 + SWATCH_SIZE2 + SWATCH_GAP2 + labelWidth + effectiveEntryGap;
9097
9125
  }, 0);
9098
- const { rowCount } = measureLegendWrap(entries, availableWidth, labelStyle);
9126
+ const { rowCount } = measureLegendWrap(
9127
+ entries,
9128
+ availableWidth,
9129
+ labelStyle,
9130
+ void 0,
9131
+ effectiveEntryGap
9132
+ );
9099
9133
  const rowHeight = SWATCH_SIZE2 + 4;
9100
- const legendHeight = rowCount * rowHeight + LEGEND_PADDING * 2;
9134
+ const legendHeight = rowCount * rowHeight + effectivePadding * 2;
9101
9135
  const offsetDx = spec.legend?.offset?.dx ?? 0;
9102
9136
  const offsetDy = spec.legend?.offset?.dy ?? 0;
9103
9137
  return {
@@ -9112,7 +9146,7 @@ function computeLegend(spec, strategy, theme, chartArea, watermark = true) {
9112
9146
  labelStyle,
9113
9147
  swatchSize: SWATCH_SIZE2,
9114
9148
  swatchGap: SWATCH_GAP2,
9115
- entryGap: ENTRY_GAP2
9149
+ entryGap: effectiveEntryGap
9116
9150
  };
9117
9151
  }
9118
9152
 
@@ -9766,12 +9800,12 @@ function compileSankey(spec, options) {
9766
9800
  theme,
9767
9801
  fullArea
9768
9802
  );
9769
- const legendGap = legend.entries.length > 0 ? 4 : 0;
9803
+ const legendGap2 = legend.entries.length > 0 ? 4 : 0;
9770
9804
  const area = {
9771
9805
  x: fullArea.x,
9772
- y: fullArea.y + legend.bounds.height + legendGap,
9806
+ y: fullArea.y + legend.bounds.height + legendGap2,
9773
9807
  width: fullArea.width,
9774
- height: fullArea.height - legend.bounds.height - legendGap
9808
+ height: fullArea.height - legend.bounds.height - legendGap2
9775
9809
  };
9776
9810
  if (area.height <= 0) {
9777
9811
  return emptyLayout(area, chrome, theme, options, watermark);
@@ -9929,7 +9963,8 @@ function compileSankey(spec, options) {
9929
9963
  height: options.height
9930
9964
  },
9931
9965
  animation: resolvedAnimation,
9932
- watermark
9966
+ watermark,
9967
+ measureText: options.measureText
9933
9968
  };
9934
9969
  }
9935
9970
  function buildSankeyLegend(nodeColorMap, colorField, data, sourceField, targetField, theme, area) {
@@ -11320,13 +11355,14 @@ function compileChart(spec, options) {
11320
11355
  const chartArea = dims.chartArea;
11321
11356
  const legendArea = { ...chartArea };
11322
11357
  if (legendLayout.entries.length > 0) {
11358
+ const gap = legendGap(options.width);
11323
11359
  switch (legendLayout.position) {
11324
11360
  case "top":
11325
- legendArea.y -= legendLayout.bounds.height + 4;
11326
- legendArea.height += legendLayout.bounds.height + 4;
11361
+ legendArea.y -= legendLayout.bounds.height + gap;
11362
+ legendArea.height += legendLayout.bounds.height + gap;
11327
11363
  break;
11328
11364
  case "bottom":
11329
- legendArea.height += legendLayout.bounds.height + 4;
11365
+ legendArea.height += legendLayout.bounds.height + gap;
11330
11366
  break;
11331
11367
  case "right":
11332
11368
  case "bottom-right":