113 lines
3.3 KiB
TypeScript
113 lines
3.3 KiB
TypeScript
import {
|
|
ComposedChart,
|
|
Line,
|
|
XAxis,
|
|
YAxis,
|
|
CartesianGrid,
|
|
Tooltip,
|
|
ResponsiveContainer,
|
|
Legend
|
|
} from 'recharts';
|
|
import type { DataSeries } from '@/lib/types';
|
|
import { getChartColors } from '../utils/chart-colors';
|
|
import { ChartTooltip } from '../primitives/chart-tooltip';
|
|
import { mergeDataSeries } from '../utils/chart-data-transformers';
|
|
|
|
type CombinationChartViewProps = {
|
|
dataSeries: DataSeries[];
|
|
formatters?: {
|
|
price?: (value: number) => string;
|
|
date?: (value: string) => string;
|
|
volume?: (value: number) => string;
|
|
};
|
|
};
|
|
|
|
export function CombinationChartView({
|
|
dataSeries,
|
|
formatters
|
|
}: CombinationChartViewProps) {
|
|
const colors = getChartColors();
|
|
|
|
if (!dataSeries || dataSeries.length === 0) {
|
|
return (
|
|
<div className="flex h-full items-center justify-center text-sm text-[color:var(--terminal-muted)]">
|
|
No data series provided
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const mergedData = mergeDataSeries(dataSeries);
|
|
const visibleSeries = dataSeries.filter(series => series.visible !== false);
|
|
const baseValues = Object.fromEntries(visibleSeries.map((series) => {
|
|
const initialPoint = mergedData.find((entry) => typeof entry[series.id] === 'number');
|
|
const baseValue = typeof initialPoint?.[series.id] === 'number' ? Number(initialPoint[series.id]) : null;
|
|
return [series.id, baseValue];
|
|
}));
|
|
const normalizedData = mergedData.map((point) => {
|
|
const normalizedPoint: Record<string, string | number | null> = { date: point.date };
|
|
|
|
visibleSeries.forEach((series) => {
|
|
const baseValue = baseValues[series.id];
|
|
const currentValue = typeof point[series.id] === 'number' ? Number(point[series.id]) : null;
|
|
|
|
normalizedPoint[series.id] = baseValue && currentValue
|
|
? ((currentValue / baseValue) - 1) * 100
|
|
: null;
|
|
});
|
|
|
|
return normalizedPoint;
|
|
});
|
|
|
|
return (
|
|
<ResponsiveContainer width="100%" height="100%">
|
|
<ComposedChart data={normalizedData}>
|
|
<CartesianGrid strokeDasharray="2 2" stroke={colors.grid} />
|
|
<XAxis
|
|
dataKey="date"
|
|
stroke={colors.muted}
|
|
fontSize={11}
|
|
tickFormatter={formatters?.date}
|
|
minTickGap={32}
|
|
/>
|
|
<YAxis
|
|
stroke={colors.muted}
|
|
fontSize={11}
|
|
tickFormatter={(value) => `${Number(value).toFixed(0)}%`}
|
|
width={60}
|
|
domain={['auto', 'auto']}
|
|
/>
|
|
<Tooltip
|
|
content={(tooltipProps) => (
|
|
<ChartTooltip
|
|
{...tooltipProps}
|
|
formatters={{
|
|
...formatters,
|
|
price: (value: number) => `${value.toFixed(2)}%`
|
|
}}
|
|
/>
|
|
)}
|
|
cursor={{ stroke: colors.muted, strokeDasharray: '3 3' }}
|
|
/>
|
|
<Legend />
|
|
|
|
{visibleSeries.map(series => {
|
|
const seriesColor = series.color || colors.primary;
|
|
return (
|
|
<Line
|
|
key={series.id}
|
|
type="linear"
|
|
dataKey={series.id}
|
|
name={series.label}
|
|
stroke={seriesColor}
|
|
strokeWidth={2}
|
|
dot={false}
|
|
connectNulls={false}
|
|
isAnimationActive={normalizedData.length <= 500}
|
|
/>
|
|
);
|
|
})}
|
|
</ComposedChart>
|
|
</ResponsiveContainer>
|
|
);
|
|
}
|