@opendata-ai/openchart-engine 2.9.0 → 2.10.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
@@ -4,6 +4,7 @@ import {
4
4
  generateAltText,
5
5
  generateDataTable,
6
6
  getBreakpoint,
7
+ getHeightClass,
7
8
  getLayoutStrategy,
8
9
  resolveTheme as resolveTheme2
9
10
  } from "@opendata-ai/openchart-core";
@@ -5979,6 +5980,7 @@ var DEFAULT_COLLISION_PADDING = 5;
5979
5980
  import {
5980
5981
  abbreviateNumber as abbreviateNumber3,
5981
5982
  buildD3Formatter as buildD3Formatter3,
5983
+ estimateTextWidth as estimateTextWidth7,
5982
5984
  formatDate,
5983
5985
  formatNumber as formatNumber3
5984
5986
  } from "@opendata-ai/openchart-core";
@@ -6086,13 +6088,29 @@ function computeAxes(scales, chartArea, strategy, theme) {
6086
6088
  position: t.position,
6087
6089
  major: true
6088
6090
  }));
6091
+ let tickAngle = scales.x.channel.axis?.tickAngle;
6092
+ if (tickAngle === void 0 && scales.x.type === "band" && ticks2.length > 1) {
6093
+ const bandwidth = scales.x.scale.bandwidth();
6094
+ let maxLabelWidth = 0;
6095
+ for (const t of ticks2) {
6096
+ const w = estimateTextWidth7(
6097
+ t.label,
6098
+ theme.fonts.sizes.axisTick,
6099
+ theme.fonts.weights.normal
6100
+ );
6101
+ if (w > maxLabelWidth) maxLabelWidth = w;
6102
+ }
6103
+ if (maxLabelWidth > bandwidth * 0.85) {
6104
+ tickAngle = -45;
6105
+ }
6106
+ }
6089
6107
  result.x = {
6090
6108
  ticks: ticks2,
6091
6109
  gridlines: scales.x.channel.axis?.grid ? gridlines : [],
6092
6110
  label: scales.x.channel.axis?.label,
6093
6111
  labelStyle: axisLabelStyle,
6094
6112
  tickLabelStyle,
6095
- tickAngle: scales.x.channel.axis?.tickAngle,
6113
+ tickAngle,
6096
6114
  start: { x: chartArea.x, y: chartArea.y + chartArea.height },
6097
6115
  end: { x: chartArea.x + chartArea.width, y: chartArea.y + chartArea.height }
6098
6116
  };
@@ -6119,7 +6137,7 @@ function computeAxes(scales, chartArea, strategy, theme) {
6119
6137
  }
6120
6138
 
6121
6139
  // src/layout/dimensions.ts
6122
- import { computeChrome as computeChrome2, estimateTextWidth as estimateTextWidth7 } from "@opendata-ai/openchart-core";
6140
+ import { computeChrome as computeChrome2, estimateTextWidth as estimateTextWidth8 } from "@opendata-ai/openchart-core";
6123
6141
  function chromeToInput(chrome) {
6124
6142
  return {
6125
6143
  title: chrome.title,
@@ -6129,11 +6147,28 @@ function chromeToInput(chrome) {
6129
6147
  footer: chrome.footer
6130
6148
  };
6131
6149
  }
6132
- function computeDimensions(spec, options, legendLayout, theme) {
6150
+ function scalePadding(basePadding, width, height) {
6151
+ const minDim = Math.min(width, height);
6152
+ if (minDim >= 500) return basePadding;
6153
+ if (minDim <= 200) return Math.max(Math.round(basePadding * 0.5), 4);
6154
+ const t = (minDim - 200) / 300;
6155
+ return Math.max(Math.round(basePadding * (0.5 + t * 0.5)), 4);
6156
+ }
6157
+ var MIN_CHART_WIDTH = 60;
6158
+ var MIN_CHART_HEIGHT = 40;
6159
+ function computeDimensions(spec, options, legendLayout, theme, strategy) {
6133
6160
  const { width, height } = options;
6134
- const padding = theme.spacing.padding;
6161
+ const padding = scalePadding(theme.spacing.padding, width, height);
6135
6162
  const axisMargin = theme.spacing.axisMargin;
6136
- const chrome = computeChrome2(chromeToInput(spec.chrome), theme, width, options.measureText);
6163
+ const chromeMode = strategy?.chromeMode ?? "full";
6164
+ const chrome = computeChrome2(
6165
+ chromeToInput(spec.chrome),
6166
+ theme,
6167
+ width,
6168
+ options.measureText,
6169
+ chromeMode,
6170
+ padding
6171
+ );
6137
6172
  const total = { x: 0, y: 0, width, height };
6138
6173
  const isRadial = spec.type === "pie" || spec.type === "donut";
6139
6174
  const encoding = spec.encoding;
@@ -6150,7 +6185,7 @@ function computeDimensions(spec, options, legendLayout, theme) {
6150
6185
  if (xField) {
6151
6186
  for (const row of spec.data) {
6152
6187
  const label = String(row[xField] ?? "");
6153
- const w = estimateTextWidth7(label, theme.fonts.sizes.axisTick, theme.fonts.weights.normal);
6188
+ const w = estimateTextWidth8(label, theme.fonts.sizes.axisTick, theme.fonts.weights.normal);
6154
6189
  if (w > maxLabelWidth) maxLabelWidth = w;
6155
6190
  }
6156
6191
  }
@@ -6176,7 +6211,7 @@ function computeDimensions(spec, options, legendLayout, theme) {
6176
6211
  const label = String(row[colorField] ?? "");
6177
6212
  if (!seen.has(label)) {
6178
6213
  seen.add(label);
6179
- const w = estimateTextWidth7(label, 11, 600);
6214
+ const w = estimateTextWidth8(label, 11, 600);
6180
6215
  if (w > maxLabelWidth) maxLabelWidth = w;
6181
6216
  }
6182
6217
  }
@@ -6191,7 +6226,7 @@ function computeDimensions(spec, options, legendLayout, theme) {
6191
6226
  let maxLabelWidth = 0;
6192
6227
  for (const row of spec.data) {
6193
6228
  const label = String(row[yField] ?? "");
6194
- const w = estimateTextWidth7(label, theme.fonts.sizes.axisTick, theme.fonts.weights.normal);
6229
+ const w = estimateTextWidth8(label, theme.fonts.sizes.axisTick, theme.fonts.weights.normal);
6195
6230
  if (w > maxLabelWidth) maxLabelWidth = w;
6196
6231
  }
6197
6232
  if (maxLabelWidth > 0) {
@@ -6213,7 +6248,7 @@ function computeDimensions(spec, options, legendLayout, theme) {
6213
6248
  else sampleLabel = "0.0";
6214
6249
  const negPrefix = spec.data.some((r) => Number(r[yField]) < 0) ? "-" : "";
6215
6250
  const labelEst = negPrefix + sampleLabel;
6216
- const labelWidth = estimateTextWidth7(
6251
+ const labelWidth = estimateTextWidth8(
6217
6252
  labelEst,
6218
6253
  theme.fonts.sizes.axisTick,
6219
6254
  theme.fonts.weights.normal
@@ -6234,12 +6269,38 @@ function computeDimensions(spec, options, legendLayout, theme) {
6234
6269
  margins.bottom += legendLayout.bounds.height + 4;
6235
6270
  }
6236
6271
  }
6237
- const chartArea = {
6272
+ let chartArea = {
6238
6273
  x: margins.left,
6239
6274
  y: margins.top,
6240
6275
  width: Math.max(0, width - margins.left - margins.right),
6241
6276
  height: Math.max(0, height - margins.top - margins.bottom)
6242
6277
  };
6278
+ if ((chartArea.width < MIN_CHART_WIDTH || chartArea.height < MIN_CHART_HEIGHT) && chromeMode !== "hidden") {
6279
+ const fallbackMode = chromeMode === "full" ? "compact" : "hidden";
6280
+ const fallbackChrome = computeChrome2(
6281
+ chromeToInput(spec.chrome),
6282
+ theme,
6283
+ width,
6284
+ options.measureText,
6285
+ fallbackMode,
6286
+ padding
6287
+ );
6288
+ const newTop = padding + fallbackChrome.topHeight + axisMargin;
6289
+ const topDelta = margins.top - newTop;
6290
+ const newBottom = padding + fallbackChrome.bottomHeight + xAxisHeight;
6291
+ const bottomDelta = margins.bottom - newBottom;
6292
+ if (topDelta > 0 || bottomDelta > 0) {
6293
+ margins.top = newTop + (legendLayout.entries.length > 0 && legendLayout.position === "top" ? legendLayout.bounds.height + 4 : 0);
6294
+ margins.bottom = newBottom;
6295
+ chartArea = {
6296
+ x: margins.left,
6297
+ y: margins.top,
6298
+ width: Math.max(0, width - margins.left - margins.right),
6299
+ height: Math.max(0, height - margins.top - margins.bottom)
6300
+ };
6301
+ return { total, chrome: fallbackChrome, chartArea, margins, theme };
6302
+ }
6303
+ }
6243
6304
  return { total, chrome, chartArea, margins, theme };
6244
6305
  }
6245
6306
 
@@ -6482,12 +6543,14 @@ function computeScales(spec, chartArea, data) {
6482
6543
  }
6483
6544
 
6484
6545
  // src/legend/compute.ts
6485
- import { estimateTextWidth as estimateTextWidth8 } from "@opendata-ai/openchart-core";
6546
+ import { estimateTextWidth as estimateTextWidth9 } from "@opendata-ai/openchart-core";
6486
6547
  var SWATCH_SIZE2 = 12;
6487
6548
  var SWATCH_GAP2 = 6;
6488
6549
  var ENTRY_GAP2 = 16;
6489
6550
  var LEGEND_PADDING = 8;
6490
6551
  var LEGEND_RIGHT_WIDTH = 120;
6552
+ var RIGHT_LEGEND_MAX_HEIGHT_RATIO = 0.4;
6553
+ var TOP_LEGEND_MAX_ROWS = 2;
6491
6554
  function swatchShapeForType(chartType) {
6492
6555
  switch (chartType) {
6493
6556
  case "line":
@@ -6513,8 +6576,41 @@ function extractColorEntries(spec, theme) {
6513
6576
  active: true
6514
6577
  }));
6515
6578
  }
6579
+ function entriesThatFit(entries, maxWidth, maxRows, labelStyle) {
6580
+ let row = 1;
6581
+ let rowWidth = 0;
6582
+ for (let i = 0; i < entries.length; i++) {
6583
+ const labelWidth = estimateTextWidth9(
6584
+ entries[i].label,
6585
+ labelStyle.fontSize,
6586
+ labelStyle.fontWeight
6587
+ );
6588
+ const entryWidth = SWATCH_SIZE2 + SWATCH_GAP2 + labelWidth + ENTRY_GAP2;
6589
+ if (rowWidth + entryWidth > maxWidth && rowWidth > 0) {
6590
+ row++;
6591
+ rowWidth = entryWidth;
6592
+ if (row > maxRows) return i;
6593
+ } else {
6594
+ rowWidth += entryWidth;
6595
+ }
6596
+ }
6597
+ return entries.length;
6598
+ }
6599
+ function truncateEntries(entries, maxCount) {
6600
+ if (maxCount >= entries.length || maxCount <= 0) return entries;
6601
+ const truncated = entries.slice(0, maxCount);
6602
+ const remaining = entries.length - maxCount;
6603
+ truncated.push({
6604
+ label: `+${remaining} more`,
6605
+ color: "#999999",
6606
+ shape: "square",
6607
+ active: false,
6608
+ overflow: true
6609
+ });
6610
+ return truncated;
6611
+ }
6516
6612
  function computeLegend(spec, strategy, theme, chartArea) {
6517
- if (spec.legend?.show === false) {
6613
+ if (spec.legend?.show === false || strategy.legendMaxHeight === 0) {
6518
6614
  return {
6519
6615
  position: "top",
6520
6616
  entries: [],
@@ -6531,7 +6627,7 @@ function computeLegend(spec, strategy, theme, chartArea) {
6531
6627
  entryGap: ENTRY_GAP2
6532
6628
  };
6533
6629
  }
6534
- const entries = extractColorEntries(spec, theme);
6630
+ let entries = extractColorEntries(spec, theme);
6535
6631
  const labelStyle = {
6536
6632
  fontFamily: theme.fonts.family,
6537
6633
  fontSize: theme.fonts.sizes.small,
@@ -6553,13 +6649,22 @@ function computeLegend(spec, strategy, theme, chartArea) {
6553
6649
  }
6554
6650
  if (resolvedPosition === "right" || resolvedPosition === "bottom-right") {
6555
6651
  const maxLabelWidth = Math.max(
6556
- ...entries.map((e) => estimateTextWidth8(e.label, labelStyle.fontSize, labelStyle.fontWeight))
6652
+ ...entries.map((e) => estimateTextWidth9(e.label, labelStyle.fontSize, labelStyle.fontWeight))
6557
6653
  );
6558
6654
  const legendWidth = Math.min(
6559
6655
  LEGEND_RIGHT_WIDTH,
6560
6656
  SWATCH_SIZE2 + SWATCH_GAP2 + maxLabelWidth + LEGEND_PADDING * 2
6561
6657
  );
6562
6658
  const entryHeight = Math.max(SWATCH_SIZE2, labelStyle.fontSize * labelStyle.lineHeight);
6659
+ const maxHeightRatio = strategy.legendMaxHeight > 0 ? strategy.legendMaxHeight : RIGHT_LEGEND_MAX_HEIGHT_RATIO;
6660
+ const maxLegendHeight = chartArea.height * maxHeightRatio;
6661
+ const maxEntries = Math.max(
6662
+ 1,
6663
+ Math.floor((maxLegendHeight - LEGEND_PADDING * 2) / (entryHeight + 4))
6664
+ );
6665
+ if (entries.length > maxEntries) {
6666
+ entries = truncateEntries(entries, maxEntries);
6667
+ }
6563
6668
  const legendHeight2 = entries.length * entryHeight + (entries.length - 1) * 4 + LEGEND_PADDING * 2;
6564
6669
  const clampedHeight = Math.min(legendHeight2, chartArea.height);
6565
6670
  const legendY = resolvedPosition === "bottom-right" ? chartArea.y + chartArea.height - clampedHeight : chartArea.y;
@@ -6580,11 +6685,29 @@ function computeLegend(spec, strategy, theme, chartArea) {
6580
6685
  entryGap: 4
6581
6686
  };
6582
6687
  }
6688
+ const availableWidth = chartArea.width - LEGEND_PADDING * 2;
6689
+ const maxFit = entriesThatFit(entries, availableWidth, TOP_LEGEND_MAX_ROWS, labelStyle);
6690
+ if (maxFit < entries.length) {
6691
+ entries = truncateEntries(entries, maxFit);
6692
+ }
6583
6693
  const totalWidth = entries.reduce((sum, entry) => {
6584
- const labelWidth = estimateTextWidth8(entry.label, labelStyle.fontSize, labelStyle.fontWeight);
6694
+ const labelWidth = estimateTextWidth9(entry.label, labelStyle.fontSize, labelStyle.fontWeight);
6585
6695
  return sum + SWATCH_SIZE2 + SWATCH_GAP2 + labelWidth + ENTRY_GAP2;
6586
6696
  }, 0);
6587
- const legendHeight = SWATCH_SIZE2 + LEGEND_PADDING * 2;
6697
+ let rowCount = 1;
6698
+ let rowWidth = 0;
6699
+ for (const entry of entries) {
6700
+ const labelWidth = estimateTextWidth9(entry.label, labelStyle.fontSize, labelStyle.fontWeight);
6701
+ const entryWidth = SWATCH_SIZE2 + SWATCH_GAP2 + labelWidth + ENTRY_GAP2;
6702
+ if (rowWidth + entryWidth > availableWidth && rowWidth > 0) {
6703
+ rowCount++;
6704
+ rowWidth = entryWidth;
6705
+ } else {
6706
+ rowWidth += entryWidth;
6707
+ }
6708
+ }
6709
+ const rowHeight = SWATCH_SIZE2 + 4;
6710
+ const legendHeight = rowCount * rowHeight + LEGEND_PADDING * 2;
6588
6711
  const offsetDx = spec.legend?.offset?.dx ?? 0;
6589
6712
  const offsetDy = spec.legend?.offset?.dy ?? 0;
6590
6713
  return {
@@ -6604,7 +6727,7 @@ function computeLegend(spec, strategy, theme, chartArea) {
6604
6727
  }
6605
6728
 
6606
6729
  // src/tables/compile-table.ts
6607
- import { computeChrome as computeChrome3, estimateTextWidth as estimateTextWidth9 } from "@opendata-ai/openchart-core";
6730
+ import { computeChrome as computeChrome3, estimateTextWidth as estimateTextWidth10 } from "@opendata-ai/openchart-core";
6608
6731
 
6609
6732
  // src/tables/bar-column.ts
6610
6733
  var NEGATIVE_BAR_COLOR = "#c44e52";
@@ -7011,13 +7134,13 @@ function estimateColumnWidth(col, data, fontSize) {
7011
7134
  if (col.image) return (col.image.width ?? 24) + PADDING;
7012
7135
  if (col.flag) return 60;
7013
7136
  const label = col.label ?? col.key;
7014
- const headerWidth = estimateTextWidth9(label, fontSize, 600) + PADDING;
7137
+ const headerWidth = estimateTextWidth10(label, fontSize, 600) + PADDING;
7015
7138
  const sampleSize = Math.min(100, data.length);
7016
7139
  let maxDataWidth = 0;
7017
7140
  for (let i = 0; i < sampleSize; i++) {
7018
7141
  const val = data[i][col.key];
7019
7142
  const text = val == null ? "" : String(val);
7020
- const width = estimateTextWidth9(text, fontSize, 400) + PADDING;
7143
+ const width = estimateTextWidth10(text, fontSize, 400) + PADDING;
7021
7144
  if (width > maxDataWidth) maxDataWidth = width;
7022
7145
  }
7023
7146
  return Math.max(MIN_WIDTH, headerWidth, maxDataWidth);
@@ -7427,7 +7550,8 @@ function compileChart(spec, options) {
7427
7550
  }
7428
7551
  let chartSpec = normalized;
7429
7552
  const breakpoint = getBreakpoint(options.width);
7430
- const strategy = getLayoutStrategy(breakpoint);
7553
+ const heightClass = getHeightClass(options.height);
7554
+ const strategy = getLayoutStrategy(breakpoint, heightClass);
7431
7555
  const rawSpec = spec;
7432
7556
  const overrides = rawSpec.overrides;
7433
7557
  if (overrides?.[breakpoint]) {
@@ -7478,7 +7602,7 @@ function compileChart(spec, options) {
7478
7602
  height: options.height
7479
7603
  };
7480
7604
  const legendLayout = computeLegend(chartSpec, strategy, theme, preliminaryArea);
7481
- const dims = computeDimensions(chartSpec, options, legendLayout, theme);
7605
+ const dims = computeDimensions(chartSpec, options, legendLayout, theme, strategy);
7482
7606
  const chartArea = dims.chartArea;
7483
7607
  const legendArea = { ...chartArea };
7484
7608
  if (legendLayout.entries.length > 0) {