diff --git a/src/components/Charts/BarChart/BarChartContent.tsx b/src/components/Charts/BarChart/BarChartContent.tsx
index 030173ecb08d..6eab22c00f57 100644
--- a/src/components/Charts/BarChart/BarChartContent.tsx
+++ b/src/components/Charts/BarChart/BarChartContent.tsx
@@ -10,7 +10,7 @@ import ChartTooltipLayer from '@components/Charts/components/ChartTooltipLayer';
import ChartXAxisLabels from '@components/Charts/components/ChartXAxisLabels';
import ChartYAxisLabels from '@components/Charts/components/ChartYAxisLabels';
import {AXIS_LABEL_GAP, CHART_CONTENT_MIN_HEIGHT, CHART_PADDING, GLYPH_PADDING, X_AXIS_LINE_WIDTH, Y_AXIS_LINE_WIDTH, Y_AXIS_TICK_COUNT} from '@components/Charts/constants';
-import type {ComputeGeometryFn, HitTestArgs} from '@components/Charts/hooks';
+import type {HitTestArgs} from '@components/Charts/hooks';
import {
useChartFontManager,
useChartInteractions,
@@ -22,7 +22,7 @@ import {
useYAxisLabelWidth,
} from '@components/Charts/hooks';
import type {CartesianChartProps, ChartDataPoint} from '@components/Charts/types';
-import {calculateMinDomainPadding, DEFAULT_CHART_COLOR, getAdditionalOffset, getChartColor, rotatedLabelYOffset} from '@components/Charts/utils';
+import {calculateMinDomainPadding, DEFAULT_CHART_COLOR, getChartColor} from '@components/Charts/utils';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
import type {SkeletonSpanReasonAttributes} from '@libs/telemetry/useSkeletonSpan';
@@ -36,29 +36,6 @@ const BAR_INNER_PADDING = 0.3;
*/
const BASE_DOMAIN_PADDING = {top: 32, bottom: 1, left: 0, right: 0};
-/**
- * Bar chart geometry for label hit-testing.
- * Labels are center-anchored: the 45° parallelogram's upper-right corner is offset
- * by (halfLabelWidth * sinA) right and up, so the box straddles the tick symmetrically.
- */
-const computeBarLabelGeometry: ComputeGeometryFn = ({ascent, descent, sinA, angleRad, labelWidths, padding}) => {
- const maxLabelWidth = labelWidths.length > 0 ? Math.max(...labelWidths) : 0;
- const centeredUpwardOffset = angleRad > 0 ? (maxLabelWidth / 2) * sinA : 0;
- const halfLabelSins = labelWidths.map((w) => (w / 2) * sinA - variables.iconSizeExtraSmall / 3);
- const halfWidths = labelWidths.map((w) => w / 2);
- const additionalOffset = getAdditionalOffset(angleRad);
- return {
- labelYOffset: AXIS_LABEL_GAP + rotatedLabelYOffset(ascent, descent, angleRad) + centeredUpwardOffset - additionalOffset,
- iconSin: variables.iconSizeExtraSmall * sinA,
- labelSins: labelWidths.map((w) => w * sinA),
- halfWidths,
- cornerAnchorDX: halfLabelSins,
- cornerAnchorDY: halfLabelSins.map((v) => -v),
- yMin90Offsets: halfWidths.map((hw) => -hw + padding),
- yMax90Offsets: halfWidths.map((hw) => hw + padding),
- };
-};
-
type BarChartProps = CartesianChartProps & {
/** Callback when a bar is pressed */
onBarPress?: (dataPoint: ChartDataPoint, index: number) => void;
@@ -141,14 +118,12 @@ function BarChartContent({data, isLoading, yAxisUnit, yAxisUnitPosition = 'left'
labelRotation,
labelSkipInterval,
chartBottom,
- computeGeometry: computeBarLabelGeometry,
});
const handleChartBoundsChange = (bounds: ChartBounds) => {
const domainWidth = bounds.right - bounds.left;
const calculatedBarWidth = ((1 - BAR_INNER_PADDING) * domainWidth) / data.length;
barWidth.set(calculatedBarWidth);
- chartBottom.set(bounds.bottom);
yZero.set(0);
setBarAreaWidth(domainWidth);
setBoundsLeft(bounds.left);
@@ -216,6 +191,10 @@ function BarChartContent({data, isLoading, yAxisUnit, yAxisUnitPosition = 'left'
if (!fontMgr || xAxisLabelHeight === undefined) {
return null;
}
+
+ const chartBoundsBottom = args.yScale(Math.min(...args.yTicks));
+ chartBottom.set(chartBoundsBottom);
+
return (
<>
{
- const iconThirdSin = (variables.iconSizeExtraSmall / 3) * sinA;
- const additionalOffset = getAdditionalOffset(angleRad);
- return {
- labelYOffset: AXIS_LABEL_GAP + rotatedLabelYOffset(ascent, descent, angleRad) - additionalOffset,
- iconSin: variables.iconSizeExtraSmall * sinA,
- labelSins: labelWidths.map((w) => w * sinA),
- halfWidths: labelWidths.map((w) => w / 2),
- cornerAnchorDX: labelWidths.map(() => -iconThirdSin),
- cornerAnchorDY: labelWidths.map(() => iconThirdSin),
- yMin90Offsets: labelWidths.map(() => padding),
- yMax90Offsets: labelWidths.map((w) => w + padding),
- };
-};
-
type LineChartProps = CartesianChartProps & {
/** Callback when a data point is pressed */
onPointPress?: (dataPoint: ChartDataPoint, index: number) => void;
@@ -139,7 +119,6 @@ function LineChartContent({data, isLoading, yAxisUnit, yAxisUnitPosition = 'left
labelAreaWidth: plotAreaWidth,
firstTickLeftSpace: boundsLeft + domainPadding.left * paddingScale,
lastTickRightSpace: chartWidth > 0 ? chartWidth - boundsRight + domainPadding.right * paddingScale : 0,
- allowTightDiagonalPacking: true,
measurements,
});
@@ -158,14 +137,12 @@ function LineChartContent({data, isLoading, yAxisUnit, yAxisUnitPosition = 'left
labelRotation,
labelSkipInterval,
chartBottom,
- computeGeometry: computeLineLabelGeometry,
});
const handleChartBoundsChange = (bounds: ChartBounds) => {
setPlotAreaWidth(bounds.right - bounds.left);
setBoundsLeft(bounds.left);
setBoundsRight(bounds.right);
- chartBottom.set(bounds.bottom);
};
const checkIsOverDot = (args: HitTestArgs) => {
@@ -197,6 +174,8 @@ function LineChartContent({data, isLoading, yAxisUnit, yAxisUnitPosition = 'left
}));
const renderOutside = (args: CartesianChartRenderArg<{x: number; y: number}, 'y'>) => {
+ const chartBoundsBottom = args.yScale(Math.min(...args.yTicks));
+ chartBottom.set(chartBoundsBottom);
return (
<>
{
@@ -83,14 +78,11 @@ function ChartXAxisLabels({
const paragraphs = useChartParagraphs(truncatedLabels, fontMgr, fontSize, labelColor, MAX_X_AXIS_LABEL_WIDTH);
- const renderedWidths = truncatedLabels.map((_, i) => paragraphs?.at(i)?.width ?? 0);
-
// Derive ascent/descent from the first available paragraph's line metrics.
const {ascent, descent} = getFontLineMetrics(fontMgr, fontSize);
const correction = rotatedLabelCenterCorrection(ascent, descent, angleRad);
- const centeredUpwardOffset = centerRotatedLabels && angleRad > 0 ? (Math.max(...renderedWidths) / 2) * Math.sin(angleRad) : 0;
- const labelY = chartBoundsBottom + AXIS_LABEL_GAP + rotatedLabelYOffset(ascent, descent, angleRad) + centeredUpwardOffset;
+ const labelY = chartBoundsBottom + AXIS_LABEL_GAP + rotatedLabelYOffset(ascent, descent, angleRad);
return truncatedLabels.map((label, i) => {
if (i % labelSkipInterval !== 0 || label.length === 0) {
@@ -111,13 +103,13 @@ function ChartXAxisLabels({
key={`x-label-${label}-${tickX}`}
paragraph={paraData.para}
x={tickX - renderWidth / 2}
- y={labelY - variables.iconSizeExtraSmall}
+ y={labelY - ascent}
width={renderWidth + GLYPH_PADDING}
/>
);
}
- const textX = centerRotatedLabels ? tickX - renderWidth / 2 : tickX - renderWidth;
+ const textX = tickX - renderWidth;
const origin = vec(tickX, labelY);
return (
diff --git a/src/components/Charts/hooks/index.ts b/src/components/Charts/hooks/index.ts
index 16931744a3a7..13781a736967 100644
--- a/src/components/Charts/hooks/index.ts
+++ b/src/components/Charts/hooks/index.ts
@@ -9,4 +9,3 @@ export {default as useChartLabelFormats} from './useChartLabelFormats';
export {default as useDynamicYDomain} from './useDynamicYDomain';
export {useTooltipData} from './useTooltipData';
export {default as useLabelHitTesting} from './useLabelHitTesting';
-export type {ComputeGeometryFn, ComputeGeometryInput} from './useLabelHitTesting';
diff --git a/src/components/Charts/hooks/useChartLabelLayout.ts b/src/components/Charts/hooks/useChartLabelLayout.ts
index 727a1df3a086..4bdd06da9aa7 100644
--- a/src/components/Charts/hooks/useChartLabelLayout.ts
+++ b/src/components/Charts/hooks/useChartLabelLayout.ts
@@ -26,9 +26,6 @@ type LabelLayoutConfig = {
/** Pixels from last tick to right edge of canvas. Defaults to Infinity (no constraint). */
lastTickRightSpace?: number;
- /** When true, allows tighter label packing at 45° by accounting for vertical offset between right-aligned labels. */
- allowTightDiagonalPacking?: boolean;
-
/** Measurements of the label text. */
measurements: ReturnType;
};
@@ -45,16 +42,7 @@ const EMPTY_LAYOUT = {
ellipsisWidth: 0,
};
-function useChartLabelLayout({
- data,
- fontMgr,
- tickSpacing,
- labelAreaWidth,
- firstTickLeftSpace = Infinity,
- lastTickRightSpace = Infinity,
- allowTightDiagonalPacking = false,
- measurements,
-}: LabelLayoutConfig) {
+function useChartLabelLayout({data, fontMgr, tickSpacing, labelAreaWidth, firstTickLeftSpace = Infinity, lastTickRightSpace = Infinity, measurements}: LabelLayoutConfig) {
// Phase 1: font/data measurements — stable across geometry-only changes (resize).
// Phase 2: layout decisions + label truncation.
@@ -83,18 +71,16 @@ function useChartLabelLayout({
rotation: LABEL_ROTATIONS.HORIZONTAL,
firstTickLeftSpace: effectiveFirstTickLeftSpace,
lastTickRightSpace: effectiveLastTickRightSpace,
- rightAligned: false,
});
if (hFitsInTicks && hEdgeFits) {
rotation = LABEL_ROTATIONS.HORIZONTAL;
} else {
- const diagonalOverlap = allowTightDiagonalPacking ? lineHeight * SIN_45 : 0;
- const minDiagWidth = minTruncatedWidth * SIN_45 - diagonalOverlap;
+ const minDiagWidth = minTruncatedWidth * SIN_45 - lineHeight * SIN_45;
const dFitsInTicks = minDiagWidth + LABEL_PADDING <= tickSpacing;
- const firstEdgeMax = edgeMaxLabelWidth(effectiveFirstTickLeftSpace, lineHeight, LABEL_ROTATIONS.DIAGONAL, allowTightDiagonalPacking, 'first');
- const lastEdgeMax = edgeMaxLabelWidth(effectiveLastTickRightSpace, lineHeight, LABEL_ROTATIONS.DIAGONAL, allowTightDiagonalPacking, 'last');
+ const firstEdgeMax = edgeMaxLabelWidth(effectiveFirstTickLeftSpace, lineHeight, LABEL_ROTATIONS.DIAGONAL, 'first');
+ const lastEdgeMax = edgeMaxLabelWidth(effectiveLastTickRightSpace, lineHeight, LABEL_ROTATIONS.DIAGONAL, 'last');
const dEdgeFits = firstEdgeMax >= firstMinTrunc && lastEdgeMax >= lastMinTrunc;
if (dFitsInTicks && dEdgeFits) {
@@ -103,17 +89,16 @@ function useChartLabelLayout({
}
// Compute per-label max-width constraints (used by ChartXAxisLabels for truncation).
- const truncDiagonalOverlap = allowTightDiagonalPacking ? lineHeight : 0;
- const tickMaxWidth = rotation === LABEL_ROTATIONS.DIAGONAL ? (tickSpacing - LABEL_PADDING) / SIN_45 + truncDiagonalOverlap : Infinity;
+ const tickMaxWidth = rotation === LABEL_ROTATIONS.DIAGONAL ? (tickSpacing - LABEL_PADDING) / SIN_45 + lineHeight : Infinity;
const labelMaxWidths = data.map((_, index) => {
let maxWidth = tickMaxWidth;
if (index === 0) {
- const edgeMax = edgeMaxLabelWidth(effectiveFirstTickLeftSpace, lineHeight, rotation, allowTightDiagonalPacking, 'first');
+ const edgeMax = edgeMaxLabelWidth(effectiveFirstTickLeftSpace, lineHeight, rotation, 'first');
maxWidth = Math.min(maxWidth, edgeMax);
}
if (index === data.length - 1) {
- const edgeMax = edgeMaxLabelWidth(effectiveLastTickRightSpace, lineHeight, rotation, allowTightDiagonalPacking, 'last');
+ const edgeMax = edgeMaxLabelWidth(effectiveLastTickRightSpace, lineHeight, rotation, 'last');
maxWidth = Math.min(maxWidth, edgeMax);
}
return maxWidth;
@@ -122,15 +107,15 @@ function useChartLabelLayout({
// Approximate truncated widths for hit-testing: exact for non-truncated labels,
// at most ellipsisWidth px over for truncated ones — acceptable for bounding boxes.
const truncatedLabelWidths = labelMaxWidths.map((maxW, i) => Math.min(labelWidths.at(i) ?? 0, maxW));
- const finalMaxWidth = Math.max(...truncatedLabelWidths);
let skipInterval = 1;
if (rotation === LABEL_ROTATIONS.VERTICAL) {
- const verticalWidth = effectiveWidth(finalMaxWidth, lineHeight, rotation);
- const visibleCount = maxVisibleCount(labelAreaWidth, verticalWidth);
+ const visibleCount = maxVisibleCount(labelAreaWidth, lineHeight);
skipInterval = visibleCount >= data.length ? 1 : Math.ceil(data.length / Math.max(1, visibleCount));
}
+ const finalMaxWidth = Math.max(...truncatedLabelWidths.filter((_, i) => i % skipInterval === 0));
+
const lastIndex = data.length - 1;
return {
diff --git a/src/components/Charts/hooks/useLabelHitTesting.ts b/src/components/Charts/hooks/useLabelHitTesting.ts
index ada0f9e47115..ccbd5f47ce0a 100644
--- a/src/components/Charts/hooks/useLabelHitTesting.ts
+++ b/src/components/Charts/hooks/useLabelHitTesting.ts
@@ -2,9 +2,9 @@ import type {SkTypefaceFontProvider} from '@shopify/react-native-skia';
import type {SharedValue} from 'react-native-reanimated';
import {useSharedValue} from 'react-native-reanimated';
import type {Scale} from 'victory-native';
-import {DIAGONAL_ANGLE_RADIAN_THRESHOLD} from '@components/Charts/constants';
+import {AXIS_LABEL_GAP, DIAGONAL_ANGLE_RADIAN_THRESHOLD} from '@components/Charts/constants';
import type {LabelRotation} from '@components/Charts/types';
-import {getFontLineMetrics, isCursorOverChartLabel} from '@components/Charts/utils';
+import {getAdditionalOffset, getFontLineMetrics, isCursorOverChartLabel, rotatedLabelYOffset} from '@components/Charts/utils';
import variables from '@styles/variables';
import type {HitTestArgs} from './useChartInteractions';
@@ -21,41 +21,19 @@ type LabelHitGeometry = {
/** Per-label: labelWidth / 2 — half-extent for 0° and 90° hit bounds */
halfWidths: number[];
- /** Per-label: rightUpperCorner.x = targetX + cornerAnchorDX[i] */
- cornerAnchorDX: number[];
+ /** rightUpperCorner.x = targetX + cornerAnchorDX */
+ cornerAnchorDX: number;
- /** Per-label: rightUpperCorner.y = labelY + cornerAnchorDY[i] */
- cornerAnchorDY: number[];
+ /** rightUpperCorner.y = labelY + cornerAnchorDY */
+ cornerAnchorDY: number;
- /** Per-label: yMin90 = labelY + yMin90Offsets[i] */
- yMin90Offsets: number[];
+ /** yMin90 = labelY + yMin90Offset */
+ yMin90Offset: number;
/** Per-label: yMax90 = labelY + yMax90Offsets[i] */
yMax90Offsets: number[];
};
-type ComputeGeometryInput = {
- /** The ascent of the font */
- ascent: number;
-
- /** The descent of the font */
- descent: number;
-
- /** The sine of the angle */
- sinA: number;
-
- /** The angle in radians */
- angleRad: number;
-
- /** The widths of the labels */
- labelWidths: number[];
-
- /** The padding of the labels */
- padding: number;
-};
-
-type ComputeGeometryFn = (input: ComputeGeometryInput) => LabelHitGeometry;
-
type UseLabelHitTestingParams = {
fontMgr: SkTypefaceFontProvider | null | undefined;
fontSize: number;
@@ -64,13 +42,6 @@ type UseLabelHitTestingParams = {
labelRotation: LabelRotation;
labelSkipInterval: number;
chartBottom: SharedValue;
-
- /**
- * Chart-specific geometry factory.
- * Receives font metrics, trig values, and per-label widths; returns the
- * normalized geometry shape. Typically a module-level constant.
- */
- computeGeometry: ComputeGeometryFn;
};
/**
@@ -78,31 +49,38 @@ type UseLabelHitTestingParams = {
*
* Encapsulates angle conversion, pre-computed hit geometry, and the
* isCursorOverLabel / findLabelCursorX worklets — all of which are identical
- * between bar and line chart except for how the hit geometry is computed.
+ * between bar and line charts.
*
* Label widths are accepted as a pre-computed array (from useChartLabelLayout)
* so no Skia measurement happens here.
*
- * Chart-specific geometry (45° corner anchor offsets, 90° vertical bounds) is supplied
- * via the `computeGeometry` callback, typically a module-level constant.
+ * Labels are right-aligned at the tick: the 45° parallelogram's upper-right corner is
+ * offset by (iconSize/3 * sinA) left and down, placing the box just below the axis line.
*/
-function useLabelHitTesting({fontMgr, fontSize, truncatedLabelWidths, labelRotation, labelSkipInterval, chartBottom, computeGeometry}: UseLabelHitTestingParams) {
+function useLabelHitTesting({fontMgr, fontSize, truncatedLabelWidths, labelRotation, labelSkipInterval, chartBottom}: UseLabelHitTestingParams) {
const tickXPositions = useSharedValue([]);
const angleRad = (Math.abs(labelRotation) * Math.PI) / 180;
const fontMetrics = fontMgr ? getFontLineMetrics(fontMgr, fontSize) : null;
- /**
- * Geometry for label hit-testing. The `computeGeometry` callback supplies the
- * chart-specific differences (bar vs. line anchor offsets).
- */
let labelHitGeometry: LabelHitGeometry | null = null;
if (fontMetrics) {
const {ascent, descent} = fontMetrics;
const sinA = Math.sin(angleRad);
const padding = variables.iconSizeExtraSmall / 2;
- labelHitGeometry = computeGeometry({ascent, descent, sinA, angleRad, labelWidths: truncatedLabelWidths, padding});
+ const iconThirdSin = (variables.iconSizeExtraSmall / 3) * sinA;
+ const additionalOffset = getAdditionalOffset(angleRad);
+ labelHitGeometry = {
+ labelYOffset: AXIS_LABEL_GAP + rotatedLabelYOffset(ascent, descent, angleRad) - additionalOffset,
+ iconSin: variables.iconSizeExtraSmall * sinA,
+ labelSins: truncatedLabelWidths.map((w) => w * sinA),
+ halfWidths: truncatedLabelWidths.map((w) => w / 2),
+ cornerAnchorDX: -iconThirdSin,
+ cornerAnchorDY: iconThirdSin,
+ yMin90Offset: padding,
+ yMax90Offsets: truncatedLabelWidths.map((w) => w + padding),
+ };
}
/**
@@ -116,7 +94,7 @@ function useLabelHitTesting({fontMgr, fontSize, truncatedLabelWidths, labelRotat
return false;
}
- const {labelYOffset, iconSin, labelSins, halfWidths, cornerAnchorDX, cornerAnchorDY, yMin90Offsets, yMax90Offsets} = labelHitGeometry;
+ const {labelYOffset, iconSin, labelSins, halfWidths, cornerAnchorDX, cornerAnchorDY, yMin90Offset, yMax90Offsets} = labelHitGeometry;
const padding = variables.iconSizeExtraSmall / 2;
const halfWidth = halfWidths.at(activeIndex) ?? 0;
const labelY = args.chartBottom + labelYOffset;
@@ -124,9 +102,7 @@ function useLabelHitTesting({fontMgr, fontSize, truncatedLabelWidths, labelRotat
let corners45: Array<{x: number; y: number}> | undefined;
if (angleRad > 0 && angleRad < DIAGONAL_ANGLE_RADIAN_THRESHOLD) {
const labelSin = labelSins.at(activeIndex) ?? 0;
- const anchorDX = cornerAnchorDX.at(activeIndex) ?? 0;
- const anchorDY = cornerAnchorDY.at(activeIndex) ?? 0;
- const rightUpperCorner = {x: args.targetX + anchorDX, y: labelY + anchorDY};
+ const rightUpperCorner = {x: args.targetX + cornerAnchorDX, y: labelY + cornerAnchorDY};
const rightLowerCorner = {x: rightUpperCorner.x + iconSin, y: rightUpperCorner.y + iconSin};
const leftUpperCorner = {x: rightUpperCorner.x - labelSin, y: rightUpperCorner.y + labelSin};
const leftLowerCorner = {x: rightLowerCorner.x - labelSin, y: rightLowerCorner.y + labelSin};
@@ -142,7 +118,7 @@ function useLabelHitTesting({fontMgr, fontSize, truncatedLabelWidths, labelRotat
halfWidth,
padding,
corners45,
- yMin90: labelY + (yMin90Offsets.at(activeIndex) ?? 0),
+ yMin90: labelY + yMin90Offset,
yMax90: labelY + (yMax90Offsets.at(activeIndex) ?? 0),
});
};
@@ -183,4 +159,3 @@ function useLabelHitTesting({fontMgr, fontSize, truncatedLabelWidths, labelRotat
}
export default useLabelHitTesting;
-export type {ComputeGeometryFn, ComputeGeometryInput, LabelHitGeometry};
diff --git a/src/components/Charts/utils.ts b/src/components/Charts/utils.ts
index 33e1919e522c..241d5be6bbc9 100644
--- a/src/components/Charts/utils.ts
+++ b/src/components/Charts/utils.ts
@@ -332,20 +332,16 @@ function maxVisibleCount(areaWidth: number, itemWidth: number): number {
* How far a label extends beyond its tick position after rotation.
* Accounts for the rotatedLabelCenterCorrection translateX applied during rendering.
*/
-function labelOverhang(labelWidth: number, lineHeight: number, rotation: LabelRotation, rightAligned: boolean): {left: number; right: number} {
+function labelOverhang(labelWidth: number, lineHeight: number, rotation: LabelRotation): {left: number; right: number} {
if (rotation === LABEL_ROTATIONS.HORIZONTAL) {
return {left: labelWidth / 2, right: labelWidth / 2};
}
if (rotation === LABEL_ROTATIONS.DIAGONAL) {
const halfLH = lineHeight / 2;
- if (rightAligned) {
- return {
- left: (labelWidth + halfLH) * SIN_45,
- right: halfLH * SIN_45,
- };
- }
- const overhang = (labelWidth / 2 + halfLH) * SIN_45;
- return {left: overhang, right: overhang};
+ return {
+ left: (labelWidth + halfLH) * SIN_45,
+ right: halfLH * SIN_45,
+ };
}
return {left: lineHeight / 2, right: lineHeight / 2};
}
@@ -358,7 +354,6 @@ function edgeLabelsFit({
rotation,
firstTickLeftSpace,
lastTickRightSpace,
- rightAligned,
}: {
firstLabelWidth: number;
lastLabelWidth: number;
@@ -366,10 +361,9 @@ function edgeLabelsFit({
rotation: LabelRotation;
firstTickLeftSpace: number;
lastTickRightSpace: number;
- rightAligned: boolean;
}): boolean {
- const first = labelOverhang(firstLabelWidth, lineHeight, rotation, rightAligned);
- const last = labelOverhang(lastLabelWidth, lineHeight, rotation, rightAligned);
+ const first = labelOverhang(firstLabelWidth, lineHeight, rotation);
+ const last = labelOverhang(lastLabelWidth, lineHeight, rotation);
return first.left <= firstTickLeftSpace && last.right <= lastTickRightSpace;
}
@@ -377,16 +371,12 @@ function edgeLabelsFit({
* Maximum label width that fits within the available edge space at a given rotation.
* Returns Infinity when the overhang at that edge doesn't depend on label width.
*/
-function edgeMaxLabelWidth(edgeSpace: number, lineHeight: number, rotation: LabelRotation, rightAligned: boolean, edge: 'first' | 'last'): number {
- const halfLH = lineHeight / 2;
+function edgeMaxLabelWidth(edgeSpace: number, lineHeight: number, rotation: LabelRotation, edge: 'first' | 'last'): number {
if (rotation === LABEL_ROTATIONS.HORIZONTAL) {
return 2 * edgeSpace;
}
if (rotation === LABEL_ROTATIONS.DIAGONAL) {
- if (rightAligned) {
- return edge === 'first' ? Math.max(0, edgeSpace / SIN_45 - halfLH) : Infinity;
- }
- return Math.max(0, 2 * (edgeSpace / SIN_45 - halfLH));
+ return edge === 'first' ? Math.max(0, edgeSpace / SIN_45 - lineHeight / 2) : Infinity;
}
return Infinity;
}
diff --git a/src/components/Search/SearchChartView.tsx b/src/components/Search/SearchChartView.tsx
index a5763fc11184..479e921e8d6b 100644
--- a/src/components/Search/SearchChartView.tsx
+++ b/src/components/Search/SearchChartView.tsx
@@ -5,6 +5,7 @@ import Log from '@libs/Log';
import Navigation from '@libs/Navigation/Navigation';
import {formatToParts} from '@libs/NumberFormatUtils';
import {buildSearchQueryJSON, buildSearchQueryString} from '@libs/SearchQueryUtils';
+import StringUtils from '@libs/StringUtils';
import CONST from '@src/CONST';
import ROUTES from '@src/ROUTES';
import CHART_GROUP_BY_CONFIG from './chartGroupByConfig';
@@ -78,7 +79,7 @@ function SearchChartView({queryJSON, view, groupBy, data, isLoading}: SearchChar
return (
StringUtils.normalize(getLabel(item))}
getFilterQuery={getFilterQuery}
onItemPress={handleItemPress}
isLoading={isLoading}
diff --git a/src/styles/index.ts b/src/styles/index.ts
index 9a33de715ddb..7dc7fe8e80b8 100644
--- a/src/styles/index.ts
+++ b/src/styles/index.ts
@@ -6009,10 +6009,10 @@ const staticStyles = (theme: ThemeColors) =>
borderRadius: variables.componentBorderRadiusLarge,
},
chartContent: {
- minHeight: CHART_CONTENT_MIN_HEIGHT,
+ height: CHART_CONTENT_MIN_HEIGHT,
},
chartActivityIndicator: {
- minHeight: CHART_CONTENT_MIN_HEIGHT,
+ height: CHART_CONTENT_MIN_HEIGHT,
justifyContent: 'center',
alignItems: 'center',
},
diff --git a/tests/unit/components/Charts/useChartLabelLayout.test.ts b/tests/unit/components/Charts/useChartLabelLayout.test.ts
index 8dfe8443015e..801bac133e36 100644
--- a/tests/unit/components/Charts/useChartLabelLayout.test.ts
+++ b/tests/unit/components/Charts/useChartLabelLayout.test.ts
@@ -136,19 +136,22 @@ describe('useChartLabelLayout', () => {
describe('edge-constrained rotation', () => {
it('same data picks 0° without edge constraint but 45° with edge constraint', () => {
- // "A".repeat(16) = 112px. At 0°: overhang = 56px.
- const config = {data: makeData('A'.repeat(16), 'BB', 'CC'), fontMgr: mockFontMgr, fontSize: FONT_SIZE, tickSpacing: 120, labelAreaWidth: 360};
+ // "A".repeat(22) = 154px. firstMinTrunc = (10+3)*7 = 91px.
+ // At 0°: centered overhang = 77px. firstTickLeftSpace=72 < 77 → 0° edge fails.
+ // At 45° right-aligned: edgeMax = 72/SIN_45−8 ≈ 93.8 ≥ 91 → 45° edge fits.
+ const config = {data: makeData('A'.repeat(22), 'BB', 'CC'), fontMgr: mockFontMgr, fontSize: FONT_SIZE, tickSpacing: 160, labelAreaWidth: 480};
const {result: noEdge} = renderLayout(config);
expect(noEdge.current.labelRotation).toBe(0);
- // firstTickLeftSpace=40 < 56 → 0° edge fails → escalates to 45°
- const {result: withEdge} = renderLayout({...config, firstTickLeftSpace: 40, lastTickRightSpace: 200});
+ // firstTickLeftSpace=72 < 77 → 0° edge fails → escalates to 45°
+ const {result: withEdge} = renderLayout({...config, firstTickLeftSpace: 72, lastTickRightSpace: 200});
expect(withEdge.current.labelRotation).toBe(45);
});
it('escalates to 90° when edge space is too small for both 0° and 45°', () => {
- // firstTickLeftSpace=5: at 45° centered edgeMax = max(0, 2*(5/SIN_45-8)) ≈ 0 → fails
+ // "AAAAAA" = 42px (6 chars <= MIN_TRUNCATED_CHARS=10), so firstMinTrunc = 42.
+ // firstTickLeftSpace=5: at 45° right-aligned edgeMax = max(0, 5/SIN_45−8) = 0 < 42 → fails
const {result} = renderLayout({
data: makeData('AAAAAA', 'BBBBBB'),
fontMgr: mockFontMgr,
@@ -160,50 +163,9 @@ describe('useChartLabelLayout', () => {
});
expect(result.current.labelRotation).toBe(90);
});
-
- it('allowTightDiagonalPacking enables 45° at tighter tick spacing', () => {
- // "AAAAAA" = 42px. tickSpacing=30.
- // Without packing: minDiagWidth = 42*SIN_45 ≈ 29.7, 29.7+4=33.7 > 30 → 45° fails
- // With packing: diagonalOverlap = 16*SIN_45 ≈ 11.3, minDiagWidth = 29.7-11.3=18.4, 18.4+4=22.4 ≤ 30 ✓
- const base = {
- data: makeData('AAAAAA', 'BBBBBB'),
- fontMgr: mockFontMgr,
- fontSize: FONT_SIZE,
- tickSpacing: 30,
- labelAreaWidth: 400,
- firstTickLeftSpace: 100,
- lastTickRightSpace: 100,
- };
-
- const {result: noPacking} = renderLayout({...base, allowTightDiagonalPacking: false});
- expect(noPacking.current.labelRotation).toBe(90);
-
- const {result: withPacking} = renderLayout({...base, allowTightDiagonalPacking: true});
- expect(withPacking.current.labelRotation).toBe(45);
- });
});
describe('edge-aware max-width constraints', () => {
- it('constrains first label below full width when centered and edge is tight', () => {
- // First label: 16 chars = 112px. tickMaxWidth ≈ 164. edgeMax ≈ 97 (stricter).
- // labelMaxWidths[0] should be < 112; middle/last labels unconstrained.
- const {result} = renderLayout({
- data: makeData('A'.repeat(16), 'BB', 'CC'),
- fontMgr: mockFontMgr,
- fontSize: FONT_SIZE,
- tickSpacing: 120,
- labelAreaWidth: 360,
- firstTickLeftSpace: 40,
- lastTickRightSpace: 200,
- });
- expect(result.current.labelRotation).toBe(45);
- // Edge constraint tightens first label below its natural width
- expect(result.current.labelMaxWidths.at(0)).toBeLessThan(16 * PX_PER_CHAR);
- // Middle and last labels are only tick-constrained (much wider than 'BB'/'CC')
- expect(result.current.labelMaxWidths.at(1)).toBeGreaterThanOrEqual(2 * PX_PER_CHAR);
- expect(result.current.labelMaxWidths.at(2)).toBeGreaterThanOrEqual(2 * PX_PER_CHAR);
- });
-
it('constrains first label below full width when right-aligned and edge is tight', () => {
// Right-aligned first label: edgeMax = 72/SIN_45 - 8 ≈ 93.8 < 112 → constrained.
const {result} = renderLayout({
@@ -214,28 +176,12 @@ describe('useChartLabelLayout', () => {
labelAreaWidth: 360,
firstTickLeftSpace: 72,
lastTickRightSpace: 200,
- allowTightDiagonalPacking: true,
});
expect(result.current.labelRotation).toBe(45);
expect(result.current.labelMaxWidths.at(0)).toBeLessThan(16 * PX_PER_CHAR);
expect(result.current.labelMaxWidths.at(1)).toBeGreaterThanOrEqual(2 * PX_PER_CHAR);
});
- it('constrains last label below full width when centered and right edge is tight', () => {
- // lastTickRightSpace=40: edgeMax = 2*(40/SIN_45-8) ≈ 97.1 < 112 → constrained.
- const {result} = renderLayout({
- data: makeData('AA', 'BB', 'A'.repeat(16)),
- fontMgr: mockFontMgr,
- fontSize: FONT_SIZE,
- tickSpacing: 200,
- labelAreaWidth: 600,
- firstTickLeftSpace: 200,
- lastTickRightSpace: 40,
- });
- expect(result.current.labelRotation).toBe(45);
- expect(result.current.labelMaxWidths.at(2)).toBeLessThan(16 * PX_PER_CHAR);
- });
-
it('does NOT constrain last label when right-aligned despite tight right edge', () => {
// Right-aligned: last label right overhang = halfLH*SIN_45 ≈ 5.6 (constant, tiny).
// lastTickRightSpace=40 >> 5.6 → edgeMax = Infinity → no edge constraint.
@@ -248,7 +194,6 @@ describe('useChartLabelLayout', () => {
labelAreaWidth: 600,
firstTickLeftSpace: 200,
lastTickRightSpace: 40,
- allowTightDiagonalPacking: true,
});
expect(result.current.labelRotation).toBe(45);
expect(result.current.labelMaxWidths.at(2)).toBeGreaterThanOrEqual(16 * PX_PER_CHAR);
diff --git a/tests/unit/components/Charts/utils.test.ts b/tests/unit/components/Charts/utils.test.ts
index ad36d30f80ae..8e89950a5f3d 100644
--- a/tests/unit/components/Charts/utils.test.ts
+++ b/tests/unit/components/Charts/utils.test.ts
@@ -98,23 +98,18 @@ describe('maxVisibleCount', () => {
describe('labelOverhang', () => {
it('returns symmetric halves at 0° (horizontal)', () => {
- const result = labelOverhang(100, LINE_HEIGHT, LABEL_ROTATIONS.HORIZONTAL, false);
+ const result = labelOverhang(100, LINE_HEIGHT, LABEL_ROTATIONS.HORIZONTAL);
expect(result.left).toBe(50);
expect(result.right).toBe(50);
});
- it('returns symmetric overhang at 45° when centered', () => {
- const result = labelOverhang(100, LINE_HEIGHT, LABEL_ROTATIONS.DIAGONAL, false);
- expect(result.left).toBeCloseTo(result.right);
- });
-
- it('returns asymmetric overhang at 45° when right-aligned', () => {
- const result = labelOverhang(100, LINE_HEIGHT, LABEL_ROTATIONS.DIAGONAL, true);
+ it('returns asymmetric overhang at 45°', () => {
+ const result = labelOverhang(100, LINE_HEIGHT, LABEL_ROTATIONS.DIAGONAL);
expect(result.left).toBeGreaterThan(result.right);
});
it('returns lineHeight/2 on both sides at 90°', () => {
- const result = labelOverhang(100, LINE_HEIGHT, LABEL_ROTATIONS.VERTICAL, false);
+ const result = labelOverhang(100, LINE_HEIGHT, LABEL_ROTATIONS.VERTICAL);
expect(result.left).toBe(LINE_HEIGHT / 2);
expect(result.right).toBe(LINE_HEIGHT / 2);
});
@@ -128,7 +123,6 @@ describe('edgeLabelsFit', () => {
rotation: LABEL_ROTATIONS.HORIZONTAL,
firstTickLeftSpace: 30,
lastTickRightSpace: 30,
- rightAligned: false,
};
it('returns true when both edges have enough space', () => {
@@ -146,32 +140,26 @@ describe('edgeLabelsFit', () => {
describe('edgeMaxLabelWidth', () => {
it('returns 2 * edgeSpace at 0°', () => {
- expect(edgeMaxLabelWidth(50, LINE_HEIGHT, LABEL_ROTATIONS.HORIZONTAL, false, 'first')).toBe(100);
+ expect(edgeMaxLabelWidth(50, LINE_HEIGHT, LABEL_ROTATIONS.HORIZONTAL, 'first')).toBe(100);
});
it('returns Infinity at 90° (overhang is constant)', () => {
- expect(edgeMaxLabelWidth(50, LINE_HEIGHT, LABEL_ROTATIONS.VERTICAL, false, 'first')).toBe(Infinity);
+ expect(edgeMaxLabelWidth(50, LINE_HEIGHT, LABEL_ROTATIONS.VERTICAL, 'first')).toBe(Infinity);
});
- it('returns finite value at 45° for first label when centered', () => {
- const result = edgeMaxLabelWidth(50, LINE_HEIGHT, LABEL_ROTATIONS.DIAGONAL, false, 'first');
+ it('returns finite value at 45° for first label', () => {
+ const result = edgeMaxLabelWidth(50, LINE_HEIGHT, LABEL_ROTATIONS.DIAGONAL, 'first');
expect(result).toBeGreaterThan(0);
expect(result).not.toBe(Infinity);
});
- it('returns Infinity at 45° for last label when right-aligned', () => {
- expect(edgeMaxLabelWidth(50, LINE_HEIGHT, LABEL_ROTATIONS.DIAGONAL, true, 'last')).toBe(Infinity);
- });
-
- it('returns finite value at 45° for first label when right-aligned', () => {
- const result = edgeMaxLabelWidth(50, LINE_HEIGHT, LABEL_ROTATIONS.DIAGONAL, true, 'first');
- expect(result).toBeGreaterThan(0);
- expect(result).not.toBe(Infinity);
+ it('returns Infinity at 45° for last label (right-aligned labels never overhang right)', () => {
+ expect(edgeMaxLabelWidth(50, LINE_HEIGHT, LABEL_ROTATIONS.DIAGONAL, 'last')).toBe(Infinity);
});
- it('returns 0 when edgeSpace is too small at 45° centered', () => {
+ it('returns 0 when edgeSpace is too small at 45°', () => {
// edgeSpace/SIN_45 - halfLH ≤ 0 → Math.max(0, ...) = 0
- expect(edgeMaxLabelWidth(1, LINE_HEIGHT, LABEL_ROTATIONS.DIAGONAL, false, 'first')).toBe(0);
+ expect(edgeMaxLabelWidth(1, LINE_HEIGHT, LABEL_ROTATIONS.DIAGONAL, 'first')).toBe(0);
});
});