import React, { useMemo, useState } from 'react';
import {
ComposedChart,
Bar,
Line,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
ResponsiveContainer,
ReferenceLine,
ReferenceArea,
Label,
LabelList,
Brush
} from 'recharts';
import { KLinePoint } from '../types';
interface LifeKLineChartProps {
data: KLinePoint[];
currentAge?: number;
onYearClick?: (year: number) => void;
}
// Moving Average calculation utility
function calculateMA(data: KLinePoint[], period: number): (number | null)[] {
return data.map((_, index) => {
if (index < period - 1) return null;
const slice = data.slice(index - period + 1, index + 1);
const sum = slice.reduce((acc, d) => acc + d.score, 0);
return Math.round((sum / period) * 10) / 10;
});
}
// Da Yun zone interface
interface DaYunZone {
daYun: string | undefined;
startAge: number;
endAge: number;
index: number;
}
const CustomTooltip = ({ active, payload }: any) => {
if (active && payload && payload.length) {
const data = payload[0].payload as KLinePoint;
const isUp = data.close >= data.open;
return (
{/* Header */}
{data.year} {data.ganZhi}年 ({data.age}岁)
大运:{data.daYun || '未知'}
{isUp ? '吉 ▲' : '凶 ▼'}
{/* Data Grid */}
开盘
{data.open}
收盘
{data.close}
最高
{data.high}
最低
{data.low}
{/* MA Info */}
{(data as any).ma5 && (
MA5: {(data as any).ma5}
)}
{(data as any).ma10 && (
MA10: {(data as any).ma10}
)}
{/* Detailed Reason */}
{data.reason}
);
}
return null;
};
// Gradient CandleShape with glow effects for extreme scores
const GradientCandleShape = (props: any) => {
const { x, y, width, height, payload, yAxis } = props;
const isUp = payload.close >= payload.open;
const isExtreme = payload.score > 90 || payload.score < 10;
// Use gradient fill
const fillId = isUp ? 'url(#gradientUp)' : 'url(#gradientDown)';
const strokeColor = isUp ? '#047857' : '#BE123C'; // emerald-700 / rose-700
let highY = y;
let lowY = y + height;
if (yAxis && typeof yAxis.scale === 'function') {
try {
highY = yAxis.scale(payload.high);
lowY = yAxis.scale(payload.low);
} catch (e) {
highY = y;
lowY = y + height;
}
}
const center = x + width / 2;
// Enforce minimum body height so flat doji candles are visible
const renderHeight = height < 2 ? 2 : height;
// Glow effect for extreme scores
const glowFilter = isExtreme ? 'url(#glowFilter)' : 'none';
return (
{/* Wick - made slightly thicker for visibility */}
{/* Body with gradient */}
);
};
// Custom Label Component for the Peak Star
const PeakLabel = (props: any) => {
const { x, y, width, value, maxHigh } = props;
// Only render if this value equals the global max high
if (value !== maxHigh) return null;
return (
{/* Golden Star Icon */}
{/* Score Text */}
{value}
);
};
// Trough Label Component
const TroughLabel = (props: any) => {
const { x, y, width, height, value, minLow } = props;
if (value !== minLow) return null;
return (
{/* Down Arrow */}
{value}
);
};
// Crosshair Cursor Component
const CrosshairCursor = (props: any) => {
const { points, width, height, top, left } = props;
if (!points || !points[0]) return null;
const { x, y } = points[0];
return (
{/* Vertical line */}
{/* Horizontal line */}
);
};
const LifeKLineChart: React.FC = ({ data, currentAge, onYearClick }) => {
const [brushDomain, setBrushDomain] = useState<[number, number] | null>(null);
// Calculate Da Yun background zones
const daYunZones = useMemo(() => {
if (!data || data.length === 0) return [];
const zones: DaYunZone[] = [];
let currentDaYun = data[0]?.daYun;
let startAge = data[0]?.age;
for (let i = 1; i <= data.length; i++) {
if (i === data.length || data[i]?.daYun !== currentDaYun) {
zones.push({
daYun: currentDaYun,
startAge,
endAge: data[i-1]?.age,
index: zones.length
});
if (i < data.length) {
currentDaYun = data[i]?.daYun;
startAge = data[i]?.age;
}
}
}
return zones;
}, [data]);
// Calculate MAs and transform data
const transformedData = useMemo(() => {
const ma5 = calculateMA(data, 5);
const ma10 = calculateMA(data, 10);
return data.map((d, i) => ({
...d,
bodyRange: [Math.min(d.open, d.close), Math.max(d.open, d.close)],
labelPoint: d.high,
ma5: ma5[i],
ma10: ma10[i],
}));
}, [data]);
// Identify Da Yun change points for reference lines
const daYunChanges = useMemo(() => {
return data.filter((d, i) => {
if (i === 0) return true;
return d.daYun !== data[i-1].daYun;
});
}, [data]);
// Calculate Global Max High and Min Low
const maxHigh = useMemo(() =>
data.length > 0 ? Math.max(...data.map(d => d.high)) : 100,
[data]
);
const minLow = useMemo(() =>
data.length > 0 ? Math.min(...data.map(d => d.low)) : 0,
[data]
);
// Calculate default brush range based on current age
const defaultBrushIndex = useMemo(() => {
if (!currentAge || !data.length) return { start: 0, end: Math.min(30, data.length - 1) };
const currentIndex = data.findIndex(d => d.age === currentAge);
if (currentIndex === -1) return { start: 0, end: Math.min(30, data.length - 1) };
const start = Math.max(0, currentIndex - 15);
const end = Math.min(data.length - 1, currentIndex + 15);
return { start, end };
}, [currentAge, data]);
if (!data || data.length === 0) {
return 无数据
;
}
return (
{/* Header with Legend */}
{
if (e?.activePayload?.[0]?.payload && onYearClick) {
onYearClick(e.activePayload[0].payload.year);
}
}}
>
{/* SVG Definitions for Gradients and Filters */}
{/* Gradient for bullish (up) candles - emerald */}
{/* Gradient for bearish (down) candles - rose */}
{/* Glow filter for extreme scores */}
{/* Da Yun Background Zones - render first so they're behind everything */}
{daYunZones.map((zone, idx) => (
))}
}
cursor={}
/>
{/* Da Yun Reference Lines with Labels */}
{daYunChanges.map((point, index) => (
))}
{/* Current Age Marker */}
{currentAge && (
)}
{/* MA10 Line (behind MA5) */}
{/* MA5 Line */}
{/* K-Line Candles with Gradient */}
}
isAnimationActive={true}
animationDuration={1500}
>
{/* Peak Label */}
}
/>
{/* Brush for zoom/pan */}
{/* Instruction hint */}
拖动底部滑块可缩放查看 • 点击K线可查看月度详情
);
};
export default LifeKLineChart;