Add untracked chart and schema files
This commit is contained in:
68
components/charts/utils/candlestick-shapes.tsx
Normal file
68
components/charts/utils/candlestick-shapes.tsx
Normal file
@@ -0,0 +1,68 @@
|
||||
import { getPriceChangeColor } from './chart-colors';
|
||||
|
||||
type CandlestickShapeProps = {
|
||||
cx?: number;
|
||||
payload?: {
|
||||
open: number;
|
||||
high: number;
|
||||
low: number;
|
||||
close: number;
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Custom candlestick shape component for Recharts
|
||||
* Renders candlestick with wick and body
|
||||
*/
|
||||
export function CandlestickShape(props: CandlestickShapeProps) {
|
||||
const { cx, payload } = props;
|
||||
|
||||
if (!payload || !cx) return null;
|
||||
|
||||
const { open, high, low, close } = payload;
|
||||
const isPositive = close >= open;
|
||||
const color = getPriceChangeColor(close - open);
|
||||
|
||||
// Calculate positions
|
||||
const bodyTop = Math.min(open, close);
|
||||
const bodyBottom = Math.max(open, close);
|
||||
const bodyHeight = Math.max(bodyBottom - bodyTop, 1);
|
||||
|
||||
// Candlestick width
|
||||
const width = 8;
|
||||
|
||||
return (
|
||||
<g>
|
||||
{/* Upper wick */}
|
||||
<line
|
||||
x1={cx}
|
||||
y1={high}
|
||||
x2={cx}
|
||||
y2={bodyTop}
|
||||
stroke={color}
|
||||
strokeWidth={1}
|
||||
/>
|
||||
|
||||
{/* Body */}
|
||||
<rect
|
||||
x={cx - width / 2}
|
||||
y={bodyTop}
|
||||
width={width}
|
||||
height={bodyHeight}
|
||||
fill={isPositive ? 'transparent' : color}
|
||||
stroke={color}
|
||||
strokeWidth={1}
|
||||
/>
|
||||
|
||||
{/* Lower wick */}
|
||||
<line
|
||||
x1={cx}
|
||||
y1={bodyBottom}
|
||||
x2={cx}
|
||||
y2={low}
|
||||
stroke={color}
|
||||
strokeWidth={1}
|
||||
/>
|
||||
</g>
|
||||
);
|
||||
}
|
||||
65
components/charts/utils/chart-colors.ts
Normal file
65
components/charts/utils/chart-colors.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
import type { ChartColorPalette } from '@/lib/types';
|
||||
|
||||
/**
|
||||
* Get chart color palette using CSS variables for theming
|
||||
* These colors match the existing dark theme in the codebase
|
||||
*/
|
||||
export function getChartColors(): ChartColorPalette {
|
||||
return {
|
||||
primary: 'var(--accent)',
|
||||
secondary: 'var(--terminal-muted)',
|
||||
positive: '#96f5bf', // Green - matches existing price-history-card
|
||||
negative: '#ff9f9f', // Red - matches existing price-history-card
|
||||
grid: 'var(--line-weak)',
|
||||
text: 'var(--terminal-bright)',
|
||||
muted: 'var(--terminal-muted)',
|
||||
tooltipBg: 'rgba(31, 34, 39, 0.96)',
|
||||
tooltipBorder: 'var(--line-strong)',
|
||||
volume: 'var(--terminal-muted)'
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get color for price change (positive/negative)
|
||||
*/
|
||||
export function getPriceChangeColor(change: number): string {
|
||||
const colors = getChartColors();
|
||||
return change >= 0 ? colors.positive : colors.negative;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert CSS variable to computed color value
|
||||
* Used for chart export since html-to-image can't render CSS variables
|
||||
*/
|
||||
export function cssVarToColor(cssVar: string): string {
|
||||
if (typeof window === 'undefined') return cssVar;
|
||||
|
||||
// If it's already a color value, return as-is
|
||||
if (!cssVar.startsWith('var(')) return cssVar;
|
||||
|
||||
// Extract variable name from var(--name)
|
||||
const varName = cssVar.match(/var\((--[^)]+)\)/)?.[1];
|
||||
if (!varName) return cssVar;
|
||||
|
||||
// Get computed color from CSS variable
|
||||
const computedColor = getComputedStyle(document.documentElement)
|
||||
.getPropertyValue(varName)
|
||||
.trim();
|
||||
|
||||
return computedColor || cssVar;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert entire color palette to computed colors for export
|
||||
*/
|
||||
export function getComputedColors(palette: Partial<ChartColorPalette>): Partial<ChartColorPalette> {
|
||||
const computed: Partial<ChartColorPalette> = {};
|
||||
|
||||
for (const [key, value] of Object.entries(palette)) {
|
||||
if (value) {
|
||||
computed[key as keyof ChartColorPalette] = cssVarToColor(value);
|
||||
}
|
||||
}
|
||||
|
||||
return computed;
|
||||
}
|
||||
157
components/charts/utils/chart-data-transformers.ts
Normal file
157
components/charts/utils/chart-data-transformers.ts
Normal file
@@ -0,0 +1,157 @@
|
||||
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()
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user