"use client"; import { useState } from "react"; import { LineChart, Line, BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Rectangle, } from "recharts"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import type { PriceData, PriceStats } from "@/lib/types"; interface PriceChartProps { priceData: PriceData[]; priceStats: PriceStats; ticker: string; } export function PriceChart({ priceData, priceStats, ticker }: PriceChartProps) { const [chartType, setChartType] = useState<"line" | "candlestick">("line"); // 格式化數字 const formatNumber = (num: number) => { return num.toLocaleString('zh-TW', { minimumFractionDigits: 2, maximumFractionDigits: 2 }); }; // 格式化日期(只顯示月-日) const formatDate = (dateStr: string) => { const date = new Date(dateStr); return `${date.getMonth() + 1}/${date.getDate()}`; }; // 計算價格範圍用於標準化 const priceValues = priceData.flatMap(d => [d.High, d.Low]); const minPrice = Math.min(...priceValues); const maxPrice = Math.max(...priceValues); const priceRange = maxPrice - minPrice; return (
{ticker} 價格走勢 setChartType(v as "line" | "candlestick")}> 折線圖 K線圖
{/* 統計資訊 */}

增長率

= 0 ? 'text-green-600' : 'text-red-600'}`}> {priceStats.growth_rate >= 0 ? '+' : ''}{priceStats.growth_rate}%

時長

{priceStats.duration_days} 天

起始價格

${formatNumber(priceStats.start_price)}

{priceStats.start_date}

結束價格

${formatNumber(priceStats.end_price)}

{priceStats.end_date}

{/* 價格圖表 */}

價格走勢

{chartType === "line" ? ( `$${value.toFixed(0)}`} /> [`$${formatNumber(value)}`, '收盤價']} labelFormatter={(label) => `日期: ${label}`} /> ) : ( // K線圖:真正的蠟燭圖實現 `$${value.toFixed(0)}`} /> { if (active && payload && payload.length) { const data = payload[0].payload; const isUp = data.Close >= data.Open; return (

日期: {data.Date}

開: ${formatNumber(data.Open)}

收: ${formatNumber(data.Close)}

高: ${formatNumber(data.High)}

低: ${formatNumber(data.Low)}

{isUp ? '↑ 上漲' : '↓ 下跌'} ${formatNumber(Math.abs(data.Close - data.Open))}

); } return null; }} /> {/* 使用自定義 shape 來繪製蠟燭 */} } />
)}
{/* 交易量圖表 */}

交易量

{ if (value >= 1000000) return `${(value / 1000000).toFixed(1)}M`; if (value >= 1000) return `${(value / 1000).toFixed(1)}K`; return value.toString(); }} /> [value.toLocaleString(), '交易量']} labelFormatter={(label) => `日期: ${label}`} />
); } // 自定義蠟燭圖形狀組件 interface CandlestickShapeProps { x?: number; y?: number; width?: number; height?: number; payload?: PriceData; minPrice: number; maxPrice: number; } const CandlestickShape: React.FC = (props) => { const { x = 0, y = 0, width = 0, height = 0, payload, minPrice, maxPrice } = props; if (!payload) return null; const { Open, Close, High, Low } = payload; const isUp = Close >= Open; // 粉綠色(上漲)和粉紅色(下跌) const fillColor = isUp ? '#86efac' : '#fca5a5'; // soft pastel green / soft pastel pink const strokeColor = isUp ? '#22c55e' : '#ef4444'; // darker green / darker red // 計算實際的 Y 坐標位置 const priceRange = maxPrice - minPrice; // The height prop passed to shape is the full height of the chart area for the Y-axis. // We need to calculate the actual chart drawing height based on the Y-axis domain. // The Y-axis in recharts is typically inverted, so higher values are lower Y coordinates. // The 'y' prop passed to shape is the y-coordinate of the data point (High in this case). // We need to adjust calculations based on the actual Y-axis scale. // Recharts Y-axis is inverted: higher price -> lower Y coordinate. // The 'y' prop for the Bar is typically the y-coordinate of the dataKey (High). // The 'height' prop for the Bar is the height of the bar if it were a standard bar chart. // For a candlestick, we need to map prices to the chart's pixel height. // Let's assume 'y' is the top of the plotting area and 'y + height' is the bottom. // The total pixel height available for the price range is 'height'. // We need to map minPrice to y + height and maxPrice to y. // Calculate the pixel value per price unit const pixelsPerPriceUnit = height / priceRange; // Calculate Y coordinates for Open, Close, High, Low // Note: Y-axis is inverted, so (maxPrice - price) gives distance from top. const highY = y + (maxPrice - High) * pixelsPerPriceUnit; const lowY = y + (maxPrice - Low) * pixelsPerPriceUnit; const openY = y + (maxPrice - Open) * pixelsPerPriceUnit; const closeY = y + (maxPrice - Close) * pixelsPerPriceUnit; // 蠟燭主體 const bodyTop = Math.min(openY, closeY); const bodyBottom = Math.max(openY, closeY); const bodyHeight = Math.max(bodyBottom - bodyTop, 1); // 至少 1px // 蠟燭寬度和影線位置 const candleWidth = width * 0.6; // 蠟燭佔 60% 寬度 const candleX = x + (width - candleWidth) / 2; const wickX = x + width / 2; // 影線在中間 return ( {/* 上影線(從最高價到蠟燭頂部) */} {/* 下影線(從蠟燭底部到最低價) */} {/* 蠟燭主體 */} ); };