Files
Neon-Desk/components/charts/utils/chart-data-transformers.ts

158 lines
4.0 KiB
TypeScript

import { subWeeks, subMonths, subYears } from 'date-fns';
import type { TimeRange, ChartDataPoint } from '@/lib/types';
/**
* Filter chart data by time range
*/
export function filterByTimeRange<T extends ChartDataPoint>(
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<T extends ChartDataPoint>(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<T extends ChartDataPoint>(
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<T extends ChartDataPoint>(
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<T extends ChartDataPoint>(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<T extends ChartDataPoint>(
seriesArray: Array<{ id: string; data: T[] }>
): Array<T & Record<string, number>> {
if (!seriesArray || seriesArray.length === 0) return [];
// Create map indexed by date
const dateMap = new Map<string, T & Record<string, number>>();
seriesArray.forEach(series => {
series.data.forEach(point => {
const date = point.date;
const existing = dateMap.get(date) || { date } as T & Record<string, number>;
// 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()
);
}