From 9e179c2db425ea8125adc824d85801e82cd9f775 Mon Sep 17 00:00:00 2001
From: Mateusz Rajski <mateusz.rajski@swmansion.com>
Date: Fri, 30 Jan 2026 11:21:43 -0800
Subject: [PATCH 01/26] Implement line chart

---
 src/CONST/index.ts                            |   2 +
 .../Charts/LineChart/LineChartContent.tsx     | 222 ++++++++++++++++++
 .../Charts/LineChart/index.native.tsx         |  12 +
 src/components/Charts/LineChart/index.tsx     |  27 +++
 src/components/Charts/constants.ts            |  16 ++
 src/components/Charts/index.ts                |   5 +-
 src/components/Charts/types.ts                |  36 ++-
 src/components/Search/SearchChartView.tsx     |   8 +-
 src/components/Search/SearchLineChart.tsx     |  91 +++++++
 src/components/Search/index.tsx               |   3 +-
 src/components/Search/types.ts                |   4 +-
 src/styles/index.ts                           |   8 +
 12 files changed, 424 insertions(+), 10 deletions(-)
 create mode 100644 src/components/Charts/LineChart/LineChartContent.tsx
 create mode 100644 src/components/Charts/LineChart/index.native.tsx
 create mode 100644 src/components/Charts/LineChart/index.tsx
 create mode 100644 src/components/Search/SearchLineChart.tsx

diff --git a/src/CONST/index.ts b/src/CONST/index.ts
index 55eaf64d919f..f7398158f345 100755
--- a/src/CONST/index.ts
+++ b/src/CONST/index.ts
@@ -7182,6 +7182,8 @@ const CONST = {
         VIEW: {
             TABLE: 'table',
             BAR: 'bar',
+            LINE: 'line',
+            PIE: 'pie',
         },
         SYNTAX_FILTER_KEYS: {
             TYPE: 'type',
diff --git a/src/components/Charts/LineChart/LineChartContent.tsx b/src/components/Charts/LineChart/LineChartContent.tsx
new file mode 100644
index 000000000000..710fb24a47a8
--- /dev/null
+++ b/src/components/Charts/LineChart/LineChartContent.tsx
@@ -0,0 +1,222 @@
+import {useFont} from '@shopify/react-native-skia';
+import React, {useCallback, useMemo, useState} from 'react';
+import type {LayoutChangeEvent} from 'react-native';
+import {View} from 'react-native';
+import Animated from 'react-native-reanimated';
+import {CartesianChart, Line, Scatter} from 'victory-native';
+import ActivityIndicator from '@components/ActivityIndicator';
+import {getChartColor} from '@components/Charts/chartColors';
+import ChartHeader from '@components/Charts/ChartHeader';
+import ChartTooltip from '@components/Charts/ChartTooltip';
+import {
+    CHART_CONTENT_MIN_HEIGHT,
+    CHART_PADDING,
+    DOT_INNER_RADIUS,
+    DOT_OUTER_RADIUS,
+    LINE_CHART_FRAME,
+    X_AXIS_LINE_WIDTH,
+    Y_AXIS_DOMAIN,
+    Y_AXIS_LABEL_OFFSET,
+    Y_AXIS_LINE_WIDTH,
+    Y_AXIS_TICK_COUNT,
+} from '@components/Charts/constants';
+import fontSource from '@components/Charts/font';
+import type {HitTestArgs} from '@components/Charts/hooks';
+import {useChartInteractions, useChartLabelFormats, useChartLabelLayout} from '@components/Charts/hooks';
+import type {LineChartProps} from '@components/Charts/types';
+import useResponsiveLayout from '@hooks/useResponsiveLayout';
+import useTheme from '@hooks/useTheme';
+import useThemeStyles from '@hooks/useThemeStyles';
+import variables from '@styles/variables';
+
+/** Symmetric domain padding for line charts */
+const LINE_DOMAIN_PADDING = {
+    left: 20,
+    right: 20,
+    top: 20,
+    bottom: 20,
+};
+
+function LineChartContent({data, title, titleIcon, isLoading, yAxisUnit, yAxisUnitPosition = 'left', onPointPress}: LineChartProps) {
+    const theme = useTheme();
+    const styles = useThemeStyles();
+    const {shouldUseNarrowLayout} = useResponsiveLayout();
+    const font = useFont(fontSource, variables.iconSizeExtraSmall);
+    const [chartWidth, setChartWidth] = useState(0);
+    const [containerHeight, setContainerHeight] = useState(0);
+
+    const chartData = useMemo(() => {
+        return data.map((point, index) => ({
+            x: index,
+            y: point.total,
+        }));
+    }, [data]);
+
+    const handlePointPress = useCallback(
+        (index: number) => {
+            if (index < 0 || index >= data.length) {
+                return;
+            }
+            const dataPoint = data.at(index);
+            if (dataPoint && onPointPress) {
+                onPointPress(dataPoint, index);
+            }
+        },
+        [data, onPointPress],
+    );
+
+    const handleLayout = useCallback((event: LayoutChangeEvent) => {
+        const {width, height} = event.nativeEvent.layout;
+        setChartWidth(width);
+        setContainerHeight(height);
+    }, []);
+
+    const {labelRotation, labelSkipInterval, truncatedLabels, maxLabelLength} = useChartLabelLayout({
+        data,
+        font,
+        chartWidth,
+        barAreaWidth: chartWidth,
+        containerHeight,
+    });
+
+    const {formatXAxisLabel, formatYAxisLabel} = useChartLabelFormats({
+        data,
+        yAxisUnit,
+        yAxisUnitPosition,
+        labelSkipInterval,
+        labelRotation,
+        truncatedLabels,
+    });
+
+    const checkIsOverDot = useCallback(
+        (args: HitTestArgs) => {
+            'worklet';
+
+            const dx = args.cursorX - args.targetX;
+            const dy = args.cursorY - args.targetY;
+            return Math.sqrt(dx * dx + dy * dy) <= DOT_INNER_RADIUS;
+        },
+        [],
+    );
+
+    const {actionsRef, customGestures, activeDataIndex, isTooltipActive, tooltipStyle} = useChartInteractions({
+        handlePress: handlePointPress,
+        checkIsOver: checkIsOverDot,
+    });
+
+    const tooltipData = useMemo(() => {
+        if (activeDataIndex < 0 || activeDataIndex >= data.length) {
+            return null;
+        }
+        const dataPoint = data.at(activeDataIndex);
+        if (!dataPoint) {
+            return null;
+        }
+        const formatted = dataPoint.total.toLocaleString();
+        let formattedAmount = formatted;
+        if (yAxisUnit) {
+            const separator = yAxisUnit.length > 1 ? ' ' : '';
+            formattedAmount = yAxisUnitPosition === 'left' ? `${yAxisUnit}${separator}${formatted}` : `${formatted}${separator}${yAxisUnit}`;
+        }
+        const totalSum = data.reduce((sum, point) => sum + Math.abs(point.total), 0);
+        const percent = totalSum > 0 ? Math.round((Math.abs(dataPoint.total) / totalSum) * 100) : 0;
+        return {
+            label: dataPoint.label,
+            amount: formattedAmount,
+            percentage: percent < 1 ? '<1%' : `${percent}%`,
+        };
+    }, [activeDataIndex, data, yAxisUnit, yAxisUnitPosition]);
+
+    const dynamicChartStyle = useMemo(
+        () => ({
+            height: CHART_CONTENT_MIN_HEIGHT + (maxLabelLength ?? 0),
+        }),
+        [maxLabelLength],
+    );
+
+    if (isLoading || !font) {
+        return (
+            <View style={[styles.lineChartContainer, styles.highlightBG, shouldUseNarrowLayout ? styles.p5 : styles.p8, styles.justifyContentCenter, styles.alignItemsCenter]}>
+                <ActivityIndicator size="large" />
+            </View>
+        );
+    }
+
+    if (data.length === 0) {
+        return null;
+    }
+
+    return (
+        <View style={[styles.lineChartContainer, styles.highlightBG, shouldUseNarrowLayout ? styles.p5 : styles.p8]}>
+            <ChartHeader
+                title={title}
+                titleIcon={titleIcon}
+            />
+            <View
+                style={[styles.lineChartChartContainer, labelRotation === -90 ? dynamicChartStyle : undefined]}
+                onLayout={handleLayout}
+            >
+                {chartWidth > 0 && (
+                    <CartesianChart
+                        xKey="x"
+                        padding={CHART_PADDING}
+                        yKeys={['y']}
+                        domainPadding={LINE_DOMAIN_PADDING}
+                        actionsRef={actionsRef}
+                        customGestures={customGestures}
+                        xAxis={{
+                            font,
+                            tickCount: data.length,
+                            labelColor: theme.textSupporting,
+                            lineWidth: X_AXIS_LINE_WIDTH,
+                            formatXLabel: formatXAxisLabel,
+                            labelRotate: labelRotation,
+                            labelOverflow: 'visible',
+                        }}
+                        yAxis={[
+                            {
+                                font,
+                                labelColor: theme.textSupporting,
+                                formatYLabel: formatYAxisLabel,
+                                tickCount: Y_AXIS_TICK_COUNT,
+                                lineWidth: Y_AXIS_LINE_WIDTH,
+                                lineColor: theme.border,
+                                labelOffset: Y_AXIS_LABEL_OFFSET,
+                                domain: Y_AXIS_DOMAIN,
+                            },
+                        ]}
+                        frame={LINE_CHART_FRAME}
+                        data={chartData}
+                    >
+                        {({points}) => (
+                            <>
+                                <Line
+                                    points={points.y}
+                                    color={getChartColor(5)}
+                                    strokeWidth={2}
+                                    curveType="linear"
+                                />
+                                <Scatter
+                                    points={points.y}
+                                    radius={DOT_INNER_RADIUS}
+                                    color={getChartColor(5)}
+                                />
+                            </>
+                        )}
+                    </CartesianChart>
+                )}
+                {isTooltipActive && !!tooltipData && (
+                    <Animated.View style={tooltipStyle}>
+                        <ChartTooltip
+                            label={tooltipData.label}
+                            amount={tooltipData.amount}
+                            percentage={tooltipData.percentage}
+                        />
+                    </Animated.View>
+                )}
+            </View>
+        </View>
+    );
+}
+
+export default LineChartContent;
diff --git a/src/components/Charts/LineChart/index.native.tsx b/src/components/Charts/LineChart/index.native.tsx
new file mode 100644
index 000000000000..f0e5af8da012
--- /dev/null
+++ b/src/components/Charts/LineChart/index.native.tsx
@@ -0,0 +1,12 @@
+import React from 'react';
+import type {LineChartProps} from '@components/Charts/types';
+import LineChartContent from './LineChartContent';
+
+function LineChart(props: LineChartProps) {
+    // eslint-disable-next-line react/jsx-props-no-spreading
+    return <LineChartContent {...props} />;
+}
+
+LineChart.displayName = 'LineChart';
+
+export default LineChart;
diff --git a/src/components/Charts/LineChart/index.tsx b/src/components/Charts/LineChart/index.tsx
new file mode 100644
index 000000000000..2fefabd884cb
--- /dev/null
+++ b/src/components/Charts/LineChart/index.tsx
@@ -0,0 +1,27 @@
+import {WithSkiaWeb} from '@shopify/react-native-skia/lib/module/web';
+import React from 'react';
+import {View} from 'react-native';
+import ActivityIndicator from '@components/ActivityIndicator';
+import type {LineChartProps} from '@components/Charts/types';
+import useThemeStyles from '@hooks/useThemeStyles';
+
+function LineChart(props: LineChartProps) {
+    const styles = useThemeStyles();
+
+    return (
+        <WithSkiaWeb
+            opts={{locateFile: (file: string) => `/${file}`}}
+            getComponent={() => import('./LineChartContent')}
+            componentProps={props}
+            fallback={
+                <View style={[styles.flex1, styles.justifyContentCenter, styles.alignItemsCenter, styles.highlightBG, styles.br4, styles.p5]}>
+                    <ActivityIndicator size="large" />
+                </View>
+            }
+        />
+    );
+}
+
+LineChart.displayName = 'LineChart';
+
+export default LineChart;
diff --git a/src/components/Charts/constants.ts b/src/components/Charts/constants.ts
index b0ed40872622..7c597f66bb15 100644
--- a/src/components/Charts/constants.ts
+++ b/src/components/Charts/constants.ts
@@ -87,6 +87,18 @@ const X_AXIS_LABEL_MAX_HEIGHT_RATIO = 0.35;
 /** Ellipsis character for truncated labels */
 const LABEL_ELLIPSIS = '...';
 
+/** Inner dot radius for line chart data points */
+const DOT_INNER_RADIUS = 6;
+
+/** Outer dot radius for line chart data points (background ring) */
+const DOT_OUTER_RADIUS = 8;
+
+/** Y-axis domain anchored at zero for line charts */
+const Y_AXIS_DOMAIN: [number] = [0];
+
+/** Frame configuration for line charts - only left and bottom borders */
+const LINE_CHART_FRAME = {lineWidth: {left: 1, bottom: 1, top: 0, right: 0}};
+
 export {
     CHART_COLORS,
     Y_AXIS_TICK_COUNT,
@@ -110,4 +122,8 @@ export {
     LABEL_PADDING,
     X_AXIS_LABEL_MAX_HEIGHT_RATIO,
     LABEL_ELLIPSIS,
+    DOT_INNER_RADIUS,
+    DOT_OUTER_RADIUS,
+    Y_AXIS_DOMAIN,
+    LINE_CHART_FRAME,
 };
diff --git a/src/components/Charts/index.ts b/src/components/Charts/index.ts
index aabb56843923..5804e553c8f4 100644
--- a/src/components/Charts/index.ts
+++ b/src/components/Charts/index.ts
@@ -1,6 +1,7 @@
 import BarChart from './BarChart';
 import ChartHeader from './ChartHeader';
 import ChartTooltip from './ChartTooltip';
+import LineChart from './LineChart';
 
-export {BarChart, ChartHeader, ChartTooltip};
-export type {BarChartDataPoint, BarChartProps} from './types';
+export {BarChart, ChartHeader, ChartTooltip, LineChart};
+export type {BarChartDataPoint, BarChartProps, LineChartDataPoint, LineChartProps} from './types';
diff --git a/src/components/Charts/types.ts b/src/components/Charts/types.ts
index e2ec4fe54072..678a0dd58b98 100644
--- a/src/components/Charts/types.ts
+++ b/src/components/Charts/types.ts
@@ -40,4 +40,38 @@ type BarChartProps = {
     useSingleColor?: boolean;
 };
 
-export type {BarChartDataPoint, BarChartProps};
+type LineChartDataPoint = {
+    /** Label displayed under the data point (e.g., "Nov 2025", "Week 3") */
+    label: string;
+
+    /** Total amount (pre-formatted, e.g., dollars not cents) */
+    total: number;
+
+    /** Query string for navigation when point is clicked (optional) */
+    onClickQuery?: string;
+};
+
+type LineChartProps = {
+    /** Data points to display */
+    data: LineChartDataPoint[];
+
+    /** Chart title (e.g., "Spend over time") */
+    title?: string;
+
+    /** Icon displayed next to the title */
+    titleIcon?: IconAsset;
+
+    /** Whether data is loading */
+    isLoading?: boolean;
+
+    /** Callback when a data point is pressed */
+    onPointPress?: (dataPoint: LineChartDataPoint, index: number) => void;
+
+    /** Symbol/unit for Y-axis labels (e.g., '$', '€', 'zł'). Empty string or undefined shows raw numbers. */
+    yAxisUnit?: string;
+
+    /** Position of the unit symbol relative to the value. Defaults to 'left'. */
+    yAxisUnitPosition?: 'left' | 'right';
+};
+
+export type {BarChartDataPoint, BarChartProps, LineChartDataPoint, LineChartProps};
diff --git a/src/components/Search/SearchChartView.tsx b/src/components/Search/SearchChartView.tsx
index 1979dad7fb6c..0d5cde51e236 100644
--- a/src/components/Search/SearchChartView.tsx
+++ b/src/components/Search/SearchChartView.tsx
@@ -27,6 +27,7 @@ import {buildSearchQueryJSON, buildSearchQueryString} from '@libs/SearchQueryUti
 import CONST from '@src/CONST';
 import ROUTES from '@src/ROUTES';
 import SearchBarChart from './SearchBarChart';
+import SearchLineChart from './SearchLineChart';
 import type {ChartView, SearchGroupBy, SearchQueryJSON} from './types';
 
 type GroupedItem =
@@ -125,8 +126,8 @@ type SearchChartViewProps = {
     /** The current search query JSON */
     queryJSON: SearchQueryJSON;
 
-    /** The view type (bar, etc.) */
-    view: Exclude<ChartView, 'line' | 'pie'>;
+    /** The view type (bar, line, etc.) */
+    view: Exclude<ChartView, 'pie'>;
 
     /** The groupBy parameter */
     groupBy: SearchGroupBy;
@@ -144,8 +145,9 @@ type SearchChartViewProps = {
 /**
  * Map of chart view types to their corresponding chart components.
  */
-const CHART_VIEW_TO_COMPONENT: Record<Exclude<ChartView, 'line' | 'pie'>, typeof SearchBarChart> = {
+const CHART_VIEW_TO_COMPONENT: Record<Exclude<ChartView, 'pie'>, typeof SearchBarChart | typeof SearchLineChart> = {
     [CONST.SEARCH.VIEW.BAR]: SearchBarChart,
+    [CONST.SEARCH.VIEW.LINE]: SearchLineChart,
 };
 
 /**
diff --git a/src/components/Search/SearchLineChart.tsx b/src/components/Search/SearchLineChart.tsx
new file mode 100644
index 000000000000..efe030eeaeb7
--- /dev/null
+++ b/src/components/Search/SearchLineChart.tsx
@@ -0,0 +1,91 @@
+import React, {useCallback, useMemo} from 'react';
+import {LineChart} from '@components/Charts';
+import type {LineChartDataPoint} from '@components/Charts';
+import type {
+    TransactionCardGroupListItemType,
+    TransactionCategoryGroupListItemType,
+    TransactionGroupListItemType,
+    TransactionMemberGroupListItemType,
+    TransactionWithdrawalIDGroupListItemType,
+} from '@components/SelectionListWithSections/types';
+import {convertToFrontendAmountAsInteger} from '@libs/CurrencyUtils';
+import type IconAsset from '@src/types/utils/IconAsset';
+
+type GroupedItem = TransactionMemberGroupListItemType | TransactionCardGroupListItemType | TransactionWithdrawalIDGroupListItemType | TransactionCategoryGroupListItemType;
+
+type SearchLineChartProps = {
+    /** Grouped transaction data from search results */
+    data: TransactionGroupListItemType[];
+
+    /** Chart title */
+    title: string;
+
+    /** Chart title icon */
+    titleIcon: IconAsset;
+
+    /** Function to extract label from grouped item */
+    getLabel: (item: GroupedItem) => string;
+
+    /** Function to build filter query from grouped item */
+    getFilterQuery: (item: GroupedItem) => string;
+
+    /** Callback when a point is pressed - receives the filter query to apply */
+    onBarPress?: (filterQuery: string) => void;
+
+    /** Whether data is loading */
+    isLoading?: boolean;
+
+    /** Currency symbol for Y-axis labels */
+    yAxisUnit?: string;
+
+    /** Position of currency symbol relative to value */
+    yAxisUnitPosition?: 'left' | 'right';
+};
+
+function SearchLineChart({data, title, titleIcon, getLabel, getFilterQuery, onBarPress, isLoading, yAxisUnit, yAxisUnitPosition}: SearchLineChartProps) {
+    const chartData: LineChartDataPoint[] = useMemo(() => {
+        return data.map((item) => {
+            const groupedItem = item as GroupedItem;
+            const currency = groupedItem.currency ?? 'USD';
+            const totalInDisplayUnits = convertToFrontendAmountAsInteger(groupedItem.total ?? 0, currency);
+
+            return {
+                label: getLabel(groupedItem),
+                total: totalInDisplayUnits,
+            };
+        });
+    }, [data, getLabel]);
+
+    const handlePointPress = useCallback(
+        (dataPoint: LineChartDataPoint, index: number) => {
+            if (!onBarPress) {
+                return;
+            }
+
+            const item = data.at(index);
+            if (!item) {
+                return;
+            }
+
+            const filterQuery = getFilterQuery(item as GroupedItem);
+            onBarPress(filterQuery);
+        },
+        [data, getFilterQuery, onBarPress],
+    );
+
+    return (
+        <LineChart
+            data={chartData}
+            title={title}
+            titleIcon={titleIcon}
+            isLoading={isLoading}
+            onPointPress={handlePointPress}
+            yAxisUnit={yAxisUnit}
+            yAxisUnitPosition={yAxisUnitPosition}
+        />
+    );
+}
+
+SearchLineChart.displayName = 'SearchLineChart';
+
+export default SearchLineChart;
diff --git a/src/components/Search/index.tsx b/src/components/Search/index.tsx
index 59e59a936344..eaa360727828 100644
--- a/src/components/Search/index.tsx
+++ b/src/components/Search/index.tsx
@@ -1305,8 +1305,7 @@ function Search({
     const shouldShowTableHeader = isLargeScreenWidth && !isChat;
     const tableHeaderVisible = canSelectMultiple || shouldShowTableHeader;
 
-    // Other charts are not implemented yet
-    const shouldShowChartView = view === CONST.SEARCH.VIEW.BAR && !!validGroupBy;
+    const shouldShowChartView = (view === CONST.SEARCH.VIEW.BAR || view === CONST.SEARCH.VIEW.LINE) && !!validGroupBy;
 
     if (shouldShowChartView) {
         return (
diff --git a/src/components/Search/types.ts b/src/components/Search/types.ts
index 7faab6847ef1..953aaca2848f 100644
--- a/src/components/Search/types.ts
+++ b/src/components/Search/types.ts
@@ -115,8 +115,8 @@ type SingularSearchStatus = ExpenseSearchStatus | ExpenseReportSearchStatus | In
 type SearchStatus = SingularSearchStatus | SingularSearchStatus[];
 type SearchGroupBy = ValueOf<typeof CONST.SEARCH.GROUP_BY>;
 type SearchView = ValueOf<typeof CONST.SEARCH.VIEW>;
-// LineChart and PieChart are not implemented so we exclude them here to prevent TypeScript errors in `SearchChartView.tsx`.
-type ChartView = Exclude<SearchView, 'table' | 'line' | 'pie'>;
+// PieChart is not implemented so we exclude it here to prevent TypeScript errors in `SearchChartView.tsx`.
+type ChartView = Exclude<SearchView, 'table' | 'pie'>;
 type TableColumnSize = ValueOf<typeof CONST.SEARCH.TABLE_COLUMN_SIZES>;
 type SearchDatePreset = ValueOf<typeof CONST.SEARCH.DATE_PRESETS>;
 type SearchWithdrawalType = ValueOf<typeof CONST.SEARCH.WITHDRAWAL_TYPE>;
diff --git a/src/styles/index.ts b/src/styles/index.ts
index 4ca6f677c111..ccb2b6443282 100644
--- a/src/styles/index.ts
+++ b/src/styles/index.ts
@@ -5805,6 +5805,14 @@ const staticStyles = (theme: ThemeColors) =>
         barChartChartContainer: {
             minHeight: 250,
         },
+        lineChartContainer: {
+            borderRadius: variables.componentBorderRadiusLarge,
+            paddingTop: variables.qrShareHorizontalPadding,
+            paddingHorizontal: variables.qrShareHorizontalPadding,
+        },
+        lineChartChartContainer: {
+            minHeight: 250,
+        },
         discoverSectionImage: {
             width: '100%',
             height: undefined,

From a8c40b2c028e35f610daefc93bc696a80d9bebd4 Mon Sep 17 00:00:00 2001
From: Mateusz Rajski <mateusz.rajski@swmansion.com>
Date: Fri, 30 Jan 2026 11:21:56 -0800
Subject: [PATCH 02/26] Add english translation

---
 src/languages/en.ts | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/src/languages/en.ts b/src/languages/en.ts
index 62a48239b88b..33ce71189d76 100644
--- a/src/languages/en.ts
+++ b/src/languages/en.ts
@@ -6981,6 +6981,8 @@ const translations = {
             label: 'View',
             table: 'Table',
             bar: 'Bar',
+            line: 'Line',
+            pie: 'Pie',
         },
         chartTitles: {
             [CONST.SEARCH.GROUP_BY.FROM]: 'From',

From 6340c82aef8ae789c7384f3719ff03269478b3dc Mon Sep 17 00:00:00 2001
From: Mateusz Rajski <mateusz.rajski@swmansion.com>
Date: Fri, 30 Jan 2026 13:30:16 -0800
Subject: [PATCH 03/26] Refactor charts directory and make it more DRY

---
 .../Charts/BarChart/BarChartContent.tsx       |  74 ++++++------
 .../Charts/BarChart/index.native.tsx          |   2 +-
 src/components/Charts/BarChart/index.tsx      |   2 +-
 .../Charts/LineChart/LineChartContent.tsx     |  62 ++++------
 .../Charts/LineChart/index.native.tsx         |   2 +-
 src/components/Charts/LineChart/index.tsx     |   2 +-
 .../Charts/{ => components}/ChartHeader.tsx   |   0
 .../Charts/{ => components}/ChartTooltip.tsx  |   6 +-
 src/components/Charts/constants.ts            | 112 +-----------------
 src/components/Charts/hooks/index.ts          |   2 +
 .../Charts/hooks/useChartInteractions.ts      |   4 +-
 .../Charts/hooks/useChartLabelFormats.ts      |   7 +-
 .../Charts/hooks/useChartLabelLayout.ts       |  28 +++--
 .../Charts/hooks/useDynamicYDomain.ts         |  12 ++
 src/components/Charts/hooks/useTooltipData.ts |  41 +++++++
 src/components/Charts/index.ts                |   8 +-
 src/components/Charts/types.ts                |  59 ++-------
 .../Charts/{chartColors.ts => utils.ts}       |   5 +-
 src/components/Search/SearchBarChart.tsx      |   7 +-
 src/components/Search/SearchLineChart.tsx     |   6 +-
 20 files changed, 166 insertions(+), 275 deletions(-)
 rename src/components/Charts/{ => components}/ChartHeader.tsx (100%)
 rename src/components/Charts/{ => components}/ChartTooltip.tsx (90%)
 create mode 100644 src/components/Charts/hooks/useDynamicYDomain.ts
 create mode 100644 src/components/Charts/hooks/useTooltipData.ts
 rename src/components/Charts/{chartColors.ts => utils.ts} (85%)

diff --git a/src/components/Charts/BarChart/BarChartContent.tsx b/src/components/Charts/BarChart/BarChartContent.tsx
index c1d60f962b6b..08cffc68f0c6 100644
--- a/src/components/Charts/BarChart/BarChartContent.tsx
+++ b/src/components/Charts/BarChart/BarChartContent.tsx
@@ -6,19 +6,12 @@ import Animated, {useSharedValue} from 'react-native-reanimated';
 import type {ChartBounds, PointsArray} from 'victory-native';
 import {Bar, CartesianChart} from 'victory-native';
 import ActivityIndicator from '@components/ActivityIndicator';
-import {getChartColor} from '@components/Charts/chartColors';
-import ChartHeader from '@components/Charts/ChartHeader';
-import ChartTooltip from '@components/Charts/ChartTooltip';
+import ChartHeader from '@components/Charts/components/ChartHeader';
+import ChartTooltip from '@components/Charts/components/ChartTooltip';
+import {DEFAULT_CHART_COLOR, getChartColor} from '@components/Charts/utils';
 import {
-    BAR_INNER_PADDING,
-    BAR_ROUNDED_CORNERS,
-    CHART_COLORS,
     CHART_CONTENT_MIN_HEIGHT,
     CHART_PADDING,
-    DEFAULT_SINGLE_BAR_COLOR_INDEX,
-    DOMAIN_PADDING,
-    DOMAIN_PADDING_SAFETY_BUFFER,
-    FRAME_LINE_WIDTH,
     X_AXIS_LINE_WIDTH,
     Y_AXIS_LABEL_OFFSET,
     Y_AXIS_LINE_WIDTH,
@@ -26,13 +19,35 @@ import {
 } from '@components/Charts/constants';
 import fontSource from '@components/Charts/font';
 import type {HitTestArgs} from '@components/Charts/hooks';
-import {useChartInteractions, useChartLabelFormats, useChartLabelLayout} from '@components/Charts/hooks';
-import type {BarChartProps} from '@components/Charts/types';
+import {useChartInteractions, useChartLabelFormats, useChartLabelLayout, useDynamicYDomain, useTooltipData} from '@components/Charts/hooks';
+import type {CartesianChartProps, ChartDataPoint} from '@components/Charts/types';
 import useResponsiveLayout from '@hooks/useResponsiveLayout';
 import useTheme from '@hooks/useTheme';
 import useThemeStyles from '@hooks/useThemeStyles';
 import variables from '@styles/variables';
 
+type BarChartProps = CartesianChartProps & {
+    /** Callback when a bar is pressed */
+    onBarPress?: (dataPoint: ChartDataPoint, index: number) => void;
+
+    /** When true, all bars use the same color. When false (default), each bar uses a different color from the palette. */
+    useSingleColor?: boolean;
+};
+
+/** Inner padding between bars (0.3 = 30% of bar width) */
+const BAR_INNER_PADDING = 0.3;
+
+/** Domain padding configuration for the bar chart */
+const DOMAIN_PADDING = {
+    left: 0,
+    right: 16,
+    top: 30,
+    bottom: 10,
+};
+
+/** Safety buffer multiplier for domain padding calculation */
+const DOMAIN_PADDING_SAFETY_BUFFER = 1.1;
+
 /**
  * Calculate minimum domainPadding required to prevent bars from overflowing chart edges.
  *
@@ -57,7 +72,7 @@ function BarChartContent({data, title, titleIcon, isLoading, yAxisUnit, yAxisUni
     const [barAreaWidth, setBarAreaWidth] = useState(0);
     const [containerHeight, setContainerHeight] = useState(0);
 
-    const defaultBarColor = CHART_COLORS.at(DEFAULT_SINGLE_BAR_COLOR_INDEX);
+    const defaultBarColor = DEFAULT_CHART_COLOR;
 
     // prepare data for display
     const chartData = useMemo(() => {
@@ -67,9 +82,7 @@ function BarChartContent({data, title, titleIcon, isLoading, yAxisUnit, yAxisUni
         }));
     }, [data]);
 
-    // Anchor Y-axis at zero so the baseline is always visible.
-    // When negative values are present, let victory-native auto-calculate the domain to avoid clipping.
-    const yAxisDomain = useMemo((): [number] | undefined => (data.some((point) => point.total < 0) ? undefined : [0]), [data]);
+    const yAxisDomain = useDynamicYDomain(data);
 
     // Handle bar press callback
     const handleBarPress = useCallback(
@@ -169,29 +182,7 @@ function BarChartContent({data, title, titleIcon, isLoading, yAxisUnit, yAxisUni
         barGeometry,
     });
 
-    const tooltipData = useMemo(() => {
-        if (activeDataIndex < 0 || activeDataIndex >= data.length) {
-            return null;
-        }
-        const dataPoint = data.at(activeDataIndex);
-        if (!dataPoint) {
-            return null;
-        }
-        const formatted = dataPoint.total.toLocaleString();
-        let formattedAmount = formatted;
-        if (yAxisUnit) {
-            // Add space for multi-character codes (e.g., "PLN 100") but not for symbols (e.g., "$100")
-            const separator = yAxisUnit.length > 1 ? ' ' : '';
-            formattedAmount = yAxisUnitPosition === 'left' ? `${yAxisUnit}${separator}${formatted}` : `${formatted}${separator}${yAxisUnit}`;
-        }
-        const totalSum = data.reduce((sum, point) => sum + Math.abs(point.total), 0);
-        const percent = totalSum > 0 ? Math.round((Math.abs(dataPoint.total) / totalSum) * 100) : 0;
-        return {
-            label: dataPoint.label,
-            amount: formattedAmount,
-            percentage: percent < 1 ? '<1%' : `${percent}%`,
-        };
-    }, [activeDataIndex, data, yAxisUnit, yAxisUnitPosition]);
+    const tooltipData = useTooltipData(activeDataIndex, data, yAxisUnit, yAxisUnitPosition);
 
     const renderBar = useCallback(
         (point: PointsArray[number], chartBounds: ChartBounds, barCount: number) => {
@@ -207,7 +198,7 @@ function BarChartContent({data, title, titleIcon, isLoading, yAxisUnit, yAxisUni
                     color={barColor}
                     barCount={barCount}
                     innerPadding={BAR_INNER_PADDING}
-                    roundedCorners={BAR_ROUNDED_CORNERS}
+                    roundedCorners={{topLeft: 8, topRight: 8, bottomLeft: 8, bottomRight: 8}}
                 />
             );
         },
@@ -276,7 +267,7 @@ function BarChartContent({data, title, titleIcon, isLoading, yAxisUnit, yAxisUni
                                 domain: yAxisDomain,
                             },
                         ]}
-                        frame={{lineWidth: FRAME_LINE_WIDTH}}
+                        frame={{lineWidth: 0}}
                         data={chartData}
                     >
                         {({points, chartBounds}) => <>{points.y.map((point) => renderBar(point, chartBounds, points.y.length))}</>}
@@ -297,3 +288,4 @@ function BarChartContent({data, title, titleIcon, isLoading, yAxisUnit, yAxisUni
 }
 
 export default BarChartContent;
+export type {BarChartProps};
diff --git a/src/components/Charts/BarChart/index.native.tsx b/src/components/Charts/BarChart/index.native.tsx
index 82396525a5b0..54f47ea8bb1a 100644
--- a/src/components/Charts/BarChart/index.native.tsx
+++ b/src/components/Charts/BarChart/index.native.tsx
@@ -1,5 +1,5 @@
 import React from 'react';
-import type {BarChartProps} from '@components/Charts/types';
+import type {BarChartProps} from './BarChartContent';
 import BarChartContent from './BarChartContent';
 
 function BarChart(props: BarChartProps) {
diff --git a/src/components/Charts/BarChart/index.tsx b/src/components/Charts/BarChart/index.tsx
index c82a92ecbf23..0d00fb501193 100644
--- a/src/components/Charts/BarChart/index.tsx
+++ b/src/components/Charts/BarChart/index.tsx
@@ -2,7 +2,7 @@ import {WithSkiaWeb} from '@shopify/react-native-skia/lib/module/web';
 import React from 'react';
 import {View} from 'react-native';
 import ActivityIndicator from '@components/ActivityIndicator';
-import type {BarChartProps} from '@components/Charts/types';
+import type {BarChartProps} from './BarChartContent';
 import useThemeStyles from '@hooks/useThemeStyles';
 
 function BarChart(props: BarChartProps) {
diff --git a/src/components/Charts/LineChart/LineChartContent.tsx b/src/components/Charts/LineChart/LineChartContent.tsx
index 710fb24a47a8..6e7e0e5fa13c 100644
--- a/src/components/Charts/LineChart/LineChartContent.tsx
+++ b/src/components/Charts/LineChart/LineChartContent.tsx
@@ -5,36 +5,32 @@ import {View} from 'react-native';
 import Animated from 'react-native-reanimated';
 import {CartesianChart, Line, Scatter} from 'victory-native';
 import ActivityIndicator from '@components/ActivityIndicator';
-import {getChartColor} from '@components/Charts/chartColors';
-import ChartHeader from '@components/Charts/ChartHeader';
-import ChartTooltip from '@components/Charts/ChartTooltip';
+import ChartHeader from '@components/Charts/components/ChartHeader';
+import ChartTooltip from '@components/Charts/components/ChartTooltip';
+import {DEFAULT_CHART_COLOR} from '@components/Charts/utils';
 import {
     CHART_CONTENT_MIN_HEIGHT,
     CHART_PADDING,
-    DOT_INNER_RADIUS,
-    DOT_OUTER_RADIUS,
-    LINE_CHART_FRAME,
     X_AXIS_LINE_WIDTH,
-    Y_AXIS_DOMAIN,
     Y_AXIS_LABEL_OFFSET,
     Y_AXIS_LINE_WIDTH,
     Y_AXIS_TICK_COUNT,
 } from '@components/Charts/constants';
 import fontSource from '@components/Charts/font';
 import type {HitTestArgs} from '@components/Charts/hooks';
-import {useChartInteractions, useChartLabelFormats, useChartLabelLayout} from '@components/Charts/hooks';
-import type {LineChartProps} from '@components/Charts/types';
+import {useChartInteractions, useChartLabelFormats, useChartLabelLayout, useDynamicYDomain, useTooltipData} from '@components/Charts/hooks';
+import type {CartesianChartProps, ChartDataPoint} from '@components/Charts/types';
 import useResponsiveLayout from '@hooks/useResponsiveLayout';
 import useTheme from '@hooks/useTheme';
 import useThemeStyles from '@hooks/useThemeStyles';
 import variables from '@styles/variables';
 
-/** Symmetric domain padding for line charts */
-const LINE_DOMAIN_PADDING = {
-    left: 20,
-    right: 20,
-    top: 20,
-    bottom: 20,
+/** Inner dot radius for line chart data points */
+const DOT_INNER_RADIUS = 6;
+
+type LineChartProps = CartesianChartProps & {
+    /** Callback when a data point is pressed */
+    onPointPress?: (dataPoint: ChartDataPoint, index: number) => void;
 };
 
 function LineChartContent({data, title, titleIcon, isLoading, yAxisUnit, yAxisUnitPosition = 'left', onPointPress}: LineChartProps) {
@@ -45,6 +41,8 @@ function LineChartContent({data, title, titleIcon, isLoading, yAxisUnit, yAxisUn
     const [chartWidth, setChartWidth] = useState(0);
     const [containerHeight, setContainerHeight] = useState(0);
 
+    const yAxisDomain = useDynamicYDomain(data);
+
     const chartData = useMemo(() => {
         return data.map((point, index) => ({
             x: index,
@@ -104,28 +102,7 @@ function LineChartContent({data, title, titleIcon, isLoading, yAxisUnit, yAxisUn
         checkIsOver: checkIsOverDot,
     });
 
-    const tooltipData = useMemo(() => {
-        if (activeDataIndex < 0 || activeDataIndex >= data.length) {
-            return null;
-        }
-        const dataPoint = data.at(activeDataIndex);
-        if (!dataPoint) {
-            return null;
-        }
-        const formatted = dataPoint.total.toLocaleString();
-        let formattedAmount = formatted;
-        if (yAxisUnit) {
-            const separator = yAxisUnit.length > 1 ? ' ' : '';
-            formattedAmount = yAxisUnitPosition === 'left' ? `${yAxisUnit}${separator}${formatted}` : `${formatted}${separator}${yAxisUnit}`;
-        }
-        const totalSum = data.reduce((sum, point) => sum + Math.abs(point.total), 0);
-        const percent = totalSum > 0 ? Math.round((Math.abs(dataPoint.total) / totalSum) * 100) : 0;
-        return {
-            label: dataPoint.label,
-            amount: formattedAmount,
-            percentage: percent < 1 ? '<1%' : `${percent}%`,
-        };
-    }, [activeDataIndex, data, yAxisUnit, yAxisUnitPosition]);
+    const tooltipData = useTooltipData(activeDataIndex, data, yAxisUnit, yAxisUnitPosition);
 
     const dynamicChartStyle = useMemo(
         () => ({
@@ -161,7 +138,7 @@ function LineChartContent({data, title, titleIcon, isLoading, yAxisUnit, yAxisUn
                         xKey="x"
                         padding={CHART_PADDING}
                         yKeys={['y']}
-                        domainPadding={LINE_DOMAIN_PADDING}
+                        domainPadding={{left: 20, right: 20, top: 20, bottom: 20}}
                         actionsRef={actionsRef}
                         customGestures={customGestures}
                         xAxis={{
@@ -182,24 +159,24 @@ function LineChartContent({data, title, titleIcon, isLoading, yAxisUnit, yAxisUn
                                 lineWidth: Y_AXIS_LINE_WIDTH,
                                 lineColor: theme.border,
                                 labelOffset: Y_AXIS_LABEL_OFFSET,
-                                domain: Y_AXIS_DOMAIN,
+                                domain: yAxisDomain,
                             },
                         ]}
-                        frame={LINE_CHART_FRAME}
+                        frame={{lineWidth: {left: 1, bottom: 1, top: 0, right: 0}}}
                         data={chartData}
                     >
                         {({points}) => (
                             <>
                                 <Line
                                     points={points.y}
-                                    color={getChartColor(5)}
+                                    color={DEFAULT_CHART_COLOR}
                                     strokeWidth={2}
                                     curveType="linear"
                                 />
                                 <Scatter
                                     points={points.y}
                                     radius={DOT_INNER_RADIUS}
-                                    color={getChartColor(5)}
+                                    color={DEFAULT_CHART_COLOR}
                                 />
                             </>
                         )}
@@ -220,3 +197,4 @@ function LineChartContent({data, title, titleIcon, isLoading, yAxisUnit, yAxisUn
 }
 
 export default LineChartContent;
+export type {LineChartProps};
diff --git a/src/components/Charts/LineChart/index.native.tsx b/src/components/Charts/LineChart/index.native.tsx
index f0e5af8da012..db7c218db9ab 100644
--- a/src/components/Charts/LineChart/index.native.tsx
+++ b/src/components/Charts/LineChart/index.native.tsx
@@ -1,5 +1,5 @@
 import React from 'react';
-import type {LineChartProps} from '@components/Charts/types';
+import type {LineChartProps} from './LineChartContent';
 import LineChartContent from './LineChartContent';
 
 function LineChart(props: LineChartProps) {
diff --git a/src/components/Charts/LineChart/index.tsx b/src/components/Charts/LineChart/index.tsx
index 2fefabd884cb..492b278697b7 100644
--- a/src/components/Charts/LineChart/index.tsx
+++ b/src/components/Charts/LineChart/index.tsx
@@ -2,7 +2,7 @@ import {WithSkiaWeb} from '@shopify/react-native-skia/lib/module/web';
 import React from 'react';
 import {View} from 'react-native';
 import ActivityIndicator from '@components/ActivityIndicator';
-import type {LineChartProps} from '@components/Charts/types';
+import type {LineChartProps} from './LineChartContent';
 import useThemeStyles from '@hooks/useThemeStyles';
 
 function LineChart(props: LineChartProps) {
diff --git a/src/components/Charts/ChartHeader.tsx b/src/components/Charts/components/ChartHeader.tsx
similarity index 100%
rename from src/components/Charts/ChartHeader.tsx
rename to src/components/Charts/components/ChartHeader.tsx
diff --git a/src/components/Charts/ChartTooltip.tsx b/src/components/Charts/components/ChartTooltip.tsx
similarity index 90%
rename from src/components/Charts/ChartTooltip.tsx
rename to src/components/Charts/components/ChartTooltip.tsx
index 5c61feef80f7..83e6d4b2f9db 100644
--- a/src/components/Charts/ChartTooltip.tsx
+++ b/src/components/Charts/components/ChartTooltip.tsx
@@ -3,7 +3,11 @@ import {View} from 'react-native';
 import Text from '@components/Text';
 import useTheme from '@hooks/useTheme';
 import useThemeStyles from '@hooks/useThemeStyles';
-import {TOOLTIP_POINTER_HEIGHT, TOOLTIP_POINTER_WIDTH} from './constants';
+/** The height of the chart tooltip pointer */
+const TOOLTIP_POINTER_HEIGHT = 4;
+
+/** The width of the chart tooltip pointer */
+const TOOLTIP_POINTER_WIDTH = 12;
 
 type ChartTooltipProps = {
     /** Label text (e.g., "Airfare", "Amazon") */
diff --git a/src/components/Charts/constants.ts b/src/components/Charts/constants.ts
index 7c597f66bb15..b8b987b9fe19 100644
--- a/src/components/Charts/constants.ts
+++ b/src/components/Charts/constants.ts
@@ -1,129 +1,19 @@
-import type {Color} from '@shopify/react-native-skia';
-import type {RoundedCorners} from 'victory-native';
-import colors from '@styles/theme/colors';
-
-/**
- * Chart color palette from Figma design.
- * Colors cycle when there are more data points than colors.
- */
-const CHART_COLORS: Color[] = [colors.yellow400, colors.tangerine400, colors.pink400, colors.green400, colors.ice400];
-
 /** Number of Y-axis ticks (including zero) */
 const Y_AXIS_TICK_COUNT = 5;
 
-/** Inner padding between bars (0.3 = 30% of bar width) */
-const BAR_INNER_PADDING = 0.3;
-
-/** Domain padding configuration for the chart */
-const DOMAIN_PADDING = {
-    left: 0,
-    right: 16,
-    top: 30,
-    bottom: 10,
-};
-
 /** Distance between Y-axis labels and the chart */
 const Y_AXIS_LABEL_OFFSET = 16;
 
-/** Rounded corners radius for bars */
-const BAR_CORNER_RADIUS = 8;
-
-/** Rounded corners configuration for bars */
-const BAR_ROUNDED_CORNERS: RoundedCorners = {
-    topLeft: BAR_CORNER_RADIUS,
-    topRight: BAR_CORNER_RADIUS,
-    bottomLeft: BAR_CORNER_RADIUS,
-    bottomRight: BAR_CORNER_RADIUS,
-};
-
 /** Chart padding */
 const CHART_PADDING = 5;
 
 /** Minimum height for the chart content area (bars, Y-axis, grid lines) */
 const CHART_CONTENT_MIN_HEIGHT = 250;
 
-/** Default bar color index when useSingleColor is true (ice blue) */
-const DEFAULT_SINGLE_BAR_COLOR_INDEX = 4;
-
-/** Safety buffer multiplier for domain padding calculation */
-const DOMAIN_PADDING_SAFETY_BUFFER = 1.1;
-
 /** Line width for X-axis (hidden) */
 const X_AXIS_LINE_WIDTH = 0;
 
 /** Line width for Y-axis grid lines */
 const Y_AXIS_LINE_WIDTH = 1;
 
-/** Line width for frame (hidden) */
-const FRAME_LINE_WIDTH = 0;
-
-/** The height of the chart tooltip pointer */
-const TOOLTIP_POINTER_HEIGHT = 4;
-
-/** The width of the chart tooltip pointer */
-const TOOLTIP_POINTER_WIDTH = 12;
-
-/** Gap between bar top and tooltip bottom */
-const TOOLTIP_BAR_GAP = 8;
-
-/** Rotation angle for X-axis labels - 45 degrees (in degrees) */
-const X_AXIS_LABEL_ROTATION_45 = -45;
-
-/** Rotation angle for X-axis labels - 90 degrees (in degrees) */
-const X_AXIS_LABEL_ROTATION_90 = -90;
-
-/** Sin of 45 degrees - used to calculate effective width of rotated labels */
-const SIN_45_DEGREES = Math.sin(Math.PI / 4); // ≈ 0.707
-
-/** Minimum padding between labels (in pixels) */
-const LABEL_PADDING = 4;
-
-/** Maximum ratio of container height that X-axis labels can occupy.
- * Victory allocates: fontHeight + yLabelOffset * 2 + rotateOffset.
- * With fontHeight ~12px and yLabelOffset = 16, base is ~44px.
- * This ratio limits total label area to prevent labels from taking too much space. */
-const X_AXIS_LABEL_MAX_HEIGHT_RATIO = 0.35;
-
-/** Ellipsis character for truncated labels */
-const LABEL_ELLIPSIS = '...';
-
-/** Inner dot radius for line chart data points */
-const DOT_INNER_RADIUS = 6;
-
-/** Outer dot radius for line chart data points (background ring) */
-const DOT_OUTER_RADIUS = 8;
-
-/** Y-axis domain anchored at zero for line charts */
-const Y_AXIS_DOMAIN: [number] = [0];
-
-/** Frame configuration for line charts - only left and bottom borders */
-const LINE_CHART_FRAME = {lineWidth: {left: 1, bottom: 1, top: 0, right: 0}};
-
-export {
-    CHART_COLORS,
-    Y_AXIS_TICK_COUNT,
-    BAR_INNER_PADDING,
-    DOMAIN_PADDING,
-    Y_AXIS_LABEL_OFFSET,
-    BAR_ROUNDED_CORNERS,
-    CHART_PADDING,
-    CHART_CONTENT_MIN_HEIGHT,
-    DEFAULT_SINGLE_BAR_COLOR_INDEX,
-    DOMAIN_PADDING_SAFETY_BUFFER,
-    X_AXIS_LINE_WIDTH,
-    Y_AXIS_LINE_WIDTH,
-    FRAME_LINE_WIDTH,
-    TOOLTIP_POINTER_HEIGHT,
-    TOOLTIP_POINTER_WIDTH,
-    TOOLTIP_BAR_GAP,
-    X_AXIS_LABEL_ROTATION_45,
-    X_AXIS_LABEL_ROTATION_90,
-    SIN_45_DEGREES,
-    LABEL_PADDING,
-    X_AXIS_LABEL_MAX_HEIGHT_RATIO,
-    LABEL_ELLIPSIS,
-    DOT_INNER_RADIUS,
-    DOT_OUTER_RADIUS,
-    Y_AXIS_DOMAIN,
-    LINE_CHART_FRAME,
-};
+export {Y_AXIS_TICK_COUNT, Y_AXIS_LABEL_OFFSET, CHART_PADDING, CHART_CONTENT_MIN_HEIGHT, X_AXIS_LINE_WIDTH, Y_AXIS_LINE_WIDTH};
diff --git a/src/components/Charts/hooks/index.ts b/src/components/Charts/hooks/index.ts
index fc5119b0a729..ac5272aa5b56 100644
--- a/src/components/Charts/hooks/index.ts
+++ b/src/components/Charts/hooks/index.ts
@@ -4,3 +4,5 @@ export {useChartInteractions} from './useChartInteractions';
 export type {HitTestArgs} from './useChartInteractions';
 export type {ChartInteractionState, ChartInteractionStateInit} from './useChartInteractionState';
 export {default as useChartLabelFormats} from './useChartLabelFormats';
+export {default as useDynamicYDomain} from './useDynamicYDomain';
+export {useTooltipData} from './useTooltipData';
diff --git a/src/components/Charts/hooks/useChartInteractions.ts b/src/components/Charts/hooks/useChartInteractions.ts
index e7a5422f5880..3cb17d796b71 100644
--- a/src/components/Charts/hooks/useChartInteractions.ts
+++ b/src/components/Charts/hooks/useChartInteractions.ts
@@ -3,9 +3,11 @@ import {Gesture} from 'react-native-gesture-handler';
 import type {SharedValue} from 'react-native-reanimated';
 import {useAnimatedReaction, useAnimatedStyle, useDerivedValue} from 'react-native-reanimated';
 import {scheduleOnRN} from 'react-native-worklets';
-import {TOOLTIP_BAR_GAP} from '@components/Charts/constants';
 import {useChartInteractionState} from './useChartInteractionState';
 
+/** Gap between bar top and tooltip bottom */
+const TOOLTIP_BAR_GAP = 8;
+
 /**
  * Arguments passed to the checkIsOver callback for hit-testing
  */
diff --git a/src/components/Charts/hooks/useChartLabelFormats.ts b/src/components/Charts/hooks/useChartLabelFormats.ts
index 3f6136c5d262..31921462d8b8 100644
--- a/src/components/Charts/hooks/useChartLabelFormats.ts
+++ b/src/components/Charts/hooks/useChartLabelFormats.ts
@@ -1,13 +1,10 @@
 import {useCallback} from 'react';
-
-type ChartDataPoint = {
-    label: string;
-};
+import type {ChartDataPoint, YAxisUnitPosition} from '@components/Charts/types';
 
 type UseChartLabelFormatsProps = {
     data: ChartDataPoint[];
     yAxisUnit?: string;
-    yAxisUnitPosition?: 'left' | 'right';
+    yAxisUnitPosition?: YAxisUnitPosition;
     labelSkipInterval: number;
     labelRotation: number;
     truncatedLabels: string[];
diff --git a/src/components/Charts/hooks/useChartLabelLayout.ts b/src/components/Charts/hooks/useChartLabelLayout.ts
index 215459488f8f..108aa027ed4f 100644
--- a/src/components/Charts/hooks/useChartLabelLayout.ts
+++ b/src/components/Charts/hooks/useChartLabelLayout.ts
@@ -1,14 +1,24 @@
 import type {SkFont} from '@shopify/react-native-skia';
 import {useMemo} from 'react';
-import {
-    LABEL_ELLIPSIS,
-    LABEL_PADDING,
-    SIN_45_DEGREES,
-    X_AXIS_LABEL_MAX_HEIGHT_RATIO,
-    X_AXIS_LABEL_ROTATION_45,
-    X_AXIS_LABEL_ROTATION_90,
-    Y_AXIS_LABEL_OFFSET,
-} from '@components/Charts/constants';
+import {Y_AXIS_LABEL_OFFSET} from '@components/Charts/constants';
+
+/** Rotation angle for X-axis labels - 45 degrees (in degrees) */
+const X_AXIS_LABEL_ROTATION_45 = -45;
+
+/** Rotation angle for X-axis labels - 90 degrees (in degrees) */
+const X_AXIS_LABEL_ROTATION_90 = -90;
+
+/** Sin of 45 degrees - used to calculate effective width of rotated labels */
+const SIN_45_DEGREES = Math.sin(Math.PI / 4); // ≈ 0.707
+
+/** Minimum padding between labels (in pixels) */
+const LABEL_PADDING = 4;
+
+/** Maximum ratio of container height that X-axis labels can occupy. */
+const X_AXIS_LABEL_MAX_HEIGHT_RATIO = 0.35;
+
+/** Ellipsis character for truncated labels */
+const LABEL_ELLIPSIS = '...';
 
 type ChartDataPoint = {
     label: string;
diff --git a/src/components/Charts/hooks/useDynamicYDomain.ts b/src/components/Charts/hooks/useDynamicYDomain.ts
new file mode 100644
index 000000000000..2868a317b540
--- /dev/null
+++ b/src/components/Charts/hooks/useDynamicYDomain.ts
@@ -0,0 +1,12 @@
+import {useMemo} from 'react';
+import type {ChartDataPoint} from '@components/Charts/types';
+
+/**
+ * Anchor Y-axis at zero so the baseline is always visible.
+ * When negative values are present, let victory-native auto-calculate the domain to avoid clipping.
+ */
+function useDynamicYDomain(data: ChartDataPoint[]): [number] | undefined {
+    return useMemo((): [number] | undefined => (data.some((point) => point.total < 0) ? undefined : [0]), [data]);
+}
+
+export default useDynamicYDomain;
diff --git a/src/components/Charts/hooks/useTooltipData.ts b/src/components/Charts/hooks/useTooltipData.ts
new file mode 100644
index 000000000000..3a75db14a106
--- /dev/null
+++ b/src/components/Charts/hooks/useTooltipData.ts
@@ -0,0 +1,41 @@
+import {useMemo} from 'react';
+import type {ChartDataPoint, YAxisUnitPosition} from '@components/Charts/types';
+
+type TooltipData = {
+    label: string;
+    amount: string;
+    percentage: string;
+};
+
+/**
+ * Formats tooltip content for the active chart data point.
+ * Computes the display amount (with optional currency unit) and the percentage relative to all data points.
+ */
+function useTooltipData(activeDataIndex: number, data: ChartDataPoint[], yAxisUnit?: string, yAxisUnitPosition?: YAxisUnitPosition): TooltipData | null {
+    return useMemo(() => {
+        if (activeDataIndex < 0 || activeDataIndex >= data.length) {
+            return null;
+        }
+        const dataPoint = data.at(activeDataIndex);
+        if (!dataPoint) {
+            return null;
+        }
+        const formatted = dataPoint.total.toLocaleString();
+        let formattedAmount = formatted;
+        if (yAxisUnit) {
+            // Add space for multi-character codes (e.g., "PLN 100") but not for symbols (e.g., "$100")
+            const separator = yAxisUnit.length > 1 ? ' ' : '';
+            formattedAmount = yAxisUnitPosition === 'left' ? `${yAxisUnit}${separator}${formatted}` : `${formatted}${separator}${yAxisUnit}`;
+        }
+        const totalSum = data.reduce((sum, point) => sum + Math.abs(point.total), 0);
+        const percent = totalSum > 0 ? Math.round((Math.abs(dataPoint.total) / totalSum) * 100) : 0;
+        return {
+            label: dataPoint.label,
+            amount: formattedAmount,
+            percentage: percent < 1 ? '<1%' : `${percent}%`,
+        };
+    }, [activeDataIndex, data, yAxisUnit, yAxisUnitPosition]);
+}
+
+export {useTooltipData};
+export type {TooltipData};
diff --git a/src/components/Charts/index.ts b/src/components/Charts/index.ts
index 5804e553c8f4..85629c33083b 100644
--- a/src/components/Charts/index.ts
+++ b/src/components/Charts/index.ts
@@ -1,7 +1,9 @@
 import BarChart from './BarChart';
-import ChartHeader from './ChartHeader';
-import ChartTooltip from './ChartTooltip';
+import ChartHeader from './components/ChartHeader';
+import ChartTooltip from './components/ChartTooltip';
 import LineChart from './LineChart';
 
 export {BarChart, ChartHeader, ChartTooltip, LineChart};
-export type {BarChartDataPoint, BarChartProps, LineChartDataPoint, LineChartProps} from './types';
+export type {ChartDataPoint, CartesianChartProps} from './types';
+export type {BarChartProps} from './BarChart/BarChartContent';
+export type {LineChartProps} from './LineChart/LineChartContent';
diff --git a/src/components/Charts/types.ts b/src/components/Charts/types.ts
index 678a0dd58b98..da769fd2aeac 100644
--- a/src/components/Charts/types.ts
+++ b/src/components/Charts/types.ts
@@ -1,24 +1,21 @@
 import type IconAsset from '@src/types/utils/IconAsset';
 
-type BarChartDataPoint = {
-    /** Label displayed under the bar (e.g., "Amazon", "Travel", "Nov 2025") */
+type ChartDataPoint = {
+    /** Label displayed under the data point (e.g., "Amazon", "Nov 2025") */
     label: string;
 
     /** Total amount (pre-formatted, e.g., dollars not cents) */
     total: number;
 
-    /** Currency code for formatting */
-    currency: string;
-
-    /** Query string for navigation when bar is clicked (optional) */
+    /** Query string for navigation when data point is clicked (optional) */
     onClickQuery?: string;
 };
 
-type BarChartProps = {
+type CartesianChartProps = {
     /** Data points to display */
-    data: BarChartDataPoint[];
+    data: ChartDataPoint[];
 
-    /** Chart title (e.g., "Top Categories", "Spend by Merchant") */
+    /** Chart title (e.g., "Top Categories", "Spend over time") */
     title?: string;
 
     /** Icon displayed next to the title */
@@ -27,51 +24,13 @@ type BarChartProps = {
     /** Whether data is loading */
     isLoading?: boolean;
 
-    /** Callback when a bar is pressed */
-    onBarPress?: (dataPoint: BarChartDataPoint, index: number) => void;
-
     /** Symbol/unit for Y-axis labels (e.g., '$', '€', 'zł'). Empty string or undefined shows raw numbers. */
     yAxisUnit?: string;
 
     /** Position of the unit symbol relative to the value. Defaults to 'left'. */
-    yAxisUnitPosition?: 'left' | 'right';
-
-    /** When true, all bars use the same color. When false (default), each bar uses a different color from the palette. */
-    useSingleColor?: boolean;
-};
-
-type LineChartDataPoint = {
-    /** Label displayed under the data point (e.g., "Nov 2025", "Week 3") */
-    label: string;
-
-    /** Total amount (pre-formatted, e.g., dollars not cents) */
-    total: number;
-
-    /** Query string for navigation when point is clicked (optional) */
-    onClickQuery?: string;
+    yAxisUnitPosition?: YAxisUnitPosition;
 };
 
-type LineChartProps = {
-    /** Data points to display */
-    data: LineChartDataPoint[];
-
-    /** Chart title (e.g., "Spend over time") */
-    title?: string;
-
-    /** Icon displayed next to the title */
-    titleIcon?: IconAsset;
-
-    /** Whether data is loading */
-    isLoading?: boolean;
-
-    /** Callback when a data point is pressed */
-    onPointPress?: (dataPoint: LineChartDataPoint, index: number) => void;
-
-    /** Symbol/unit for Y-axis labels (e.g., '$', '€', 'zł'). Empty string or undefined shows raw numbers. */
-    yAxisUnit?: string;
-
-    /** Position of the unit symbol relative to the value. Defaults to 'left'. */
-    yAxisUnitPosition?: 'left' | 'right';
-};
+type YAxisUnitPosition = 'left' | 'right';
 
-export type {BarChartDataPoint, BarChartProps, LineChartDataPoint, LineChartProps};
+export type {ChartDataPoint, CartesianChartProps, YAxisUnitPosition};
diff --git a/src/components/Charts/chartColors.ts b/src/components/Charts/utils.ts
similarity index 85%
rename from src/components/Charts/chartColors.ts
rename to src/components/Charts/utils.ts
index c34ea720c040..f14e4d5efbd0 100644
--- a/src/components/Charts/chartColors.ts
+++ b/src/components/Charts/utils.ts
@@ -36,4 +36,7 @@ function getChartColor(index: number): string {
     return CHART_PALETTE.at(index % CHART_PALETTE.length) ?? colors.black;
 }
 
-export {CHART_PALETTE, getChartColor};
+/** Default color used for single-color charts (e.g., line chart, single-color bar chart) */
+const DEFAULT_CHART_COLOR = getChartColor(4);
+
+export {getChartColor, DEFAULT_CHART_COLOR};
diff --git a/src/components/Search/SearchBarChart.tsx b/src/components/Search/SearchBarChart.tsx
index 68d1e2f89e1a..a8b012167ade 100644
--- a/src/components/Search/SearchBarChart.tsx
+++ b/src/components/Search/SearchBarChart.tsx
@@ -1,6 +1,6 @@
 import React, {useCallback, useMemo} from 'react';
 import {BarChart} from '@components/Charts';
-import type {BarChartDataPoint} from '@components/Charts';
+import type {ChartDataPoint} from '@components/Charts';
 import type {
     TransactionCardGroupListItemType,
     TransactionCategoryGroupListItemType,
@@ -44,7 +44,7 @@ type SearchBarChartProps = {
 
 function SearchBarChart({data, title, titleIcon, getLabel, getFilterQuery, onBarPress, isLoading, yAxisUnit, yAxisUnitPosition}: SearchBarChartProps) {
     // Transform grouped transaction data to BarChart format
-    const chartData: BarChartDataPoint[] = useMemo(() => {
+    const chartData: ChartDataPoint[] = useMemo(() => {
         return data.map((item) => {
             const groupedItem = item as GroupedItem;
             const currency = groupedItem.currency ?? 'USD';
@@ -53,13 +53,12 @@ function SearchBarChart({data, title, titleIcon, getLabel, getFilterQuery, onBar
             return {
                 label: getLabel(groupedItem),
                 total: totalInDisplayUnits,
-                currency,
             };
         });
     }, [data, getLabel]);
 
     const handleBarPress = useCallback(
-        (dataPoint: BarChartDataPoint, index: number) => {
+        (dataPoint: ChartDataPoint, index: number) => {
             if (!onBarPress) {
                 return;
             }
diff --git a/src/components/Search/SearchLineChart.tsx b/src/components/Search/SearchLineChart.tsx
index efe030eeaeb7..a726dddf4641 100644
--- a/src/components/Search/SearchLineChart.tsx
+++ b/src/components/Search/SearchLineChart.tsx
@@ -1,6 +1,6 @@
 import React, {useCallback, useMemo} from 'react';
 import {LineChart} from '@components/Charts';
-import type {LineChartDataPoint} from '@components/Charts';
+import type {ChartDataPoint} from '@components/Charts';
 import type {
     TransactionCardGroupListItemType,
     TransactionCategoryGroupListItemType,
@@ -43,7 +43,7 @@ type SearchLineChartProps = {
 };
 
 function SearchLineChart({data, title, titleIcon, getLabel, getFilterQuery, onBarPress, isLoading, yAxisUnit, yAxisUnitPosition}: SearchLineChartProps) {
-    const chartData: LineChartDataPoint[] = useMemo(() => {
+    const chartData: ChartDataPoint[] = useMemo(() => {
         return data.map((item) => {
             const groupedItem = item as GroupedItem;
             const currency = groupedItem.currency ?? 'USD';
@@ -57,7 +57,7 @@ function SearchLineChart({data, title, titleIcon, getLabel, getFilterQuery, onBa
     }, [data, getLabel]);
 
     const handlePointPress = useCallback(
-        (dataPoint: LineChartDataPoint, index: number) => {
+        (dataPoint: ChartDataPoint, index: number) => {
             if (!onBarPress) {
                 return;
             }

From 2c42fde7dce08b7863e1a471eba1015eef9d9320 Mon Sep 17 00:00:00 2001
From: Mateusz Rajski <mateusz.rajski@swmansion.com>
Date: Fri, 30 Jan 2026 13:38:14 -0800
Subject: [PATCH 04/26] Add remaining translations

---
 src/CONST/index.ts       | 1 -
 src/languages/de.ts      | 2 +-
 src/languages/en.ts      | 1 -
 src/languages/es.ts      | 2 +-
 src/languages/fr.ts      | 2 +-
 src/languages/it.ts      | 2 +-
 src/languages/ja.ts      | 2 +-
 src/languages/nl.ts      | 2 +-
 src/languages/pl.ts      | 2 +-
 src/languages/pt-BR.ts   | 2 +-
 src/languages/zh-hans.ts | 2 +-
 11 files changed, 9 insertions(+), 11 deletions(-)

diff --git a/src/CONST/index.ts b/src/CONST/index.ts
index f7398158f345..4dba9ffdc9bf 100755
--- a/src/CONST/index.ts
+++ b/src/CONST/index.ts
@@ -7183,7 +7183,6 @@ const CONST = {
             TABLE: 'table',
             BAR: 'bar',
             LINE: 'line',
-            PIE: 'pie',
         },
         SYNTAX_FILTER_KEYS: {
             TYPE: 'type',
diff --git a/src/languages/de.ts b/src/languages/de.ts
index 8da341287e34..755ffefffc84 100644
--- a/src/languages/de.ts
+++ b/src/languages/de.ts
@@ -7079,7 +7079,7 @@ Fordere Spesendetails wie Belege und Beschreibungen an, lege Limits und Standard
             allMatchingItemsSelected: 'Alle passenden Elemente ausgewählt',
         },
         topSpenders: 'Top-Ausgaben',
-        view: {label: 'Ansehen', table: 'Tabelle', bar: 'Bar'},
+        view: {label: 'Ansehen', table: 'Tabelle', bar: 'Bar', line: 'Linie'},
         chartTitles: {
             [CONST.SEARCH.GROUP_BY.FROM]: 'Von',
             [CONST.SEARCH.GROUP_BY.CARD]: 'Karten',
diff --git a/src/languages/en.ts b/src/languages/en.ts
index 33ce71189d76..54690165917b 100644
--- a/src/languages/en.ts
+++ b/src/languages/en.ts
@@ -6982,7 +6982,6 @@ const translations = {
             table: 'Table',
             bar: 'Bar',
             line: 'Line',
-            pie: 'Pie',
         },
         chartTitles: {
             [CONST.SEARCH.GROUP_BY.FROM]: 'From',
diff --git a/src/languages/es.ts b/src/languages/es.ts
index 550115b5b902..fbf00adfaca2 100644
--- a/src/languages/es.ts
+++ b/src/languages/es.ts
@@ -6639,7 +6639,7 @@ ${amount} para ${merchant} - ${date}`,
         unapprovedCard: 'Tarjeta no aprobada',
         reconciliation: 'Conciliación',
         topSpenders: 'Mayores gastadores',
-        view: {label: 'Ver', table: 'Tabla', bar: 'Barra'},
+        view: {label: 'Ver', table: 'Tabla', bar: 'Barra', line: 'Línea'},
         saveSearch: 'Guardar búsqueda',
         savedSearchesMenuItemTitle: 'Guardadas',
         topCategories: 'Categorías principales',
diff --git a/src/languages/fr.ts b/src/languages/fr.ts
index 726c88e995a4..ca9980563cc1 100644
--- a/src/languages/fr.ts
+++ b/src/languages/fr.ts
@@ -7091,7 +7091,7 @@ Exigez des informations de dépense comme les reçus et les descriptions, défin
             allMatchingItemsSelected: 'Tous les éléments correspondants sont sélectionnés',
         },
         topSpenders: 'Plus gros dépensiers',
-        view: {label: 'Afficher', table: 'Tableau', bar: 'Barre'},
+        view: {label: 'Afficher', table: 'Tableau', bar: 'Barre', line: 'Ligne'},
         chartTitles: {
             [CONST.SEARCH.GROUP_BY.FROM]: 'De',
             [CONST.SEARCH.GROUP_BY.CARD]: 'Cartes',
diff --git a/src/languages/it.ts b/src/languages/it.ts
index 33c0428bfdf4..64063d39b9fc 100644
--- a/src/languages/it.ts
+++ b/src/languages/it.ts
@@ -7068,7 +7068,7 @@ Richiedi dettagli di spesa come ricevute e descrizioni, imposta limiti e valori
             allMatchingItemsSelected: 'Tutti gli elementi corrispondenti selezionati',
         },
         topSpenders: 'Maggiori spenditori',
-        view: {label: 'Visualizza', table: 'Tabella', bar: 'Bar'},
+        view: {label: 'Visualizza', table: 'Tabella', bar: 'Bar', line: 'Linea'},
         chartTitles: {
             [CONST.SEARCH.GROUP_BY.FROM]: 'Da',
             [CONST.SEARCH.GROUP_BY.CARD]: 'Carte',
diff --git a/src/languages/ja.ts b/src/languages/ja.ts
index 761da2729ec5..85f5160d36d7 100644
--- a/src/languages/ja.ts
+++ b/src/languages/ja.ts
@@ -7006,7 +7006,7 @@ ${reportName}
             allMatchingItemsSelected: '一致する項目をすべて選択済み',
         },
         topSpenders: 'トップ支出者',
-        view: {label: '表示', table: 'テーブル', bar: 'バー'},
+        view: {label: '表示', table: 'テーブル', bar: 'バー', line: '折れ線'},
         chartTitles: {
             [CONST.SEARCH.GROUP_BY.FROM]: '差出人',
             [CONST.SEARCH.GROUP_BY.CARD]: 'カード',
diff --git a/src/languages/nl.ts b/src/languages/nl.ts
index f4f2720e3872..fb4d0a2dcd89 100644
--- a/src/languages/nl.ts
+++ b/src/languages/nl.ts
@@ -7051,7 +7051,7 @@ Vraag verplichte uitgavedetails zoals bonnetjes en beschrijvingen, stel limieten
             allMatchingItemsSelected: 'Alle overeenkomende items geselecteerd',
         },
         topSpenders: 'Grootste uitgaven',
-        view: {label: 'Bekijken', table: 'Tabel', bar: 'Bar'},
+        view: {label: 'Bekijken', table: 'Tabel', bar: 'Bar', line: 'Lijn'},
         chartTitles: {
             [CONST.SEARCH.GROUP_BY.FROM]: 'Van',
             [CONST.SEARCH.GROUP_BY.CARD]: 'Kaarten',
diff --git a/src/languages/pl.ts b/src/languages/pl.ts
index 7c790e3888ce..697f0a768c83 100644
--- a/src/languages/pl.ts
+++ b/src/languages/pl.ts
@@ -7039,7 +7039,7 @@ Wymagaj szczegółów wydatków, takich jak paragony i opisy, ustawiaj limity i
             allMatchingItemsSelected: 'Wybrano wszystkie pasujące elementy',
         },
         topSpenders: 'Najwięksi wydający',
-        view: {label: 'Zobacz', table: 'Tabela', bar: 'Pasek'},
+        view: {label: 'Zobacz', table: 'Tabela', bar: 'Pasek', line: 'Linia'},
         chartTitles: {
             [CONST.SEARCH.GROUP_BY.FROM]: 'Od',
             [CONST.SEARCH.GROUP_BY.CARD]: 'Karty',
diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts
index 84ec54156619..8077b928f3d9 100644
--- a/src/languages/pt-BR.ts
+++ b/src/languages/pt-BR.ts
@@ -7040,7 +7040,7 @@ Exija detalhes de despesas como recibos e descrições, defina limites e padrõe
             allMatchingItemsSelected: 'Todos os itens correspondentes selecionados',
         },
         topSpenders: 'Maiores gastadores',
-        view: {label: 'Ver', table: 'Tabela', bar: 'Bar'},
+        view: {label: 'Ver', table: 'Tabela', bar: 'Bar', line: 'Linha'},
         chartTitles: {
             [CONST.SEARCH.GROUP_BY.FROM]: 'De',
             [CONST.SEARCH.GROUP_BY.CARD]: 'Cartões',
diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts
index 7630d11c09fb..70aac591ce31 100644
--- a/src/languages/zh-hans.ts
+++ b/src/languages/zh-hans.ts
@@ -6886,7 +6886,7 @@ ${reportName}
             allMatchingItemsSelected: '已选择所有匹配的项目',
         },
         topSpenders: '最高支出者',
-        view: {label: '查看', table: '表格', bar: '栏'},
+        view: {label: '查看', table: '表格', bar: '栏', line: '折线'},
         chartTitles: {
             [CONST.SEARCH.GROUP_BY.FROM]: '来自',
             [CONST.SEARCH.GROUP_BY.CARD]: '卡片',

From acec8e616d31a4d36b29df130fb3f8d366c87501 Mon Sep 17 00:00:00 2001
From: Mateusz Rajski <mateusz.rajski@swmansion.com>
Date: Fri, 30 Jan 2026 13:49:33 -0800
Subject: [PATCH 05/26] Fix default color

---
 src/components/Charts/utils.ts | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/components/Charts/utils.ts b/src/components/Charts/utils.ts
index f14e4d5efbd0..f56cd568b28c 100644
--- a/src/components/Charts/utils.ts
+++ b/src/components/Charts/utils.ts
@@ -37,6 +37,6 @@ function getChartColor(index: number): string {
 }
 
 /** Default color used for single-color charts (e.g., line chart, single-color bar chart) */
-const DEFAULT_CHART_COLOR = getChartColor(4);
+const DEFAULT_CHART_COLOR = getChartColor(5);
 
 export {getChartColor, DEFAULT_CHART_COLOR};

From 03dc033b6bb9607a64042e50f2c543747f6beddf Mon Sep 17 00:00:00 2001
From: Mateusz Rajski <mateusz.rajski@swmansion.com>
Date: Fri, 30 Jan 2026 13:57:25 -0800
Subject: [PATCH 06/26] Move GroupedItem to common types

---
 src/components/Search/SearchBarChart.tsx  | 11 ++------
 src/components/Search/SearchChartView.tsx | 28 ++------------------
 src/components/Search/SearchLineChart.tsx | 11 ++------
 src/components/Search/types.ts            | 31 ++++++++++++++++++++++-
 4 files changed, 36 insertions(+), 45 deletions(-)

diff --git a/src/components/Search/SearchBarChart.tsx b/src/components/Search/SearchBarChart.tsx
index a8b012167ade..874ee22f6393 100644
--- a/src/components/Search/SearchBarChart.tsx
+++ b/src/components/Search/SearchBarChart.tsx
@@ -1,17 +1,10 @@
 import React, {useCallback, useMemo} from 'react';
 import {BarChart} from '@components/Charts';
 import type {ChartDataPoint} from '@components/Charts';
-import type {
-    TransactionCardGroupListItemType,
-    TransactionCategoryGroupListItemType,
-    TransactionGroupListItemType,
-    TransactionMemberGroupListItemType,
-    TransactionWithdrawalIDGroupListItemType,
-} from '@components/SelectionListWithSections/types';
+import type {TransactionGroupListItemType} from '@components/SelectionListWithSections/types';
 import {convertToFrontendAmountAsInteger} from '@libs/CurrencyUtils';
 import type IconAsset from '@src/types/utils/IconAsset';
-
-type GroupedItem = TransactionMemberGroupListItemType | TransactionCardGroupListItemType | TransactionWithdrawalIDGroupListItemType | TransactionCategoryGroupListItemType;
+import type {GroupedItem} from './types';
 
 type SearchBarChartProps = {
     /** Grouped transaction data from search results */
diff --git a/src/components/Search/SearchChartView.tsx b/src/components/Search/SearchChartView.tsx
index 0d5cde51e236..f57b66cdaaf2 100644
--- a/src/components/Search/SearchChartView.tsx
+++ b/src/components/Search/SearchChartView.tsx
@@ -2,19 +2,7 @@ import React, {useCallback, useMemo} from 'react';
 import type {NativeScrollEvent, NativeSyntheticEvent} from 'react-native';
 import {View} from 'react-native';
 import Animated from 'react-native-reanimated';
-import type {
-    TransactionCardGroupListItemType,
-    TransactionCategoryGroupListItemType,
-    TransactionGroupListItemType,
-    TransactionMemberGroupListItemType,
-    TransactionMerchantGroupListItemType,
-    TransactionMonthGroupListItemType,
-    TransactionQuarterGroupListItemType,
-    TransactionTagGroupListItemType,
-    TransactionWeekGroupListItemType,
-    TransactionWithdrawalIDGroupListItemType,
-    TransactionYearGroupListItemType,
-} from '@components/SelectionListWithSections/types';
+import type {TransactionGroupListItemType} from '@components/SelectionListWithSections/types';
 import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset';
 import useLocalize from '@hooks/useLocalize';
 import useResponsiveLayout from '@hooks/useResponsiveLayout';
@@ -28,19 +16,7 @@ import CONST from '@src/CONST';
 import ROUTES from '@src/ROUTES';
 import SearchBarChart from './SearchBarChart';
 import SearchLineChart from './SearchLineChart';
-import type {ChartView, SearchGroupBy, SearchQueryJSON} from './types';
-
-type GroupedItem =
-    | TransactionMemberGroupListItemType
-    | TransactionCardGroupListItemType
-    | TransactionWithdrawalIDGroupListItemType
-    | TransactionCategoryGroupListItemType
-    | TransactionMerchantGroupListItemType
-    | TransactionTagGroupListItemType
-    | TransactionMonthGroupListItemType
-    | TransactionWeekGroupListItemType
-    | TransactionYearGroupListItemType
-    | TransactionQuarterGroupListItemType;
+import type {ChartView, GroupedItem, SearchGroupBy, SearchQueryJSON} from './types';
 
 type ChartGroupByConfig = {
     titleIconName: 'Users' | 'CreditCard' | 'Send' | 'Folder' | 'Basket' | 'Tag' | 'Calendar';
diff --git a/src/components/Search/SearchLineChart.tsx b/src/components/Search/SearchLineChart.tsx
index a726dddf4641..d83607b7cc02 100644
--- a/src/components/Search/SearchLineChart.tsx
+++ b/src/components/Search/SearchLineChart.tsx
@@ -1,17 +1,10 @@
 import React, {useCallback, useMemo} from 'react';
 import {LineChart} from '@components/Charts';
 import type {ChartDataPoint} from '@components/Charts';
-import type {
-    TransactionCardGroupListItemType,
-    TransactionCategoryGroupListItemType,
-    TransactionGroupListItemType,
-    TransactionMemberGroupListItemType,
-    TransactionWithdrawalIDGroupListItemType,
-} from '@components/SelectionListWithSections/types';
+import type {TransactionGroupListItemType} from '@components/SelectionListWithSections/types';
 import {convertToFrontendAmountAsInteger} from '@libs/CurrencyUtils';
 import type IconAsset from '@src/types/utils/IconAsset';
-
-type GroupedItem = TransactionMemberGroupListItemType | TransactionCardGroupListItemType | TransactionWithdrawalIDGroupListItemType | TransactionCategoryGroupListItemType;
+import type {GroupedItem} from './types';
 
 type SearchLineChartProps = {
     /** Grouped transaction data from search results */
diff --git a/src/components/Search/types.ts b/src/components/Search/types.ts
index 953aaca2848f..a232c0463ec8 100644
--- a/src/components/Search/types.ts
+++ b/src/components/Search/types.ts
@@ -1,6 +1,21 @@
 import type {ValueOf} from 'type-fest';
 import type {PaymentMethod} from '@components/KYCWall/types';
-import type {ReportActionListItemType, TaskListItemType, TransactionGroupListItemType, TransactionListItemType} from '@components/SelectionListWithSections/types';
+import type {
+    ReportActionListItemType,
+    TaskListItemType,
+    TransactionCardGroupListItemType,
+    TransactionCategoryGroupListItemType,
+    TransactionGroupListItemType,
+    TransactionListItemType,
+    TransactionMemberGroupListItemType,
+    TransactionMerchantGroupListItemType,
+    TransactionMonthGroupListItemType,
+    TransactionQuarterGroupListItemType,
+    TransactionTagGroupListItemType,
+    TransactionWeekGroupListItemType,
+    TransactionWithdrawalIDGroupListItemType,
+    TransactionYearGroupListItemType,
+} from '@components/SelectionListWithSections/types';
 import type {SearchKey} from '@libs/SearchUIUtils';
 import type CONST from '@src/CONST';
 import type {Report, ReportAction, SearchResults, Transaction} from '@src/types/onyx';
@@ -307,6 +322,19 @@ type BankAccountMenuItem = {
     value: PaymentMethod;
 };
 
+/** Union type representing all possible grouped transaction item types used in chart views */
+type GroupedItem =
+    | TransactionMemberGroupListItemType
+    | TransactionCardGroupListItemType
+    | TransactionWithdrawalIDGroupListItemType
+    | TransactionCategoryGroupListItemType
+    | TransactionMerchantGroupListItemType
+    | TransactionTagGroupListItemType
+    | TransactionMonthGroupListItemType
+    | TransactionWeekGroupListItemType
+    | TransactionYearGroupListItemType
+    | TransactionQuarterGroupListItemType;
+
 export type {
     SelectedTransactionInfo,
     SelectedTransactions,
@@ -353,4 +381,5 @@ export type {
     SearchTextFilterKeys,
     BankAccountMenuItem,
     SearchCustomColumnIds,
+    GroupedItem,
 };

From e53afcd53e9cd6d1c544771027e7dbfb9f5e30c5 Mon Sep 17 00:00:00 2001
From: Mateusz Rajski <mateusz.rajski@swmansion.com>
Date: Fri, 30 Jan 2026 14:05:36 -0800
Subject: [PATCH 07/26] Fix domain padding in BarChart

---
 src/components/Charts/BarChart/BarChartContent.tsx | 11 ++++-------
 1 file changed, 4 insertions(+), 7 deletions(-)

diff --git a/src/components/Charts/BarChart/BarChartContent.tsx b/src/components/Charts/BarChart/BarChartContent.tsx
index 08cffc68f0c6..e3e6be5e8102 100644
--- a/src/components/Charts/BarChart/BarChartContent.tsx
+++ b/src/components/Charts/BarChart/BarChartContent.tsx
@@ -37,12 +37,9 @@ type BarChartProps = CartesianChartProps & {
 /** Inner padding between bars (0.3 = 30% of bar width) */
 const BAR_INNER_PADDING = 0.3;
 
-/** Domain padding configuration for the bar chart */
+/** Extra pixel spacing between the chart boundary and the data range, applied per side (Victory's `domainPadding` prop) */
 const DOMAIN_PADDING = {
-    left: 0,
-    right: 16,
-    top: 30,
-    bottom: 10,
+    top: 32,
 };
 
 /** Safety buffer multiplier for domain padding calculation */
@@ -114,10 +111,10 @@ function BarChartContent({data, title, titleIcon, isLoading, yAxisUnit, yAxisUni
 
     const domainPadding = useMemo(() => {
         if (chartWidth === 0) {
-            return {left: 0, right: 0, top: DOMAIN_PADDING.top, bottom: DOMAIN_PADDING.bottom};
+            return {...DOMAIN_PADDING, left: 0, right: 0};
         }
         const horizontalPadding = calculateMinDomainPadding(chartWidth, data.length, BAR_INNER_PADDING);
-        return {left: horizontalPadding, right: horizontalPadding + DOMAIN_PADDING.right, top: DOMAIN_PADDING.top, bottom: DOMAIN_PADDING.bottom};
+        return {...DOMAIN_PADDING, left: horizontalPadding, right: horizontalPadding + DOMAIN_PADDING.right};
     }, [chartWidth, data.length]);
 
     const {formatXAxisLabel, formatYAxisLabel} = useChartLabelFormats({

From d5966d6d8cdf6c2662d5775a2f4813807a40833d Mon Sep 17 00:00:00 2001
From: Mateusz Rajski <mateusz.rajski@swmansion.com>
Date: Fri, 30 Jan 2026 14:09:24 -0800
Subject: [PATCH 08/26] Fix BarChart domain padding

---
 src/components/Charts/BarChart/BarChartContent.tsx | 5 ++++-
 1 file changed, 4 insertions(+), 1 deletion(-)

diff --git a/src/components/Charts/BarChart/BarChartContent.tsx b/src/components/Charts/BarChart/BarChartContent.tsx
index e3e6be5e8102..5b52b9913f6b 100644
--- a/src/components/Charts/BarChart/BarChartContent.tsx
+++ b/src/components/Charts/BarChart/BarChartContent.tsx
@@ -40,6 +40,9 @@ const BAR_INNER_PADDING = 0.3;
 /** Extra pixel spacing between the chart boundary and the data range, applied per side (Victory's `domainPadding` prop) */
 const DOMAIN_PADDING = {
     top: 32,
+    bottom: 0,
+    left: 0,
+    right: 0,
 };
 
 /** Safety buffer multiplier for domain padding calculation */
@@ -114,7 +117,7 @@ function BarChartContent({data, title, titleIcon, isLoading, yAxisUnit, yAxisUni
             return {...DOMAIN_PADDING, left: 0, right: 0};
         }
         const horizontalPadding = calculateMinDomainPadding(chartWidth, data.length, BAR_INNER_PADDING);
-        return {...DOMAIN_PADDING, left: horizontalPadding, right: horizontalPadding + DOMAIN_PADDING.right};
+        return {...DOMAIN_PADDING, right: horizontalPadding + DOMAIN_PADDING.right, left: horizontalPadding};
     }, [chartWidth, data.length]);
 
     const {formatXAxisLabel, formatYAxisLabel} = useChartLabelFormats({

From d3f8df9ee279954c8ce9a206122ed44baa6fc1a4 Mon Sep 17 00:00:00 2001
From: Mateusz Rajski <mateusz.rajski@swmansion.com>
Date: Fri, 30 Jan 2026 14:12:33 -0800
Subject: [PATCH 09/26] Bring back removed type imports

---
 src/components/Search/SearchChartView.tsx | 14 +++++++++++++-
 1 file changed, 13 insertions(+), 1 deletion(-)

diff --git a/src/components/Search/SearchChartView.tsx b/src/components/Search/SearchChartView.tsx
index f57b66cdaaf2..32cb4f9385f9 100644
--- a/src/components/Search/SearchChartView.tsx
+++ b/src/components/Search/SearchChartView.tsx
@@ -2,7 +2,19 @@ import React, {useCallback, useMemo} from 'react';
 import type {NativeScrollEvent, NativeSyntheticEvent} from 'react-native';
 import {View} from 'react-native';
 import Animated from 'react-native-reanimated';
-import type {TransactionGroupListItemType} from '@components/SelectionListWithSections/types';
+import type {
+    TransactionCardGroupListItemType,
+    TransactionCategoryGroupListItemType,
+    TransactionGroupListItemType,
+    TransactionMemberGroupListItemType,
+    TransactionMerchantGroupListItemType,
+    TransactionMonthGroupListItemType,
+    TransactionQuarterGroupListItemType,
+    TransactionTagGroupListItemType,
+    TransactionWeekGroupListItemType,
+    TransactionWithdrawalIDGroupListItemType,
+    TransactionYearGroupListItemType,
+} from '@components/SelectionListWithSections/types';
 import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset';
 import useLocalize from '@hooks/useLocalize';
 import useResponsiveLayout from '@hooks/useResponsiveLayout';

From c6435780ad0ff665a80063d002d9579b5b015963 Mon Sep 17 00:00:00 2001
From: Mateusz Rajski <mateusz.rajski@swmansion.com>
Date: Fri, 30 Jan 2026 14:30:03 -0800
Subject: [PATCH 10/26] Address comments from previous PR

---
 .../Charts/components/ChartTooltip.tsx        |  1 +
 .../Charts/hooks/useChartInteractionState.ts  | 26 +++------------
 .../Charts/hooks/useChartInteractions.ts      | 32 +++++--------------
 .../Charts/hooks/useChartLabelLayout.ts       |  4 +--
 src/components/Charts/utils.ts                | 16 ++++++----
 src/components/Search/SearchBarChart.tsx      | 16 +++++-----
 src/components/Search/SearchChartView.tsx     |  8 ++---
 src/components/Search/SearchLineChart.tsx     | 16 +++++-----
 8 files changed, 43 insertions(+), 76 deletions(-)

diff --git a/src/components/Charts/components/ChartTooltip.tsx b/src/components/Charts/components/ChartTooltip.tsx
index 83e6d4b2f9db..be005042afb5 100644
--- a/src/components/Charts/components/ChartTooltip.tsx
+++ b/src/components/Charts/components/ChartTooltip.tsx
@@ -3,6 +3,7 @@ import {View} from 'react-native';
 import Text from '@components/Text';
 import useTheme from '@hooks/useTheme';
 import useThemeStyles from '@hooks/useThemeStyles';
+
 /** The height of the chart tooltip pointer */
 const TOOLTIP_POINTER_HEIGHT = 4;
 
diff --git a/src/components/Charts/hooks/useChartInteractionState.ts b/src/components/Charts/hooks/useChartInteractionState.ts
index 344832485fe1..a2d15a86e4d6 100644
--- a/src/components/Charts/hooks/useChartInteractionState.ts
+++ b/src/components/Charts/hooks/useChartInteractionState.ts
@@ -22,13 +22,16 @@ type ChartInteractionStateInit = {
 type ChartInteractionState<Init extends ChartInteractionStateInit> = {
     /** Whether interaction (hover/press) is currently active */
     isActive: SharedValue<boolean>;
+
     /** Index of the matched data point (-1 if none) */
     matchedIndex: SharedValue<number>;
+
     /** X-axis value and position */
     x: {
         value: SharedValue<Init['x']>;
         position: SharedValue<number>;
     };
+
     /** Y-axis values and positions for each y key */
     y: Record<
         keyof Init['y'],
@@ -37,8 +40,10 @@ type ChartInteractionState<Init extends ChartInteractionStateInit> = {
             position: SharedValue<number>;
         }
     >;
+
     /** Y index for stacked bar charts */
     yIndex: SharedValue<number>;
+
     /** Raw cursor position */
     cursor: {
         x: SharedValue<number>;
@@ -68,27 +73,6 @@ function useIsInteractionActive<Init extends ChartInteractionStateInit>(state: C
 /**
  * Creates shared state for chart interactions (hover, tap, press).
  * Compatible with Victory Native's handleTouch function exposed via actionsRef.
- *
- * @param initialValues - Initial x and y values matching your chart data structure
- * @returns Object containing the interaction state and a boolean indicating if interaction is active
- *
- * @example
- * ```tsx
- * const { state, isActive } = useChartInteractionState({
- *   x: '',
- *   y: { value: 0 }
- * });
- *
- * // Use with customGestures and actionsRef
- * const hoverGesture = Gesture.Hover()
- *   .onUpdate((e) => {
- *     state.isActive.set(true);
- *     actionsRef.current?.handleTouch(state, e.x, e.y);
- *   })
- *   .onEnd(() => {
- *     state.isActive.set(false);
- *   });
- * ```
  */
 function useChartInteractionState<Init extends ChartInteractionStateInit>(
     initialValues: Init,
diff --git a/src/components/Charts/hooks/useChartInteractions.ts b/src/components/Charts/hooks/useChartInteractions.ts
index 3cb17d796b71..065eadcd3188 100644
--- a/src/components/Charts/hooks/useChartInteractions.ts
+++ b/src/components/Charts/hooks/useChartInteractions.ts
@@ -14,12 +14,16 @@ const TOOLTIP_BAR_GAP = 8;
 type HitTestArgs = {
     /** Current raw X position of the cursor */
     cursorX: number;
+
     /** Current raw Y position of the cursor */
     cursorY: number;
+
     /** Calculated X position of the matched data point */
     targetX: number;
+
     /** Calculated Y position of the matched data point */
     targetY: number;
+
     /** The bottom boundary of the chart area */
     chartBottom: number;
 };
@@ -30,11 +34,13 @@ type HitTestArgs = {
 type UseChartInteractionsProps = {
     /** Callback triggered when a valid data point is tapped/clicked */
     handlePress: (index: number) => void;
+
     /**
      * Worklet function to determine if the cursor is technically "hovering"
      * over a specific chart element (e.g., within a bar's width or a point's radius).
      */
     checkIsOver: (args: HitTestArgs) => boolean;
+
     /** Optional shared value containing bar dimensions used for hit-testing in bar charts */
     barGeometry?: SharedValue<{barWidth: number; chartBottom: number; yZero: number}>;
 };
@@ -48,33 +54,11 @@ type CartesianActionsHandle = {
 };
 
 /**
- * Hook to manage complex chart interactions including hover gestures (web),
+ * Hook to manage chart interactions including hover gestures (web),
  * tap gestures (mobile/web), hit-testing, and animated tooltip positioning.
  *
- * It synchronizes high-frequency interaction data from the UI thread to React state
+ * Synchronizes high-frequency interaction data from the UI thread to React state
  * for metadata display (like tooltips) and navigation.
- *
- * @param props - Configuration including press handlers and hit-test logic.
- * @returns An object containing refs, gestures, and state for the chart component.
- *
- * @example
- * ```tsx
- * const { actionsRef, customGestures, activeDataIndex, isTooltipActive, tooltipStyle } = useChartInteractions({
- * handlePress: (index) => console.log("Pressed index:", index),
- * checkIsOver: ({ cursorX, targetX, barWidth }) => {
- * 'worklet';
- * return Math.abs(cursorX - targetX) < barWidth / 2;
- * },
- * barGeometry: myBarSharedValue,
- * });
- *
- * return (
- * <View>
- * <CartesianChart customGestures={customGestures} actionsRef={actionsRef} ... />
- * {isTooltipActive && <Animated.View style={tooltipStyle}><Tooltip index={activeDataIndex} /></Animated.View>}
- * </View>
- * );
- * ```
  */
 function useChartInteractions({handlePress, checkIsOver, barGeometry}: UseChartInteractionsProps) {
     /** Interaction state compatible with Victory Native's internal logic */
diff --git a/src/components/Charts/hooks/useChartLabelLayout.ts b/src/components/Charts/hooks/useChartLabelLayout.ts
index 108aa027ed4f..e41bd9ff2129 100644
--- a/src/components/Charts/hooks/useChartLabelLayout.ts
+++ b/src/components/Charts/hooks/useChartLabelLayout.ts
@@ -2,10 +2,10 @@ import type {SkFont} from '@shopify/react-native-skia';
 import {useMemo} from 'react';
 import {Y_AXIS_LABEL_OFFSET} from '@components/Charts/constants';
 
-/** Rotation angle for X-axis labels - 45 degrees (in degrees) */
+/** Rotation angle for X-axis labels - 45 degrees */
 const X_AXIS_LABEL_ROTATION_45 = -45;
 
-/** Rotation angle for X-axis labels - 90 degrees (in degrees) */
+/** Rotation angle for X-axis labels - 90 degrees */
 const X_AXIS_LABEL_ROTATION_90 = -90;
 
 /** Sin of 45 degrees - used to calculate effective width of rotated labels */
diff --git a/src/components/Charts/utils.ts b/src/components/Charts/utils.ts
index f56cd568b28c..3dca6dc372f6 100644
--- a/src/components/Charts/utils.ts
+++ b/src/components/Charts/utils.ts
@@ -2,20 +2,22 @@ import colors from '@styles/theme/colors';
 
 /**
  * Expensify Chart Color Palette.
- * Sequence logic:
- * 1. Row Sequence: 400, 600, 300, 500, 700
- * 2. Hue Order: Yellow, Tangerine, Pink, Green, Ice, Blue
+ *
+ * Shades are ordered (400, 600, 300, 500, 700) so that sequential colors have
+ * maximum contrast, making adjacent chart segments easy to distinguish.
+ *
+ * Within each shade, hues cycle: Yellow, Tangerine, Pink, Green, Ice, Blue.
  */
 const CHART_PALETTE: string[] = (() => {
-    const rows = [400, 600, 300, 500, 700] as const;
+    const shades = [400, 600, 300, 500, 700] as const;
     const hues = ['yellow', 'tangerine', 'pink', 'green', 'ice', 'blue'] as const;
 
     const palette: string[] = [];
 
-    // Generate the 30 unique combinations (5 rows × 6 hues)
-    for (const row of rows) {
+    // Generate the 30 unique combinations (5 shades × 6 hues)
+    for (const shade of shades) {
         for (const hue of hues) {
-            const colorKey = `${hue}${row}`;
+            const colorKey = `${hue}${shade}`;
             if (colors[colorKey]) {
                 palette.push(colors[colorKey]);
             }
diff --git a/src/components/Search/SearchBarChart.tsx b/src/components/Search/SearchBarChart.tsx
index 874ee22f6393..684ed04ed8b9 100644
--- a/src/components/Search/SearchBarChart.tsx
+++ b/src/components/Search/SearchBarChart.tsx
@@ -1,6 +1,6 @@
 import React, {useCallback, useMemo} from 'react';
 import {BarChart} from '@components/Charts';
-import type {ChartDataPoint} from '@components/Charts';
+import type {ChartDataPoint, YAxisUnitPosition} from '@components/Charts';
 import type {TransactionGroupListItemType} from '@components/SelectionListWithSections/types';
 import {convertToFrontendAmountAsInteger} from '@libs/CurrencyUtils';
 import type IconAsset from '@src/types/utils/IconAsset';
@@ -22,8 +22,8 @@ type SearchBarChartProps = {
     /** Function to build filter query from grouped item */
     getFilterQuery: (item: GroupedItem) => string;
 
-    /** Callback when a bar is pressed - receives the filter query to apply */
-    onBarPress?: (filterQuery: string) => void;
+    /** Callback when a chart item is pressed - receives the filter query to apply */
+    onItemPress?: (filterQuery: string) => void;
 
     /** Whether data is loading */
     isLoading?: boolean;
@@ -32,10 +32,10 @@ type SearchBarChartProps = {
     yAxisUnit?: string;
 
     /** Position of currency symbol relative to value */
-    yAxisUnitPosition?: 'left' | 'right';
+    yAxisUnitPosition?: YAxisUnitPosition;
 };
 
-function SearchBarChart({data, title, titleIcon, getLabel, getFilterQuery, onBarPress, isLoading, yAxisUnit, yAxisUnitPosition}: SearchBarChartProps) {
+function SearchBarChart({data, title, titleIcon, getLabel, getFilterQuery, onItemPress, isLoading, yAxisUnit, yAxisUnitPosition}: SearchBarChartProps) {
     // Transform grouped transaction data to BarChart format
     const chartData: ChartDataPoint[] = useMemo(() => {
         return data.map((item) => {
@@ -52,7 +52,7 @@ function SearchBarChart({data, title, titleIcon, getLabel, getFilterQuery, onBar
 
     const handleBarPress = useCallback(
         (dataPoint: ChartDataPoint, index: number) => {
-            if (!onBarPress) {
+            if (!onItemPress) {
                 return;
             }
 
@@ -62,9 +62,9 @@ function SearchBarChart({data, title, titleIcon, getLabel, getFilterQuery, onBar
             }
 
             const filterQuery = getFilterQuery(item as GroupedItem);
-            onBarPress(filterQuery);
+            onItemPress(filterQuery);
         },
-        [data, getFilterQuery, onBarPress],
+        [data, getFilterQuery, onItemPress],
     );
 
     return (
diff --git a/src/components/Search/SearchChartView.tsx b/src/components/Search/SearchChartView.tsx
index 32cb4f9385f9..7b0b82b56b8b 100644
--- a/src/components/Search/SearchChartView.tsx
+++ b/src/components/Search/SearchChartView.tsx
@@ -152,9 +152,8 @@ function SearchChartView({queryJSON, view, groupBy, data, isLoading, onScroll}:
     const title = translate(`search.chartTitles.${groupBy}`);
     const ChartComponent = CHART_VIEW_TO_COMPONENT[view];
 
-    const handleBarPress = useCallback(
+    const handleItemPress = useCallback(
         (filterQuery: string) => {
-            // Build new query string from current query + filter, then parse it
             const currentQueryString = buildSearchQueryString(queryJSON);
             const newQueryJSON = buildSearchQueryJSON(`${currentQueryString} ${filterQuery}`);
 
@@ -163,18 +162,15 @@ function SearchChartView({queryJSON, view, groupBy, data, isLoading, onScroll}:
                 return;
             }
 
-            // Modify the query object directly: remove groupBy and view to show table
             newQueryJSON.groupBy = undefined;
             newQueryJSON.view = CONST.SEARCH.VIEW.TABLE;
 
-            // Build the final query string and navigate
             const newQueryString = buildSearchQueryString(newQueryJSON);
             Navigation.navigate(ROUTES.SEARCH_ROOT.getRoute({query: newQueryString}));
         },
         [queryJSON],
     );
 
-    // Get currency symbol and position from first data item
     const {yAxisUnit, yAxisUnitPosition} = useMemo((): {yAxisUnit: string; yAxisUnitPosition: 'left' | 'right'} => {
         const firstItem = data.at(0) as GroupedItem | undefined;
         const currency = firstItem?.currency ?? 'USD';
@@ -200,7 +196,7 @@ function SearchChartView({queryJSON, view, groupBy, data, isLoading, onScroll}:
                     titleIcon={titleIcon}
                     getLabel={getLabel}
                     getFilterQuery={getFilterQuery}
-                    onBarPress={handleBarPress}
+                    onItemPress={handleItemPress}
                     isLoading={isLoading}
                     yAxisUnit={yAxisUnit}
                     yAxisUnitPosition={yAxisUnitPosition}
diff --git a/src/components/Search/SearchLineChart.tsx b/src/components/Search/SearchLineChart.tsx
index d83607b7cc02..5505a2a647d3 100644
--- a/src/components/Search/SearchLineChart.tsx
+++ b/src/components/Search/SearchLineChart.tsx
@@ -1,6 +1,6 @@
 import React, {useCallback, useMemo} from 'react';
 import {LineChart} from '@components/Charts';
-import type {ChartDataPoint} from '@components/Charts';
+import type {ChartDataPoint, YAxisUnitPosition} from '@components/Charts';
 import type {TransactionGroupListItemType} from '@components/SelectionListWithSections/types';
 import {convertToFrontendAmountAsInteger} from '@libs/CurrencyUtils';
 import type IconAsset from '@src/types/utils/IconAsset';
@@ -22,8 +22,8 @@ type SearchLineChartProps = {
     /** Function to build filter query from grouped item */
     getFilterQuery: (item: GroupedItem) => string;
 
-    /** Callback when a point is pressed - receives the filter query to apply */
-    onBarPress?: (filterQuery: string) => void;
+    /** Callback when a chart item is pressed - receives the filter query to apply */
+    onItemPress?: (filterQuery: string) => void;
 
     /** Whether data is loading */
     isLoading?: boolean;
@@ -32,10 +32,10 @@ type SearchLineChartProps = {
     yAxisUnit?: string;
 
     /** Position of currency symbol relative to value */
-    yAxisUnitPosition?: 'left' | 'right';
+    yAxisUnitPosition?: YAxisUnitPosition;
 };
 
-function SearchLineChart({data, title, titleIcon, getLabel, getFilterQuery, onBarPress, isLoading, yAxisUnit, yAxisUnitPosition}: SearchLineChartProps) {
+function SearchLineChart({data, title, titleIcon, getLabel, getFilterQuery, onItemPress, isLoading, yAxisUnit, yAxisUnitPosition}: SearchLineChartProps) {
     const chartData: ChartDataPoint[] = useMemo(() => {
         return data.map((item) => {
             const groupedItem = item as GroupedItem;
@@ -51,7 +51,7 @@ function SearchLineChart({data, title, titleIcon, getLabel, getFilterQuery, onBa
 
     const handlePointPress = useCallback(
         (dataPoint: ChartDataPoint, index: number) => {
-            if (!onBarPress) {
+            if (!onItemPress) {
                 return;
             }
 
@@ -61,9 +61,9 @@ function SearchLineChart({data, title, titleIcon, getLabel, getFilterQuery, onBa
             }
 
             const filterQuery = getFilterQuery(item as GroupedItem);
-            onBarPress(filterQuery);
+            onItemPress(filterQuery);
         },
-        [data, getFilterQuery, onBarPress],
+        [data, getFilterQuery, onItemPress],
     );
 
     return (

From 513f1ac24ba4acfc39510e9c3f80e85193111891 Mon Sep 17 00:00:00 2001
From: Mateusz Rajski <mateusz.rajski@swmansion.com>
Date: Fri, 30 Jan 2026 14:31:26 -0800
Subject: [PATCH 11/26] Add missing import

---
 src/components/Charts/index.ts | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/components/Charts/index.ts b/src/components/Charts/index.ts
index 85629c33083b..f86738fa140a 100644
--- a/src/components/Charts/index.ts
+++ b/src/components/Charts/index.ts
@@ -4,6 +4,6 @@ import ChartTooltip from './components/ChartTooltip';
 import LineChart from './LineChart';
 
 export {BarChart, ChartHeader, ChartTooltip, LineChart};
-export type {ChartDataPoint, CartesianChartProps} from './types';
+export type {ChartDataPoint, CartesianChartProps, YAxisUnitPosition} from './types';
 export type {BarChartProps} from './BarChart/BarChartContent';
 export type {LineChartProps} from './LineChart/LineChartContent';

From b7b1a172c66bc4163ac98ac32f9ea2fa825e436c Mon Sep 17 00:00:00 2001
From: Mateusz Rajski <mateusz.rajski@swmansion.com>
Date: Fri, 30 Jan 2026 14:47:27 -0800
Subject: [PATCH 12/26] Format code and add type guard for grouped data

---
 .../Charts/BarChart/BarChartContent.tsx       | 11 ++------
 src/components/Charts/BarChart/index.tsx      |  2 +-
 .../Charts/LineChart/LineChartContent.tsx     | 26 ++++++-------------
 src/components/Charts/LineChart/index.tsx     |  2 +-
 src/components/Search/SearchBarChart.tsx      | 12 ++++-----
 src/components/Search/SearchChartView.tsx     |  5 ++--
 src/components/Search/SearchLineChart.tsx     | 12 ++++-----
 src/components/Search/index.tsx               |  5 ++--
 src/libs/SearchUIUtils.ts                     | 11 ++++++++
 9 files changed, 38 insertions(+), 48 deletions(-)

diff --git a/src/components/Charts/BarChart/BarChartContent.tsx b/src/components/Charts/BarChart/BarChartContent.tsx
index 5b52b9913f6b..af40d0e8c6d4 100644
--- a/src/components/Charts/BarChart/BarChartContent.tsx
+++ b/src/components/Charts/BarChart/BarChartContent.tsx
@@ -8,19 +8,12 @@ import {Bar, CartesianChart} from 'victory-native';
 import ActivityIndicator from '@components/ActivityIndicator';
 import ChartHeader from '@components/Charts/components/ChartHeader';
 import ChartTooltip from '@components/Charts/components/ChartTooltip';
-import {DEFAULT_CHART_COLOR, getChartColor} from '@components/Charts/utils';
-import {
-    CHART_CONTENT_MIN_HEIGHT,
-    CHART_PADDING,
-    X_AXIS_LINE_WIDTH,
-    Y_AXIS_LABEL_OFFSET,
-    Y_AXIS_LINE_WIDTH,
-    Y_AXIS_TICK_COUNT,
-} from '@components/Charts/constants';
+import {CHART_CONTENT_MIN_HEIGHT, CHART_PADDING, X_AXIS_LINE_WIDTH, Y_AXIS_LABEL_OFFSET, Y_AXIS_LINE_WIDTH, Y_AXIS_TICK_COUNT} from '@components/Charts/constants';
 import fontSource from '@components/Charts/font';
 import type {HitTestArgs} from '@components/Charts/hooks';
 import {useChartInteractions, useChartLabelFormats, useChartLabelLayout, useDynamicYDomain, useTooltipData} from '@components/Charts/hooks';
 import type {CartesianChartProps, ChartDataPoint} from '@components/Charts/types';
+import {DEFAULT_CHART_COLOR, getChartColor} from '@components/Charts/utils';
 import useResponsiveLayout from '@hooks/useResponsiveLayout';
 import useTheme from '@hooks/useTheme';
 import useThemeStyles from '@hooks/useThemeStyles';
diff --git a/src/components/Charts/BarChart/index.tsx b/src/components/Charts/BarChart/index.tsx
index 0d00fb501193..90e4bee47edb 100644
--- a/src/components/Charts/BarChart/index.tsx
+++ b/src/components/Charts/BarChart/index.tsx
@@ -2,8 +2,8 @@ import {WithSkiaWeb} from '@shopify/react-native-skia/lib/module/web';
 import React from 'react';
 import {View} from 'react-native';
 import ActivityIndicator from '@components/ActivityIndicator';
-import type {BarChartProps} from './BarChartContent';
 import useThemeStyles from '@hooks/useThemeStyles';
+import type {BarChartProps} from './BarChartContent';
 
 function BarChart(props: BarChartProps) {
     const styles = useThemeStyles();
diff --git a/src/components/Charts/LineChart/LineChartContent.tsx b/src/components/Charts/LineChart/LineChartContent.tsx
index 6e7e0e5fa13c..140f704a9f0a 100644
--- a/src/components/Charts/LineChart/LineChartContent.tsx
+++ b/src/components/Charts/LineChart/LineChartContent.tsx
@@ -7,19 +7,12 @@ import {CartesianChart, Line, Scatter} from 'victory-native';
 import ActivityIndicator from '@components/ActivityIndicator';
 import ChartHeader from '@components/Charts/components/ChartHeader';
 import ChartTooltip from '@components/Charts/components/ChartTooltip';
-import {DEFAULT_CHART_COLOR} from '@components/Charts/utils';
-import {
-    CHART_CONTENT_MIN_HEIGHT,
-    CHART_PADDING,
-    X_AXIS_LINE_WIDTH,
-    Y_AXIS_LABEL_OFFSET,
-    Y_AXIS_LINE_WIDTH,
-    Y_AXIS_TICK_COUNT,
-} from '@components/Charts/constants';
+import {CHART_CONTENT_MIN_HEIGHT, CHART_PADDING, X_AXIS_LINE_WIDTH, Y_AXIS_LABEL_OFFSET, Y_AXIS_LINE_WIDTH, Y_AXIS_TICK_COUNT} from '@components/Charts/constants';
 import fontSource from '@components/Charts/font';
 import type {HitTestArgs} from '@components/Charts/hooks';
 import {useChartInteractions, useChartLabelFormats, useChartLabelLayout, useDynamicYDomain, useTooltipData} from '@components/Charts/hooks';
 import type {CartesianChartProps, ChartDataPoint} from '@components/Charts/types';
+import {DEFAULT_CHART_COLOR} from '@components/Charts/utils';
 import useResponsiveLayout from '@hooks/useResponsiveLayout';
 import useTheme from '@hooks/useTheme';
 import useThemeStyles from '@hooks/useThemeStyles';
@@ -86,16 +79,13 @@ function LineChartContent({data, title, titleIcon, isLoading, yAxisUnit, yAxisUn
         truncatedLabels,
     });
 
-    const checkIsOverDot = useCallback(
-        (args: HitTestArgs) => {
-            'worklet';
+    const checkIsOverDot = useCallback((args: HitTestArgs) => {
+        'worklet';
 
-            const dx = args.cursorX - args.targetX;
-            const dy = args.cursorY - args.targetY;
-            return Math.sqrt(dx * dx + dy * dy) <= DOT_INNER_RADIUS;
-        },
-        [],
-    );
+        const dx = args.cursorX - args.targetX;
+        const dy = args.cursorY - args.targetY;
+        return Math.sqrt(dx * dx + dy * dy) <= DOT_INNER_RADIUS;
+    }, []);
 
     const {actionsRef, customGestures, activeDataIndex, isTooltipActive, tooltipStyle} = useChartInteractions({
         handlePress: handlePointPress,
diff --git a/src/components/Charts/LineChart/index.tsx b/src/components/Charts/LineChart/index.tsx
index 492b278697b7..907d385722f8 100644
--- a/src/components/Charts/LineChart/index.tsx
+++ b/src/components/Charts/LineChart/index.tsx
@@ -2,8 +2,8 @@ import {WithSkiaWeb} from '@shopify/react-native-skia/lib/module/web';
 import React from 'react';
 import {View} from 'react-native';
 import ActivityIndicator from '@components/ActivityIndicator';
-import type {LineChartProps} from './LineChartContent';
 import useThemeStyles from '@hooks/useThemeStyles';
+import type {LineChartProps} from './LineChartContent';
 
 function LineChart(props: LineChartProps) {
     const styles = useThemeStyles();
diff --git a/src/components/Search/SearchBarChart.tsx b/src/components/Search/SearchBarChart.tsx
index 684ed04ed8b9..18b4beb3045f 100644
--- a/src/components/Search/SearchBarChart.tsx
+++ b/src/components/Search/SearchBarChart.tsx
@@ -1,14 +1,13 @@
 import React, {useCallback, useMemo} from 'react';
 import {BarChart} from '@components/Charts';
 import type {ChartDataPoint, YAxisUnitPosition} from '@components/Charts';
-import type {TransactionGroupListItemType} from '@components/SelectionListWithSections/types';
 import {convertToFrontendAmountAsInteger} from '@libs/CurrencyUtils';
 import type IconAsset from '@src/types/utils/IconAsset';
 import type {GroupedItem} from './types';
 
 type SearchBarChartProps = {
     /** Grouped transaction data from search results */
-    data: TransactionGroupListItemType[];
+    data: GroupedItem[];
 
     /** Chart title */
     title: string;
@@ -39,12 +38,11 @@ function SearchBarChart({data, title, titleIcon, getLabel, getFilterQuery, onIte
     // Transform grouped transaction data to BarChart format
     const chartData: ChartDataPoint[] = useMemo(() => {
         return data.map((item) => {
-            const groupedItem = item as GroupedItem;
-            const currency = groupedItem.currency ?? 'USD';
-            const totalInDisplayUnits = convertToFrontendAmountAsInteger(groupedItem.total ?? 0, currency);
+            const currency = item.currency ?? 'USD';
+            const totalInDisplayUnits = convertToFrontendAmountAsInteger(item.total ?? 0, currency);
 
             return {
-                label: getLabel(groupedItem),
+                label: getLabel(item),
                 total: totalInDisplayUnits,
             };
         });
@@ -61,7 +59,7 @@ function SearchBarChart({data, title, titleIcon, getLabel, getFilterQuery, onIte
                 return;
             }
 
-            const filterQuery = getFilterQuery(item as GroupedItem);
+            const filterQuery = getFilterQuery(item);
             onItemPress(filterQuery);
         },
         [data, getFilterQuery, onItemPress],
diff --git a/src/components/Search/SearchChartView.tsx b/src/components/Search/SearchChartView.tsx
index 7b0b82b56b8b..4f34f3e67a52 100644
--- a/src/components/Search/SearchChartView.tsx
+++ b/src/components/Search/SearchChartView.tsx
@@ -5,7 +5,6 @@ import Animated from 'react-native-reanimated';
 import type {
     TransactionCardGroupListItemType,
     TransactionCategoryGroupListItemType,
-    TransactionGroupListItemType,
     TransactionMemberGroupListItemType,
     TransactionMerchantGroupListItemType,
     TransactionMonthGroupListItemType,
@@ -121,7 +120,7 @@ type SearchChartViewProps = {
     groupBy: SearchGroupBy;
 
     /** Grouped transaction data from search results */
-    data: TransactionGroupListItemType[];
+    data: GroupedItem[];
 
     /** Whether data is loading */
     isLoading?: boolean;
@@ -172,7 +171,7 @@ function SearchChartView({queryJSON, view, groupBy, data, isLoading, onScroll}:
     );
 
     const {yAxisUnit, yAxisUnitPosition} = useMemo((): {yAxisUnit: string; yAxisUnitPosition: 'left' | 'right'} => {
-        const firstItem = data.at(0) as GroupedItem | undefined;
+        const firstItem = data.at(0);
         const currency = firstItem?.currency ?? 'USD';
         const {symbol, position} = getCurrencyDisplayInfoForCharts(currency);
 
diff --git a/src/components/Search/SearchLineChart.tsx b/src/components/Search/SearchLineChart.tsx
index 5505a2a647d3..b06699021933 100644
--- a/src/components/Search/SearchLineChart.tsx
+++ b/src/components/Search/SearchLineChart.tsx
@@ -1,14 +1,13 @@
 import React, {useCallback, useMemo} from 'react';
 import {LineChart} from '@components/Charts';
 import type {ChartDataPoint, YAxisUnitPosition} from '@components/Charts';
-import type {TransactionGroupListItemType} from '@components/SelectionListWithSections/types';
 import {convertToFrontendAmountAsInteger} from '@libs/CurrencyUtils';
 import type IconAsset from '@src/types/utils/IconAsset';
 import type {GroupedItem} from './types';
 
 type SearchLineChartProps = {
     /** Grouped transaction data from search results */
-    data: TransactionGroupListItemType[];
+    data: GroupedItem[];
 
     /** Chart title */
     title: string;
@@ -38,12 +37,11 @@ type SearchLineChartProps = {
 function SearchLineChart({data, title, titleIcon, getLabel, getFilterQuery, onItemPress, isLoading, yAxisUnit, yAxisUnitPosition}: SearchLineChartProps) {
     const chartData: ChartDataPoint[] = useMemo(() => {
         return data.map((item) => {
-            const groupedItem = item as GroupedItem;
-            const currency = groupedItem.currency ?? 'USD';
-            const totalInDisplayUnits = convertToFrontendAmountAsInteger(groupedItem.total ?? 0, currency);
+            const currency = item.currency ?? 'USD';
+            const totalInDisplayUnits = convertToFrontendAmountAsInteger(item.total ?? 0, currency);
 
             return {
-                label: getLabel(groupedItem),
+                label: getLabel(item),
                 total: totalInDisplayUnits,
             };
         });
@@ -60,7 +58,7 @@ function SearchLineChart({data, title, titleIcon, getLabel, getFilterQuery, onIt
                 return;
             }
 
-            const filterQuery = getFilterQuery(item as GroupedItem);
+            const filterQuery = getFilterQuery(item);
             onItemPress(filterQuery);
         },
         [data, getFilterQuery, onItemPress],
diff --git a/src/components/Search/index.tsx b/src/components/Search/index.tsx
index eaa360727828..2e1d087b315f 100644
--- a/src/components/Search/index.tsx
+++ b/src/components/Search/index.tsx
@@ -56,6 +56,7 @@ import {
     isTransactionCardGroupListItemType,
     isTransactionCategoryGroupListItemType,
     isTransactionGroupListItemType,
+    isGroupedItemArray,
     isTransactionListItemType,
     isTransactionMemberGroupListItemType,
     isTransactionMerchantGroupListItemType,
@@ -1307,14 +1308,14 @@ function Search({
 
     const shouldShowChartView = (view === CONST.SEARCH.VIEW.BAR || view === CONST.SEARCH.VIEW.LINE) && !!validGroupBy;
 
-    if (shouldShowChartView) {
+    if (shouldShowChartView && isGroupedItemArray(sortedData)) {
         return (
             <SearchScopeProvider>
                 <SearchChartView
                     queryJSON={queryJSON}
                     view={view}
                     groupBy={validGroupBy}
-                    data={sortedData as TransactionGroupListItemType[]}
+                    data={sortedData}
                     isLoading={shouldShowLoadingState}
                     onScroll={onSearchListScroll}
                 />
diff --git a/src/libs/SearchUIUtils.ts b/src/libs/SearchUIUtils.ts
index d6b32ac05645..387cfec48982 100644
--- a/src/libs/SearchUIUtils.ts
+++ b/src/libs/SearchUIUtils.ts
@@ -25,6 +25,7 @@ import type {
     SelectedTransactionInfo,
     SingularSearchStatus,
     SortOrder,
+    GroupedItem,
 } from '@components/Search/types';
 import ChatListItem from '@components/SelectionListWithSections/ChatListItem';
 import ExpenseReportListItem from '@components/SelectionListWithSections/Search/ExpenseReportListItem';
@@ -1036,6 +1037,15 @@ function isTransactionQuarterGroupListItemType(item: ListItem): item is Transact
     return isTransactionGroupListItemType(item) && 'groupedBy' in item && item.groupedBy === CONST.SEARCH.GROUP_BY.QUARTER;
 }
 
+/**
+ * Type guard that checks if a list of search items contains grouped transaction data.
+ * When a search has a groupBy parameter, all items share the same shape, so checking the first element is sufficient.
+ */
+function isGroupedItemArray(data: ListItem[]): data is GroupedItem[] {
+    const first = data.at(0);
+    return data.length === 0 || (first !== undefined && isTransactionGroupListItemType(first) && 'groupedBy' in first);
+}
+
 /**
  * Type guard that checks if something is a TransactionListItemType
  */
@@ -4496,6 +4506,7 @@ export {
     isTransactionWeekGroupListItemType,
     isTransactionYearGroupListItemType,
     isTransactionQuarterGroupListItemType,
+    isGroupedItemArray,
     isSearchResultsEmpty,
     isTransactionListItemType,
     isReportActionListItemType,

From 01ab33f5ce088a5600af0c518dc661fe0c848d2f Mon Sep 17 00:00:00 2001
From: Mateusz Rajski <mateusz.rajski@swmansion.com>
Date: Fri, 30 Jan 2026 15:11:00 -0800
Subject: [PATCH 13/26] Fix label rendering issues

---
 .../Charts/BarChart/BarChartContent.tsx       | 39 +++++++++----------
 .../Charts/LineChart/LineChartContent.tsx     |  9 +++--
 src/components/Charts/hooks/index.ts          |  1 +
 .../Charts/hooks/useChartBoundsTracking.ts    | 27 +++++++++++++
 .../Charts/hooks/useChartLabelLayout.ts       | 10 ++---
 src/components/Search/index.tsx               |  2 +-
 src/libs/SearchUIUtils.ts                     |  2 +-
 7 files changed, 60 insertions(+), 30 deletions(-)
 create mode 100644 src/components/Charts/hooks/useChartBoundsTracking.ts

diff --git a/src/components/Charts/BarChart/BarChartContent.tsx b/src/components/Charts/BarChart/BarChartContent.tsx
index af40d0e8c6d4..06672891b072 100644
--- a/src/components/Charts/BarChart/BarChartContent.tsx
+++ b/src/components/Charts/BarChart/BarChartContent.tsx
@@ -11,7 +11,7 @@ import ChartTooltip from '@components/Charts/components/ChartTooltip';
 import {CHART_CONTENT_MIN_HEIGHT, CHART_PADDING, X_AXIS_LINE_WIDTH, Y_AXIS_LABEL_OFFSET, Y_AXIS_LINE_WIDTH, Y_AXIS_TICK_COUNT} from '@components/Charts/constants';
 import fontSource from '@components/Charts/font';
 import type {HitTestArgs} from '@components/Charts/hooks';
-import {useChartInteractions, useChartLabelFormats, useChartLabelLayout, useDynamicYDomain, useTooltipData} from '@components/Charts/hooks';
+import {useChartBoundsTracking, useChartInteractions, useChartLabelFormats, useChartLabelLayout, useDynamicYDomain, useTooltipData} from '@components/Charts/hooks';
 import type {CartesianChartProps, ChartDataPoint} from '@components/Charts/types';
 import {DEFAULT_CHART_COLOR, getChartColor} from '@components/Charts/utils';
 import useResponsiveLayout from '@hooks/useResponsiveLayout';
@@ -62,7 +62,6 @@ function BarChartContent({data, title, titleIcon, isLoading, yAxisUnit, yAxisUni
     const {shouldUseNarrowLayout} = useResponsiveLayout();
     const font = useFont(fontSource, variables.iconSizeExtraSmall);
     const [chartWidth, setChartWidth] = useState(0);
-    const [barAreaWidth, setBarAreaWidth] = useState(0);
     const [containerHeight, setContainerHeight] = useState(0);
 
     const defaultBarColor = DEFAULT_CHART_COLOR;
@@ -97,11 +96,28 @@ function BarChartContent({data, title, titleIcon, isLoading, yAxisUnit, yAxisUni
         setContainerHeight(height);
     }, []);
 
+    // Store bar geometry for hit-testing (only constants, no arrays)
+    const barGeometry = useSharedValue({barWidth: 0, chartBottom: 0, yZero: 0});
+
+    const onBoundsChange = useCallback(
+        (bounds: ChartBounds, width: number) => {
+            const calculatedBarWidth = ((1 - BAR_INNER_PADDING) * width) / data.length;
+            barGeometry.set({
+                ...barGeometry.get(),
+                barWidth: calculatedBarWidth,
+                chartBottom: bounds.bottom,
+            });
+        },
+        [data.length, barGeometry],
+    );
+
+    const {plotAreaWidth, handleChartBoundsChange} = useChartBoundsTracking(onBoundsChange);
+
     const {labelRotation, labelSkipInterval, truncatedLabels, maxLabelLength} = useChartLabelLayout({
         data,
         font,
         chartWidth,
-        barAreaWidth,
+        plotAreaWidth,
         containerHeight,
     });
 
@@ -122,23 +138,6 @@ function BarChartContent({data, title, titleIcon, isLoading, yAxisUnit, yAxisUni
         truncatedLabels,
     });
 
-    // Store bar geometry for hit-testing (only constants, no arrays)
-    const barGeometry = useSharedValue({barWidth: 0, chartBottom: 0, yZero: 0});
-
-    const handleChartBoundsChange = useCallback(
-        (bounds: ChartBounds) => {
-            const domainWidth = bounds.right - bounds.left;
-            const calculatedBarWidth = ((1 - BAR_INNER_PADDING) * domainWidth) / data.length;
-            barGeometry.set({
-                ...barGeometry.get(),
-                barWidth: calculatedBarWidth,
-                chartBottom: bounds.bottom,
-            });
-            setBarAreaWidth(domainWidth);
-        },
-        [data.length, barGeometry],
-    );
-
     const handleScaleChange = useCallback(
         (_xScale: unknown, yScale: (value: number) => number) => {
             barGeometry.set({
diff --git a/src/components/Charts/LineChart/LineChartContent.tsx b/src/components/Charts/LineChart/LineChartContent.tsx
index 140f704a9f0a..340bf66ebe10 100644
--- a/src/components/Charts/LineChart/LineChartContent.tsx
+++ b/src/components/Charts/LineChart/LineChartContent.tsx
@@ -10,7 +10,7 @@ import ChartTooltip from '@components/Charts/components/ChartTooltip';
 import {CHART_CONTENT_MIN_HEIGHT, CHART_PADDING, X_AXIS_LINE_WIDTH, Y_AXIS_LABEL_OFFSET, Y_AXIS_LINE_WIDTH, Y_AXIS_TICK_COUNT} from '@components/Charts/constants';
 import fontSource from '@components/Charts/font';
 import type {HitTestArgs} from '@components/Charts/hooks';
-import {useChartInteractions, useChartLabelFormats, useChartLabelLayout, useDynamicYDomain, useTooltipData} from '@components/Charts/hooks';
+import {useChartBoundsTracking, useChartInteractions, useChartLabelFormats, useChartLabelLayout, useDynamicYDomain, useTooltipData} from '@components/Charts/hooks';
 import type {CartesianChartProps, ChartDataPoint} from '@components/Charts/types';
 import {DEFAULT_CHART_COLOR} from '@components/Charts/utils';
 import useResponsiveLayout from '@hooks/useResponsiveLayout';
@@ -62,11 +62,13 @@ function LineChartContent({data, title, titleIcon, isLoading, yAxisUnit, yAxisUn
         setContainerHeight(height);
     }, []);
 
+    const {plotAreaWidth, handleChartBoundsChange} = useChartBoundsTracking();
+
     const {labelRotation, labelSkipInterval, truncatedLabels, maxLabelLength} = useChartLabelLayout({
         data,
         font,
         chartWidth,
-        barAreaWidth: chartWidth,
+        plotAreaWidth,
         containerHeight,
     });
 
@@ -128,9 +130,10 @@ function LineChartContent({data, title, titleIcon, isLoading, yAxisUnit, yAxisUn
                         xKey="x"
                         padding={CHART_PADDING}
                         yKeys={['y']}
-                        domainPadding={{left: 20, right: 20, top: 20, bottom: 20}}
+                        domainPadding={{left: 20, right: 40, top: 20, bottom: 20}}
                         actionsRef={actionsRef}
                         customGestures={customGestures}
+                        onChartBoundsChange={handleChartBoundsChange}
                         xAxis={{
                             font,
                             tickCount: data.length,
diff --git a/src/components/Charts/hooks/index.ts b/src/components/Charts/hooks/index.ts
index ac5272aa5b56..c1996fad8d44 100644
--- a/src/components/Charts/hooks/index.ts
+++ b/src/components/Charts/hooks/index.ts
@@ -1,3 +1,4 @@
+export {useChartBoundsTracking} from './useChartBoundsTracking';
 export {useChartInteractionState} from './useChartInteractionState';
 export {useChartLabelLayout} from './useChartLabelLayout';
 export {useChartInteractions} from './useChartInteractions';
diff --git a/src/components/Charts/hooks/useChartBoundsTracking.ts b/src/components/Charts/hooks/useChartBoundsTracking.ts
new file mode 100644
index 000000000000..8b5188929617
--- /dev/null
+++ b/src/components/Charts/hooks/useChartBoundsTracking.ts
@@ -0,0 +1,27 @@
+import {useCallback, useState} from 'react';
+import type {ChartBounds} from 'victory-native';
+
+type OnBoundsChange = (bounds: ChartBounds, plotAreaWidth: number) => void;
+
+/**
+ * Reusable hook that tracks the plot area width from CartesianChart's `onChartBoundsChange`.
+ *
+ * @param onBoundsChange - Optional callback for chart-specific logic (e.g. BarChart bar geometry).
+ */
+function useChartBoundsTracking(onBoundsChange?: OnBoundsChange) {
+    const [plotAreaWidth, setPlotAreaWidth] = useState(0);
+
+    const handleChartBoundsChange = useCallback(
+        (bounds: ChartBounds) => {
+            const width = bounds.right - bounds.left;
+            setPlotAreaWidth(width);
+            onBoundsChange?.(bounds, width);
+        },
+        [onBoundsChange],
+    );
+
+    return {plotAreaWidth, handleChartBoundsChange};
+}
+
+export {useChartBoundsTracking};
+export type {OnBoundsChange};
diff --git a/src/components/Charts/hooks/useChartLabelLayout.ts b/src/components/Charts/hooks/useChartLabelLayout.ts
index e41bd9ff2129..18a3a4d1a45e 100644
--- a/src/components/Charts/hooks/useChartLabelLayout.ts
+++ b/src/components/Charts/hooks/useChartLabelLayout.ts
@@ -29,7 +29,7 @@ type LabelLayoutConfig = {
     data: ChartDataPoint[];
     font: SkFont | null;
     chartWidth: number;
-    barAreaWidth: number;
+    plotAreaWidth: number;
     containerHeight: number;
 };
 
@@ -43,7 +43,7 @@ function measureTextWidth(text: string, font: SkFont): number {
     return glyphWidths.reduce((sum, w) => sum + w, 0);
 }
 
-function useChartLabelLayout({data, font, chartWidth, barAreaWidth, containerHeight}: LabelLayoutConfig) {
+function useChartLabelLayout({data, font, chartWidth, plotAreaWidth, containerHeight}: LabelLayoutConfig) {
     return useMemo(() => {
         if (!font || chartWidth === 0 || containerHeight === 0 || data.length === 0) {
             return {labelRotation: 0, labelSkipInterval: 1, truncatedLabels: data.map((p) => p.label)};
@@ -133,9 +133,9 @@ function useChartLabelLayout({data, font, chartWidth, barAreaWidth, containerHei
         // Calculate skip interval using spec formula:
         // maxVisibleLabels = floor(barAreaWidth / (effectiveWidth + MIN_LABEL_GAP))
         // skipInterval = ceil(barCount / maxVisibleLabels)
-        // Use barAreaWidth (actual plotting area from chartBounds) rather than chartWidth
+        // Use plotAreaWidth (actual plotting area from chartBounds) rather than chartWidth
         // (full container) so Y-axis labels and padding don't inflate the count.
-        const labelAreaWidth = barAreaWidth || chartWidth;
+        const labelAreaWidth = plotAreaWidth || chartWidth;
         const maxVisibleLabels = Math.floor(labelAreaWidth / (effectiveWidth + LABEL_PADDING));
         // When maxVisibleLabels is 0 (area too narrow for even one label) or less than
         // data.length, compute the interval. data.length is the safe upper bound — show
@@ -151,7 +151,7 @@ function useChartLabelLayout({data, font, chartWidth, barAreaWidth, containerHei
         }
 
         return {labelRotation: rotationValue, labelSkipInterval: skipInterval, truncatedLabels: finalLabels, maxLabelLength};
-    }, [font, chartWidth, barAreaWidth, containerHeight, data]);
+    }, [font, chartWidth, plotAreaWidth, containerHeight, data]);
 }
 
 export {useChartLabelLayout};
diff --git a/src/components/Search/index.tsx b/src/components/Search/index.tsx
index 2e1d087b315f..8750ddb30642 100644
--- a/src/components/Search/index.tsx
+++ b/src/components/Search/index.tsx
@@ -49,6 +49,7 @@ import {
     getSortedSections,
     getSuggestedSearches,
     getWideAmountIndicators,
+    isGroupedItemArray,
     isReportActionListItemType,
     isSearchDataLoaded,
     isSearchResultsEmpty as isSearchResultsEmptyUtil,
@@ -56,7 +57,6 @@ import {
     isTransactionCardGroupListItemType,
     isTransactionCategoryGroupListItemType,
     isTransactionGroupListItemType,
-    isGroupedItemArray,
     isTransactionListItemType,
     isTransactionMemberGroupListItemType,
     isTransactionMerchantGroupListItemType,
diff --git a/src/libs/SearchUIUtils.ts b/src/libs/SearchUIUtils.ts
index 387cfec48982..446cc1df6bb8 100644
--- a/src/libs/SearchUIUtils.ts
+++ b/src/libs/SearchUIUtils.ts
@@ -11,6 +11,7 @@ import type {MenuItemWithLink} from '@components/MenuItemList';
 import type {MultiSelectItem} from '@components/Search/FilterDropdowns/MultiSelectPopup';
 import type {SingleSelectItem} from '@components/Search/FilterDropdowns/SingleSelectPopup';
 import type {
+    GroupedItem,
     QueryFilters,
     SearchAction,
     SearchColumnType,
@@ -25,7 +26,6 @@ import type {
     SelectedTransactionInfo,
     SingularSearchStatus,
     SortOrder,
-    GroupedItem,
 } from '@components/Search/types';
 import ChatListItem from '@components/SelectionListWithSections/ChatListItem';
 import ExpenseReportListItem from '@components/SelectionListWithSections/Search/ExpenseReportListItem';

From d584243d9ac2d221581c44ea59b2fbb9c10dc31d Mon Sep 17 00:00:00 2001
From: Mateusz Rajski <mateusz.rajski@swmansion.com>
Date: Fri, 30 Jan 2026 15:18:17 -0800
Subject: [PATCH 14/26] Revert "Fix label rendering issues"

This reverts commit 01ab33f5ce088a5600af0c518dc661fe0c848d2f.
---
 .../Charts/BarChart/BarChartContent.tsx       | 39 ++++++++++---------
 .../Charts/LineChart/LineChartContent.tsx     |  9 ++---
 src/components/Charts/hooks/index.ts          |  1 -
 .../Charts/hooks/useChartBoundsTracking.ts    | 27 -------------
 .../Charts/hooks/useChartLabelLayout.ts       | 10 ++---
 src/components/Search/index.tsx               |  2 +-
 src/libs/SearchUIUtils.ts                     |  2 +-
 7 files changed, 30 insertions(+), 60 deletions(-)
 delete mode 100644 src/components/Charts/hooks/useChartBoundsTracking.ts

diff --git a/src/components/Charts/BarChart/BarChartContent.tsx b/src/components/Charts/BarChart/BarChartContent.tsx
index 06672891b072..af40d0e8c6d4 100644
--- a/src/components/Charts/BarChart/BarChartContent.tsx
+++ b/src/components/Charts/BarChart/BarChartContent.tsx
@@ -11,7 +11,7 @@ import ChartTooltip from '@components/Charts/components/ChartTooltip';
 import {CHART_CONTENT_MIN_HEIGHT, CHART_PADDING, X_AXIS_LINE_WIDTH, Y_AXIS_LABEL_OFFSET, Y_AXIS_LINE_WIDTH, Y_AXIS_TICK_COUNT} from '@components/Charts/constants';
 import fontSource from '@components/Charts/font';
 import type {HitTestArgs} from '@components/Charts/hooks';
-import {useChartBoundsTracking, useChartInteractions, useChartLabelFormats, useChartLabelLayout, useDynamicYDomain, useTooltipData} from '@components/Charts/hooks';
+import {useChartInteractions, useChartLabelFormats, useChartLabelLayout, useDynamicYDomain, useTooltipData} from '@components/Charts/hooks';
 import type {CartesianChartProps, ChartDataPoint} from '@components/Charts/types';
 import {DEFAULT_CHART_COLOR, getChartColor} from '@components/Charts/utils';
 import useResponsiveLayout from '@hooks/useResponsiveLayout';
@@ -62,6 +62,7 @@ function BarChartContent({data, title, titleIcon, isLoading, yAxisUnit, yAxisUni
     const {shouldUseNarrowLayout} = useResponsiveLayout();
     const font = useFont(fontSource, variables.iconSizeExtraSmall);
     const [chartWidth, setChartWidth] = useState(0);
+    const [barAreaWidth, setBarAreaWidth] = useState(0);
     const [containerHeight, setContainerHeight] = useState(0);
 
     const defaultBarColor = DEFAULT_CHART_COLOR;
@@ -96,28 +97,11 @@ function BarChartContent({data, title, titleIcon, isLoading, yAxisUnit, yAxisUni
         setContainerHeight(height);
     }, []);
 
-    // Store bar geometry for hit-testing (only constants, no arrays)
-    const barGeometry = useSharedValue({barWidth: 0, chartBottom: 0, yZero: 0});
-
-    const onBoundsChange = useCallback(
-        (bounds: ChartBounds, width: number) => {
-            const calculatedBarWidth = ((1 - BAR_INNER_PADDING) * width) / data.length;
-            barGeometry.set({
-                ...barGeometry.get(),
-                barWidth: calculatedBarWidth,
-                chartBottom: bounds.bottom,
-            });
-        },
-        [data.length, barGeometry],
-    );
-
-    const {plotAreaWidth, handleChartBoundsChange} = useChartBoundsTracking(onBoundsChange);
-
     const {labelRotation, labelSkipInterval, truncatedLabels, maxLabelLength} = useChartLabelLayout({
         data,
         font,
         chartWidth,
-        plotAreaWidth,
+        barAreaWidth,
         containerHeight,
     });
 
@@ -138,6 +122,23 @@ function BarChartContent({data, title, titleIcon, isLoading, yAxisUnit, yAxisUni
         truncatedLabels,
     });
 
+    // Store bar geometry for hit-testing (only constants, no arrays)
+    const barGeometry = useSharedValue({barWidth: 0, chartBottom: 0, yZero: 0});
+
+    const handleChartBoundsChange = useCallback(
+        (bounds: ChartBounds) => {
+            const domainWidth = bounds.right - bounds.left;
+            const calculatedBarWidth = ((1 - BAR_INNER_PADDING) * domainWidth) / data.length;
+            barGeometry.set({
+                ...barGeometry.get(),
+                barWidth: calculatedBarWidth,
+                chartBottom: bounds.bottom,
+            });
+            setBarAreaWidth(domainWidth);
+        },
+        [data.length, barGeometry],
+    );
+
     const handleScaleChange = useCallback(
         (_xScale: unknown, yScale: (value: number) => number) => {
             barGeometry.set({
diff --git a/src/components/Charts/LineChart/LineChartContent.tsx b/src/components/Charts/LineChart/LineChartContent.tsx
index 340bf66ebe10..140f704a9f0a 100644
--- a/src/components/Charts/LineChart/LineChartContent.tsx
+++ b/src/components/Charts/LineChart/LineChartContent.tsx
@@ -10,7 +10,7 @@ import ChartTooltip from '@components/Charts/components/ChartTooltip';
 import {CHART_CONTENT_MIN_HEIGHT, CHART_PADDING, X_AXIS_LINE_WIDTH, Y_AXIS_LABEL_OFFSET, Y_AXIS_LINE_WIDTH, Y_AXIS_TICK_COUNT} from '@components/Charts/constants';
 import fontSource from '@components/Charts/font';
 import type {HitTestArgs} from '@components/Charts/hooks';
-import {useChartBoundsTracking, useChartInteractions, useChartLabelFormats, useChartLabelLayout, useDynamicYDomain, useTooltipData} from '@components/Charts/hooks';
+import {useChartInteractions, useChartLabelFormats, useChartLabelLayout, useDynamicYDomain, useTooltipData} from '@components/Charts/hooks';
 import type {CartesianChartProps, ChartDataPoint} from '@components/Charts/types';
 import {DEFAULT_CHART_COLOR} from '@components/Charts/utils';
 import useResponsiveLayout from '@hooks/useResponsiveLayout';
@@ -62,13 +62,11 @@ function LineChartContent({data, title, titleIcon, isLoading, yAxisUnit, yAxisUn
         setContainerHeight(height);
     }, []);
 
-    const {plotAreaWidth, handleChartBoundsChange} = useChartBoundsTracking();
-
     const {labelRotation, labelSkipInterval, truncatedLabels, maxLabelLength} = useChartLabelLayout({
         data,
         font,
         chartWidth,
-        plotAreaWidth,
+        barAreaWidth: chartWidth,
         containerHeight,
     });
 
@@ -130,10 +128,9 @@ function LineChartContent({data, title, titleIcon, isLoading, yAxisUnit, yAxisUn
                         xKey="x"
                         padding={CHART_PADDING}
                         yKeys={['y']}
-                        domainPadding={{left: 20, right: 40, top: 20, bottom: 20}}
+                        domainPadding={{left: 20, right: 20, top: 20, bottom: 20}}
                         actionsRef={actionsRef}
                         customGestures={customGestures}
-                        onChartBoundsChange={handleChartBoundsChange}
                         xAxis={{
                             font,
                             tickCount: data.length,
diff --git a/src/components/Charts/hooks/index.ts b/src/components/Charts/hooks/index.ts
index c1996fad8d44..ac5272aa5b56 100644
--- a/src/components/Charts/hooks/index.ts
+++ b/src/components/Charts/hooks/index.ts
@@ -1,4 +1,3 @@
-export {useChartBoundsTracking} from './useChartBoundsTracking';
 export {useChartInteractionState} from './useChartInteractionState';
 export {useChartLabelLayout} from './useChartLabelLayout';
 export {useChartInteractions} from './useChartInteractions';
diff --git a/src/components/Charts/hooks/useChartBoundsTracking.ts b/src/components/Charts/hooks/useChartBoundsTracking.ts
deleted file mode 100644
index 8b5188929617..000000000000
--- a/src/components/Charts/hooks/useChartBoundsTracking.ts
+++ /dev/null
@@ -1,27 +0,0 @@
-import {useCallback, useState} from 'react';
-import type {ChartBounds} from 'victory-native';
-
-type OnBoundsChange = (bounds: ChartBounds, plotAreaWidth: number) => void;
-
-/**
- * Reusable hook that tracks the plot area width from CartesianChart's `onChartBoundsChange`.
- *
- * @param onBoundsChange - Optional callback for chart-specific logic (e.g. BarChart bar geometry).
- */
-function useChartBoundsTracking(onBoundsChange?: OnBoundsChange) {
-    const [plotAreaWidth, setPlotAreaWidth] = useState(0);
-
-    const handleChartBoundsChange = useCallback(
-        (bounds: ChartBounds) => {
-            const width = bounds.right - bounds.left;
-            setPlotAreaWidth(width);
-            onBoundsChange?.(bounds, width);
-        },
-        [onBoundsChange],
-    );
-
-    return {plotAreaWidth, handleChartBoundsChange};
-}
-
-export {useChartBoundsTracking};
-export type {OnBoundsChange};
diff --git a/src/components/Charts/hooks/useChartLabelLayout.ts b/src/components/Charts/hooks/useChartLabelLayout.ts
index 18a3a4d1a45e..e41bd9ff2129 100644
--- a/src/components/Charts/hooks/useChartLabelLayout.ts
+++ b/src/components/Charts/hooks/useChartLabelLayout.ts
@@ -29,7 +29,7 @@ type LabelLayoutConfig = {
     data: ChartDataPoint[];
     font: SkFont | null;
     chartWidth: number;
-    plotAreaWidth: number;
+    barAreaWidth: number;
     containerHeight: number;
 };
 
@@ -43,7 +43,7 @@ function measureTextWidth(text: string, font: SkFont): number {
     return glyphWidths.reduce((sum, w) => sum + w, 0);
 }
 
-function useChartLabelLayout({data, font, chartWidth, plotAreaWidth, containerHeight}: LabelLayoutConfig) {
+function useChartLabelLayout({data, font, chartWidth, barAreaWidth, containerHeight}: LabelLayoutConfig) {
     return useMemo(() => {
         if (!font || chartWidth === 0 || containerHeight === 0 || data.length === 0) {
             return {labelRotation: 0, labelSkipInterval: 1, truncatedLabels: data.map((p) => p.label)};
@@ -133,9 +133,9 @@ function useChartLabelLayout({data, font, chartWidth, plotAreaWidth, containerHe
         // Calculate skip interval using spec formula:
         // maxVisibleLabels = floor(barAreaWidth / (effectiveWidth + MIN_LABEL_GAP))
         // skipInterval = ceil(barCount / maxVisibleLabels)
-        // Use plotAreaWidth (actual plotting area from chartBounds) rather than chartWidth
+        // Use barAreaWidth (actual plotting area from chartBounds) rather than chartWidth
         // (full container) so Y-axis labels and padding don't inflate the count.
-        const labelAreaWidth = plotAreaWidth || chartWidth;
+        const labelAreaWidth = barAreaWidth || chartWidth;
         const maxVisibleLabels = Math.floor(labelAreaWidth / (effectiveWidth + LABEL_PADDING));
         // When maxVisibleLabels is 0 (area too narrow for even one label) or less than
         // data.length, compute the interval. data.length is the safe upper bound — show
@@ -151,7 +151,7 @@ function useChartLabelLayout({data, font, chartWidth, plotAreaWidth, containerHe
         }
 
         return {labelRotation: rotationValue, labelSkipInterval: skipInterval, truncatedLabels: finalLabels, maxLabelLength};
-    }, [font, chartWidth, plotAreaWidth, containerHeight, data]);
+    }, [font, chartWidth, barAreaWidth, containerHeight, data]);
 }
 
 export {useChartLabelLayout};
diff --git a/src/components/Search/index.tsx b/src/components/Search/index.tsx
index 8750ddb30642..2e1d087b315f 100644
--- a/src/components/Search/index.tsx
+++ b/src/components/Search/index.tsx
@@ -49,7 +49,6 @@ import {
     getSortedSections,
     getSuggestedSearches,
     getWideAmountIndicators,
-    isGroupedItemArray,
     isReportActionListItemType,
     isSearchDataLoaded,
     isSearchResultsEmpty as isSearchResultsEmptyUtil,
@@ -57,6 +56,7 @@ import {
     isTransactionCardGroupListItemType,
     isTransactionCategoryGroupListItemType,
     isTransactionGroupListItemType,
+    isGroupedItemArray,
     isTransactionListItemType,
     isTransactionMemberGroupListItemType,
     isTransactionMerchantGroupListItemType,
diff --git a/src/libs/SearchUIUtils.ts b/src/libs/SearchUIUtils.ts
index 446cc1df6bb8..387cfec48982 100644
--- a/src/libs/SearchUIUtils.ts
+++ b/src/libs/SearchUIUtils.ts
@@ -11,7 +11,6 @@ import type {MenuItemWithLink} from '@components/MenuItemList';
 import type {MultiSelectItem} from '@components/Search/FilterDropdowns/MultiSelectPopup';
 import type {SingleSelectItem} from '@components/Search/FilterDropdowns/SingleSelectPopup';
 import type {
-    GroupedItem,
     QueryFilters,
     SearchAction,
     SearchColumnType,
@@ -26,6 +25,7 @@ import type {
     SelectedTransactionInfo,
     SingularSearchStatus,
     SortOrder,
+    GroupedItem,
 } from '@components/Search/types';
 import ChatListItem from '@components/SelectionListWithSections/ChatListItem';
 import ExpenseReportListItem from '@components/SelectionListWithSections/Search/ExpenseReportListItem';

From f21f713b29699530e3b74d70419175eb3ec61811 Mon Sep 17 00:00:00 2001
From: Mateusz Rajski <mateusz.rajski@swmansion.com>
Date: Tue, 3 Feb 2026 13:59:01 +0100
Subject: [PATCH 15/26] Add mising d3 types to devDependencies

---
 package-lock.json | 18 ++++++++++++++++++
 package.json      |  3 ++-
 2 files changed, 20 insertions(+), 1 deletion(-)

diff --git a/package-lock.json b/package-lock.json
index d29b3b0c6df4..930c4b00eb96 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -204,6 +204,7 @@
         "@types/base-64": "^1.0.2",
         "@types/canvas-size": "^1.2.2",
         "@types/concurrently": "^7.0.0",
+        "@types/d3-scale": "^4.0.9",
         "@types/howler": "^2.2.12",
         "@types/jest": "^29.5.14",
         "@types/jest-when": "^3.5.2",
@@ -16350,6 +16351,23 @@
         "@types/node": "*"
       }
     },
+    "node_modules/@types/d3-scale": {
+      "version": "4.0.9",
+      "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz",
+      "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@types/d3-time": "*"
+      }
+    },
+    "node_modules/@types/d3-time": {
+      "version": "3.0.4",
+      "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz",
+      "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==",
+      "dev": true,
+      "license": "MIT"
+    },
     "node_modules/@types/deep-eql": {
       "version": "4.0.2",
       "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz",
diff --git a/package.json b/package.json
index 0d7308e98476..d5a5888ad2c2 100644
--- a/package.json
+++ b/package.json
@@ -143,9 +143,9 @@
     "expo-location": "^19.0.7",
     "expo-modules-core": "3.0.18",
     "expo-secure-store": "~14.2.4",
-    "expo-video": "^3.0.12",
     "expo-store-review": "~9.0.8",
     "expo-task-manager": "~14.0.9",
+    "expo-video": "^3.0.12",
     "fast-equals": "^5.2.2",
     "focus-trap-react": "^11.0.3",
     "group-ib-fp": "file:modules/group-ib-fp",
@@ -273,6 +273,7 @@
     "@types/base-64": "^1.0.2",
     "@types/canvas-size": "^1.2.2",
     "@types/concurrently": "^7.0.0",
+    "@types/d3-scale": "^4.0.9",
     "@types/howler": "^2.2.12",
     "@types/jest": "^29.5.14",
     "@types/jest-when": "^3.5.2",

From 023a2a1beacacd534a57b5beea674f4abee2c607 Mon Sep 17 00:00:00 2001
From: Mateusz Rajski <mateusz.rajski@swmansion.com>
Date: Tue, 3 Feb 2026 14:38:19 +0100
Subject: [PATCH 16/26] Fix label rendering in LineChart by introducing custom
 label component

---
 .../Charts/BarChart/BarChartContent.tsx       |  54 +++--
 .../Charts/LineChart/LineChartContent.tsx     | 107 +++++++++-
 src/components/Charts/hooks/index.ts          |   2 +-
 .../Charts/hooks/useChartLabelFormats.ts      |   3 +-
 .../Charts/hooks/useChartLabelLayout.ts       | 201 ++++++++----------
 src/components/Charts/utils.ts                |  12 +-
 src/components/Search/index.tsx               |   2 +-
 src/libs/SearchUIUtils.ts                     |   2 +-
 8 files changed, 225 insertions(+), 158 deletions(-)

diff --git a/src/components/Charts/BarChart/BarChartContent.tsx b/src/components/Charts/BarChart/BarChartContent.tsx
index af40d0e8c6d4..71b4e329d681 100644
--- a/src/components/Charts/BarChart/BarChartContent.tsx
+++ b/src/components/Charts/BarChart/BarChartContent.tsx
@@ -3,7 +3,7 @@ import React, {useCallback, useMemo, useState} from 'react';
 import type {LayoutChangeEvent} from 'react-native';
 import {View} from 'react-native';
 import Animated, {useSharedValue} from 'react-native-reanimated';
-import type {ChartBounds, PointsArray} from 'victory-native';
+import type {ChartBounds, PointsArray, Scale} from 'victory-native';
 import {Bar, CartesianChart} from 'victory-native';
 import ActivityIndicator from '@components/ActivityIndicator';
 import ChartHeader from '@components/Charts/components/ChartHeader';
@@ -11,7 +11,7 @@ import ChartTooltip from '@components/Charts/components/ChartTooltip';
 import {CHART_CONTENT_MIN_HEIGHT, CHART_PADDING, X_AXIS_LINE_WIDTH, Y_AXIS_LABEL_OFFSET, Y_AXIS_LINE_WIDTH, Y_AXIS_TICK_COUNT} from '@components/Charts/constants';
 import fontSource from '@components/Charts/font';
 import type {HitTestArgs} from '@components/Charts/hooks';
-import {useChartInteractions, useChartLabelFormats, useChartLabelLayout, useDynamicYDomain, useTooltipData} from '@components/Charts/hooks';
+import {LABEL_ROTATIONS, useChartInteractions, useChartLabelFormats, useChartLabelLayout, useDynamicYDomain, useTooltipData} from '@components/Charts/hooks';
 import type {CartesianChartProps, ChartDataPoint} from '@components/Charts/types';
 import {DEFAULT_CHART_COLOR, getChartColor} from '@components/Charts/utils';
 import useResponsiveLayout from '@hooks/useResponsiveLayout';
@@ -19,43 +19,38 @@ import useTheme from '@hooks/useTheme';
 import useThemeStyles from '@hooks/useThemeStyles';
 import variables from '@styles/variables';
 
-type BarChartProps = CartesianChartProps & {
-    /** Callback when a bar is pressed */
-    onBarPress?: (dataPoint: ChartDataPoint, index: number) => void;
-
-    /** When true, all bars use the same color. When false (default), each bar uses a different color from the palette. */
-    useSingleColor?: boolean;
-};
-
 /** Inner padding between bars (0.3 = 30% of bar width) */
 const BAR_INNER_PADDING = 0.3;
 
-/** Extra pixel spacing between the chart boundary and the data range, applied per side (Victory's `domainPadding` prop) */
-const DOMAIN_PADDING = {
-    top: 32,
-    bottom: 0,
-    left: 0,
-    right: 0,
-};
-
 /** Safety buffer multiplier for domain padding calculation */
 const DOMAIN_PADDING_SAFETY_BUFFER = 1.1;
 
+/** Extra pixel spacing between the chart boundary and the data range, applied per side (Victory's `domainPadding` prop) */
+const BASE_DOMAIN_PADDING = {top: 32, bottom: 0, left: 0, right: 0};
+
 /**
- * Calculate minimum domainPadding required to prevent bars from overflowing chart edges.
+ * Calculate minimum domainPadding required to prevent data points from crowding chart edges.
  *
- * The issue: victory-native calculates bar width as (1 - innerPadding) * chartWidth / barCount,
- * but positions bars at indices [0, 1, ..., n-1] scaled to the chart width with domainPadding.
- * For small bar counts, the default padding is insufficient and bars overflow.
+ * For evenly-spaced data points, Victory maps indices [0..N-1] into the output range.
+ * Without enough padding the first/last points sit too close to the View boundary
+ * and their centered labels get clipped by overflow:hidden.
  */
-function calculateMinDomainPadding(chartWidth: number, barCount: number, innerPadding: number): number {
-    if (barCount <= 0) {
+function calculateMinDomainPadding(chartWidth: number, pointCount: number, innerPadding: number): number {
+    if (pointCount <= 0) {
         return 0;
     }
-    const minPaddingRatio = (1 - innerPadding) / (2 * (barCount - 1 + innerPadding));
+    const minPaddingRatio = (1 - innerPadding) / (2 * (pointCount - 1 + innerPadding));
     return Math.ceil(chartWidth * minPaddingRatio * DOMAIN_PADDING_SAFETY_BUFFER);
 }
 
+type BarChartProps = CartesianChartProps & {
+    /** Callback when a bar is pressed */
+    onBarPress?: (dataPoint: ChartDataPoint, index: number) => void;
+
+    /** When true, all bars use the same color. When false (default), each bar uses a different color from the palette. */
+    useSingleColor?: boolean;
+};
+
 function BarChartContent({data, title, titleIcon, isLoading, yAxisUnit, yAxisUnitPosition = 'left', useSingleColor = false, onBarPress}: BarChartProps) {
     const theme = useTheme();
     const styles = useThemeStyles();
@@ -64,7 +59,6 @@ function BarChartContent({data, title, titleIcon, isLoading, yAxisUnit, yAxisUni
     const [chartWidth, setChartWidth] = useState(0);
     const [barAreaWidth, setBarAreaWidth] = useState(0);
     const [containerHeight, setContainerHeight] = useState(0);
-
     const defaultBarColor = DEFAULT_CHART_COLOR;
 
     // prepare data for display
@@ -107,10 +101,10 @@ function BarChartContent({data, title, titleIcon, isLoading, yAxisUnit, yAxisUni
 
     const domainPadding = useMemo(() => {
         if (chartWidth === 0) {
-            return {...DOMAIN_PADDING, left: 0, right: 0};
+            return BASE_DOMAIN_PADDING;
         }
         const horizontalPadding = calculateMinDomainPadding(chartWidth, data.length, BAR_INNER_PADDING);
-        return {...DOMAIN_PADDING, right: horizontalPadding + DOMAIN_PADDING.right, left: horizontalPadding};
+        return {...BASE_DOMAIN_PADDING, left: horizontalPadding, right: horizontalPadding};
     }, [chartWidth, data.length]);
 
     const {formatXAxisLabel, formatYAxisLabel} = useChartLabelFormats({
@@ -140,7 +134,7 @@ function BarChartContent({data, title, titleIcon, isLoading, yAxisUnit, yAxisUni
     );
 
     const handleScaleChange = useCallback(
-        (_xScale: unknown, yScale: (value: number) => number) => {
+        (_xScale: Scale, yScale: Scale) => {
             barGeometry.set({
                 ...barGeometry.get(),
                 yZero: yScale(0),
@@ -226,7 +220,7 @@ function BarChartContent({data, title, titleIcon, isLoading, yAxisUnit, yAxisUni
                 titleIcon={titleIcon}
             />
             <View
-                style={[styles.barChartChartContainer, labelRotation === -90 ? dynamicChartStyle : undefined]}
+                style={[styles.barChartChartContainer, labelRotation === -LABEL_ROTATIONS.VERTICAL ? dynamicChartStyle : undefined]}
                 onLayout={handleLayout}
             >
                 {chartWidth > 0 && (
diff --git a/src/components/Charts/LineChart/LineChartContent.tsx b/src/components/Charts/LineChart/LineChartContent.tsx
index 140f704a9f0a..1e5fcf96ef53 100644
--- a/src/components/Charts/LineChart/LineChartContent.tsx
+++ b/src/components/Charts/LineChart/LineChartContent.tsx
@@ -1,8 +1,9 @@
-import {useFont} from '@shopify/react-native-skia';
+import {Group, Text as SkiaText, useFont, vec} from '@shopify/react-native-skia';
 import React, {useCallback, useMemo, useState} from 'react';
 import type {LayoutChangeEvent} from 'react-native';
 import {View} from 'react-native';
 import Animated from 'react-native-reanimated';
+import type {CartesianChartRenderArg, ChartBounds} from 'victory-native';
 import {CartesianChart, Line, Scatter} from 'victory-native';
 import ActivityIndicator from '@components/ActivityIndicator';
 import ChartHeader from '@components/Charts/components/ChartHeader';
@@ -10,16 +11,16 @@ import ChartTooltip from '@components/Charts/components/ChartTooltip';
 import {CHART_CONTENT_MIN_HEIGHT, CHART_PADDING, X_AXIS_LINE_WIDTH, Y_AXIS_LABEL_OFFSET, Y_AXIS_LINE_WIDTH, Y_AXIS_TICK_COUNT} from '@components/Charts/constants';
 import fontSource from '@components/Charts/font';
 import type {HitTestArgs} from '@components/Charts/hooks';
-import {useChartInteractions, useChartLabelFormats, useChartLabelLayout, useDynamicYDomain, useTooltipData} from '@components/Charts/hooks';
+import {LABEL_ROTATIONS, useChartInteractions, useChartLabelFormats, useChartLabelLayout, useDynamicYDomain, useTooltipData} from '@components/Charts/hooks';
 import type {CartesianChartProps, ChartDataPoint} from '@components/Charts/types';
-import {DEFAULT_CHART_COLOR} from '@components/Charts/utils';
+import {DEFAULT_CHART_COLOR, measureTextWidth} from '@components/Charts/utils';
 import useResponsiveLayout from '@hooks/useResponsiveLayout';
 import useTheme from '@hooks/useTheme';
 import useThemeStyles from '@hooks/useThemeStyles';
 import variables from '@styles/variables';
 
 /** Inner dot radius for line chart data points */
-const DOT_INNER_RADIUS = 6;
+const DOT_RADIUS = 6;
 
 type LineChartProps = CartesianChartProps & {
     /** Callback when a data point is pressed */
@@ -33,6 +34,7 @@ function LineChartContent({data, title, titleIcon, isLoading, yAxisUnit, yAxisUn
     const font = useFont(fontSource, variables.iconSizeExtraSmall);
     const [chartWidth, setChartWidth] = useState(0);
     const [containerHeight, setContainerHeight] = useState(0);
+    const [plotAreaWidth, setPlotAreaWidth] = useState(0);
 
     const yAxisDomain = useDynamicYDomain(data);
 
@@ -62,14 +64,29 @@ function LineChartContent({data, title, titleIcon, isLoading, yAxisUnit, yAxisUn
         setContainerHeight(height);
     }, []);
 
+    const handleChartBoundsChange = useCallback((bounds: ChartBounds) => {
+        setPlotAreaWidth(bounds.right - bounds.left);
+    }, []);
+
     const {labelRotation, labelSkipInterval, truncatedLabels, maxLabelLength} = useChartLabelLayout({
         data,
         font,
         chartWidth,
-        barAreaWidth: chartWidth,
+        barAreaWidth: plotAreaWidth,
         containerHeight,
     });
 
+    // Measure label widths for custom positioning in `renderOutside`
+    const labelWidths = useMemo(() => {
+        if (!font) {
+            return [] as number[];
+        }
+        return truncatedLabels.map((label) => measureTextWidth(label, font));
+    }, [font, truncatedLabels]);
+
+    // Convert hook's degree rotation to radians for Skia rendering
+    const angleRad = (Math.abs(labelRotation) * Math.PI) / 180;
+
     const {formatXAxisLabel, formatYAxisLabel} = useChartLabelFormats({
         data,
         yAxisUnit,
@@ -84,7 +101,7 @@ function LineChartContent({data, title, titleIcon, isLoading, yAxisUnit, yAxisUn
 
         const dx = args.cursorX - args.targetX;
         const dy = args.cursorY - args.targetY;
-        return Math.sqrt(dx * dx + dy * dy) <= DOT_INNER_RADIUS;
+        return Math.sqrt(dx * dx + dy * dy) <= DOT_RADIUS;
     }, []);
 
     const {actionsRef, customGestures, activeDataIndex, isTooltipActive, tooltipStyle} = useChartInteractions({
@@ -94,6 +111,72 @@ function LineChartContent({data, title, titleIcon, isLoading, yAxisUnit, yAxisUn
 
     const tooltipData = useTooltipData(activeDataIndex, data, yAxisUnit, yAxisUnitPosition);
 
+    // Victory's built-in x-axis labels center each label under its tick mark,
+    // which works well for bar charts where bars have width and natural spacing.
+    // For line charts, data points sit at the edges of the plot area, so centered
+    // labels get clipped or overflow the chart bounds. We render labels manually
+    // via `renderOutside` so we can right-align each label's last character at its
+    // tick position and clamp edge labels within the canvas.
+    const renderCustomXLabels = useCallback(
+        (args: CartesianChartRenderArg<{x: number; y: number}, 'y'>) => {
+            if (!font) {
+                return null;
+            }
+
+            const fontMetrics = font.getMetrics();
+            const lineHeight = Math.abs(fontMetrics.ascent) + Math.abs(fontMetrics.descent);
+            const fontSize = font.getSize();
+            const labelY = args.chartBounds.bottom + 2 + fontSize;
+
+            return truncatedLabels.map((label, i) => {
+                if (i % labelSkipInterval !== 0) {
+                    return null;
+                }
+
+                const tickX = args.xScale(i);
+                const labelWidth = labelWidths.at(i) ?? 0;
+
+                // Last character anchored at tickX, clamped to canvas edges.
+                const idealX = tickX - labelWidth;
+                const clampedX = Math.max(0, Math.min(args.canvasSize.width - labelWidth, idealX));
+
+                if (angleRad === 0) {
+                    return (
+                        <SkiaText
+                            key={`xlabel-${label}`}
+                            x={clampedX}
+                            y={labelY}
+                            text={label}
+                            font={font}
+                            color={theme.textSupporting}
+                        />
+                    );
+                }
+
+                // At 90° the rotated label's horizontal footprint is lineHeight;
+                // shift by half to center it on the tick mark.
+                const centeringOffset = labelRotation === -LABEL_ROTATIONS.VERTICAL ? lineHeight / 2 : 0;
+                const origin = vec(clampedX + labelWidth + centeringOffset, labelY);
+                return (
+                    <Group
+                        key={`xlabel-${label}`}
+                        origin={origin}
+                        transform={[{rotate: -angleRad}]}
+                    >
+                        <SkiaText
+                            x={clampedX}
+                            y={labelY}
+                            text={label}
+                            font={font}
+                            color={theme.textSupporting}
+                        />
+                    </Group>
+                );
+            });
+        },
+        [font, truncatedLabels, labelSkipInterval, labelWidths, angleRad, theme.textSupporting],
+    );
+
     const dynamicChartStyle = useMemo(
         () => ({
             height: CHART_CONTENT_MIN_HEIGHT + (maxLabelLength ?? 0),
@@ -120,7 +203,7 @@ function LineChartContent({data, title, titleIcon, isLoading, yAxisUnit, yAxisUn
                 titleIcon={titleIcon}
             />
             <View
-                style={[styles.lineChartChartContainer, labelRotation === -90 ? dynamicChartStyle : undefined]}
+                style={[styles.lineChartChartContainer, labelRotation === -LABEL_ROTATIONS.VERTICAL ? dynamicChartStyle : undefined]}
                 onLayout={handleLayout}
             >
                 {chartWidth > 0 && (
@@ -128,13 +211,15 @@ function LineChartContent({data, title, titleIcon, isLoading, yAxisUnit, yAxisUn
                         xKey="x"
                         padding={CHART_PADDING}
                         yKeys={['y']}
-                        domainPadding={{left: 20, right: 20, top: 20, bottom: 20}}
+                        domainPadding={16}
                         actionsRef={actionsRef}
                         customGestures={customGestures}
+                        onChartBoundsChange={handleChartBoundsChange}
+                        renderOutside={renderCustomXLabels}
                         xAxis={{
                             font,
                             tickCount: data.length,
-                            labelColor: theme.textSupporting,
+                            labelColor: 'transparent',
                             lineWidth: X_AXIS_LINE_WIDTH,
                             formatXLabel: formatXAxisLabel,
                             labelRotate: labelRotation,
@@ -152,7 +237,7 @@ function LineChartContent({data, title, titleIcon, isLoading, yAxisUnit, yAxisUn
                                 domain: yAxisDomain,
                             },
                         ]}
-                        frame={{lineWidth: {left: 1, bottom: 1, top: 0, right: 0}}}
+                        frame={{lineWidth: {left: 1, bottom: 1, top: 0, right: 0}, lineColor: theme.border}}
                         data={chartData}
                     >
                         {({points}) => (
@@ -165,7 +250,7 @@ function LineChartContent({data, title, titleIcon, isLoading, yAxisUnit, yAxisUn
                                 />
                                 <Scatter
                                     points={points.y}
-                                    radius={DOT_INNER_RADIUS}
+                                    radius={DOT_RADIUS}
                                     color={DEFAULT_CHART_COLOR}
                                 />
                             </>
diff --git a/src/components/Charts/hooks/index.ts b/src/components/Charts/hooks/index.ts
index ac5272aa5b56..6b90bfca65bf 100644
--- a/src/components/Charts/hooks/index.ts
+++ b/src/components/Charts/hooks/index.ts
@@ -1,5 +1,5 @@
 export {useChartInteractionState} from './useChartInteractionState';
-export {useChartLabelLayout} from './useChartLabelLayout';
+export {LABEL_ROTATIONS, useChartLabelLayout} from './useChartLabelLayout';
 export {useChartInteractions} from './useChartInteractions';
 export type {HitTestArgs} from './useChartInteractions';
 export type {ChartInteractionState, ChartInteractionStateInit} from './useChartInteractionState';
diff --git a/src/components/Charts/hooks/useChartLabelFormats.ts b/src/components/Charts/hooks/useChartLabelFormats.ts
index 31921462d8b8..c87a2c6177ba 100644
--- a/src/components/Charts/hooks/useChartLabelFormats.ts
+++ b/src/components/Charts/hooks/useChartLabelFormats.ts
@@ -1,5 +1,6 @@
 import {useCallback} from 'react';
 import type {ChartDataPoint, YAxisUnitPosition} from '@components/Charts/types';
+import {LABEL_ROTATIONS} from './useChartLabelLayout';
 
 type UseChartLabelFormatsProps = {
     data: ChartDataPoint[];
@@ -36,7 +37,7 @@ export default function useChartLabelFormats({data, yAxisUnit, yAxisUnitPosition
             // Use pre-truncated labels
             // If rotation is vertical (-90), we usually want full labels
             // because they have more space vertically.
-            const sourceToUse = labelRotation === -90 ? data.map((p) => p.label) : truncatedLabels;
+            const sourceToUse = labelRotation === -LABEL_ROTATIONS.VERTICAL ? data.map((p) => p.label) : truncatedLabels;
 
             return sourceToUse.at(index) ?? '';
         },
diff --git a/src/components/Charts/hooks/useChartLabelLayout.ts b/src/components/Charts/hooks/useChartLabelLayout.ts
index e41bd9ff2129..39fce259acad 100644
--- a/src/components/Charts/hooks/useChartLabelLayout.ts
+++ b/src/components/Charts/hooks/useChartLabelLayout.ts
@@ -1,24 +1,24 @@
 import type {SkFont} from '@shopify/react-native-skia';
 import {useMemo} from 'react';
 import {Y_AXIS_LABEL_OFFSET} from '@components/Charts/constants';
+import {measureTextWidth} from '@components/Charts/utils';
 
-/** Rotation angle for X-axis labels - 45 degrees */
-const X_AXIS_LABEL_ROTATION_45 = -45;
+/** Supported label rotation angles in degrees */
+const LABEL_ROTATIONS = {
+    HORIZONTAL: 0,
+    DIAGONAL: 45,
+    VERTICAL: 90,
+} as const;
 
-/** Rotation angle for X-axis labels - 90 degrees */
-const X_AXIS_LABEL_ROTATION_90 = -90;
+const SIN_45 = Math.sin(Math.PI / 4);
 
-/** Sin of 45 degrees - used to calculate effective width of rotated labels */
-const SIN_45_DEGREES = Math.sin(Math.PI / 4); // ≈ 0.707
-
-/** Minimum padding between labels (in pixels) */
+/** Minimum gap between adjacent labels (px) */
 const LABEL_PADDING = 4;
 
-/** Maximum ratio of container height that X-axis labels can occupy. */
-const X_AXIS_LABEL_MAX_HEIGHT_RATIO = 0.35;
+/** X-axis labels can occupy at most this fraction of container height */
+const MAX_LABEL_HEIGHT_RATIO = 0.35;
 
-/** Ellipsis character for truncated labels */
-const LABEL_ELLIPSIS = '...';
+const ELLIPSIS = '...';
 
 type ChartDataPoint = {
     label: string;
@@ -33,14 +33,65 @@ type LabelLayoutConfig = {
     containerHeight: number;
 };
 
+/** Truncate `label` so its pixel width fits within `maxWidth`, adding ellipsis. */
+function truncateLabel(label: string, labelWidth: number, maxWidth: number, ellipsisWidth: number): string {
+    if (labelWidth <= maxWidth) {
+        return label;
+    }
+    const available = maxWidth - ellipsisWidth;
+    if (available <= 0) {
+        return ELLIPSIS;
+    }
+    const maxChars = Math.max(1, Math.floor(label.length * (available / labelWidth)));
+    return label.slice(0, maxChars) + ELLIPSIS;
+}
+
+/** Horizontal footprint of a label at a given rotation angle. */
+function effectiveWidth(labelWidth: number, lineHeight: number, rotation: number): number {
+    if (rotation === LABEL_ROTATIONS.VERTICAL) {
+        return lineHeight;
+    }
+    if (rotation === LABEL_ROTATIONS.DIAGONAL) {
+        return labelWidth * SIN_45;
+    }
+    return labelWidth;
+}
+
+/** How many labels fit side-by-side in `areaWidth` given each takes `itemWidth`. */
+function maxVisibleCount(areaWidth: number, itemWidth: number): number {
+    return Math.floor(areaWidth / (itemWidth + LABEL_PADDING));
+}
+
+/**
+ * Pick the smallest rotation (0 → 45 → 90) where labels don't overlap,
+ * preferring rotation over skip interval.
+ */
+function pickRotation(maxLabelWidth: number, lineHeight: number, availableWidthPerLabel: number, labelArea: number, dataCount: number): number {
+    const candidates = [LABEL_ROTATIONS.HORIZONTAL, LABEL_ROTATIONS.DIAGONAL, LABEL_ROTATIONS.VERTICAL] as const;
+    for (const angle of candidates) {
+        const ew = effectiveWidth(maxLabelWidth, lineHeight, angle);
+        if (ew <= availableWidthPerLabel && maxVisibleCount(labelArea, ew) >= dataCount) {
+            return angle;
+        }
+    }
+    return LABEL_ROTATIONS.VERTICAL;
+}
+
 /**
- * Measure the width of a text string using the font's glyph widths.
- * Uses getGlyphWidths as measureText is not implemented on React Native Web.
+ * Max label pixel width allowed at a given rotation so labels don't exceed
+ * `MAX_LABEL_HEIGHT_RATIO` of the container height.
+ *
+ * Based on Victory's internal allocation formula:
+ *   totalHeight = lineHeight + Y_AXIS_LABEL_OFFSET * 2 + labelWidth * sin(angle)
+ * (from victory-native-xl/src/cartesian/utils/transformInputData.ts)
  */
-function measureTextWidth(text: string, font: SkFont): number {
-    const glyphIDs = font.getGlyphIDs(text);
-    const glyphWidths = font.getGlyphWidths(glyphIDs);
-    return glyphWidths.reduce((sum, w) => sum + w, 0);
+function maxAllowedWidth(rotation: number, lineHeight: number, containerHeight: number): number {
+    if (rotation === LABEL_ROTATIONS.DIAGONAL) {
+        const budget = containerHeight * MAX_LABEL_HEIGHT_RATIO - (lineHeight + Y_AXIS_LABEL_OFFSET * 2);
+        return Math.max(0, budget) / SIN_45;
+    }
+    // 0° and 90°: no truncation (0° uses skip, 90° expands container)
+    return Infinity;
 }
 
 function useChartLabelLayout({data, font, chartWidth, barAreaWidth, containerHeight}: LabelLayoutConfig) {
@@ -49,110 +100,36 @@ function useChartLabelLayout({data, font, chartWidth, barAreaWidth, containerHei
             return {labelRotation: 0, labelSkipInterval: 1, truncatedLabels: data.map((p) => p.label)};
         }
 
-        // Get font metrics
         const fontMetrics = font.getMetrics();
         const lineHeight = Math.abs(fontMetrics.descent) + Math.abs(fontMetrics.ascent);
-        const ellipsisWidth = measureTextWidth(LABEL_ELLIPSIS, font);
+        const ellipsisWidth = measureTextWidth(ELLIPSIS, font);
 
-        // Calculate available dimensions
-        const availableWidthPerBar = chartWidth / data.length - LABEL_PADDING;
-
-        // Measure original labels
+        const availableWidthPerLabel = chartWidth / data.length - LABEL_PADDING;
         const labelWidths = data.map((p) => measureTextWidth(p.label, font));
         const maxLabelLength = Math.max(...labelWidths);
+        const labelArea = barAreaWidth || chartWidth;
 
-        // Helper to truncate a label to fit a max pixel width
-        const truncateToWidth = (label: string, labelWidth: number, maxWidth: number): string => {
-            if (labelWidth <= maxWidth) {
-                return label;
-            }
-            const availableWidth = maxWidth - ellipsisWidth;
-            if (availableWidth <= 0) {
-                return LABEL_ELLIPSIS;
-            }
-            const ratio = availableWidth / labelWidth;
-            const maxChars = Math.max(1, Math.floor(label.length * ratio));
-            return label.slice(0, maxChars) + LABEL_ELLIPSIS;
-        };
-
-        // === DETERMINE ROTATION (based on WIDTH constraint, monotonic: 0° → 45° → 90°) ===
-        let rotation = 0;
-        if (maxLabelLength > availableWidthPerBar) {
-            // Labels don't fit at 0°, try 45°
-            const effectiveWidthAt45 = maxLabelLength * SIN_45_DEGREES;
-            if (effectiveWidthAt45 <= availableWidthPerBar) {
-                rotation = 45;
-            } else {
-                // 45° doesn't fit either, use 90°
-                rotation = 90;
-            }
-        }
-
-        // === DETERMINE TRUNCATION ===
-        // Limit label area to X_AXIS_LABEL_MAX_HEIGHT_RATIO of container height.
-        //
-        // IMPLEMENTATION NOTE: We assume Victory allocates space for X-axis labels using:
-        //   totalHeight = fontHeight + yAxis.labelOffset * 2 + labelWidth * sin(angle)
-        // This formula was found in: victory-native-xl/src/cartesian/utils/transformInputData.ts
-        // If Victory changes this formula, these calculations will need adjustment.
-        //
-        // We calculate max labelWidth so total allocation stays within our limit.
-        const maxLabelHeight = containerHeight * X_AXIS_LABEL_MAX_HEIGHT_RATIO;
-        const victoryBaseAllocation = lineHeight + Y_AXIS_LABEL_OFFSET * 2;
-        const availableForRotation = Math.max(0, maxLabelHeight - victoryBaseAllocation);
-
-        let maxAllowedLabelWidth: number;
-
-        if (rotation === 0) {
-            // At 0°: no truncation, use skip interval instead (like Google Sheets)
-            maxAllowedLabelWidth = Infinity;
-        } else if (rotation === 45) {
-            // At 45°: labelWidth * sin(45°) <= availableForRotation
-            // labelWidth <= availableForRotation / sin(45°)
-            maxAllowedLabelWidth = availableForRotation / SIN_45_DEGREES;
-        } else {
-            // At 90°: no truncation, container expands to accommodate labels
-            maxAllowedLabelWidth = Infinity;
-        }
+        // 1. Pick rotation
+        const rotation = pickRotation(maxLabelLength, lineHeight, availableWidthPerLabel, labelArea, data.length);
 
-        // Generate truncated labels
-        const finalLabels = data.map((p, i) => truncateToWidth(p.label, labelWidths.at(i) ?? 0, maxAllowedLabelWidth));
+        // 2. Truncate labels (only effective at 45°)
+        const maxWidth = maxAllowedWidth(rotation, lineHeight, containerHeight);
+        const finalLabels = data.map((p, i) => truncateLabel(p.label, labelWidths.at(i) ?? 0, maxWidth, ellipsisWidth));
 
-        // === CALCULATE SKIP INTERVAL ===
-        // Calculate effective width based on rotation angle
+        // 3. Compute skip interval
         const finalMaxWidth = Math.max(...finalLabels.map((l) => measureTextWidth(l, font)));
-        let effectiveWidth: number;
-        if (rotation === 0) {
-            effectiveWidth = finalMaxWidth;
-        } else if (rotation === 45) {
-            effectiveWidth = finalMaxWidth * SIN_45_DEGREES;
-        } else {
-            effectiveWidth = lineHeight; // At 90°, width is the line height
-        }
-
-        // Calculate skip interval using spec formula:
-        // maxVisibleLabels = floor(barAreaWidth / (effectiveWidth + MIN_LABEL_GAP))
-        // skipInterval = ceil(barCount / maxVisibleLabels)
-        // Use barAreaWidth (actual plotting area from chartBounds) rather than chartWidth
-        // (full container) so Y-axis labels and padding don't inflate the count.
-        const labelAreaWidth = barAreaWidth || chartWidth;
-        const maxVisibleLabels = Math.floor(labelAreaWidth / (effectiveWidth + LABEL_PADDING));
-        // When maxVisibleLabels is 0 (area too narrow for even one label) or less than
-        // data.length, compute the interval. data.length is the safe upper bound — show
-        // at most the first label.
-        const skipInterval = maxVisibleLabels >= data.length ? 1 : Math.ceil(data.length / Math.max(1, maxVisibleLabels));
-
-        // Convert rotation to negative degrees for Victory chart
-        let rotationValue = 0;
-        if (rotation === 45) {
-            rotationValue = X_AXIS_LABEL_ROTATION_45;
-        } else if (rotation === 90) {
-            rotationValue = X_AXIS_LABEL_ROTATION_90;
-        }
-
-        return {labelRotation: rotationValue, labelSkipInterval: skipInterval, truncatedLabels: finalLabels, maxLabelLength};
+        const ew = effectiveWidth(finalMaxWidth, lineHeight, rotation);
+        const visible = maxVisibleCount(labelArea, ew);
+        const skipInterval = visible >= data.length ? 1 : Math.ceil(data.length / Math.max(1, visible));
+
+        return {
+            labelRotation: -rotation,
+            labelSkipInterval: skipInterval,
+            truncatedLabels: finalLabels,
+            maxLabelLength,
+        };
     }, [font, chartWidth, barAreaWidth, containerHeight, data]);
 }
 
-export {useChartLabelLayout};
+export {LABEL_ROTATIONS, useChartLabelLayout};
 export type {LabelLayoutConfig};
diff --git a/src/components/Charts/utils.ts b/src/components/Charts/utils.ts
index 3dca6dc372f6..e63b87e19774 100644
--- a/src/components/Charts/utils.ts
+++ b/src/components/Charts/utils.ts
@@ -1,3 +1,4 @@
+import type {SkFont} from '@shopify/react-native-skia';
 import colors from '@styles/theme/colors';
 
 /**
@@ -41,4 +42,13 @@ function getChartColor(index: number): string {
 /** Default color used for single-color charts (e.g., line chart, single-color bar chart) */
 const DEFAULT_CHART_COLOR = getChartColor(5);
 
-export {getChartColor, DEFAULT_CHART_COLOR};
+/**
+ * Measure pixel width of a string via glyph widths.
+ * (measureText is not implemented on React Native Web)
+ */
+function measureTextWidth(text: string, font: SkFont): number {
+    const glyphIDs = font.getGlyphIDs(text);
+    return font.getGlyphWidths(glyphIDs).reduce((sum, w) => sum + w, 0);
+}
+
+export {getChartColor, DEFAULT_CHART_COLOR, measureTextWidth};
diff --git a/src/components/Search/index.tsx b/src/components/Search/index.tsx
index 2e1d087b315f..8750ddb30642 100644
--- a/src/components/Search/index.tsx
+++ b/src/components/Search/index.tsx
@@ -49,6 +49,7 @@ import {
     getSortedSections,
     getSuggestedSearches,
     getWideAmountIndicators,
+    isGroupedItemArray,
     isReportActionListItemType,
     isSearchDataLoaded,
     isSearchResultsEmpty as isSearchResultsEmptyUtil,
@@ -56,7 +57,6 @@ import {
     isTransactionCardGroupListItemType,
     isTransactionCategoryGroupListItemType,
     isTransactionGroupListItemType,
-    isGroupedItemArray,
     isTransactionListItemType,
     isTransactionMemberGroupListItemType,
     isTransactionMerchantGroupListItemType,
diff --git a/src/libs/SearchUIUtils.ts b/src/libs/SearchUIUtils.ts
index 387cfec48982..446cc1df6bb8 100644
--- a/src/libs/SearchUIUtils.ts
+++ b/src/libs/SearchUIUtils.ts
@@ -11,6 +11,7 @@ import type {MenuItemWithLink} from '@components/MenuItemList';
 import type {MultiSelectItem} from '@components/Search/FilterDropdowns/MultiSelectPopup';
 import type {SingleSelectItem} from '@components/Search/FilterDropdowns/SingleSelectPopup';
 import type {
+    GroupedItem,
     QueryFilters,
     SearchAction,
     SearchColumnType,
@@ -25,7 +26,6 @@ import type {
     SelectedTransactionInfo,
     SingularSearchStatus,
     SortOrder,
-    GroupedItem,
 } from '@components/Search/types';
 import ChatListItem from '@components/SelectionListWithSections/ChatListItem';
 import ExpenseReportListItem from '@components/SelectionListWithSections/Search/ExpenseReportListItem';

From b283a70e8f2527b121d6c97a4fd2038681b18f99 Mon Sep 17 00:00:00 2001
From: Mateusz Rajski <mateusz.rajski@swmansion.com>
Date: Tue, 3 Feb 2026 15:05:47 +0100
Subject: [PATCH 17/26] Fix spellcheck

---
 src/components/Charts/LineChart/LineChartContent.tsx | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/src/components/Charts/LineChart/LineChartContent.tsx b/src/components/Charts/LineChart/LineChartContent.tsx
index 1e5fcf96ef53..29b4324f7419 100644
--- a/src/components/Charts/LineChart/LineChartContent.tsx
+++ b/src/components/Charts/LineChart/LineChartContent.tsx
@@ -143,7 +143,7 @@ function LineChartContent({data, title, titleIcon, isLoading, yAxisUnit, yAxisUn
                 if (angleRad === 0) {
                     return (
                         <SkiaText
-                            key={`xlabel-${label}`}
+                            key={`x-label-${label}`}
                             x={clampedX}
                             y={labelY}
                             text={label}
@@ -159,7 +159,7 @@ function LineChartContent({data, title, titleIcon, isLoading, yAxisUnit, yAxisUn
                 const origin = vec(clampedX + labelWidth + centeringOffset, labelY);
                 return (
                     <Group
-                        key={`xlabel-${label}`}
+                        key={`x-label-${label}`}
                         origin={origin}
                         transform={[{rotate: -angleRad}]}
                     >
@@ -174,7 +174,7 @@ function LineChartContent({data, title, titleIcon, isLoading, yAxisUnit, yAxisUn
                 );
             });
         },
-        [font, truncatedLabels, labelSkipInterval, labelWidths, angleRad, theme.textSupporting],
+        [font, truncatedLabels, labelSkipInterval, labelWidths, angleRad, labelRotation, theme.textSupporting],
     );
 
     const dynamicChartStyle = useMemo(

From b631ccb28b8ca9f5a1cdc4daba59e01855247868 Mon Sep 17 00:00:00 2001
From: Mateusz Rajski <mateusz.rajski@swmansion.com>
Date: Tue, 3 Feb 2026 15:14:31 +0100
Subject: [PATCH 18/26] Refactor useTooltipData and fix tests

---
 src/components/Charts/hooks/useTooltipData.ts | 5 +++--
 tests/unit/Search/SearchUIUtilsTest.ts        | 7 ++++---
 2 files changed, 7 insertions(+), 5 deletions(-)

diff --git a/src/components/Charts/hooks/useTooltipData.ts b/src/components/Charts/hooks/useTooltipData.ts
index 3a75db14a106..2ec687681de9 100644
--- a/src/components/Charts/hooks/useTooltipData.ts
+++ b/src/components/Charts/hooks/useTooltipData.ts
@@ -12,6 +12,8 @@ type TooltipData = {
  * Computes the display amount (with optional currency unit) and the percentage relative to all data points.
  */
 function useTooltipData(activeDataIndex: number, data: ChartDataPoint[], yAxisUnit?: string, yAxisUnitPosition?: YAxisUnitPosition): TooltipData | null {
+    const totalSum = useMemo(() => data.reduce((sum, point) => sum + Math.abs(point.total), 0), [data]);
+
     return useMemo(() => {
         if (activeDataIndex < 0 || activeDataIndex >= data.length) {
             return null;
@@ -27,14 +29,13 @@ function useTooltipData(activeDataIndex: number, data: ChartDataPoint[], yAxisUn
             const separator = yAxisUnit.length > 1 ? ' ' : '';
             formattedAmount = yAxisUnitPosition === 'left' ? `${yAxisUnit}${separator}${formatted}` : `${formatted}${separator}${yAxisUnit}`;
         }
-        const totalSum = data.reduce((sum, point) => sum + Math.abs(point.total), 0);
         const percent = totalSum > 0 ? Math.round((Math.abs(dataPoint.total) / totalSum) * 100) : 0;
         return {
             label: dataPoint.label,
             amount: formattedAmount,
             percentage: percent < 1 ? '<1%' : `${percent}%`,
         };
-    }, [activeDataIndex, data, yAxisUnit, yAxisUnitPosition]);
+    }, [activeDataIndex, data, totalSum, yAxisUnit, yAxisUnitPosition]);
 }
 
 export {useTooltipData};
diff --git a/tests/unit/Search/SearchUIUtilsTest.ts b/tests/unit/Search/SearchUIUtilsTest.ts
index 0dc7c41788c2..e9bab9ee4b3a 100644
--- a/tests/unit/Search/SearchUIUtilsTest.ts
+++ b/tests/unit/Search/SearchUIUtilsTest.ts
@@ -6242,11 +6242,12 @@ describe('SearchUIUtils', () => {
     });
 
     describe('view autocomplete values', () => {
-        test('should include all view values (table, bar)', () => {
+        test('should include all view values (table, bar, line)', () => {
             const viewValues = Object.values(CONST.SEARCH.VIEW);
             expect(viewValues).toContain('table');
             expect(viewValues).toContain('bar');
-            expect(viewValues).toHaveLength(2);
+            expect(viewValues).toContain('line');
+            expect(viewValues).toHaveLength(3);
         });
 
         test('should correctly map view values to user-friendly values', () => {
@@ -6254,7 +6255,7 @@ describe('SearchUIUtils', () => {
             const userFriendlyValues = viewValues.map((value) => getUserFriendlyValue(value));
 
             // All view values should be mapped (they may be the same or different)
-            expect(userFriendlyValues).toHaveLength(2);
+            expect(userFriendlyValues).toHaveLength(3);
             expect(userFriendlyValues.every((value) => typeof value === 'string')).toBe(true);
         });
     });

From c090cca69cad21e4f3550115b24668e9bf73ef41 Mon Sep 17 00:00:00 2001
From: Mateusz Rajski <mateusz.rajski@swmansion.com>
Date: Wed, 4 Feb 2026 15:29:59 +0100
Subject: [PATCH 19/26] Improve label layout primitives for LineChart

---
 .../Charts/BarChart/BarChartContent.tsx       |  20 ++-
 .../Charts/LineChart/LineChartContent.tsx     |  95 ++++++-------
 src/components/Charts/hooks/index.ts          |   2 +-
 .../Charts/hooks/useChartInteractions.ts      |  32 +++--
 .../Charts/hooks/useChartLabelFormats.ts      |  10 +-
 .../Charts/hooks/useChartLabelLayout.ts       | 130 +++++++++++-------
 src/components/Charts/utils.ts                |  13 +-
 7 files changed, 178 insertions(+), 124 deletions(-)

diff --git a/src/components/Charts/BarChart/BarChartContent.tsx b/src/components/Charts/BarChart/BarChartContent.tsx
index 71b4e329d681..55f79db985ec 100644
--- a/src/components/Charts/BarChart/BarChartContent.tsx
+++ b/src/components/Charts/BarChart/BarChartContent.tsx
@@ -11,7 +11,7 @@ import ChartTooltip from '@components/Charts/components/ChartTooltip';
 import {CHART_CONTENT_MIN_HEIGHT, CHART_PADDING, X_AXIS_LINE_WIDTH, Y_AXIS_LABEL_OFFSET, Y_AXIS_LINE_WIDTH, Y_AXIS_TICK_COUNT} from '@components/Charts/constants';
 import fontSource from '@components/Charts/font';
 import type {HitTestArgs} from '@components/Charts/hooks';
-import {LABEL_ROTATIONS, useChartInteractions, useChartLabelFormats, useChartLabelLayout, useDynamicYDomain, useTooltipData} from '@components/Charts/hooks';
+import {useChartInteractions, useChartLabelFormats, useChartLabelLayout, useDynamicYDomain, useTooltipData} from '@components/Charts/hooks';
 import type {CartesianChartProps, ChartDataPoint} from '@components/Charts/types';
 import {DEFAULT_CHART_COLOR, getChartColor} from '@components/Charts/utils';
 import useResponsiveLayout from '@hooks/useResponsiveLayout';
@@ -58,7 +58,6 @@ function BarChartContent({data, title, titleIcon, isLoading, yAxisUnit, yAxisUni
     const font = useFont(fontSource, variables.iconSizeExtraSmall);
     const [chartWidth, setChartWidth] = useState(0);
     const [barAreaWidth, setBarAreaWidth] = useState(0);
-    const [containerHeight, setContainerHeight] = useState(0);
     const defaultBarColor = DEFAULT_CHART_COLOR;
 
     // prepare data for display
@@ -86,17 +85,14 @@ function BarChartContent({data, title, titleIcon, isLoading, yAxisUnit, yAxisUni
     );
 
     const handleLayout = useCallback((event: LayoutChangeEvent) => {
-        const {width, height} = event.nativeEvent.layout;
-        setChartWidth(width);
-        setContainerHeight(height);
+        setChartWidth(event.nativeEvent.layout.width);
     }, []);
 
-    const {labelRotation, labelSkipInterval, truncatedLabels, maxLabelLength} = useChartLabelLayout({
+    const {labelRotation, labelSkipInterval, truncatedLabels, xAxisLabelHeight} = useChartLabelLayout({
         data,
         font,
-        chartWidth,
-        barAreaWidth,
-        containerHeight,
+        tickSpacing: barAreaWidth > 0 ? barAreaWidth / data.length : 0,
+        labelAreaWidth: barAreaWidth,
     });
 
     const domainPadding = useMemo(() => {
@@ -196,9 +192,9 @@ function BarChartContent({data, title, titleIcon, isLoading, yAxisUnit, yAxisUni
     // This keeps bar area at ~250px while giving labels their needed vertical space
     const dynamicChartStyle = useMemo(
         () => ({
-            height: CHART_CONTENT_MIN_HEIGHT + (maxLabelLength ?? 0),
+            height: CHART_CONTENT_MIN_HEIGHT + (xAxisLabelHeight ?? 0),
         }),
-        [maxLabelLength],
+        [xAxisLabelHeight],
     );
 
     if (isLoading || !font) {
@@ -220,7 +216,7 @@ function BarChartContent({data, title, titleIcon, isLoading, yAxisUnit, yAxisUni
                 titleIcon={titleIcon}
             />
             <View
-                style={[styles.barChartChartContainer, labelRotation === -LABEL_ROTATIONS.VERTICAL ? dynamicChartStyle : undefined]}
+                style={[styles.barChartChartContainer, dynamicChartStyle]}
                 onLayout={handleLayout}
             >
                 {chartWidth > 0 && (
diff --git a/src/components/Charts/LineChart/LineChartContent.tsx b/src/components/Charts/LineChart/LineChartContent.tsx
index 29b4324f7419..6bc439d801b1 100644
--- a/src/components/Charts/LineChart/LineChartContent.tsx
+++ b/src/components/Charts/LineChart/LineChartContent.tsx
@@ -11,9 +11,9 @@ import ChartTooltip from '@components/Charts/components/ChartTooltip';
 import {CHART_CONTENT_MIN_HEIGHT, CHART_PADDING, X_AXIS_LINE_WIDTH, Y_AXIS_LABEL_OFFSET, Y_AXIS_LINE_WIDTH, Y_AXIS_TICK_COUNT} from '@components/Charts/constants';
 import fontSource from '@components/Charts/font';
 import type {HitTestArgs} from '@components/Charts/hooks';
-import {LABEL_ROTATIONS, useChartInteractions, useChartLabelFormats, useChartLabelLayout, useDynamicYDomain, useTooltipData} from '@components/Charts/hooks';
+import {useChartInteractions, useChartLabelFormats, useChartLabelLayout, useDynamicYDomain, useTooltipData} from '@components/Charts/hooks';
 import type {CartesianChartProps, ChartDataPoint} from '@components/Charts/types';
-import {DEFAULT_CHART_COLOR, measureTextWidth} from '@components/Charts/utils';
+import {DEFAULT_CHART_COLOR, measureTextWidth, rotatedLabelCenterCorrection} from '@components/Charts/utils';
 import useResponsiveLayout from '@hooks/useResponsiveLayout';
 import useTheme from '@hooks/useTheme';
 import useThemeStyles from '@hooks/useThemeStyles';
@@ -22,6 +22,12 @@ import variables from '@styles/variables';
 /** Inner dot radius for line chart data points */
 const DOT_RADIUS = 6;
 
+/** Extra pixel spacing between the plot boundary and the first/last data point */
+const LINE_CHART_DOMAIN_PADDING = 16;
+
+/** Gap between the x-axis line and the top of label glyphs */
+const X_AXIS_LABEL_GAP = 2;
+
 type LineChartProps = CartesianChartProps & {
     /** Callback when a data point is pressed */
     onPointPress?: (dataPoint: ChartDataPoint, index: number) => void;
@@ -33,8 +39,7 @@ function LineChartContent({data, title, titleIcon, isLoading, yAxisUnit, yAxisUn
     const {shouldUseNarrowLayout} = useResponsiveLayout();
     const font = useFont(fontSource, variables.iconSizeExtraSmall);
     const [chartWidth, setChartWidth] = useState(0);
-    const [containerHeight, setContainerHeight] = useState(0);
-    const [plotAreaWidth, setPlotAreaWidth] = useState(0);
+    const [chartBoundsInfo, setChartBoundsInfo] = useState({plotAreaWidth: 0, firstTickOffset: 0});
 
     const yAxisDomain = useDynamicYDomain(data);
 
@@ -59,21 +64,31 @@ function LineChartContent({data, title, titleIcon, isLoading, yAxisUnit, yAxisUn
     );
 
     const handleLayout = useCallback((event: LayoutChangeEvent) => {
-        const {width, height} = event.nativeEvent.layout;
-        setChartWidth(width);
-        setContainerHeight(height);
+        setChartWidth(event.nativeEvent.layout.width);
     }, []);
 
     const handleChartBoundsChange = useCallback((bounds: ChartBounds) => {
-        setPlotAreaWidth(bounds.right - bounds.left);
+        setChartBoundsInfo({
+            plotAreaWidth: bounds.right - bounds.left,
+            firstTickOffset: bounds.left + LINE_CHART_DOMAIN_PADDING,
+        });
     }, []);
 
-    const {labelRotation, labelSkipInterval, truncatedLabels, maxLabelLength} = useChartLabelLayout({
+    const {plotAreaWidth, firstTickOffset} = chartBoundsInfo;
+
+    const tickSpacing = useMemo(() => {
+        if (plotAreaWidth === 0 || data.length <= 1) {
+            return 0;
+        }
+        return (plotAreaWidth - 2 * LINE_CHART_DOMAIN_PADDING) / (data.length - 1);
+    }, [plotAreaWidth, data.length]);
+
+    const {labelRotation, labelSkipInterval, truncatedLabels, xAxisLabelHeight} = useChartLabelLayout({
         data,
         font,
-        chartWidth,
-        barAreaWidth: plotAreaWidth,
-        containerHeight,
+        tickSpacing,
+        labelAreaWidth: plotAreaWidth,
+        firstTickOffset,
     });
 
     // Measure label widths for custom positioning in `renderOutside`
@@ -87,13 +102,10 @@ function LineChartContent({data, title, titleIcon, isLoading, yAxisUnit, yAxisUn
     // Convert hook's degree rotation to radians for Skia rendering
     const angleRad = (Math.abs(labelRotation) * Math.PI) / 180;
 
-    const {formatXAxisLabel, formatYAxisLabel} = useChartLabelFormats({
+    const {formatYAxisLabel} = useChartLabelFormats({
         data,
         yAxisUnit,
         yAxisUnitPosition,
-        labelSkipInterval,
-        labelRotation,
-        truncatedLabels,
     });
 
     const checkIsOverDot = useCallback((args: HitTestArgs) => {
@@ -112,11 +124,8 @@ function LineChartContent({data, title, titleIcon, isLoading, yAxisUnit, yAxisUn
     const tooltipData = useTooltipData(activeDataIndex, data, yAxisUnit, yAxisUnitPosition);
 
     // Victory's built-in x-axis labels center each label under its tick mark,
-    // which works well for bar charts where bars have width and natural spacing.
-    // For line charts, data points sit at the edges of the plot area, so centered
-    // labels get clipped or overflow the chart bounds. We render labels manually
-    // via `renderOutside` so we can right-align each label's last character at its
-    // tick position and clamp edge labels within the canvas.
+    // which works for bar charts but clips labels on line charts where data points
+    // sit at the edges. We render labels via `renderOutside` with custom positioning.
     const renderCustomXLabels = useCallback(
         (args: CartesianChartRenderArg<{x: number; y: number}, 'y'>) => {
             if (!font) {
@@ -124,9 +133,9 @@ function LineChartContent({data, title, titleIcon, isLoading, yAxisUnit, yAxisUn
             }
 
             const fontMetrics = font.getMetrics();
-            const lineHeight = Math.abs(fontMetrics.ascent) + Math.abs(fontMetrics.descent);
-            const fontSize = font.getSize();
-            const labelY = args.chartBounds.bottom + 2 + fontSize;
+            const ascent = Math.abs(fontMetrics.ascent);
+            const descent = Math.abs(fontMetrics.descent);
+            const labelY = args.chartBounds.bottom + X_AXIS_LABEL_GAP + font.getSize();
 
             return truncatedLabels.map((label, i) => {
                 if (i % labelSkipInterval !== 0) {
@@ -136,15 +145,11 @@ function LineChartContent({data, title, titleIcon, isLoading, yAxisUnit, yAxisUn
                 const tickX = args.xScale(i);
                 const labelWidth = labelWidths.at(i) ?? 0;
 
-                // Last character anchored at tickX, clamped to canvas edges.
-                const idealX = tickX - labelWidth;
-                const clampedX = Math.max(0, Math.min(args.canvasSize.width - labelWidth, idealX));
-
                 if (angleRad === 0) {
                     return (
                         <SkiaText
                             key={`x-label-${label}`}
-                            x={clampedX}
+                            x={tickX - labelWidth}
                             y={labelY}
                             text={label}
                             font={font}
@@ -153,18 +158,21 @@ function LineChartContent({data, title, titleIcon, isLoading, yAxisUnit, yAxisUn
                     );
                 }
 
-                // At 90° the rotated label's horizontal footprint is lineHeight;
-                // shift by half to center it on the tick mark.
-                const centeringOffset = labelRotation === -LABEL_ROTATIONS.VERTICAL ? lineHeight / 2 : 0;
-                const origin = vec(clampedX + labelWidth + centeringOffset, labelY);
+                const textX = tickX - labelWidth;
+                const origin = vec(tickX, labelY);
+
+                // Rotate around the anchor, then translate to correct for ascent/descent
+                // asymmetry (ascent > descent shifts the visual center left of the anchor).
+                const correction = rotatedLabelCenterCorrection(ascent, descent, angleRad);
+
                 return (
                     <Group
                         key={`x-label-${label}`}
                         origin={origin}
-                        transform={[{rotate: -angleRad}]}
+                        transform={[{translateX: correction}, {rotate: -angleRad}]}
                     >
                         <SkiaText
-                            x={clampedX}
+                            x={textX}
                             y={labelY}
                             text={label}
                             font={font}
@@ -174,14 +182,14 @@ function LineChartContent({data, title, titleIcon, isLoading, yAxisUnit, yAxisUn
                 );
             });
         },
-        [font, truncatedLabels, labelSkipInterval, labelWidths, angleRad, labelRotation, theme.textSupporting],
+        [font, truncatedLabels, labelSkipInterval, labelWidths, angleRad, theme.textSupporting],
     );
 
     const dynamicChartStyle = useMemo(
         () => ({
-            height: CHART_CONTENT_MIN_HEIGHT + (maxLabelLength ?? 0),
+            height: CHART_CONTENT_MIN_HEIGHT + (xAxisLabelHeight ?? 0),
         }),
-        [maxLabelLength],
+        [xAxisLabelHeight],
     );
 
     if (isLoading || !font) {
@@ -203,27 +211,22 @@ function LineChartContent({data, title, titleIcon, isLoading, yAxisUnit, yAxisUn
                 titleIcon={titleIcon}
             />
             <View
-                style={[styles.lineChartChartContainer, labelRotation === -LABEL_ROTATIONS.VERTICAL ? dynamicChartStyle : undefined]}
+                style={[styles.lineChartChartContainer, dynamicChartStyle]}
                 onLayout={handleLayout}
             >
                 {chartWidth > 0 && (
                     <CartesianChart
                         xKey="x"
-                        padding={CHART_PADDING}
+                        padding={{top: CHART_PADDING, left: CHART_PADDING, right: CHART_PADDING, bottom: (xAxisLabelHeight ?? 0) + CHART_PADDING}}
                         yKeys={['y']}
-                        domainPadding={16}
+                        domainPadding={LINE_CHART_DOMAIN_PADDING}
                         actionsRef={actionsRef}
                         customGestures={customGestures}
                         onChartBoundsChange={handleChartBoundsChange}
                         renderOutside={renderCustomXLabels}
                         xAxis={{
-                            font,
                             tickCount: data.length,
-                            labelColor: 'transparent',
                             lineWidth: X_AXIS_LINE_WIDTH,
-                            formatXLabel: formatXAxisLabel,
-                            labelRotate: labelRotation,
-                            labelOverflow: 'visible',
                         }}
                         yAxis={[
                             {
diff --git a/src/components/Charts/hooks/index.ts b/src/components/Charts/hooks/index.ts
index 6b90bfca65bf..ac5272aa5b56 100644
--- a/src/components/Charts/hooks/index.ts
+++ b/src/components/Charts/hooks/index.ts
@@ -1,5 +1,5 @@
 export {useChartInteractionState} from './useChartInteractionState';
-export {LABEL_ROTATIONS, useChartLabelLayout} from './useChartLabelLayout';
+export {useChartLabelLayout} from './useChartLabelLayout';
 export {useChartInteractions} from './useChartInteractions';
 export type {HitTestArgs} from './useChartInteractions';
 export type {ChartInteractionState, ChartInteractionStateInit} from './useChartInteractionState';
diff --git a/src/components/Charts/hooks/useChartInteractions.ts b/src/components/Charts/hooks/useChartInteractions.ts
index 065eadcd3188..3cb17d796b71 100644
--- a/src/components/Charts/hooks/useChartInteractions.ts
+++ b/src/components/Charts/hooks/useChartInteractions.ts
@@ -14,16 +14,12 @@ const TOOLTIP_BAR_GAP = 8;
 type HitTestArgs = {
     /** Current raw X position of the cursor */
     cursorX: number;
-
     /** Current raw Y position of the cursor */
     cursorY: number;
-
     /** Calculated X position of the matched data point */
     targetX: number;
-
     /** Calculated Y position of the matched data point */
     targetY: number;
-
     /** The bottom boundary of the chart area */
     chartBottom: number;
 };
@@ -34,13 +30,11 @@ type HitTestArgs = {
 type UseChartInteractionsProps = {
     /** Callback triggered when a valid data point is tapped/clicked */
     handlePress: (index: number) => void;
-
     /**
      * Worklet function to determine if the cursor is technically "hovering"
      * over a specific chart element (e.g., within a bar's width or a point's radius).
      */
     checkIsOver: (args: HitTestArgs) => boolean;
-
     /** Optional shared value containing bar dimensions used for hit-testing in bar charts */
     barGeometry?: SharedValue<{barWidth: number; chartBottom: number; yZero: number}>;
 };
@@ -54,11 +48,33 @@ type CartesianActionsHandle = {
 };
 
 /**
- * Hook to manage chart interactions including hover gestures (web),
+ * Hook to manage complex chart interactions including hover gestures (web),
  * tap gestures (mobile/web), hit-testing, and animated tooltip positioning.
  *
- * Synchronizes high-frequency interaction data from the UI thread to React state
+ * It synchronizes high-frequency interaction data from the UI thread to React state
  * for metadata display (like tooltips) and navigation.
+ *
+ * @param props - Configuration including press handlers and hit-test logic.
+ * @returns An object containing refs, gestures, and state for the chart component.
+ *
+ * @example
+ * ```tsx
+ * const { actionsRef, customGestures, activeDataIndex, isTooltipActive, tooltipStyle } = useChartInteractions({
+ * handlePress: (index) => console.log("Pressed index:", index),
+ * checkIsOver: ({ cursorX, targetX, barWidth }) => {
+ * 'worklet';
+ * return Math.abs(cursorX - targetX) < barWidth / 2;
+ * },
+ * barGeometry: myBarSharedValue,
+ * });
+ *
+ * return (
+ * <View>
+ * <CartesianChart customGestures={customGestures} actionsRef={actionsRef} ... />
+ * {isTooltipActive && <Animated.View style={tooltipStyle}><Tooltip index={activeDataIndex} /></Animated.View>}
+ * </View>
+ * );
+ * ```
  */
 function useChartInteractions({handlePress, checkIsOver, barGeometry}: UseChartInteractionsProps) {
     /** Interaction state compatible with Victory Native's internal logic */
diff --git a/src/components/Charts/hooks/useChartLabelFormats.ts b/src/components/Charts/hooks/useChartLabelFormats.ts
index c87a2c6177ba..7c32baa2c1ab 100644
--- a/src/components/Charts/hooks/useChartLabelFormats.ts
+++ b/src/components/Charts/hooks/useChartLabelFormats.ts
@@ -6,12 +6,12 @@ type UseChartLabelFormatsProps = {
     data: ChartDataPoint[];
     yAxisUnit?: string;
     yAxisUnitPosition?: YAxisUnitPosition;
-    labelSkipInterval: number;
-    labelRotation: number;
-    truncatedLabels: string[];
+    labelSkipInterval?: number;
+    labelRotation?: number;
+    truncatedLabels?: string[];
 };
 
-export default function useChartLabelFormats({data, yAxisUnit, yAxisUnitPosition = 'left', labelSkipInterval, labelRotation, truncatedLabels}: UseChartLabelFormatsProps) {
+export default function useChartLabelFormats({data, yAxisUnit, yAxisUnitPosition = 'left', labelSkipInterval = 1, labelRotation = 0, truncatedLabels}: UseChartLabelFormatsProps) {
     const formatYAxisLabel = useCallback(
         (value: number) => {
             const formatted = value.toLocaleString();
@@ -37,7 +37,7 @@ export default function useChartLabelFormats({data, yAxisUnit, yAxisUnitPosition
             // Use pre-truncated labels
             // If rotation is vertical (-90), we usually want full labels
             // because they have more space vertically.
-            const sourceToUse = labelRotation === -LABEL_ROTATIONS.VERTICAL ? data.map((p) => p.label) : truncatedLabels;
+            const sourceToUse = labelRotation === -LABEL_ROTATIONS.VERTICAL || !truncatedLabels ? data.map((p) => p.label) : truncatedLabels;
 
             return sourceToUse.at(index) ?? '';
         },
diff --git a/src/components/Charts/hooks/useChartLabelLayout.ts b/src/components/Charts/hooks/useChartLabelLayout.ts
index 39fce259acad..b11508758f7f 100644
--- a/src/components/Charts/hooks/useChartLabelLayout.ts
+++ b/src/components/Charts/hooks/useChartLabelLayout.ts
@@ -1,6 +1,6 @@
 import type {SkFont} from '@shopify/react-native-skia';
 import {useMemo} from 'react';
-import {Y_AXIS_LABEL_OFFSET} from '@components/Charts/constants';
+import type {ChartDataPoint} from '@components/Charts/types';
 import {measureTextWidth} from '@components/Charts/utils';
 
 /** Supported label rotation angles in degrees */
@@ -15,22 +15,17 @@ const SIN_45 = Math.sin(Math.PI / 4);
 /** Minimum gap between adjacent labels (px) */
 const LABEL_PADDING = 4;
 
-/** X-axis labels can occupy at most this fraction of container height */
-const MAX_LABEL_HEIGHT_RATIO = 0.35;
-
 const ELLIPSIS = '...';
 
-type ChartDataPoint = {
-    label: string;
-    [key: string]: unknown;
-};
+/** Minimum visible characters (excluding ellipsis) for truncation to be worthwhile */
+const MIN_TRUNCATED_CHARS = 10;
 
 type LabelLayoutConfig = {
     data: ChartDataPoint[];
     font: SkFont | null;
-    chartWidth: number;
-    barAreaWidth: number;
-    containerHeight: number;
+    tickSpacing: number;
+    labelAreaWidth: number;
+    firstTickOffset?: number;
 };
 
 /** Truncate `label` so its pixel width fits within `maxWidth`, adding ellipsis. */
@@ -57,78 +52,111 @@ function effectiveWidth(labelWidth: number, lineHeight: number, rotation: number
     return labelWidth;
 }
 
+/** Vertical footprint of a label at a given rotation angle. */
+function effectiveHeight(labelWidth: number, lineHeight: number, rotation: number): number {
+    if (rotation === LABEL_ROTATIONS.VERTICAL) {
+        return labelWidth;
+    }
+    if (rotation === LABEL_ROTATIONS.DIAGONAL) {
+        return labelWidth * SIN_45 + lineHeight * SIN_45;
+    }
+    return lineHeight;
+}
+
 /** How many labels fit side-by-side in `areaWidth` given each takes `itemWidth`. */
 function maxVisibleCount(areaWidth: number, itemWidth: number): number {
     return Math.floor(areaWidth / (itemWidth + LABEL_PADDING));
 }
 
 /**
- * Pick the smallest rotation (0 → 45 → 90) where labels don't overlap,
- * preferring rotation over skip interval.
+ * Left extent of a rotated label at 45° from its anchor point.
+ * At 45° cos and sin are equal (SIN_45), so extent = (labelWidth + lineHeight) * SIN_45.
  */
-function pickRotation(maxLabelWidth: number, lineHeight: number, availableWidthPerLabel: number, labelArea: number, dataCount: number): number {
-    const candidates = [LABEL_ROTATIONS.HORIZONTAL, LABEL_ROTATIONS.DIAGONAL, LABEL_ROTATIONS.VERTICAL] as const;
-    for (const angle of candidates) {
-        const ew = effectiveWidth(maxLabelWidth, lineHeight, angle);
-        if (ew <= availableWidthPerLabel && maxVisibleCount(labelArea, ew) >= dataCount) {
-            return angle;
-        }
-    }
-    return LABEL_ROTATIONS.VERTICAL;
+function leftExtentAt45(labelWidth: number, lineHeight: number): number {
+    return (labelWidth + lineHeight) * SIN_45;
 }
 
 /**
- * Max label pixel width allowed at a given rotation so labels don't exceed
- * `MAX_LABEL_HEIGHT_RATIO` of the container height.
- *
- * Based on Victory's internal allocation formula:
- *   totalHeight = lineHeight + Y_AXIS_LABEL_OFFSET * 2 + labelWidth * sin(angle)
- * (from victory-native-xl/src/cartesian/utils/transformInputData.ts)
+ * Pick the smallest rotation (0 → 45 → 90) where labels don't overlap,
+ * preferring rotation over skip interval.
  */
-function maxAllowedWidth(rotation: number, lineHeight: number, containerHeight: number): number {
-    if (rotation === LABEL_ROTATIONS.DIAGONAL) {
-        const budget = containerHeight * MAX_LABEL_HEIGHT_RATIO - (lineHeight + Y_AXIS_LABEL_OFFSET * 2);
-        return Math.max(0, budget) / SIN_45;
+function pickRotation(maxLabelWidth: number, lineHeight: number, tickSpacing: number, labelArea: number, dataCount: number, minTruncatedWidth: number, firstTickOffset?: number): number {
+    // 0°: labels fit horizontally without truncation and first label won't be clipped
+    const ew0 = effectiveWidth(maxLabelWidth, lineHeight, LABEL_ROTATIONS.HORIZONTAL);
+    const fits0 = ew0 + LABEL_PADDING <= tickSpacing && maxVisibleCount(labelArea, ew0) >= dataCount;
+    const firstLabelClipped = firstTickOffset !== undefined && maxLabelWidth > firstTickOffset;
+    if (fits0 && !firstLabelClipped) {
+        return LABEL_ROTATIONS.HORIZONTAL;
     }
-    // 0° and 90°: no truncation (0° uses skip, 90° expands container)
-    return Infinity;
+
+    // 45°: viable if MIN_TRUNCATED_CHARS + ellipsis fits between ticks
+    // and the minimum truncated label fits within firstTickOffset
+    const minEw45 = minTruncatedWidth * SIN_45;
+    const fitsBetweenTicks = minEw45 + LABEL_PADDING <= tickSpacing;
+    const fitsFirstTick = firstTickOffset === undefined || leftExtentAt45(minTruncatedWidth, lineHeight) <= firstTickOffset;
+    if (fitsBetweenTicks && fitsFirstTick) {
+        return LABEL_ROTATIONS.DIAGONAL;
+    }
+
+    // 90°: fallback
+    return LABEL_ROTATIONS.VERTICAL;
 }
 
-function useChartLabelLayout({data, font, chartWidth, barAreaWidth, containerHeight}: LabelLayoutConfig) {
+function useChartLabelLayout({data, font, tickSpacing, labelAreaWidth, firstTickOffset}: LabelLayoutConfig) {
     return useMemo(() => {
-        if (!font || chartWidth === 0 || containerHeight === 0 || data.length === 0) {
+        if (!font || tickSpacing <= 0 || labelAreaWidth <= 0 || data.length === 0) {
             return {labelRotation: 0, labelSkipInterval: 1, truncatedLabels: data.map((p) => p.label)};
         }
 
         const fontMetrics = font.getMetrics();
         const lineHeight = Math.abs(fontMetrics.descent) + Math.abs(fontMetrics.ascent);
         const ellipsisWidth = measureTextWidth(ELLIPSIS, font);
-
-        const availableWidthPerLabel = chartWidth / data.length - LABEL_PADDING;
         const labelWidths = data.map((p) => measureTextWidth(p.label, font));
         const maxLabelLength = Math.max(...labelWidths);
-        const labelArea = barAreaWidth || chartWidth;
-
-        // 1. Pick rotation
-        const rotation = pickRotation(maxLabelLength, lineHeight, availableWidthPerLabel, labelArea, data.length);
 
-        // 2. Truncate labels (only effective at 45°)
-        const maxWidth = maxAllowedWidth(rotation, lineHeight, containerHeight);
-        const finalLabels = data.map((p, i) => truncateLabel(p.label, labelWidths.at(i) ?? 0, maxWidth, ellipsisWidth));
+        // Maximum width of labels after truncation to MIN_TRUNCATED_CHARS characters.
+        // Labels shorter than the threshold keep their full width (can't be truncated further).
+        const minTruncatedWidth = Math.max(
+            ...data.map((p, i) => {
+                if (p.label.length <= MIN_TRUNCATED_CHARS) {
+                    return labelWidths.at(i) ?? 0;
+                }
+                return measureTextWidth(p.label.slice(0, MIN_TRUNCATED_CHARS) + ELLIPSIS, font);
+            }),
+        );
 
-        // 3. Compute skip interval
+        // 1. Pick rotation
+        const rotation = pickRotation(maxLabelLength, lineHeight, tickSpacing, labelAreaWidth, data.length, minTruncatedWidth, firstTickOffset);
+
+        // 2. Truncate labels (only at 45°)
+        //    Tick-spacing constraint: labelWidth * sin(45°) + padding <= tickSpacing
+        //    First-label constraint: (labelWidth + lineHeight) * sin(45°) <= firstTickOffset
+        const tickSpacingLimit = rotation === LABEL_ROTATIONS.DIAGONAL ? (tickSpacing - LABEL_PADDING) / SIN_45 : Infinity;
+        const firstTickLimit = rotation === LABEL_ROTATIONS.DIAGONAL && firstTickOffset !== undefined ? firstTickOffset / SIN_45 - lineHeight : Infinity;
+        const finalLabels = data.map((p, i) => {
+            const limit = i === 0 ? Math.min(tickSpacingLimit, firstTickLimit) : tickSpacingLimit;
+            return truncateLabel(p.label, labelWidths.at(i) ?? 0, limit, ellipsisWidth);
+        });
+
+        // 3. Compute skip interval (only at 90°)
         const finalMaxWidth = Math.max(...finalLabels.map((l) => measureTextWidth(l, font)));
-        const ew = effectiveWidth(finalMaxWidth, lineHeight, rotation);
-        const visible = maxVisibleCount(labelArea, ew);
-        const skipInterval = visible >= data.length ? 1 : Math.ceil(data.length / Math.max(1, visible));
+        let skipInterval = 1;
+        if (rotation === LABEL_ROTATIONS.VERTICAL) {
+            const ew = effectiveWidth(finalMaxWidth, lineHeight, rotation);
+            const visible = maxVisibleCount(labelAreaWidth, ew);
+            skipInterval = visible >= data.length ? 1 : Math.ceil(data.length / Math.max(1, visible));
+        }
+
+        // 4. Compute vertical space needed for x-axis labels
+        const xAxisLabelHeight = effectiveHeight(finalMaxWidth, lineHeight, rotation);
 
         return {
             labelRotation: -rotation,
             labelSkipInterval: skipInterval,
             truncatedLabels: finalLabels,
-            maxLabelLength,
+            xAxisLabelHeight,
         };
-    }, [font, chartWidth, barAreaWidth, containerHeight, data]);
+    }, [font, tickSpacing, labelAreaWidth, firstTickOffset, data]);
 }
 
 export {LABEL_ROTATIONS, useChartLabelLayout};
diff --git a/src/components/Charts/utils.ts b/src/components/Charts/utils.ts
index e63b87e19774..6c920a407457 100644
--- a/src/components/Charts/utils.ts
+++ b/src/components/Charts/utils.ts
@@ -51,4 +51,15 @@ function measureTextWidth(text: string, font: SkFont): number {
     return font.getGlyphWidths(glyphIDs).reduce((sum, w) => sum + w, 0);
 }
 
-export {getChartColor, DEFAULT_CHART_COLOR, measureTextWidth};
+/**
+ * Post-rotation horizontal translation to center a rotated label on its tick mark.
+ *
+ * Text baselines sit closer to glyph tops (ascent > descent), so rotating around
+ * the baseline end creates asymmetric horizontal extent. This returns the correction
+ * to apply as a translateX AFTER rotation.
+ */
+function rotatedLabelCenterCorrection(ascent: number, descent: number, angleRad: number): number {
+    return ((ascent - descent) * Math.sin(angleRad)) / 2;
+}
+
+export {getChartColor, DEFAULT_CHART_COLOR, measureTextWidth, rotatedLabelCenterCorrection};

From ef1a0ae57bf21afb3f35746947cb4a4e1a0a757a Mon Sep 17 00:00:00 2001
From: Mateusz Rajski <mateusz.rajski@swmansion.com>
Date: Wed, 4 Feb 2026 16:55:26 +0100
Subject: [PATCH 20/26] Address review comments

---
 src/components/Charts/LineChart/LineChartContent.tsx | 5 ++++-
 src/libs/SearchQueryUtils.ts                         | 4 ++--
 2 files changed, 6 insertions(+), 3 deletions(-)

diff --git a/src/components/Charts/LineChart/LineChartContent.tsx b/src/components/Charts/LineChart/LineChartContent.tsx
index 6bc439d801b1..3df2a48f0c89 100644
--- a/src/components/Charts/LineChart/LineChartContent.tsx
+++ b/src/components/Charts/LineChart/LineChartContent.tsx
@@ -22,6 +22,9 @@ import variables from '@styles/variables';
 /** Inner dot radius for line chart data points */
 const DOT_RADIUS = 6;
 
+/** Extra hover area beyond the dot radius for easier touch targeting */
+const DOT_HOVER_EXTRA_RADIUS = 2;
+
 /** Extra pixel spacing between the plot boundary and the first/last data point */
 const LINE_CHART_DOMAIN_PADDING = 16;
 
@@ -113,7 +116,7 @@ function LineChartContent({data, title, titleIcon, isLoading, yAxisUnit, yAxisUn
 
         const dx = args.cursorX - args.targetX;
         const dy = args.cursorY - args.targetY;
-        return Math.sqrt(dx * dx + dy * dy) <= DOT_RADIUS;
+        return Math.sqrt(dx * dx + dy * dy) <= DOT_RADIUS + DOT_HOVER_EXTRA_RADIUS;
     }, []);
 
     const {actionsRef, customGestures, activeDataIndex, isTooltipActive, tooltipStyle} = useChartInteractions({
diff --git a/src/libs/SearchQueryUtils.ts b/src/libs/SearchQueryUtils.ts
index 79d9b6320aec..531abde6b29b 100644
--- a/src/libs/SearchQueryUtils.ts
+++ b/src/libs/SearchQueryUtils.ts
@@ -442,9 +442,9 @@ function buildSearchQueryJSON(query: SearchQueryString, rawQuery?: SearchQuerySt
             result.policyID = [result.policyID];
         }
 
-        // Default groupBy to category when a chart view is specified without an explicit groupBy
+        // Default groupBy when a chart view is specified without an explicit groupBy
         if (result.view !== CONST.SEARCH.VIEW.TABLE && !result.groupBy) {
-            result.groupBy = CONST.SEARCH.GROUP_BY.CATEGORY;
+            result.groupBy = result.view === CONST.SEARCH.VIEW.LINE ? CONST.SEARCH.GROUP_BY.MONTH : CONST.SEARCH.GROUP_BY.CATEGORY;
         }
 
         // Normalize limit before computing hashes to ensure invalid values don't affect hash

From 2a70f4fe28f005875ae8635c9fcc5d06c0fe6c9a Mon Sep 17 00:00:00 2001
From: Mateusz Rajski <mateusz.rajski@swmansion.com>
Date: Wed, 4 Feb 2026 17:05:58 +0100
Subject: [PATCH 21/26] Fix LineChart layout issue of first left-most label

---
 .../Charts/hooks/useChartLabelLayout.ts          | 16 +++++++++++++---
 1 file changed, 13 insertions(+), 3 deletions(-)

diff --git a/src/components/Charts/hooks/useChartLabelLayout.ts b/src/components/Charts/hooks/useChartLabelLayout.ts
index b11508758f7f..9013c07d27e5 100644
--- a/src/components/Charts/hooks/useChartLabelLayout.ts
+++ b/src/components/Charts/hooks/useChartLabelLayout.ts
@@ -80,11 +80,20 @@ function leftExtentAt45(labelWidth: number, lineHeight: number): number {
  * Pick the smallest rotation (0 → 45 → 90) where labels don't overlap,
  * preferring rotation over skip interval.
  */
-function pickRotation(maxLabelWidth: number, lineHeight: number, tickSpacing: number, labelArea: number, dataCount: number, minTruncatedWidth: number, firstTickOffset?: number): number {
+function pickRotation(
+    maxLabelWidth: number,
+    firstLabelWidth: number,
+    lineHeight: number,
+    tickSpacing: number,
+    labelArea: number,
+    dataCount: number,
+    minTruncatedWidth: number,
+    firstTickOffset?: number,
+): number {
     // 0°: labels fit horizontally without truncation and first label won't be clipped
     const ew0 = effectiveWidth(maxLabelWidth, lineHeight, LABEL_ROTATIONS.HORIZONTAL);
     const fits0 = ew0 + LABEL_PADDING <= tickSpacing && maxVisibleCount(labelArea, ew0) >= dataCount;
-    const firstLabelClipped = firstTickOffset !== undefined && maxLabelWidth > firstTickOffset;
+    const firstLabelClipped = firstTickOffset !== undefined && firstLabelWidth > firstTickOffset;
     if (fits0 && !firstLabelClipped) {
         return LABEL_ROTATIONS.HORIZONTAL;
     }
@@ -126,7 +135,8 @@ function useChartLabelLayout({data, font, tickSpacing, labelAreaWidth, firstTick
         );
 
         // 1. Pick rotation
-        const rotation = pickRotation(maxLabelLength, lineHeight, tickSpacing, labelAreaWidth, data.length, minTruncatedWidth, firstTickOffset);
+        const firstLabelWidth = labelWidths.at(0) ?? 0;
+        const rotation = pickRotation(maxLabelLength, firstLabelWidth, lineHeight, tickSpacing, labelAreaWidth, data.length, minTruncatedWidth, firstTickOffset);
 
         // 2. Truncate labels (only at 45°)
         //    Tick-spacing constraint: labelWidth * sin(45°) + padding <= tickSpacing

From 993739dbe8b6c20653e0f9d9d895239a5c8913f5 Mon Sep 17 00:00:00 2001
From: Mateusz Rajski <mateusz.rajski@swmansion.com>
Date: Thu, 5 Feb 2026 09:37:51 +0100
Subject: [PATCH 22/26] Change default sorting order for line chart

---
 src/libs/SearchQueryUtils.ts              | 33 ++++++++++++++++++++---
 tests/unit/Search/SearchQueryUtilsTest.ts |  3 ++-
 2 files changed, 31 insertions(+), 5 deletions(-)

diff --git a/src/libs/SearchQueryUtils.ts b/src/libs/SearchQueryUtils.ts
index 531abde6b29b..13c9524948c8 100644
--- a/src/libs/SearchQueryUtils.ts
+++ b/src/libs/SearchQueryUtils.ts
@@ -410,6 +410,34 @@ function isFilterSupported(filter: SearchAdvancedFiltersKey, type: SearchDataTyp
     });
 }
 
+/**
+ * Returns default values for chart views (non-table views).
+ * - Sets default groupBy based on view type (month for line charts, category for others)
+ * - Sets default sortOrder to ASC for line charts (time-based graphs show chronologically oldest to newest)
+ */
+function getChartViewDefaults(queryJSON: SearchQueryJSON): Partial<SearchQueryJSON> {
+    if (queryJSON.view === CONST.SEARCH.VIEW.TABLE) {
+        return {};
+    }
+
+    const defaults: Partial<SearchQueryJSON> = {};
+
+    // Default groupBy when not explicitly set
+    if (!queryJSON.groupBy) {
+        defaults.groupBy = queryJSON.view === CONST.SEARCH.VIEW.LINE ? CONST.SEARCH.GROUP_BY.MONTH : CONST.SEARCH.GROUP_BY.CATEGORY;
+    }
+
+    // Default sortOrder to ASC for LINE view, only if not explicitly set by the user
+    if (queryJSON.view === CONST.SEARCH.VIEW.LINE) {
+        const wasSortOrderExplicitlySet = queryJSON.rawFilterList?.some((filter) => filter.key === CONST.SEARCH.SYNTAX_ROOT_KEYS.SORT_ORDER) ?? false;
+        if (!wasSortOrderExplicitlySet) {
+            defaults.sortOrder = CONST.SEARCH.SORT_ORDER.ASC;
+        }
+    }
+
+    return defaults;
+}
+
 /**
  * Parses a given search query string into a structured `SearchQueryJSON` format.
  * This format of query is most commonly shared between components and also sent to backend to retrieve search results.
@@ -442,10 +470,7 @@ function buildSearchQueryJSON(query: SearchQueryString, rawQuery?: SearchQuerySt
             result.policyID = [result.policyID];
         }
 
-        // Default groupBy when a chart view is specified without an explicit groupBy
-        if (result.view !== CONST.SEARCH.VIEW.TABLE && !result.groupBy) {
-            result.groupBy = result.view === CONST.SEARCH.VIEW.LINE ? CONST.SEARCH.GROUP_BY.MONTH : CONST.SEARCH.GROUP_BY.CATEGORY;
-        }
+        Object.assign(result, getChartViewDefaults(result));
 
         // Normalize limit before computing hashes to ensure invalid values don't affect hash
         if (result.limit !== undefined) {
diff --git a/tests/unit/Search/SearchQueryUtilsTest.ts b/tests/unit/Search/SearchQueryUtilsTest.ts
index bd010f505426..778c228c4329 100644
--- a/tests/unit/Search/SearchQueryUtilsTest.ts
+++ b/tests/unit/Search/SearchQueryUtilsTest.ts
@@ -108,7 +108,8 @@ describe('SearchQueryUtils', () => {
 
             const result = getQueryWithUpdatedValues(userQuery);
 
-            expect(result).toEqual(`${defaultQuery} view:line groupBy:category category:travel`);
+            // LINE view defaults to sortOrder:asc (chronological) and groupBy:month
+            expect(result).toEqual('type:expense sortBy:date sortOrder:asc view:line groupBy:month category:travel');
         });
 
         test('returns query with view:pie', () => {

From dd295b8ec3bd3861a0257c26dce4144273c9d394 Mon Sep 17 00:00:00 2001
From: Mateusz Rajski <mateusz.rajski@swmansion.com>
Date: Thu, 5 Feb 2026 11:14:11 +0100
Subject: [PATCH 23/26] Center LineChart labels when rotation degree is 0

---
 .../Charts/BarChart/BarChartContent.tsx       | 20 +-----
 .../Charts/LineChart/LineChartContent.tsx     | 68 +++++++++++++------
 .../Charts/hooks/useChartLabelLayout.ts       | 49 +++----------
 src/components/Charts/utils.ts                | 18 ++++-
 4 files changed, 76 insertions(+), 79 deletions(-)

diff --git a/src/components/Charts/BarChart/BarChartContent.tsx b/src/components/Charts/BarChart/BarChartContent.tsx
index 55f79db985ec..19dc0a723db5 100644
--- a/src/components/Charts/BarChart/BarChartContent.tsx
+++ b/src/components/Charts/BarChart/BarChartContent.tsx
@@ -13,7 +13,7 @@ import fontSource from '@components/Charts/font';
 import type {HitTestArgs} from '@components/Charts/hooks';
 import {useChartInteractions, useChartLabelFormats, useChartLabelLayout, useDynamicYDomain, useTooltipData} from '@components/Charts/hooks';
 import type {CartesianChartProps, ChartDataPoint} from '@components/Charts/types';
-import {DEFAULT_CHART_COLOR, getChartColor} from '@components/Charts/utils';
+import {calculateMinDomainPadding, DEFAULT_CHART_COLOR, getChartColor} from '@components/Charts/utils';
 import useResponsiveLayout from '@hooks/useResponsiveLayout';
 import useTheme from '@hooks/useTheme';
 import useThemeStyles from '@hooks/useThemeStyles';
@@ -22,27 +22,9 @@ import variables from '@styles/variables';
 /** Inner padding between bars (0.3 = 30% of bar width) */
 const BAR_INNER_PADDING = 0.3;
 
-/** Safety buffer multiplier for domain padding calculation */
-const DOMAIN_PADDING_SAFETY_BUFFER = 1.1;
-
 /** Extra pixel spacing between the chart boundary and the data range, applied per side (Victory's `domainPadding` prop) */
 const BASE_DOMAIN_PADDING = {top: 32, bottom: 0, left: 0, right: 0};
 
-/**
- * Calculate minimum domainPadding required to prevent data points from crowding chart edges.
- *
- * For evenly-spaced data points, Victory maps indices [0..N-1] into the output range.
- * Without enough padding the first/last points sit too close to the View boundary
- * and their centered labels get clipped by overflow:hidden.
- */
-function calculateMinDomainPadding(chartWidth: number, pointCount: number, innerPadding: number): number {
-    if (pointCount <= 0) {
-        return 0;
-    }
-    const minPaddingRatio = (1 - innerPadding) / (2 * (pointCount - 1 + innerPadding));
-    return Math.ceil(chartWidth * minPaddingRatio * DOMAIN_PADDING_SAFETY_BUFFER);
-}
-
 type BarChartProps = CartesianChartProps & {
     /** Callback when a bar is pressed */
     onBarPress?: (dataPoint: ChartDataPoint, index: number) => void;
diff --git a/src/components/Charts/LineChart/LineChartContent.tsx b/src/components/Charts/LineChart/LineChartContent.tsx
index 3df2a48f0c89..6726675eea09 100644
--- a/src/components/Charts/LineChart/LineChartContent.tsx
+++ b/src/components/Charts/LineChart/LineChartContent.tsx
@@ -13,7 +13,7 @@ import fontSource from '@components/Charts/font';
 import type {HitTestArgs} from '@components/Charts/hooks';
 import {useChartInteractions, useChartLabelFormats, useChartLabelLayout, useDynamicYDomain, useTooltipData} from '@components/Charts/hooks';
 import type {CartesianChartProps, ChartDataPoint} from '@components/Charts/types';
-import {DEFAULT_CHART_COLOR, measureTextWidth, rotatedLabelCenterCorrection} from '@components/Charts/utils';
+import {calculateMinDomainPadding, DEFAULT_CHART_COLOR, measureTextWidth, rotatedLabelCenterCorrection} from '@components/Charts/utils';
 import useResponsiveLayout from '@hooks/useResponsiveLayout';
 import useTheme from '@hooks/useTheme';
 import useThemeStyles from '@hooks/useThemeStyles';
@@ -25,8 +25,8 @@ const DOT_RADIUS = 6;
 /** Extra hover area beyond the dot radius for easier touch targeting */
 const DOT_HOVER_EXTRA_RADIUS = 2;
 
-/** Extra pixel spacing between the plot boundary and the first/last data point */
-const LINE_CHART_DOMAIN_PADDING = 16;
+/** Base domain padding applied to all sides */
+const BASE_DOMAIN_PADDING = {top: 16, bottom: 16, left: 0, right: 0};
 
 /** Gap between the x-axis line and the top of label glyphs */
 const X_AXIS_LABEL_GAP = 2;
@@ -42,7 +42,7 @@ function LineChartContent({data, title, titleIcon, isLoading, yAxisUnit, yAxisUn
     const {shouldUseNarrowLayout} = useResponsiveLayout();
     const font = useFont(fontSource, variables.iconSizeExtraSmall);
     const [chartWidth, setChartWidth] = useState(0);
-    const [chartBoundsInfo, setChartBoundsInfo] = useState({plotAreaWidth: 0, firstTickOffset: 0});
+    const [plotAreaWidth, setPlotAreaWidth] = useState(0);
 
     const yAxisDomain = useDynamicYDomain(data);
 
@@ -71,27 +71,51 @@ function LineChartContent({data, title, titleIcon, isLoading, yAxisUnit, yAxisUn
     }, []);
 
     const handleChartBoundsChange = useCallback((bounds: ChartBounds) => {
-        setChartBoundsInfo({
-            plotAreaWidth: bounds.right - bounds.left,
-            firstTickOffset: bounds.left + LINE_CHART_DOMAIN_PADDING,
-        });
+        setPlotAreaWidth(bounds.right - bounds.left);
     }, []);
 
-    const {plotAreaWidth, firstTickOffset} = chartBoundsInfo;
+    // Calculate dynamic domain padding for centered labels
+    // Optimize by reducing wasted space when edge labels are shorter than tick spacing
+    const domainPadding = useMemo(() => {
+        if (chartWidth === 0 || data.length === 0) {
+            return BASE_DOMAIN_PADDING;
+        }
+
+        const geometricPadding = calculateMinDomainPadding(chartWidth, data.length);
 
-    const tickSpacing = useMemo(() => {
-        if (plotAreaWidth === 0 || data.length <= 1) {
-            return 0;
+        // Without font, use geometric padding (safe fallback)
+        if (!font) {
+            return {...BASE_DOMAIN_PADDING, left: geometricPadding, right: geometricPadding};
         }
-        return (plotAreaWidth - 2 * LINE_CHART_DOMAIN_PADDING) / (data.length - 1);
-    }, [plotAreaWidth, data.length]);
+
+        // Measure edge labels to see if we can reduce padding
+        const firstLabelWidth = measureTextWidth(data.at(0)?.label ?? '', font);
+        const lastLabelWidth = measureTextWidth(data.at(-1)?.label ?? '', font);
+
+        // At 0° rotation, centered labels extend by half their width
+        const firstLabelNeeds = firstLabelWidth / 2;
+        const lastLabelNeeds = lastLabelWidth / 2;
+
+        // How much space is wasted on each side
+        const wastedLeft = geometricPadding - firstLabelNeeds;
+        const wastedRight = geometricPadding - lastLabelNeeds;
+        const canReduce = Math.min(wastedLeft, wastedRight);
+
+        // Only reduce if both sides have excess space (labels short enough for 0°)
+        // If canReduce <= 0, labels are too long and hook will use rotation/truncation
+        const horizontalPadding = canReduce > 0 ? geometricPadding - canReduce : geometricPadding;
+
+        return {...BASE_DOMAIN_PADDING, left: horizontalPadding, right: horizontalPadding};
+    }, [chartWidth, data, font]);
+
+    // For centered labels, tick spacing is evenly distributed across the plot area (same as BarChart)
+    const tickSpacing = plotAreaWidth > 0 && data.length > 0 ? plotAreaWidth / data.length : 0;
 
     const {labelRotation, labelSkipInterval, truncatedLabels, xAxisLabelHeight} = useChartLabelLayout({
         data,
         font,
         tickSpacing,
         labelAreaWidth: plotAreaWidth,
-        firstTickOffset,
     });
 
     // Measure label widths for custom positioning in `renderOutside`
@@ -126,9 +150,9 @@ function LineChartContent({data, title, titleIcon, isLoading, yAxisUnit, yAxisUn
 
     const tooltipData = useTooltipData(activeDataIndex, data, yAxisUnit, yAxisUnitPosition);
 
-    // Victory's built-in x-axis labels center each label under its tick mark,
-    // which works for bar charts but clips labels on line charts where data points
-    // sit at the edges. We render labels via `renderOutside` with custom positioning.
+    // Custom x-axis labels with hybrid positioning:
+    // - At 0° (horizontal): center label under the point (like bar chart)
+    // - At 45° (rotated): right-align so the last character is under the point
     const renderCustomXLabels = useCallback(
         (args: CartesianChartRenderArg<{x: number; y: number}, 'y'>) => {
             if (!font) {
@@ -148,11 +172,13 @@ function LineChartContent({data, title, titleIcon, isLoading, yAxisUnit, yAxisUn
                 const tickX = args.xScale(i);
                 const labelWidth = labelWidths.at(i) ?? 0;
 
+                // At 0°: center the label under the point (like bar chart)
+                // At 45°: right-align so the last character is under the point
                 if (angleRad === 0) {
                     return (
                         <SkiaText
                             key={`x-label-${label}`}
-                            x={tickX - labelWidth}
+                            x={tickX - labelWidth / 2}
                             y={labelY}
                             text={label}
                             font={font}
@@ -161,7 +187,7 @@ function LineChartContent({data, title, titleIcon, isLoading, yAxisUnit, yAxisUn
                     );
                 }
 
-                const textX = tickX - labelWidth;
+                const textX = tickX - labelWidth; // right-aligned for rotated labels
                 const origin = vec(tickX, labelY);
 
                 // Rotate around the anchor, then translate to correct for ascent/descent
@@ -222,7 +248,7 @@ function LineChartContent({data, title, titleIcon, isLoading, yAxisUnit, yAxisUn
                         xKey="x"
                         padding={{top: CHART_PADDING, left: CHART_PADDING, right: CHART_PADDING, bottom: (xAxisLabelHeight ?? 0) + CHART_PADDING}}
                         yKeys={['y']}
-                        domainPadding={LINE_CHART_DOMAIN_PADDING}
+                        domainPadding={domainPadding}
                         actionsRef={actionsRef}
                         customGestures={customGestures}
                         onChartBoundsChange={handleChartBoundsChange}
diff --git a/src/components/Charts/hooks/useChartLabelLayout.ts b/src/components/Charts/hooks/useChartLabelLayout.ts
index 9013c07d27e5..9f6fec14f9c9 100644
--- a/src/components/Charts/hooks/useChartLabelLayout.ts
+++ b/src/components/Charts/hooks/useChartLabelLayout.ts
@@ -25,7 +25,6 @@ type LabelLayoutConfig = {
     font: SkFont | null;
     tickSpacing: number;
     labelAreaWidth: number;
-    firstTickOffset?: number;
 };
 
 /** Truncate `label` so its pixel width fits within `maxWidth`, adding ellipsis. */
@@ -68,42 +67,20 @@ function maxVisibleCount(areaWidth: number, itemWidth: number): number {
     return Math.floor(areaWidth / (itemWidth + LABEL_PADDING));
 }
 
-/**
- * Left extent of a rotated label at 45° from its anchor point.
- * At 45° cos and sin are equal (SIN_45), so extent = (labelWidth + lineHeight) * SIN_45.
- */
-function leftExtentAt45(labelWidth: number, lineHeight: number): number {
-    return (labelWidth + lineHeight) * SIN_45;
-}
-
 /**
  * Pick the smallest rotation (0 → 45 → 90) where labels don't overlap,
  * preferring rotation over skip interval.
  */
-function pickRotation(
-    maxLabelWidth: number,
-    firstLabelWidth: number,
-    lineHeight: number,
-    tickSpacing: number,
-    labelArea: number,
-    dataCount: number,
-    minTruncatedWidth: number,
-    firstTickOffset?: number,
-): number {
-    // 0°: labels fit horizontally without truncation and first label won't be clipped
+function pickRotation(maxLabelWidth: number, lineHeight: number, tickSpacing: number, labelArea: number, dataCount: number, minTruncatedWidth: number): number {
+    // 0°: labels fit horizontally without truncation
     const ew0 = effectiveWidth(maxLabelWidth, lineHeight, LABEL_ROTATIONS.HORIZONTAL);
-    const fits0 = ew0 + LABEL_PADDING <= tickSpacing && maxVisibleCount(labelArea, ew0) >= dataCount;
-    const firstLabelClipped = firstTickOffset !== undefined && firstLabelWidth > firstTickOffset;
-    if (fits0 && !firstLabelClipped) {
+    if (ew0 + LABEL_PADDING <= tickSpacing && maxVisibleCount(labelArea, ew0) >= dataCount) {
         return LABEL_ROTATIONS.HORIZONTAL;
     }
 
     // 45°: viable if MIN_TRUNCATED_CHARS + ellipsis fits between ticks
-    // and the minimum truncated label fits within firstTickOffset
     const minEw45 = minTruncatedWidth * SIN_45;
-    const fitsBetweenTicks = minEw45 + LABEL_PADDING <= tickSpacing;
-    const fitsFirstTick = firstTickOffset === undefined || leftExtentAt45(minTruncatedWidth, lineHeight) <= firstTickOffset;
-    if (fitsBetweenTicks && fitsFirstTick) {
+    if (minEw45 + LABEL_PADDING <= tickSpacing) {
         return LABEL_ROTATIONS.DIAGONAL;
     }
 
@@ -111,10 +88,10 @@ function pickRotation(
     return LABEL_ROTATIONS.VERTICAL;
 }
 
-function useChartLabelLayout({data, font, tickSpacing, labelAreaWidth, firstTickOffset}: LabelLayoutConfig) {
+function useChartLabelLayout({data, font, tickSpacing, labelAreaWidth}: LabelLayoutConfig) {
     return useMemo(() => {
-        if (!font || tickSpacing <= 0 || labelAreaWidth <= 0 || data.length === 0) {
-            return {labelRotation: 0, labelSkipInterval: 1, truncatedLabels: data.map((p) => p.label)};
+        if (!font || data.length === 0 || tickSpacing <= 0 || labelAreaWidth <= 0) {
+            return {labelRotation: 0, labelSkipInterval: 1, truncatedLabels: []};
         }
 
         const fontMetrics = font.getMetrics();
@@ -135,17 +112,13 @@ function useChartLabelLayout({data, font, tickSpacing, labelAreaWidth, firstTick
         );
 
         // 1. Pick rotation
-        const firstLabelWidth = labelWidths.at(0) ?? 0;
-        const rotation = pickRotation(maxLabelLength, firstLabelWidth, lineHeight, tickSpacing, labelAreaWidth, data.length, minTruncatedWidth, firstTickOffset);
+        const rotation = pickRotation(maxLabelLength, lineHeight, tickSpacing, labelAreaWidth, data.length, minTruncatedWidth);
 
         // 2. Truncate labels (only at 45°)
         //    Tick-spacing constraint: labelWidth * sin(45°) + padding <= tickSpacing
-        //    First-label constraint: (labelWidth + lineHeight) * sin(45°) <= firstTickOffset
-        const tickSpacingLimit = rotation === LABEL_ROTATIONS.DIAGONAL ? (tickSpacing - LABEL_PADDING) / SIN_45 : Infinity;
-        const firstTickLimit = rotation === LABEL_ROTATIONS.DIAGONAL && firstTickOffset !== undefined ? firstTickOffset / SIN_45 - lineHeight : Infinity;
+        const maxLabelWidth = rotation === LABEL_ROTATIONS.DIAGONAL ? (tickSpacing - LABEL_PADDING) / SIN_45 : Infinity;
         const finalLabels = data.map((p, i) => {
-            const limit = i === 0 ? Math.min(tickSpacingLimit, firstTickLimit) : tickSpacingLimit;
-            return truncateLabel(p.label, labelWidths.at(i) ?? 0, limit, ellipsisWidth);
+            return truncateLabel(p.label, labelWidths.at(i) ?? 0, maxLabelWidth, ellipsisWidth);
         });
 
         // 3. Compute skip interval (only at 90°)
@@ -166,7 +139,7 @@ function useChartLabelLayout({data, font, tickSpacing, labelAreaWidth, firstTick
             truncatedLabels: finalLabels,
             xAxisLabelHeight,
         };
-    }, [font, tickSpacing, labelAreaWidth, firstTickOffset, data]);
+    }, [font, tickSpacing, labelAreaWidth, data]);
 }
 
 export {LABEL_ROTATIONS, useChartLabelLayout};
diff --git a/src/components/Charts/utils.ts b/src/components/Charts/utils.ts
index 6c920a407457..af0c9e0fdafa 100644
--- a/src/components/Charts/utils.ts
+++ b/src/components/Charts/utils.ts
@@ -62,4 +62,20 @@ function rotatedLabelCenterCorrection(ascent: number, descent: number, angleRad:
     return ((ascent - descent) * Math.sin(angleRad)) / 2;
 }
 
-export {getChartColor, DEFAULT_CHART_COLOR, measureTextWidth, rotatedLabelCenterCorrection};
+/**
+ * Calculate minimum horizontal domainPadding so that edge data points
+ * (and their centered labels) aren't clipped by the chart boundary.
+ *
+ * @param chartWidth - Total chart width in pixels
+ * @param pointCount - Number of data points
+ * @param innerPadding - Padding ratio between points (0 for line charts, ~0.3 for bar charts)
+ */
+function calculateMinDomainPadding(chartWidth: number, pointCount: number, innerPadding = 0): number {
+    if (pointCount <= 1) {
+        return 0;
+    }
+    const minPaddingRatio = (1 - innerPadding) / (2 * (pointCount - 1 + innerPadding));
+    return Math.ceil(chartWidth * minPaddingRatio);
+}
+
+export {getChartColor, DEFAULT_CHART_COLOR, measureTextWidth, rotatedLabelCenterCorrection, calculateMinDomainPadding};

From fa5a338bdf08d2d57cfeac572953adeeae29f08c Mon Sep 17 00:00:00 2001
From: Mateusz Rajski <mateusz.rajski@swmansion.com>
Date: Thu, 5 Feb 2026 11:18:18 +0100
Subject: [PATCH 24/26] Improve naming conventions

---
 .../Charts/hooks/useChartLabelLayout.ts       | 30 +++++++++----------
 1 file changed, 15 insertions(+), 15 deletions(-)

diff --git a/src/components/Charts/hooks/useChartLabelLayout.ts b/src/components/Charts/hooks/useChartLabelLayout.ts
index 9f6fec14f9c9..857daa7e0b0a 100644
--- a/src/components/Charts/hooks/useChartLabelLayout.ts
+++ b/src/components/Charts/hooks/useChartLabelLayout.ts
@@ -73,14 +73,14 @@ function maxVisibleCount(areaWidth: number, itemWidth: number): number {
  */
 function pickRotation(maxLabelWidth: number, lineHeight: number, tickSpacing: number, labelArea: number, dataCount: number, minTruncatedWidth: number): number {
     // 0°: labels fit horizontally without truncation
-    const ew0 = effectiveWidth(maxLabelWidth, lineHeight, LABEL_ROTATIONS.HORIZONTAL);
-    if (ew0 + LABEL_PADDING <= tickSpacing && maxVisibleCount(labelArea, ew0) >= dataCount) {
+    const horizontalWidth = effectiveWidth(maxLabelWidth, lineHeight, LABEL_ROTATIONS.HORIZONTAL);
+    if (horizontalWidth + LABEL_PADDING <= tickSpacing && maxVisibleCount(labelArea, horizontalWidth) >= dataCount) {
         return LABEL_ROTATIONS.HORIZONTAL;
     }
 
     // 45°: viable if MIN_TRUNCATED_CHARS + ellipsis fits between ticks
-    const minEw45 = minTruncatedWidth * SIN_45;
-    if (minEw45 + LABEL_PADDING <= tickSpacing) {
+    const minDiagonalWidth = minTruncatedWidth * SIN_45;
+    if (minDiagonalWidth + LABEL_PADDING <= tickSpacing) {
         return LABEL_ROTATIONS.DIAGONAL;
     }
 
@@ -97,17 +97,17 @@ function useChartLabelLayout({data, font, tickSpacing, labelAreaWidth}: LabelLay
         const fontMetrics = font.getMetrics();
         const lineHeight = Math.abs(fontMetrics.descent) + Math.abs(fontMetrics.ascent);
         const ellipsisWidth = measureTextWidth(ELLIPSIS, font);
-        const labelWidths = data.map((p) => measureTextWidth(p.label, font));
+        const labelWidths = data.map((point) => measureTextWidth(point.label, font));
         const maxLabelLength = Math.max(...labelWidths);
 
         // Maximum width of labels after truncation to MIN_TRUNCATED_CHARS characters.
         // Labels shorter than the threshold keep their full width (can't be truncated further).
         const minTruncatedWidth = Math.max(
-            ...data.map((p, i) => {
-                if (p.label.length <= MIN_TRUNCATED_CHARS) {
-                    return labelWidths.at(i) ?? 0;
+            ...data.map((point, index) => {
+                if (point.label.length <= MIN_TRUNCATED_CHARS) {
+                    return labelWidths.at(index) ?? 0;
                 }
-                return measureTextWidth(p.label.slice(0, MIN_TRUNCATED_CHARS) + ELLIPSIS, font);
+                return measureTextWidth(point.label.slice(0, MIN_TRUNCATED_CHARS) + ELLIPSIS, font);
             }),
         );
 
@@ -117,17 +117,17 @@ function useChartLabelLayout({data, font, tickSpacing, labelAreaWidth}: LabelLay
         // 2. Truncate labels (only at 45°)
         //    Tick-spacing constraint: labelWidth * sin(45°) + padding <= tickSpacing
         const maxLabelWidth = rotation === LABEL_ROTATIONS.DIAGONAL ? (tickSpacing - LABEL_PADDING) / SIN_45 : Infinity;
-        const finalLabels = data.map((p, i) => {
-            return truncateLabel(p.label, labelWidths.at(i) ?? 0, maxLabelWidth, ellipsisWidth);
+        const finalLabels = data.map((point, index) => {
+            return truncateLabel(point.label, labelWidths.at(index) ?? 0, maxLabelWidth, ellipsisWidth);
         });
 
         // 3. Compute skip interval (only at 90°)
-        const finalMaxWidth = Math.max(...finalLabels.map((l) => measureTextWidth(l, font)));
+        const finalMaxWidth = Math.max(...finalLabels.map((label) => measureTextWidth(label, font)));
         let skipInterval = 1;
         if (rotation === LABEL_ROTATIONS.VERTICAL) {
-            const ew = effectiveWidth(finalMaxWidth, lineHeight, rotation);
-            const visible = maxVisibleCount(labelAreaWidth, ew);
-            skipInterval = visible >= data.length ? 1 : Math.ceil(data.length / Math.max(1, visible));
+            const verticalWidth = effectiveWidth(finalMaxWidth, lineHeight, rotation);
+            const visibleCount = maxVisibleCount(labelAreaWidth, verticalWidth);
+            skipInterval = visibleCount >= data.length ? 1 : Math.ceil(data.length / Math.max(1, visibleCount));
         }
 
         // 4. Compute vertical space needed for x-axis labels

From d28141dd789cd5f623d6fe4771c6d60ebee991cb Mon Sep 17 00:00:00 2001
From: Mateusz Rajski <mateusz.rajski@swmansion.com>
Date: Thu, 5 Feb 2026 13:23:53 +0100
Subject: [PATCH 25/26] Maintain constant 8px gap between plot and labels

---
 .../Charts/LineChart/LineChartContent.tsx     | 19 ++++++++++---
 .../Charts/hooks/useChartLabelLayout.ts       | 27 ++++++++++++++-----
 2 files changed, 36 insertions(+), 10 deletions(-)

diff --git a/src/components/Charts/LineChart/LineChartContent.tsx b/src/components/Charts/LineChart/LineChartContent.tsx
index 6726675eea09..508682c5ee1b 100644
--- a/src/components/Charts/LineChart/LineChartContent.tsx
+++ b/src/components/Charts/LineChart/LineChartContent.tsx
@@ -28,8 +28,8 @@ const DOT_HOVER_EXTRA_RADIUS = 2;
 /** Base domain padding applied to all sides */
 const BASE_DOMAIN_PADDING = {top: 16, bottom: 16, left: 0, right: 0};
 
-/** Gap between the x-axis line and the top of label glyphs */
-const X_AXIS_LABEL_GAP = 2;
+/** Consistent gap between x-axis and closest point of label (regardless of rotation) */
+const LABEL_GAP = 8;
 
 type LineChartProps = CartesianChartProps & {
     /** Callback when a data point is pressed */
@@ -116,6 +116,7 @@ function LineChartContent({data, title, titleIcon, isLoading, yAxisUnit, yAxisUn
         font,
         tickSpacing,
         labelAreaWidth: plotAreaWidth,
+        allowTightDiagonalPacking: true,
     });
 
     // Measure label widths for custom positioning in `renderOutside`
@@ -162,7 +163,19 @@ function LineChartContent({data, title, titleIcon, isLoading, yAxisUnit, yAxisUn
             const fontMetrics = font.getMetrics();
             const ascent = Math.abs(fontMetrics.ascent);
             const descent = Math.abs(fontMetrics.descent);
-            const labelY = args.chartBounds.bottom + X_AXIS_LABEL_GAP + font.getSize();
+
+            // Calculate labelY to maintain consistent LABEL_GAP from axis to closest point of text
+            // At 0°: closest point is top of text (baseline - ascent)
+            // At 45°: closest point is top-right corner, ascent projects as ascent * cos(45°)
+            // At 90°: text is vertical, closest point is at descent from baseline
+            let labelY: number;
+            if (angleRad === 0) {
+                labelY = args.chartBounds.bottom + LABEL_GAP + ascent;
+            } else if (angleRad >= Math.PI / 2) {
+                labelY = args.chartBounds.bottom + LABEL_GAP + descent;
+            } else {
+                labelY = args.chartBounds.bottom + LABEL_GAP + ascent * Math.cos(angleRad);
+            }
 
             return truncatedLabels.map((label, i) => {
                 if (i % labelSkipInterval !== 0) {
diff --git a/src/components/Charts/hooks/useChartLabelLayout.ts b/src/components/Charts/hooks/useChartLabelLayout.ts
index 857daa7e0b0a..78564dfc131c 100644
--- a/src/components/Charts/hooks/useChartLabelLayout.ts
+++ b/src/components/Charts/hooks/useChartLabelLayout.ts
@@ -25,6 +25,8 @@ type LabelLayoutConfig = {
     font: SkFont | null;
     tickSpacing: number;
     labelAreaWidth: number;
+    /** When true, allows tighter label packing at 45° by accounting for vertical offset. Useful for line charts. */
+    allowTightDiagonalPacking?: boolean;
 };
 
 /** Truncate `label` so its pixel width fits within `maxWidth`, adding ellipsis. */
@@ -71,7 +73,15 @@ function maxVisibleCount(areaWidth: number, itemWidth: number): number {
  * Pick the smallest rotation (0 → 45 → 90) where labels don't overlap,
  * preferring rotation over skip interval.
  */
-function pickRotation(maxLabelWidth: number, lineHeight: number, tickSpacing: number, labelArea: number, dataCount: number, minTruncatedWidth: number): number {
+function pickRotation(
+    maxLabelWidth: number,
+    lineHeight: number,
+    tickSpacing: number,
+    labelArea: number,
+    dataCount: number,
+    minTruncatedWidth: number,
+    allowTightDiagonalPacking: boolean,
+): number {
     // 0°: labels fit horizontally without truncation
     const horizontalWidth = effectiveWidth(maxLabelWidth, lineHeight, LABEL_ROTATIONS.HORIZONTAL);
     if (horizontalWidth + LABEL_PADDING <= tickSpacing && maxVisibleCount(labelArea, horizontalWidth) >= dataCount) {
@@ -79,7 +89,9 @@ function pickRotation(maxLabelWidth: number, lineHeight: number, tickSpacing: nu
     }
 
     // 45°: viable if MIN_TRUNCATED_CHARS + ellipsis fits between ticks
-    const minDiagonalWidth = minTruncatedWidth * SIN_45;
+    // With tight packing, labels can overlap horizontally by lineHeight * sin(45°) due to vertical offset
+    const diagonalOverlap = allowTightDiagonalPacking ? lineHeight * SIN_45 : 0;
+    const minDiagonalWidth = minTruncatedWidth * SIN_45 - diagonalOverlap;
     if (minDiagonalWidth + LABEL_PADDING <= tickSpacing) {
         return LABEL_ROTATIONS.DIAGONAL;
     }
@@ -88,7 +100,7 @@ function pickRotation(maxLabelWidth: number, lineHeight: number, tickSpacing: nu
     return LABEL_ROTATIONS.VERTICAL;
 }
 
-function useChartLabelLayout({data, font, tickSpacing, labelAreaWidth}: LabelLayoutConfig) {
+function useChartLabelLayout({data, font, tickSpacing, labelAreaWidth, allowTightDiagonalPacking = false}: LabelLayoutConfig) {
     return useMemo(() => {
         if (!font || data.length === 0 || tickSpacing <= 0 || labelAreaWidth <= 0) {
             return {labelRotation: 0, labelSkipInterval: 1, truncatedLabels: []};
@@ -112,11 +124,12 @@ function useChartLabelLayout({data, font, tickSpacing, labelAreaWidth}: LabelLay
         );
 
         // 1. Pick rotation
-        const rotation = pickRotation(maxLabelLength, lineHeight, tickSpacing, labelAreaWidth, data.length, minTruncatedWidth);
+        const rotation = pickRotation(maxLabelLength, lineHeight, tickSpacing, labelAreaWidth, data.length, minTruncatedWidth, allowTightDiagonalPacking);
 
         // 2. Truncate labels (only at 45°)
-        //    Tick-spacing constraint: labelWidth * sin(45°) + padding <= tickSpacing
-        const maxLabelWidth = rotation === LABEL_ROTATIONS.DIAGONAL ? (tickSpacing - LABEL_PADDING) / SIN_45 : Infinity;
+        //    With tight packing, labels can be longer due to allowed horizontal overlap
+        const diagonalOverlap = allowTightDiagonalPacking ? lineHeight : 0;
+        const maxLabelWidth = rotation === LABEL_ROTATIONS.DIAGONAL ? (tickSpacing - LABEL_PADDING) / SIN_45 + diagonalOverlap : Infinity;
         const finalLabels = data.map((point, index) => {
             return truncateLabel(point.label, labelWidths.at(index) ?? 0, maxLabelWidth, ellipsisWidth);
         });
@@ -139,7 +152,7 @@ function useChartLabelLayout({data, font, tickSpacing, labelAreaWidth}: LabelLay
             truncatedLabels: finalLabels,
             xAxisLabelHeight,
         };
-    }, [font, tickSpacing, labelAreaWidth, data]);
+    }, [font, tickSpacing, labelAreaWidth, data, allowTightDiagonalPacking]);
 }
 
 export {LABEL_ROTATIONS, useChartLabelLayout};

From 2d11275ecf00d0c929308b8f385147623e795a1f Mon Sep 17 00:00:00 2001
From: Mateusz Rajski <mateusz.rajski@swmansion.com>
Date: Thu, 5 Feb 2026 13:28:10 +0100
Subject: [PATCH 26/26] Move labelY utility function to utils

---
 .../Charts/LineChart/LineChartContent.tsx     | 16 ++-------------
 src/components/Charts/utils.ts                | 20 ++++++++++++++++++-
 2 files changed, 21 insertions(+), 15 deletions(-)

diff --git a/src/components/Charts/LineChart/LineChartContent.tsx b/src/components/Charts/LineChart/LineChartContent.tsx
index 508682c5ee1b..94e16c793699 100644
--- a/src/components/Charts/LineChart/LineChartContent.tsx
+++ b/src/components/Charts/LineChart/LineChartContent.tsx
@@ -13,7 +13,7 @@ import fontSource from '@components/Charts/font';
 import type {HitTestArgs} from '@components/Charts/hooks';
 import {useChartInteractions, useChartLabelFormats, useChartLabelLayout, useDynamicYDomain, useTooltipData} from '@components/Charts/hooks';
 import type {CartesianChartProps, ChartDataPoint} from '@components/Charts/types';
-import {calculateMinDomainPadding, DEFAULT_CHART_COLOR, measureTextWidth, rotatedLabelCenterCorrection} from '@components/Charts/utils';
+import {calculateMinDomainPadding, DEFAULT_CHART_COLOR, measureTextWidth, rotatedLabelCenterCorrection, rotatedLabelYOffset} from '@components/Charts/utils';
 import useResponsiveLayout from '@hooks/useResponsiveLayout';
 import useTheme from '@hooks/useTheme';
 import useThemeStyles from '@hooks/useThemeStyles';
@@ -163,19 +163,7 @@ function LineChartContent({data, title, titleIcon, isLoading, yAxisUnit, yAxisUn
             const fontMetrics = font.getMetrics();
             const ascent = Math.abs(fontMetrics.ascent);
             const descent = Math.abs(fontMetrics.descent);
-
-            // Calculate labelY to maintain consistent LABEL_GAP from axis to closest point of text
-            // At 0°: closest point is top of text (baseline - ascent)
-            // At 45°: closest point is top-right corner, ascent projects as ascent * cos(45°)
-            // At 90°: text is vertical, closest point is at descent from baseline
-            let labelY: number;
-            if (angleRad === 0) {
-                labelY = args.chartBounds.bottom + LABEL_GAP + ascent;
-            } else if (angleRad >= Math.PI / 2) {
-                labelY = args.chartBounds.bottom + LABEL_GAP + descent;
-            } else {
-                labelY = args.chartBounds.bottom + LABEL_GAP + ascent * Math.cos(angleRad);
-            }
+            const labelY = args.chartBounds.bottom + LABEL_GAP + rotatedLabelYOffset(ascent, descent, angleRad);
 
             return truncatedLabels.map((label, i) => {
                 if (i % labelSkipInterval !== 0) {
diff --git a/src/components/Charts/utils.ts b/src/components/Charts/utils.ts
index af0c9e0fdafa..8918d6fa2d5b 100644
--- a/src/components/Charts/utils.ts
+++ b/src/components/Charts/utils.ts
@@ -62,6 +62,24 @@ function rotatedLabelCenterCorrection(ascent: number, descent: number, angleRad:
     return ((ascent - descent) * Math.sin(angleRad)) / 2;
 }
 
+/**
+ * Calculate the vertical offset from axis to text baseline that maintains
+ * a consistent visual gap regardless of label rotation.
+ *
+ * At 0°: gap to top of text (ascent above baseline)
+ * At 45°: gap to top-right corner after rotation (ascent projects as ascent * cos(45°))
+ * At 90°: gap to closest point of vertical text (descent from baseline)
+ */
+function rotatedLabelYOffset(ascent: number, descent: number, angleRad: number): number {
+    if (angleRad === 0) {
+        return ascent;
+    }
+    if (angleRad >= Math.PI / 2) {
+        return descent;
+    }
+    return ascent * Math.cos(angleRad);
+}
+
 /**
  * Calculate minimum horizontal domainPadding so that edge data points
  * (and their centered labels) aren't clipped by the chart boundary.
@@ -78,4 +96,4 @@ function calculateMinDomainPadding(chartWidth: number, pointCount: number, inner
     return Math.ceil(chartWidth * minPaddingRatio);
 }
 
-export {getChartColor, DEFAULT_CHART_COLOR, measureTextWidth, rotatedLabelCenterCorrection, calculateMinDomainPadding};
+export {getChartColor, DEFAULT_CHART_COLOR, measureTextWidth, rotatedLabelCenterCorrection, rotatedLabelYOffset, calculateMinDomainPadding};
