@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 +20 -1
- package/dist/index.js +49 -30
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/axes.test.ts +165 -0
- package/src/__tests__/dimensions.test.ts +49 -4
- package/src/__tests__/legend.test.ts +51 -0
- package/src/compile.ts +5 -0
- package/src/index.ts +8 -0
- package/src/layout/axes/ticks.ts +20 -5
- package/src/layout/axes.ts +40 -15
- package/src/layout/dimensions.ts +39 -12
- package/src/layout/plan.ts +3 -2
- package/src/legend/compute.ts +7 -1
- package/src/legend/wrap.ts +8 -0
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
|
-
|
|
10837
|
-
|
|
10838
|
-
|
|
10839
|
-
|
|
10840
|
-
|
|
10841
|
-
|
|
10842
|
-
|
|
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 {
|
|
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
|
-
|
|
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,
|