import { subWeeks, subMonths, subYears } from 'date-fns'; import type { TimeRange, ChartDataPoint } from '@/lib/types'; /** * Filter chart data by time range */ export function filterByTimeRange( data: T[], range: TimeRange ): T[] { if (data.length === 0) return data; const now = new Date(); const cutoffDate = { '1W': subWeeks(now, 1), '1M': subMonths(now, 1), '3M': subMonths(now, 3), '1Y': subYears(now, 1), '3Y': subYears(now, 3), '5Y': subYears(now, 5), '10Y': subYears(now, 10), '20Y': subYears(now, 20) }[range]; return data.filter(point => new Date(point.date) >= cutoffDate); } /** * Check if data point has OHLCV fields */ export function isOHLCVData(data: ChartDataPoint): data is { date: string; open: number; high: number; low: number; close: number; volume: number; } { return 'open' in data && 'high' in data && 'low' in data && 'close' in data && 'volume' in data; } /** * Check if data point has simple price field */ export function isPriceData(data: ChartDataPoint): data is { date: string; price: number } { return 'price' in data; } /** * Normalize data to ensure consistent structure * Converts price data to OHLCV-like structure for candlestick charts */ export function normalizeChartData(data: T[]): T[] { if (!data || data.length === 0) return []; // Sort by date ascending return [...data].sort((a, b) => new Date(a.date).getTime() - new Date(b.date).getTime() ); } /** * Sample data for performance with large datasets * Keeps every Nth point when dataset is too large */ export function sampleData( data: T[], maxPoints: number = 1000 ): T[] { if (data.length <= maxPoints) return data; const samplingRate = Math.ceil(data.length / maxPoints); const sampled: T[] = []; for (let i = 0; i < data.length; i += samplingRate) { sampled.push(data[i]); } // Always include the last point if (sampled[sampled.length - 1] !== data[data.length - 1]) { sampled.push(data[data.length - 1]); } return sampled; } /** * Calculate min/max values for Y-axis domain */ export function calculateYAxisDomain( data: T[], padding: number = 0.1 ): [number, number] { if (data.length === 0) return [0, 100]; let min = Infinity; let max = -Infinity; data.forEach(point => { if (isOHLCVData(point)) { min = Math.min(min, point.low); max = Math.max(max, point.high); } else if (isPriceData(point)) { min = Math.min(min, point.price); max = Math.max(max, point.price); } }); // Add padding const range = max - min; const paddedMin = min - (range * padding); const paddedMax = max + (range * padding); return [paddedMin, paddedMax]; } /** * Calculate volume max for volume indicator Y-axis */ export function calculateVolumeMax(data: T[]): number { if (data.length === 0 || !isOHLCVData(data[0])) return 0; return Math.max(...data.map(d => (isOHLCVData(d) ? d.volume : 0))); } /** * Merge multiple data series by date for combination charts */ export function mergeDataSeries( seriesArray: Array<{ id: string; data: T[] }> ): Array> { if (!seriesArray || seriesArray.length === 0) return []; // Create map indexed by date const dateMap = new Map>(); seriesArray.forEach(series => { series.data.forEach(point => { const date = point.date; const existing = dateMap.get(date) || { date } as T & Record; // Add value for this series if (isOHLCVData(point)) { existing[series.id] = point.close; } else if (isPriceData(point)) { existing[series.id] = point.price; } dateMap.set(date, existing); }); }); // Convert to array and sort by date return Array.from(dateMap.values()).sort((a, b) => new Date(a.date).getTime() - new Date(b.date).getTime() ); }