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 */}

流年大运轨迹图

吉运 (涨)
凶运 (跌)
MA5
MA10
{ 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;