import { subWeeks, subMonths, subYears } from 'date-fns'; import type { TimeRange, ChartDataPoint } from '@/lib/types'; function toTradingDayKey(value: string): string { const parsed = Date.parse(value); if (!Number.isFinite(parsed)) { return value; } return new Date(parsed).toISOString().slice(0, 10); } /** * 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 */ 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 */ 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 */ 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 */ 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<{ date: string } & Record> { if (!seriesArray || seriesArray.length === 0) return []; // Create map indexed by normalized trading day. const dateMap = new Map>(); const seriesPointMaps = seriesArray.map((series) => { const pointMap = new Map(); series.data.forEach(point => { const date = toTradingDayKey(point.date); if (isOHLCVData(point)) { pointMap.set(date, point.close); } else if (isPriceData(point)) { pointMap.set(date, point.price); } }); pointMap.forEach((_value, date) => { const existing = dateMap.get(date) || { date } as { date: string } & Record; dateMap.set(date, existing); }); return { id: series.id, pointMap }; }); const mergedDates = Array.from(dateMap.keys()).sort((a, b) => new Date(a).getTime() - new Date(b).getTime() ); // Fill forward each series to keep benchmark/comparison lines aligned on trading days. seriesPointMaps.forEach(({ id, pointMap }) => { let lastKnownValue: number | null = null; mergedDates.forEach((date) => { const currentValue = pointMap.get(date); if (typeof currentValue === 'number') { lastKnownValue = currentValue; } const existing = dateMap.get(date); if (existing && lastKnownValue !== null) { existing[id] = lastKnownValue; } }); }); return mergedDates.map((date) => dateMap.get(date)!); }