diff --git a/package-lock.json b/package-lock.json
index ec061e3a51d2..c1a8a42e3aa1 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -203,6 +203,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",
@@ -16349,6 +16350,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 a081cb938276..a7e022a083fe 100644
--- a/package.json
+++ b/package.json
@@ -272,6 +272,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",
diff --git a/src/CONST/index.ts b/src/CONST/index.ts
index e23570f40a7e..c45ae63c589f 100755
--- a/src/CONST/index.ts
+++ b/src/CONST/index.ts
@@ -7169,6 +7169,7 @@ const CONST = {
         VIEW: {
             TABLE: 'table',
             BAR: 'bar',
+            LINE: 'line',
         },
         SYNTAX_FILTER_KEYS: {
             TYPE: 'type',
diff --git a/src/components/Charts/BarChart/BarChartContent.tsx b/src/components/Charts/BarChart/BarChartContent.tsx
index c1d60f962b6b..19dc0a723db5 100644
--- a/src/components/Charts/BarChart/BarChartContent.tsx
+++ b/src/components/Charts/BarChart/BarChartContent.tsx
@@ -3,50 +3,35 @@ 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 {getChartColor} from '@components/Charts/chartColors';
-import ChartHeader from '@components/Charts/ChartHeader';
-import ChartTooltip from '@components/Charts/ChartTooltip';
-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,
-    Y_AXIS_TICK_COUNT,
-} from '@components/Charts/constants';
+import ChartHeader from '@components/Charts/components/ChartHeader';
+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} 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 {calculateMinDomainPadding, DEFAULT_CHART_COLOR, getChartColor} from '@components/Charts/utils';
 import useResponsiveLayout from '@hooks/useResponsiveLayout';
 import useTheme from '@hooks/useTheme';
 import useThemeStyles from '@hooks/useThemeStyles';
 import variables from '@styles/variables';
 
-/**
- * Calculate minimum domainPadding required to prevent bars from overflowing 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.
- */
-function calculateMinDomainPadding(chartWidth: number, barCount: number, innerPadding: number): number {
-    if (barCount <= 0) {
-        return 0;
-    }
-    const minPaddingRatio = (1 - innerPadding) / (2 * (barCount - 1 + innerPadding));
-    return Math.ceil(chartWidth * minPaddingRatio * DOMAIN_PADDING_SAFETY_BUFFER);
-}
+/** 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 BASE_DOMAIN_PADDING = {top: 32, bottom: 0, left: 0, right: 0};
+
+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();
@@ -55,9 +40,7 @@ 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 = CHART_COLORS.at(DEFAULT_SINGLE_BAR_COLOR_INDEX);
+    const defaultBarColor = DEFAULT_CHART_COLOR;
 
     // prepare data for display
     const chartData = useMemo(() => {
@@ -67,9 +50,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(
@@ -86,25 +67,22 @@ 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(() => {
         if (chartWidth === 0) {
-            return {left: 0, right: 0, top: DOMAIN_PADDING.top, bottom: DOMAIN_PADDING.bottom};
+            return BASE_DOMAIN_PADDING;
         }
         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 {...BASE_DOMAIN_PADDING, left: horizontalPadding, right: horizontalPadding};
     }, [chartWidth, data.length]);
 
     const {formatXAxisLabel, formatYAxisLabel} = useChartLabelFormats({
@@ -134,7 +112,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),
@@ -169,29 +147,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 +163,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}}
                 />
             );
         },
@@ -218,9 +174,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) {
@@ -242,7 +198,7 @@ function BarChartContent({data, title, titleIcon, isLoading, yAxisUnit, yAxisUni
                 titleIcon={titleIcon}
             />
             <View
-                style={[styles.barChartChartContainer, labelRotation === -90 ? dynamicChartStyle : undefined]}
+                style={[styles.barChartChartContainer, dynamicChartStyle]}
                 onLayout={handleLayout}
             >
                 {chartWidth > 0 && (
@@ -276,7 +232,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 +253,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..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 '@components/Charts/types';
 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
new file mode 100644
index 000000000000..94e16c793699
--- /dev/null
+++ b/src/components/Charts/LineChart/LineChartContent.tsx
@@ -0,0 +1,308 @@
+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';
+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 type {CartesianChartProps, ChartDataPoint} from '@components/Charts/types';
+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';
+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;
+
+/** Base domain padding applied to all sides */
+const BASE_DOMAIN_PADDING = {top: 16, bottom: 16, left: 0, right: 0};
+
+/** 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 */
+    onPointPress?: (dataPoint: ChartDataPoint, index: number) => void;
+};
+
+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 [plotAreaWidth, setPlotAreaWidth] = useState(0);
+
+    const yAxisDomain = useDynamicYDomain(data);
+
+    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) => {
+        setChartWidth(event.nativeEvent.layout.width);
+    }, []);
+
+    const handleChartBoundsChange = useCallback((bounds: ChartBounds) => {
+        setPlotAreaWidth(bounds.right - bounds.left);
+    }, []);
+
+    // 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);
+
+        // Without font, use geometric padding (safe fallback)
+        if (!font) {
+            return {...BASE_DOMAIN_PADDING, left: geometricPadding, right: geometricPadding};
+        }
+
+        // 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,
+        allowTightDiagonalPacking: true,
+    });
+
+    // 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 {formatYAxisLabel} = useChartLabelFormats({
+        data,
+        yAxisUnit,
+        yAxisUnitPosition,
+    });
+
+    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_RADIUS + DOT_HOVER_EXTRA_RADIUS;
+    }, []);
+
+    const {actionsRef, customGestures, activeDataIndex, isTooltipActive, tooltipStyle} = useChartInteractions({
+        handlePress: handlePointPress,
+        checkIsOver: checkIsOverDot,
+    });
+
+    const tooltipData = useTooltipData(activeDataIndex, data, yAxisUnit, yAxisUnitPosition);
+
+    // 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) {
+                return null;
+            }
+
+            const fontMetrics = font.getMetrics();
+            const ascent = Math.abs(fontMetrics.ascent);
+            const descent = Math.abs(fontMetrics.descent);
+            const labelY = args.chartBounds.bottom + LABEL_GAP + rotatedLabelYOffset(ascent, descent, angleRad);
+
+            return truncatedLabels.map((label, i) => {
+                if (i % labelSkipInterval !== 0) {
+                    return null;
+                }
+
+                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 / 2}
+                            y={labelY}
+                            text={label}
+                            font={font}
+                            color={theme.textSupporting}
+                        />
+                    );
+                }
+
+                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
+                // 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={[{translateX: correction}, {rotate: -angleRad}]}
+                    >
+                        <SkiaText
+                            x={textX}
+                            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 + (xAxisLabelHeight ?? 0),
+        }),
+        [xAxisLabelHeight],
+    );
+
+    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, dynamicChartStyle]}
+                onLayout={handleLayout}
+            >
+                {chartWidth > 0 && (
+                    <CartesianChart
+                        xKey="x"
+                        padding={{top: CHART_PADDING, left: CHART_PADDING, right: CHART_PADDING, bottom: (xAxisLabelHeight ?? 0) + CHART_PADDING}}
+                        yKeys={['y']}
+                        domainPadding={domainPadding}
+                        actionsRef={actionsRef}
+                        customGestures={customGestures}
+                        onChartBoundsChange={handleChartBoundsChange}
+                        renderOutside={renderCustomXLabels}
+                        xAxis={{
+                            tickCount: data.length,
+                            lineWidth: X_AXIS_LINE_WIDTH,
+                        }}
+                        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: yAxisDomain,
+                            },
+                        ]}
+                        frame={{lineWidth: {left: 1, bottom: 1, top: 0, right: 0}, lineColor: theme.border}}
+                        data={chartData}
+                    >
+                        {({points}) => (
+                            <>
+                                <Line
+                                    points={points.y}
+                                    color={DEFAULT_CHART_COLOR}
+                                    strokeWidth={2}
+                                    curveType="linear"
+                                />
+                                <Scatter
+                                    points={points.y}
+                                    radius={DOT_RADIUS}
+                                    color={DEFAULT_CHART_COLOR}
+                                />
+                            </>
+                        )}
+                    </CartesianChart>
+                )}
+                {isTooltipActive && !!tooltipData && (
+                    <Animated.View style={tooltipStyle}>
+                        <ChartTooltip
+                            label={tooltipData.label}
+                            amount={tooltipData.amount}
+                            percentage={tooltipData.percentage}
+                        />
+                    </Animated.View>
+                )}
+            </View>
+        </View>
+    );
+}
+
+export default LineChartContent;
+export type {LineChartProps};
diff --git a/src/components/Charts/LineChart/index.native.tsx b/src/components/Charts/LineChart/index.native.tsx
new file mode 100644
index 000000000000..db7c218db9ab
--- /dev/null
+++ b/src/components/Charts/LineChart/index.native.tsx
@@ -0,0 +1,12 @@
+import React from 'react';
+import type {LineChartProps} from './LineChartContent';
+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..907d385722f8
--- /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 useThemeStyles from '@hooks/useThemeStyles';
+import type {LineChartProps} from './LineChartContent';
+
+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/chartColors.ts b/src/components/Charts/chartColors.ts
deleted file mode 100644
index c34ea720c040..000000000000
--- a/src/components/Charts/chartColors.ts
+++ /dev/null
@@ -1,39 +0,0 @@
-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
- */
-const CHART_PALETTE: string[] = (() => {
-    const rows = [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) {
-        for (const hue of hues) {
-            const colorKey = `${hue}${row}`;
-            if (colors[colorKey]) {
-                palette.push(colors[colorKey]);
-            }
-        }
-    }
-
-    return palette;
-})();
-
-/**
- * Gets a color from the chart palette based on index.
- * Automatically loops back to the start if the index exceeds 29.
- */
-function getChartColor(index: number): string {
-    if (CHART_PALETTE.length === 0) {
-        return colors.black; // Fallback
-    }
-    return CHART_PALETTE.at(index % CHART_PALETTE.length) ?? colors.black;
-}
-
-export {CHART_PALETTE, getChartColor};
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..be005042afb5 100644
--- a/src/components/Charts/ChartTooltip.tsx
+++ b/src/components/Charts/components/ChartTooltip.tsx
@@ -3,7 +3,12 @@ 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 b0ed40872622..b8b987b9fe19 100644
--- a/src/components/Charts/constants.ts
+++ b/src/components/Charts/constants.ts
@@ -1,113 +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 = '...';
-
-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,
-};
+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/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 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..7c32baa2c1ab 100644
--- a/src/components/Charts/hooks/useChartLabelFormats.ts
+++ b/src/components/Charts/hooks/useChartLabelFormats.ts
@@ -1,19 +1,17 @@
 import {useCallback} from 'react';
-
-type ChartDataPoint = {
-    label: string;
-};
+import type {ChartDataPoint, YAxisUnitPosition} from '@components/Charts/types';
+import {LABEL_ROTATIONS} from './useChartLabelLayout';
 
 type UseChartLabelFormatsProps = {
     data: ChartDataPoint[];
     yAxisUnit?: string;
-    yAxisUnitPosition?: 'left' | 'right';
-    labelSkipInterval: number;
-    labelRotation: number;
-    truncatedLabels: string[];
+    yAxisUnitPosition?: YAxisUnitPosition;
+    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();
@@ -39,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 || !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 215459488f8f..78564dfc131c 100644
--- a/src/components/Charts/hooks/useChartLabelLayout.ts
+++ b/src/components/Charts/hooks/useChartLabelLayout.ts
@@ -1,148 +1,159 @@
 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';
-
-type ChartDataPoint = {
-    label: string;
-    [key: string]: unknown;
-};
+import type {ChartDataPoint} from '@components/Charts/types';
+import {measureTextWidth} from '@components/Charts/utils';
+
+/** Supported label rotation angles in degrees */
+const LABEL_ROTATIONS = {
+    HORIZONTAL: 0,
+    DIAGONAL: 45,
+    VERTICAL: 90,
+} as const;
+
+const SIN_45 = Math.sin(Math.PI / 4);
+
+/** Minimum gap between adjacent labels (px) */
+const LABEL_PADDING = 4;
+
+const ELLIPSIS = '...';
+
+/** 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;
+    /** 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. */
+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;
+}
+
+/** 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));
+}
+
 /**
- * Measure the width of a text string using the font's glyph widths.
- * Uses getGlyphWidths as measureText is not implemented on React Native Web.
+ * Pick the smallest rotation (0 → 45 → 90) where labels don't overlap,
+ * preferring rotation over skip interval.
  */
-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 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) {
+        return LABEL_ROTATIONS.HORIZONTAL;
+    }
+
+    // 45°: viable if MIN_TRUNCATED_CHARS + ellipsis fits between ticks
+    // 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;
+    }
+
+    // 90°: fallback
+    return LABEL_ROTATIONS.VERTICAL;
 }
 
-function useChartLabelLayout({data, font, chartWidth, barAreaWidth, containerHeight}: LabelLayoutConfig) {
+function useChartLabelLayout({data, font, tickSpacing, labelAreaWidth, allowTightDiagonalPacking = false}: LabelLayoutConfig) {
     return useMemo(() => {
-        if (!font || chartWidth === 0 || containerHeight === 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: []};
         }
 
-        // Get font metrics
         const fontMetrics = font.getMetrics();
         const lineHeight = Math.abs(fontMetrics.descent) + Math.abs(fontMetrics.ascent);
-        const ellipsisWidth = measureTextWidth(LABEL_ELLIPSIS, font);
-
-        // Calculate available dimensions
-        const availableWidthPerBar = chartWidth / data.length - LABEL_PADDING;
-
-        // Measure original labels
-        const labelWidths = data.map((p) => measureTextWidth(p.label, font));
+        const ellipsisWidth = measureTextWidth(ELLIPSIS, font);
+        const labelWidths = data.map((point) => measureTextWidth(point.label, font));
         const maxLabelLength = Math.max(...labelWidths);
 
-        // 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;
-            }
+        // 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((point, index) => {
+                if (point.label.length <= MIN_TRUNCATED_CHARS) {
+                    return labelWidths.at(index) ?? 0;
+                }
+                return measureTextWidth(point.label.slice(0, MIN_TRUNCATED_CHARS) + ELLIPSIS, font);
+            }),
+        );
+
+        // 1. Pick rotation
+        const rotation = pickRotation(maxLabelLength, lineHeight, tickSpacing, labelAreaWidth, data.length, minTruncatedWidth, allowTightDiagonalPacking);
+
+        // 2. Truncate labels (only at 45°)
+        //    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);
+        });
+
+        // 3. Compute skip interval (only at 90°)
+        const finalMaxWidth = Math.max(...finalLabels.map((label) => measureTextWidth(label, font)));
+        let skipInterval = 1;
+        if (rotation === LABEL_ROTATIONS.VERTICAL) {
+            const verticalWidth = effectiveWidth(finalMaxWidth, lineHeight, rotation);
+            const visibleCount = maxVisibleCount(labelAreaWidth, verticalWidth);
+            skipInterval = visibleCount >= data.length ? 1 : Math.ceil(data.length / Math.max(1, visibleCount));
         }
 
-        // === 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;
-        }
-
-        // Generate truncated labels
-        const finalLabels = data.map((p, i) => truncateToWidth(p.label, labelWidths.at(i) ?? 0, maxAllowedLabelWidth));
-
-        // === CALCULATE SKIP INTERVAL ===
-        // Calculate effective width based on rotation angle
-        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
-        }
+        // 4. Compute vertical space needed for x-axis labels
+        const xAxisLabelHeight = effectiveHeight(finalMaxWidth, lineHeight, rotation);
 
-        // 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};
-    }, [font, chartWidth, barAreaWidth, containerHeight, data]);
+        return {
+            labelRotation: -rotation,
+            labelSkipInterval: skipInterval,
+            truncatedLabels: finalLabels,
+            xAxisLabelHeight,
+        };
+    }, [font, tickSpacing, labelAreaWidth, data, allowTightDiagonalPacking]);
 }
 
-export {useChartLabelLayout};
+export {LABEL_ROTATIONS, useChartLabelLayout};
 export type {LabelLayoutConfig};
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..2ec687681de9
--- /dev/null
+++ b/src/components/Charts/hooks/useTooltipData.ts
@@ -0,0 +1,42 @@
+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 {
+    const totalSum = useMemo(() => data.reduce((sum, point) => sum + Math.abs(point.total), 0), [data]);
+
+    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 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, totalSum, yAxisUnit, yAxisUnitPosition]);
+}
+
+export {useTooltipData};
+export type {TooltipData};
diff --git a/src/components/Charts/index.ts b/src/components/Charts/index.ts
index aabb56843923..f86738fa140a 100644
--- a/src/components/Charts/index.ts
+++ b/src/components/Charts/index.ts
@@ -1,6 +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};
-export type {BarChartDataPoint, BarChartProps} from './types';
+export {BarChart, ChartHeader, ChartTooltip, LineChart};
+export type {ChartDataPoint, CartesianChartProps, YAxisUnitPosition} 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 e2ec4fe54072..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,17 +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;
+    yAxisUnitPosition?: YAxisUnitPosition;
 };
 
-export type {BarChartDataPoint, BarChartProps};
+type YAxisUnitPosition = 'left' | 'right';
+
+export type {ChartDataPoint, CartesianChartProps, YAxisUnitPosition};
diff --git a/src/components/Charts/utils.ts b/src/components/Charts/utils.ts
new file mode 100644
index 000000000000..8918d6fa2d5b
--- /dev/null
+++ b/src/components/Charts/utils.ts
@@ -0,0 +1,99 @@
+import type {SkFont} from '@shopify/react-native-skia';
+import colors from '@styles/theme/colors';
+
+/**
+ * Expensify Chart Color Palette.
+ *
+ * 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 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 shades × 6 hues)
+    for (const shade of shades) {
+        for (const hue of hues) {
+            const colorKey = `${hue}${shade}`;
+            if (colors[colorKey]) {
+                palette.push(colors[colorKey]);
+            }
+        }
+    }
+
+    return palette;
+})();
+
+/**
+ * Gets a color from the chart palette based on index.
+ * Automatically loops back to the start if the index exceeds 29.
+ */
+function getChartColor(index: number): string {
+    if (CHART_PALETTE.length === 0) {
+        return colors.black; // Fallback
+    }
+    return CHART_PALETTE.at(index % CHART_PALETTE.length) ?? colors.black;
+}
+
+/** Default color used for single-color charts (e.g., line chart, single-color bar chart) */
+const DEFAULT_CHART_COLOR = getChartColor(5);
+
+/**
+ * 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);
+}
+
+/**
+ * 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;
+}
+
+/**
+ * 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.
+ *
+ * @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, rotatedLabelYOffset, calculateMinDomainPadding};
diff --git a/src/components/Search/SearchBarChart.tsx b/src/components/Search/SearchBarChart.tsx
index 68d1e2f89e1a..18b4beb3045f 100644
--- a/src/components/Search/SearchBarChart.tsx
+++ b/src/components/Search/SearchBarChart.tsx
@@ -1,21 +1,13 @@
 import React, {useCallback, useMemo} from 'react';
 import {BarChart} from '@components/Charts';
-import type {BarChartDataPoint} from '@components/Charts';
-import type {
-    TransactionCardGroupListItemType,
-    TransactionCategoryGroupListItemType,
-    TransactionGroupListItemType,
-    TransactionMemberGroupListItemType,
-    TransactionWithdrawalIDGroupListItemType,
-} from '@components/SelectionListWithSections/types';
+import type {ChartDataPoint, YAxisUnitPosition} from '@components/Charts';
 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 */
-    data: TransactionGroupListItemType[];
+    data: GroupedItem[];
 
     /** Chart title */
     title: string;
@@ -29,8 +21,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;
@@ -39,28 +31,26 @@ 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: BarChartDataPoint[] = useMemo(() => {
+    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,
-                currency,
             };
         });
     }, [data, getLabel]);
 
     const handleBarPress = useCallback(
-        (dataPoint: BarChartDataPoint, index: number) => {
-            if (!onBarPress) {
+        (dataPoint: ChartDataPoint, index: number) => {
+            if (!onItemPress) {
                 return;
             }
 
@@ -69,10 +59,10 @@ function SearchBarChart({data, title, titleIcon, getLabel, getFilterQuery, onBar
                 return;
             }
 
-            const filterQuery = getFilterQuery(item as GroupedItem);
-            onBarPress(filterQuery);
+            const filterQuery = getFilterQuery(item);
+            onItemPress(filterQuery);
         },
-        [data, getFilterQuery, onBarPress],
+        [data, getFilterQuery, onItemPress],
     );
 
     return (
diff --git a/src/components/Search/SearchChartView.tsx b/src/components/Search/SearchChartView.tsx
index 1979dad7fb6c..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,
@@ -27,19 +26,8 @@ import {buildSearchQueryJSON, buildSearchQueryString} from '@libs/SearchQueryUti
 import CONST from '@src/CONST';
 import ROUTES from '@src/ROUTES';
 import SearchBarChart from './SearchBarChart';
-import type {ChartView, SearchGroupBy, SearchQueryJSON} from './types';
-
-type GroupedItem =
-    | TransactionMemberGroupListItemType
-    | TransactionCardGroupListItemType
-    | TransactionWithdrawalIDGroupListItemType
-    | TransactionCategoryGroupListItemType
-    | TransactionMerchantGroupListItemType
-    | TransactionTagGroupListItemType
-    | TransactionMonthGroupListItemType
-    | TransactionWeekGroupListItemType
-    | TransactionYearGroupListItemType
-    | TransactionQuarterGroupListItemType;
+import SearchLineChart from './SearchLineChart';
+import type {ChartView, GroupedItem, SearchGroupBy, SearchQueryJSON} from './types';
 
 type ChartGroupByConfig = {
     titleIconName: 'Users' | 'CreditCard' | 'Send' | 'Folder' | 'Basket' | 'Tag' | 'Calendar';
@@ -125,14 +113,14 @@ 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;
 
     /** Grouped transaction data from search results */
-    data: TransactionGroupListItemType[];
+    data: GroupedItem[];
 
     /** Whether data is loading */
     isLoading?: boolean;
@@ -144,8 +132,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,
 };
 
 /**
@@ -162,9 +151,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}`);
 
@@ -173,20 +161,17 @@ 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 firstItem = data.at(0);
         const currency = firstItem?.currency ?? 'USD';
         const {symbol, position} = getCurrencyDisplayInfoForCharts(currency);
 
@@ -210,7 +195,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
new file mode 100644
index 000000000000..b06699021933
--- /dev/null
+++ b/src/components/Search/SearchLineChart.tsx
@@ -0,0 +1,82 @@
+import React, {useCallback, useMemo} from 'react';
+import {LineChart} from '@components/Charts';
+import type {ChartDataPoint, YAxisUnitPosition} from '@components/Charts';
+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: GroupedItem[];
+
+    /** 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 chart item is pressed - receives the filter query to apply */
+    onItemPress?: (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?: YAxisUnitPosition;
+};
+
+function SearchLineChart({data, title, titleIcon, getLabel, getFilterQuery, onItemPress, isLoading, yAxisUnit, yAxisUnitPosition}: SearchLineChartProps) {
+    const chartData: ChartDataPoint[] = useMemo(() => {
+        return data.map((item) => {
+            const currency = item.currency ?? 'USD';
+            const totalInDisplayUnits = convertToFrontendAmountAsInteger(item.total ?? 0, currency);
+
+            return {
+                label: getLabel(item),
+                total: totalInDisplayUnits,
+            };
+        });
+    }, [data, getLabel]);
+
+    const handlePointPress = useCallback(
+        (dataPoint: ChartDataPoint, index: number) => {
+            if (!onItemPress) {
+                return;
+            }
+
+            const item = data.at(index);
+            if (!item) {
+                return;
+            }
+
+            const filterQuery = getFilterQuery(item);
+            onItemPress(filterQuery);
+        },
+        [data, getFilterQuery, onItemPress],
+    );
+
+    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..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,
@@ -1305,17 +1306,16 @@ 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) {
+    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/components/Search/types.ts b/src/components/Search/types.ts
index 7faab6847ef1..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';
@@ -115,8 +130,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>;
@@ -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,
 };
diff --git a/src/languages/de.ts b/src/languages/de.ts
index 95ce823e52e8..ce0b0d8be57c 100644
--- a/src/languages/de.ts
+++ b/src/languages/de.ts
@@ -7081,6 +7081,7 @@ Fordern Sie Spesendetails wie Belege und Beschreibungen an, legen Sie Limits und
             label: 'Anzeigen',
             table: 'Tabelle',
             bar: 'Leiste',
+            line: 'Linie',
         },
         chartTitles: {
             [CONST.SEARCH.GROUP_BY.FROM]: 'Von',
diff --git a/src/languages/en.ts b/src/languages/en.ts
index 058ae9ea21bb..eeddf1a02eab 100644
--- a/src/languages/en.ts
+++ b/src/languages/en.ts
@@ -7004,6 +7004,7 @@ const translations = {
             label: 'View',
             table: 'Table',
             bar: 'Bar',
+            line: 'Line',
         },
         chartTitles: {
             [CONST.SEARCH.GROUP_BY.FROM]: 'From',
diff --git a/src/languages/es.ts b/src/languages/es.ts
index aa9a9f802e57..7620d5638b99 100644
--- a/src/languages/es.ts
+++ b/src/languages/es.ts
@@ -6661,7 +6661,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 ae288dfd8238..b3fecdf1af97 100644
--- a/src/languages/fr.ts
+++ b/src/languages/fr.ts
@@ -7097,6 +7097,7 @@ Rendez obligatoires des informations de dépense comme les reçus et les descrip
             label: 'Afficher',
             table: 'Table',
             bar: 'Bar',
+            line: 'Ligne',
         },
         chartTitles: {
             [CONST.SEARCH.GROUP_BY.FROM]: 'De',
diff --git a/src/languages/it.ts b/src/languages/it.ts
index ae040c048fa3..7dcb9a516006 100644
--- a/src/languages/it.ts
+++ b/src/languages/it.ts
@@ -7064,6 +7064,7 @@ Richiedi dettagli sulle spese come ricevute e descrizioni, imposta limiti e valo
             label: 'Visualizza',
             table: 'Tabella',
             bar: 'Bar',
+            line: 'Linea',
         },
         chartTitles: {
             [CONST.SEARCH.GROUP_BY.FROM]: 'Da',
diff --git a/src/languages/ja.ts b/src/languages/ja.ts
index f169d12a662b..6f9be529521b 100644
--- a/src/languages/ja.ts
+++ b/src/languages/ja.ts
@@ -7005,6 +7005,7 @@ ${reportName}
             label: '表示',
             table: 'テーブル',
             bar: 'バー',
+            line: '折れ線',
         },
         chartTitles: {
             [CONST.SEARCH.GROUP_BY.FROM]: '差出人',
@@ -7039,7 +7040,7 @@ ${reportName}
         exportedTo: 'エクスポート先',
         exportAll: {
             selectAllMatchingItems: '一致する項目をすべて選択',
-            allMatchingItemsSelected: '一致するすべての項目を選択済み',
+            allMatchingItemsSelected: '一致する項目をすべて選択済み',
         },
     },
     genericErrorPage: {
diff --git a/src/languages/nl.ts b/src/languages/nl.ts
index f1b4424ee451..f45535b27732 100644
--- a/src/languages/nl.ts
+++ b/src/languages/nl.ts
@@ -7052,6 +7052,7 @@ Vereis onkostendetails zoals bonnen en beschrijvingen, stel limieten en standaar
             label: 'Bekijken',
             table: 'Tabel',
             bar: 'Balk',
+            line: 'Lijn',
         },
         chartTitles: {
             [CONST.SEARCH.GROUP_BY.FROM]: 'Van',
diff --git a/src/languages/pl.ts b/src/languages/pl.ts
index d3dbb5696f5b..78bfc1249f55 100644
--- a/src/languages/pl.ts
+++ b/src/languages/pl.ts
@@ -7035,6 +7035,7 @@ Wymagaj szczegółów wydatków, takich jak paragony i opisy, ustawiaj limity i
             label: 'Pokaż',
             table: 'Tabela',
             bar: 'Bar',
+            line: 'Linia',
         },
         chartTitles: {
             [CONST.SEARCH.GROUP_BY.FROM]: 'Od',
diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts
index ad95bf3a24f0..bdc057d96b21 100644
--- a/src/languages/pt-BR.ts
+++ b/src/languages/pt-BR.ts
@@ -7034,6 +7034,7 @@ Exija dados de despesas como recibos e descrições, defina limites e padrões e
             label: 'Visualizar',
             table: 'Tabela',
             bar: 'Bar',
+            line: 'Linha',
         },
         chartTitles: {
             [CONST.SEARCH.GROUP_BY.FROM]: 'De',
diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts
index e8d83be9ce05..6e50d9bb4989 100644
--- a/src/languages/zh-hans.ts
+++ b/src/languages/zh-hans.ts
@@ -6901,6 +6901,7 @@ ${reportName}
             label: '查看',
             table: '表',
             bar: '栏',
+            line: '折线',
         },
         chartTitles: {
             [CONST.SEARCH.GROUP_BY.FROM]: '来自',
diff --git a/src/libs/SearchQueryUtils.ts b/src/libs/SearchQueryUtils.ts
index 79d9b6320aec..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 to category 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;
-        }
+        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/src/libs/SearchUIUtils.ts b/src/libs/SearchUIUtils.ts
index 87026e8681d6..62f3edfe9dc8 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,
@@ -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,
diff --git a/src/styles/index.ts b/src/styles/index.ts
index bc1bb2ce1217..d79b2ba517a0 100644
--- a/src/styles/index.ts
+++ b/src/styles/index.ts
@@ -5789,6 +5789,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,
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', () => {
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);
         });
     });
