@opendata-ai/openchart-engine 7.9.3 → 7.11.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
@@ -658,6 +658,25 @@ declare function getChartRenderer(type: string): ChartRenderer | undefined;
658
658
  */
659
659
  declare function clearRenderers(): void;
660
660
 
661
+ /**
662
+ * Legend row-wrap geometry.
663
+ *
664
+ * Shared helper for measuring how legend entries flow across horizontal rows
665
+ * when wrapped at a max width. Both the main legend compute and the sankey
666
+ * legend compile use this to size their legends — the main legend uses
667
+ * `fittingCount` for truncation decisions, while sankey uses `rowCount` to
668
+ * reserve vertical height.
669
+ *
670
+ * The geometry matches the existing layout exactly: each entry occupies
671
+ * SWATCH_SIZE + SWATCH_GAP + labelWidth + ENTRY_GAP pixels, a new row is
672
+ * started when the accumulated row width plus the next entry would exceed
673
+ * maxWidth (and the current row is non-empty), and rowWidths captures the
674
+ * in-row accumulated width at the point of wrapping.
675
+ */
676
+
677
+ /** Gap between legend and chart area, responsive to container width. */
678
+ declare function legendGap(width: number): number;
679
+
661
680
  /**
662
681
  * Bin transform: discretizes a continuous field into bins.
663
682
  *
@@ -792,4 +811,4 @@ declare function runTimeUnit(data: DataRow[], transform: TimeUnitTransform): Dat
792
811
  */
793
812
  declare function runTransforms(data: DataRow[], transforms: Transform[]): DataRow[];
794
813
 
795
- export { type AnnotationContext, type AnnotationMeasureTextFn, type ChartRenderer, type CompileResult, type CompiledGraphEdge, type CompiledGraphNode, ENTER_DEFAULTS, EXIT_DEFAULTS, type GraphCompilation, type NormalizedBarListSpec, type NormalizedChartSpec, type NormalizedChrome, type NormalizedGraphSpec, type NormalizedSankeySpec, type NormalizedSpec, type NormalizedTableSpec, type NormalizedTileMapSpec, type ObstacleKind, type PlacementObstacle, type SimulationConfig, UPDATE_DEFAULTS, type ValidationError, type ValidationErrorCode, type ValidationResult, buildAreaPath, buildLinePath, clampStaggerDelay, clearRenderers, compile, compileBarList, compileChart, compileGraph, compileLayer, compileSankey, compileTable, compileTileMap, dedupeKeys, evaluatePredicate, getChartRenderer, heuristicMeasure, isConditionalValueDef, normalizeSpec, registerChartRenderer, resolveAnimation, resolveConditionalValue, runBin, runCalculate, runFilter, runTimeUnit, runTransforms, serializeKeyValue, validateSpec };
814
+ export { type AnnotationContext, type AnnotationMeasureTextFn, type ChartRenderer, type CompileResult, type CompiledGraphEdge, type CompiledGraphNode, ENTER_DEFAULTS, EXIT_DEFAULTS, type GraphCompilation, type NormalizedBarListSpec, type NormalizedChartSpec, type NormalizedChrome, type NormalizedGraphSpec, type NormalizedSankeySpec, type NormalizedSpec, type NormalizedTableSpec, type NormalizedTileMapSpec, type ObstacleKind, type PlacementObstacle, type SimulationConfig, UPDATE_DEFAULTS, type ValidationError, type ValidationErrorCode, type ValidationResult, buildAreaPath, buildLinePath, clampStaggerDelay, clearRenderers, compile, compileBarList, compileChart, compileGraph, compileLayer, compileSankey, compileTable, compileTileMap, dedupeKeys, evaluatePredicate, getChartRenderer, heuristicMeasure, isConditionalValueDef, legendGap, normalizeSpec, registerChartRenderer, resolveAnimation, resolveConditionalValue, runBin, runCalculate, runFilter, runTimeUnit, runTransforms, serializeKeyValue, validateSpec };
package/dist/index.js CHANGED
@@ -10153,6 +10153,7 @@ var SWATCH_GAP = 6;
10153
10153
  var ENTRY_GAP = 16;
10154
10154
  var ENTRY_GAP_COMPACT = 10;
10155
10155
  var LEGEND_GAP = 8;
10156
+ var TOP_LEGEND_GAP_ABOVE = 6;
10156
10157
  function legendGap(width) {
10157
10158
  return width < COMPACT_WIDTH ? 0 : LEGEND_GAP;
10158
10159
  }
@@ -10449,7 +10450,7 @@ function computeLegendContent(spec, strategy, theme, availableWidth, availableHe
10449
10450
  rowHeight
10450
10451
  };
10451
10452
  }
10452
- function placeLegend(content, chartArea, containerWidth, _theme, xAxisHeight) {
10453
+ function placeLegend(content, chartArea, containerWidth, _theme, xAxisHeight, axisGapBelowLegend = 0) {
10453
10454
  const { position, entries, labelStyle, legendWidth, height, offset } = content;
10454
10455
  if (entries.length === 0) {
10455
10456
  return {
@@ -10477,7 +10478,7 @@ function placeLegend(content, chartArea, containerWidth, _theme, xAxisHeight) {
10477
10478
  boundsY = chartArea.y + chartArea.height + xAxisHeight + gap + offsetDy;
10478
10479
  } else {
10479
10480
  boundsX = chartArea.x + offsetDx;
10480
- boundsY = chartArea.y - gap - height + offsetDy;
10481
+ boundsY = chartArea.y - axisGapBelowLegend - gap - height + offsetDy;
10481
10482
  }
10482
10483
  const bounds = { x: boundsX, y: boundsY, width: legendWidth, height };
10483
10484
  const entryPositions = resolveEntryPositions(content.relativePositions, bounds);
@@ -10637,7 +10638,7 @@ var NUMERIC_SCALE_TYPES = /* @__PURE__ */ new Set([
10637
10638
  "threshold"
10638
10639
  ]);
10639
10640
  var TEMPORAL_SCALE_TYPES = /* @__PURE__ */ new Set(["time", "utc"]);
10640
- function formatTickLabel(value2, resolvedScale) {
10641
+ function formatTickLabel(value2, resolvedScale, compact = false) {
10641
10642
  const axisConfig = resolvedScale.channel.axis || void 0;
10642
10643
  const formatStr = axisConfig?.format;
10643
10644
  const suffix = axisConfig?.labelSuffix ?? "";
@@ -10645,7 +10646,7 @@ function formatTickLabel(value2, resolvedScale) {
10645
10646
  const temporalFmt = buildTemporalFormatter(formatStr);
10646
10647
  if (temporalFmt) return temporalFmt(value2) + suffix;
10647
10648
  const useUtc = resolvedScale.type === "utc";
10648
- return formatDate(value2, void 0, void 0, useUtc) + suffix;
10649
+ return formatDate(value2, void 0, void 0, useUtc, compact) + suffix;
10649
10650
  }
10650
10651
  if (NUMERIC_SCALE_TYPES.has(resolvedScale.type)) {
10651
10652
  const num = value2;
@@ -10672,7 +10673,7 @@ function continuousTicks(resolvedScale, density, targetCount) {
10672
10673
  const count = axCfg?.tickCount ?? targetCount ?? TICK_COUNTS[density];
10673
10674
  return buildContinuousTicks(resolvedScale, count);
10674
10675
  }
10675
- function buildContinuousTicks(resolvedScale, count) {
10676
+ function buildContinuousTicks(resolvedScale, count, compact = false) {
10676
10677
  const scale = resolvedScale.scale;
10677
10678
  if (!("ticks" in scale) || typeof scale.ticks !== "function") {
10678
10679
  return continuousTicks(resolvedScale, "full");
@@ -10695,7 +10696,7 @@ function buildContinuousTicks(resolvedScale, count) {
10695
10696
  return ticks2.map((value2) => ({
10696
10697
  value: value2,
10697
10698
  position: scale(value2),
10698
- label: formatTickLabel(value2, resolvedScale)
10699
+ label: formatTickLabel(value2, resolvedScale, compact)
10699
10700
  }));
10700
10701
  }
10701
10702
  function scaleSupportsTickCount(resolvedScale) {
@@ -10810,12 +10811,12 @@ function effectiveDensity(baseDensity, axisLength, minimalThreshold, reducedThre
10810
10811
  var CONTINUOUS_TICK_FLOOR = 4;
10811
10812
  var OVERSHOOT_TOLERANCE = 1.5;
10812
10813
  var MIN_QUANTITATIVE_TICKS = 2;
10813
- function ensureMinContinuousTicks(scale, ticks2, requestedCount) {
10814
+ function ensureMinContinuousTicks(scale, ticks2, requestedCount, compact = false) {
10814
10815
  if (ticks2.length >= MIN_QUANTITATIVE_TICKS) return ticks2;
10815
10816
  if (!scale || !scaleSupportsTickCount(scale)) return ticks2;
10816
10817
  let best = ticks2;
10817
10818
  for (let n = Math.max(requestedCount, MIN_QUANTITATIVE_TICKS) + 1; n <= 10; n++) {
10818
- const candidate = buildContinuousTicks(scale, n);
10819
+ const candidate = buildContinuousTicks(scale, n, compact);
10819
10820
  if (candidate.length >= MIN_QUANTITATIVE_TICKS) return candidate;
10820
10821
  if (candidate.length > best.length) best = candidate;
10821
10822
  }
@@ -10828,26 +10829,30 @@ function fitContinuousTicks(scale, initialTicks, initialCount, fontSize, fontWei
10828
10829
  const overshoots = flooredTicks.length > tolerance;
10829
10830
  const overlaps = ticksOverlap(flooredTicks, fontSize, fontWeight, measureText, orientation);
10830
10831
  if (!overshoots && !overlaps) return flooredTicks;
10832
+ const useCompact = (scale.type === "time" || scale.type === "utc") && orientation === "horizontal";
10833
+ const fitsCleanly = (candidate) => candidate.length <= tolerance && !ticksOverlap(candidate, fontSize, fontWeight, measureText, orientation);
10834
+ if (useCompact) {
10835
+ const compactAtInitial = buildContinuousTicks(scale, initialCount, true);
10836
+ if (fitsCleanly(compactAtInitial)) return compactAtInitial;
10837
+ }
10831
10838
  const minThreshold = orientation === "vertical" ? HEIGHT_MINIMAL_THRESHOLD : WIDTH_MINIMAL_THRESHOLD;
10832
10839
  const floor = axisLength >= minThreshold ? CONTINUOUS_TICK_FLOOR : 2;
10833
10840
  let bestWithinFloor;
10841
+ let bestWithinFloorCount;
10834
10842
  for (let n = initialCount - 1; n >= 2; n--) {
10835
10843
  const candidate = buildContinuousTicks(scale, n);
10836
- const candidateOvershoots = candidate.length > tolerance;
10837
- const candidateOverlaps = ticksOverlap(
10838
- candidate,
10839
- fontSize,
10840
- fontWeight,
10841
- measureText,
10842
- orientation
10843
- );
10844
- if (!candidateOvershoots && !candidateOverlaps) {
10845
- return candidate;
10844
+ if (fitsCleanly(candidate)) return candidate;
10845
+ if (useCompact) {
10846
+ const compact = buildContinuousTicks(scale, n, true);
10847
+ if (fitsCleanly(compact)) return compact;
10848
+ }
10849
+ if (candidate.length >= floor) {
10850
+ bestWithinFloor = candidate;
10851
+ bestWithinFloorCount = n;
10846
10852
  }
10847
- if (candidate.length >= floor) bestWithinFloor = candidate;
10848
10853
  }
10849
- const rawFallback = bestWithinFloor ?? buildContinuousTicks(scale, floor);
10850
- const fallback = ensureMinContinuousTicks(scale, rawFallback, floor);
10854
+ const rawFallback = useCompact ? buildContinuousTicks(scale, bestWithinFloorCount ?? floor, true) : bestWithinFloor ?? buildContinuousTicks(scale, floor);
10855
+ const fallback = ensureMinContinuousTicks(scale, rawFallback, floor, useCompact);
10851
10856
  return thinTicksUntilFit(fallback, fontSize, fontWeight, measureText, orientation);
10852
10857
  }
10853
10858
  function applyBandLabelStride(ticks2, stride) {
@@ -11811,7 +11816,7 @@ function resolveLayoutPlan(chartSpec, renderSpec, options, theme, strategy, wate
11811
11816
  const topAxisGap = axisMargin + inlineTickOverhang;
11812
11817
  const effectiveTopAxisGap = hasTopLegend ? inlineTickOverhang : topAxisGap;
11813
11818
  let topMargin = topPad + chrome.topHeight;
11814
- if (hasTopLegend) topMargin += legendContent.height + gap;
11819
+ if (hasTopLegend) topMargin += TOP_LEGEND_GAP_ABOVE + legendContent.height + gap;
11815
11820
  topMargin += effectiveTopAxisGap;
11816
11821
  const bMargin = bottomMargin(chrome.bottomHeight, padding, xAxisExtent);
11817
11822
  const chartHeight = Math.max(0, height - topMargin - bMargin);
@@ -12965,7 +12970,15 @@ function computeDimensions(spec, options, legendLayout, theme, strategy, waterma
12965
12970
  width: Math.max(0, width - margins2.left - margins2.right),
12966
12971
  height: Math.max(0, height - margins2.top - margins2.bottom)
12967
12972
  };
12968
- return { total: total2, chrome, chartArea: chartArea2, margins: margins2, theme, xAxisHeight: xAxisSpace };
12973
+ return {
12974
+ total: total2,
12975
+ chrome,
12976
+ chartArea: chartArea2,
12977
+ margins: margins2,
12978
+ theme,
12979
+ xAxisHeight: xAxisSpace,
12980
+ effectiveAxisGap: 0
12981
+ };
12969
12982
  }
12970
12983
  const total = { x: 0, y: 0, width, height };
12971
12984
  const isRadial = spec.markType === "arc";
@@ -13201,10 +13214,11 @@ function computeDimensions(spec, options, legendLayout, theme, strategy, waterma
13201
13214
  if (legendPos === "right" || legendPos === "bottom-right") {
13202
13215
  margins.right += legendBoundsWidth + 8;
13203
13216
  } else if (legendPos === "top") {
13204
- margins.top += legendHeight + gap;
13217
+ margins.top += TOP_LEGEND_GAP_ABOVE + legendHeight + gap;
13205
13218
  }
13206
13219
  }
13207
- margins.top += hasTopLegend ? inlineTickOverhang : topAxisGap;
13220
+ const effectiveAxisGap = hasTopLegend ? inlineTickOverhang : topAxisGap;
13221
+ margins.top += effectiveAxisGap;
13208
13222
  let chartArea = {
13209
13223
  x: margins.left,
13210
13224
  y: margins.top,
@@ -13232,7 +13246,7 @@ function computeDimensions(spec, options, legendLayout, theme, strategy, waterma
13232
13246
  const bottomDelta = margins.bottom - newBottom;
13233
13247
  if (topDelta > 0 || bottomDelta > 0) {
13234
13248
  const gap = legendGap(width);
13235
- margins.top = newTop + (hasTopLegend ? legendHeight + gap : 0) + fallbackEffectiveAxisGap;
13249
+ margins.top = newTop + (hasTopLegend ? TOP_LEGEND_GAP_ABOVE + legendHeight + gap : 0) + fallbackEffectiveAxisGap;
13236
13250
  margins.bottom = newBottom;
13237
13251
  chartArea = {
13238
13252
  x: margins.left,
@@ -13265,7 +13279,8 @@ function computeDimensions(spec, options, legendLayout, theme, strategy, waterma
13265
13279
  margins,
13266
13280
  theme,
13267
13281
  metrics: fallbackMetrics,
13268
- xAxisHeight
13282
+ xAxisHeight,
13283
+ effectiveAxisGap: fallbackEffectiveAxisGap
13269
13284
  };
13270
13285
  }
13271
13286
  }
@@ -13287,7 +13302,8 @@ function computeDimensions(spec, options, legendLayout, theme, strategy, waterma
13287
13302
  margins,
13288
13303
  theme,
13289
13304
  metrics: resolvedMetrics,
13290
- xAxisHeight
13305
+ xAxisHeight,
13306
+ effectiveAxisGap
13291
13307
  };
13292
13308
  }
13293
13309
  function resolveMetrics(spec, metricsTopY, metricsArea, remainingChartHeight, measureText, theme) {
@@ -16462,7 +16478,8 @@ function compileChart(spec, options) {
16462
16478
  chartArea,
16463
16479
  options.width,
16464
16480
  theme,
16465
- plan.xAxisExtent
16481
+ plan.xAxisExtent,
16482
+ dims.effectiveAxisGap
16466
16483
  );
16467
16484
  let scaleArea = chartArea;
16468
16485
  if (plan.inlineYLabelInset > 0 && plan.inlineYLabelInset < chartArea.width) {
@@ -16636,7 +16653,8 @@ function compileFaceted(chartSpec, renderSpec, facetChannel, options, theme, str
16636
16653
  chartArea,
16637
16654
  options.width,
16638
16655
  theme,
16639
- plan.xAxisExtent
16656
+ plan.xAxisExtent,
16657
+ dims.effectiveAxisGap
16640
16658
  );
16641
16659
  const leftReservation = plan.leftGutter;
16642
16660
  const bottomReservation = plan.xAxisExtent;
@@ -16835,6 +16853,7 @@ export {
16835
16853
  getChartRenderer,
16836
16854
  heuristicMeasure,
16837
16855
  isConditionalValueDef,
16856
+ legendGap,
16838
16857
  normalizeSpec,
16839
16858
  registerChartRenderer,
16840
16859
  resolveAnimation,