feat: Add OddsLineMovementChart and KellyBetSizing components
This commit is contained in:
@@ -1,77 +1,83 @@
|
||||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
import {
|
||||
CartesianGrid,
|
||||
Line,
|
||||
LineChart,
|
||||
ResponsiveContainer,
|
||||
Tooltip,
|
||||
AreaChart,
|
||||
Area,
|
||||
XAxis,
|
||||
YAxis,
|
||||
CartesianGrid,
|
||||
Tooltip,
|
||||
ResponsiveContainer
|
||||
} from 'recharts';
|
||||
|
||||
type ChartPoint = {
|
||||
time: string;
|
||||
odds: number;
|
||||
bookmaker: string;
|
||||
};
|
||||
|
||||
type GroupedData = {
|
||||
time: string;
|
||||
[bookmaker: string]: string | number;
|
||||
};
|
||||
|
||||
function normalizeForChart(points: ChartPoint[]): GroupedData[] {
|
||||
const map = new Map<string, GroupedData>();
|
||||
|
||||
for (const row of points) {
|
||||
const current = map.get(row.time) || { time: row.time };
|
||||
current[row.bookmaker] = row.odds;
|
||||
map.set(row.time, current);
|
||||
}
|
||||
|
||||
return Array.from(map.values()).sort((a, b) =>
|
||||
a.time.localeCompare(b.time),
|
||||
);
|
||||
// 定義傳入組件的歷史賠率資料介面
|
||||
export interface OddsDataPoint {
|
||||
time: string; // 格式化後的時間 (如: 14:30)
|
||||
odds: number; // 小數賠率
|
||||
}
|
||||
|
||||
type Props = {
|
||||
data: ChartPoint[];
|
||||
};
|
||||
|
||||
export function OddsLineMovementChart({ data }: Props) {
|
||||
const bookmakers = Array.from(new Set(data.map((row) => row.bookmaker)));
|
||||
const lines = normalizeForChart(data);
|
||||
|
||||
const palette = ['#b83822', '#7a4a2c', '#dcb53b', '#5f4031', '#8c5b38'];
|
||||
interface OddsLineMovementChartProps {
|
||||
data: OddsDataPoint[];
|
||||
teamName: string;
|
||||
}
|
||||
|
||||
export default function OddsLineMovementChart({ data, teamName }: OddsLineMovementChartProps) {
|
||||
return (
|
||||
<div className="panel-glow rounded-2xl p-4">
|
||||
<h3 className="dot-matrix text-xl text-[#7d2a15]">賠率走勢:小數賠率變化</h3>
|
||||
<div className="mt-3 h-72 w-full">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<LineChart data={lines} margin={{ top: 12, right: 24, bottom: 12, left: 6 }}>
|
||||
<CartesianGrid strokeDasharray="4 4" stroke="#eadcb9" />
|
||||
<XAxis dataKey="time" tick={{ fill: '#6d4d39' }} />
|
||||
<YAxis tick={{ fill: '#6d4d39' }} />
|
||||
<Tooltip
|
||||
contentStyle={{ background: '#fff8e6', borderColor: '#d8b58c' }}
|
||||
itemStyle={{ color: '#5f4031' }}
|
||||
/>
|
||||
{bookmakers.map((bookmaker, idx) => (
|
||||
<Line
|
||||
key={bookmaker}
|
||||
type="monotone"
|
||||
dataKey={bookmaker}
|
||||
stroke={palette[idx % palette.length]}
|
||||
strokeWidth={2.3}
|
||||
dot={false}
|
||||
/>
|
||||
))}
|
||||
</LineChart>
|
||||
</ResponsiveContainer>
|
||||
<div className="w-full h-72 bg-stone-50 p-4 rounded-lg border border-stone-200 shadow-sm">
|
||||
<div className="mb-4">
|
||||
<h3 className="text-sm text-stone-500 uppercase tracking-wider font-semibold">
|
||||
Line Movement
|
||||
</h3>
|
||||
<h2 className="text-xl text-stone-900 font-dotmatrix">
|
||||
{teamName} 賠率走勢
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<ResponsiveContainer width="100%" height="80%">
|
||||
<AreaChart data={data} margin={{ top: 10, right: 10, left: -20, bottom: 0 }}>
|
||||
<defs>
|
||||
{/* 定義暖橘色漸層填充,增強視覺專業感 */}
|
||||
<linearGradient id="colorOdds" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="5%" stopColor="#ea580c" stopOpacity={0.3}/>
|
||||
<stop offset="95%" stopColor="#ea580c" stopOpacity={0}/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<CartesianGrid strokeDasharray="3 3" vertical={false} stroke="#e5e5e5" />
|
||||
<XAxis
|
||||
dataKey="time"
|
||||
axisLine={false}
|
||||
tickLine={false}
|
||||
tick={{ fontSize: 12, fill: '#78716c' }}
|
||||
dy={10}
|
||||
/>
|
||||
<YAxis
|
||||
domain={['dataMin - 0.1', 'dataMax + 0.1']}
|
||||
axisLine={false}
|
||||
tickLine={false}
|
||||
tick={{ fontSize: 12, fill: '#78716c' }}
|
||||
tickFormatter={(val) => val.toFixed(2)}
|
||||
/>
|
||||
<Tooltip
|
||||
contentStyle={{
|
||||
backgroundColor: '#1A1A1A',
|
||||
borderColor: '#ea580c',
|
||||
color: '#FAF6F0',
|
||||
fontFamily: '"DotGothic16", monospace'
|
||||
}}
|
||||
itemStyle={{ color: '#ea580c' }}
|
||||
/>
|
||||
<Area
|
||||
type="monotone"
|
||||
dataKey="odds"
|
||||
stroke="#ea580c"
|
||||
strokeWidth={3}
|
||||
fillOpacity={1}
|
||||
fill="url(#colorOdds)"
|
||||
isAnimationActive={true}
|
||||
/>
|
||||
</AreaChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user