@opendata-ai/openchart-engine 7.1.3 → 7.2.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 +1 -1
- package/dist/index.js +55 -28
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/__snapshots__/compile-snapshot.test.ts.snap +51 -45
- package/src/__tests__/dimensions.test.ts +108 -0
- package/src/charts/bar/__tests__/labels.test.ts +72 -0
- package/src/charts/bar/index.ts +4 -1
- package/src/charts/bar/labels.ts +11 -6
- package/src/charts/column/index.ts +2 -0
- package/src/charts/column/labels.ts +9 -5
- package/src/compiler/normalize.ts +2 -0
- package/src/compiler/types.ts +1 -1
- package/src/layout/axes/ticks.ts +7 -6
- package/src/layout/dimensions.ts +15 -8
- package/src/layout/scales.ts +22 -4
package/src/layout/dimensions.ts
CHANGED
|
@@ -23,6 +23,7 @@ import type {
|
|
|
23
23
|
ResolvedTheme,
|
|
24
24
|
} from '@opendata-ai/openchart-core';
|
|
25
25
|
import {
|
|
26
|
+
AXIS_TITLE_GAP,
|
|
26
27
|
AXIS_TITLE_TRAILING_PAD,
|
|
27
28
|
BREAKPOINT_COMPACT_MAX,
|
|
28
29
|
computeChrome,
|
|
@@ -337,7 +338,8 @@ export function computeDimensions(
|
|
|
337
338
|
const labelHeight = Math.min(rotatedHeight, 120);
|
|
338
339
|
xAxisHeight = hasXAxisLabel ? labelHeight + 20 : labelHeight;
|
|
339
340
|
} else {
|
|
340
|
-
|
|
341
|
+
const base = theme.spacing.xAxisHeight;
|
|
342
|
+
xAxisHeight = hasXAxisLabel ? base + 22 : base;
|
|
341
343
|
}
|
|
342
344
|
|
|
343
345
|
// Resolve effective y-axis tickPosition early so margin math can account
|
|
@@ -633,9 +635,8 @@ export function computeDimensions(
|
|
|
633
635
|
);
|
|
634
636
|
}
|
|
635
637
|
// Mirror the renderer's dynamic offset formula:
|
|
636
|
-
// dynamicOffset = TICK_LABEL_OFFSET(6) + maxTickLabelWidth +
|
|
637
|
-
// titleOffset = max(dynamicOffset,
|
|
638
|
-
const AXIS_TITLE_GAP = 8;
|
|
638
|
+
// dynamicOffset = TICK_LABEL_OFFSET(6) + maxTickLabelWidth + AXIS_TITLE_GAP(14)
|
|
639
|
+
// titleOffset = max(dynamicOffset, getAxisTitleOffset(width))
|
|
639
640
|
const dynamicTitleOffset = TICK_LABEL_OFFSET + estTickLabelWidth + AXIS_TITLE_GAP;
|
|
640
641
|
const axisTitleOffset = Math.max(dynamicTitleOffset, getAxisTitleOffset(width));
|
|
641
642
|
const halfGlyph = Math.ceil(theme.fonts.sizes.body / 2);
|
|
@@ -658,6 +659,8 @@ export function computeDimensions(
|
|
|
658
659
|
// here. The legend lands below the x-axis tick row (which is reserved via
|
|
659
660
|
// `xAxisHeight` in the base bottom margin) and source/byline/footer chrome
|
|
660
661
|
// stacks underneath the legend band rather than colliding with it.
|
|
662
|
+
const hasTopLegend =
|
|
663
|
+
'entries' in legendLayout && legendLayout.entries.length > 0 && legendLayout.position === 'top';
|
|
661
664
|
if ('entries' in legendLayout && legendLayout.entries.length > 0) {
|
|
662
665
|
const gap = legendGap(width);
|
|
663
666
|
if (legendLayout.position === 'right' || legendLayout.position === 'bottom-right') {
|
|
@@ -669,9 +672,12 @@ export function computeDimensions(
|
|
|
669
672
|
// above.
|
|
670
673
|
}
|
|
671
674
|
|
|
672
|
-
//
|
|
673
|
-
//
|
|
674
|
-
|
|
675
|
+
// topAxisGap sits between the legend (or chrome, if no legend) and the
|
|
676
|
+
// chart area. When a top legend is present, the legendGap already provides
|
|
677
|
+
// breathing room, so only the inlineTickOverhang is needed (the axisMargin
|
|
678
|
+
// component would double up with legendGap). Without a top legend, the
|
|
679
|
+
// full topAxisGap (axisMargin + inlineTickOverhang) applies.
|
|
680
|
+
margins.top += hasTopLegend ? inlineTickOverhang : topAxisGap;
|
|
675
681
|
|
|
676
682
|
// Chart area is what's left after margins
|
|
677
683
|
let chartArea: Rect = {
|
|
@@ -707,6 +713,7 @@ export function computeDimensions(
|
|
|
707
713
|
// until resolveMetrics decides otherwise).
|
|
708
714
|
const fallbackTopAxisGap =
|
|
709
715
|
isRadial && fallbackChrome.topHeight === 0 ? 0 : axisMargin + inlineTickOverhang;
|
|
716
|
+
const fallbackEffectiveAxisGap = hasTopLegend ? inlineTickOverhang : fallbackTopAxisGap;
|
|
710
717
|
const newTop = topPad + fallbackChrome.topHeight + tentativeMetricsHeight;
|
|
711
718
|
const topDelta = margins.top - newTop;
|
|
712
719
|
const newBottom = bottomMargin(fallbackChrome.bottomHeight, padding, xAxisHeight);
|
|
@@ -721,7 +728,7 @@ export function computeDimensions(
|
|
|
721
728
|
legendLayout.position === 'top'
|
|
722
729
|
? legendLayout.bounds.height + gap
|
|
723
730
|
: 0) +
|
|
724
|
-
|
|
731
|
+
fallbackEffectiveAxisGap;
|
|
725
732
|
margins.bottom = newBottom;
|
|
726
733
|
|
|
727
734
|
chartArea = {
|
package/src/layout/scales.ts
CHANGED
|
@@ -679,8 +679,17 @@ export function computeScales(
|
|
|
679
679
|
xStackEnabled
|
|
680
680
|
) {
|
|
681
681
|
if (encoding.x.stack === 'normalize') {
|
|
682
|
-
// Normalize: domain is [0, 1]
|
|
683
|
-
|
|
682
|
+
// Normalize: domain is [0, 1], default to percentage axis
|
|
683
|
+
const existingAxis = encoding.x.axis;
|
|
684
|
+
const axis =
|
|
685
|
+
existingAxis === false || existingAxis?.format
|
|
686
|
+
? existingAxis
|
|
687
|
+
: { ...(typeof existingAxis === 'object' ? existingAxis : {}), format: '.0%' };
|
|
688
|
+
xChannel = {
|
|
689
|
+
...encoding.x,
|
|
690
|
+
scale: { ...encoding.x.scale, domain: [0, 1], nice: false },
|
|
691
|
+
axis,
|
|
692
|
+
};
|
|
684
693
|
} else if (encoding.x.stack === 'center') {
|
|
685
694
|
// Center: compute max half-sum for symmetric domain
|
|
686
695
|
const yField = encoding.y?.field;
|
|
@@ -785,8 +794,17 @@ export function computeScales(
|
|
|
785
794
|
}
|
|
786
795
|
if ((isBarStacked || isAreaStacked) && encoding.color && encoding.y.type === 'quantitative') {
|
|
787
796
|
if (encoding.y.stack === 'normalize') {
|
|
788
|
-
// Normalize: domain is [0, 1] (VL convention)
|
|
789
|
-
|
|
797
|
+
// Normalize: domain is [0, 1] (VL convention), default to percentage axis
|
|
798
|
+
const existingAxis = encoding.y.axis;
|
|
799
|
+
const axis =
|
|
800
|
+
existingAxis === false || existingAxis?.format
|
|
801
|
+
? existingAxis
|
|
802
|
+
: { ...(typeof existingAxis === 'object' ? existingAxis : {}), format: '.0%' };
|
|
803
|
+
yChannel = {
|
|
804
|
+
...encoding.y,
|
|
805
|
+
scale: { ...encoding.y.scale, domain: [0, 1], nice: false },
|
|
806
|
+
axis,
|
|
807
|
+
};
|
|
790
808
|
} else if (encoding.y.stack === 'center') {
|
|
791
809
|
// Center: compute max half-sum for symmetric domain
|
|
792
810
|
const xField = encoding.x?.field;
|