mirror of
https://github.com/ershisan99/www.git
synced 2025-12-17 12:34:17 +00:00
add rolling winrate chart
This commit is contained in:
170
src/app/(home)/players/[id]/_components/winrate-trend-chart.tsx
Normal file
170
src/app/(home)/players/[id]/_components/winrate-trend-chart.tsx
Normal file
@@ -0,0 +1,170 @@
|
||||
'use client'
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from '@/components/ui/card'
|
||||
import {
|
||||
type ChartConfig,
|
||||
ChartContainer,
|
||||
ChartTooltip,
|
||||
ChartTooltipContent,
|
||||
} from '@/components/ui/chart'
|
||||
import { Slider } from '@/components/ui/slider'
|
||||
import type { SelectGames } from '@/server/db/types'
|
||||
import { useState } from 'react'
|
||||
import { CartesianGrid, Line, LineChart, XAxis, YAxis } from 'recharts'
|
||||
|
||||
const chartConfig = {
|
||||
winrate: {
|
||||
label: 'Winrate',
|
||||
color: 'var(--color-emerald-500)',
|
||||
},
|
||||
} satisfies ChartConfig
|
||||
|
||||
export function WinrateTrendChart({ games }: { games: SelectGames[] }) {
|
||||
const [gamesWindow, setGamesWindow] = useState(30)
|
||||
|
||||
// Sort games by date (oldest to newest)
|
||||
const sortedGames = [...games].sort(
|
||||
(a, b) => a.gameTime.getTime() - b.gameTime.getTime()
|
||||
)
|
||||
|
||||
// Calculate rolling winrate
|
||||
const chartData = calculateRollingWinrate(sortedGames, gamesWindow)
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader className='flex flex-row items-center justify-between'>
|
||||
<div>
|
||||
<CardTitle>Winrate Trends</CardTitle>
|
||||
<CardDescription>Rolling winrate over time</CardDescription>
|
||||
</div>
|
||||
<div className='flex w-[200px] flex-col gap-2'>
|
||||
<div className='flex items-center justify-between'>
|
||||
<span className='text-muted-foreground text-sm'>
|
||||
Window size: {gamesWindow} games
|
||||
</span>
|
||||
</div>
|
||||
<Slider
|
||||
value={[gamesWindow]}
|
||||
onValueChange={(value) => setGamesWindow(value[0])}
|
||||
min={5}
|
||||
max={Math.min(100, games.length)}
|
||||
step={1}
|
||||
/>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className={'p-2'}>
|
||||
<ChartContainer config={chartConfig}>
|
||||
<LineChart
|
||||
accessibilityLayer
|
||||
data={chartData}
|
||||
margin={{
|
||||
left: 12,
|
||||
right: 12,
|
||||
}}
|
||||
>
|
||||
<CartesianGrid vertical={false} />
|
||||
<XAxis
|
||||
dataKey='date'
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
tickMargin={8}
|
||||
tickFormatter={(value) =>
|
||||
new Date(value).toLocaleDateString('en-US', {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
})
|
||||
}
|
||||
/>
|
||||
<YAxis
|
||||
dataKey={'winrate'}
|
||||
width={40}
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
tickFormatter={(value) => `${value}%`}
|
||||
domain={[0, 100]}
|
||||
/>
|
||||
<ChartTooltip
|
||||
cursor={false}
|
||||
content={
|
||||
<ChartTooltipContent
|
||||
hideLabel
|
||||
formatter={(value, name, entry) => {
|
||||
const date = new Date(entry.payload.date)
|
||||
const formattedDate = date.toLocaleDateString('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
})
|
||||
return (
|
||||
<div className='flex flex-col'>
|
||||
<span>{value}%</span>
|
||||
<span className='text-muted-foreground'>
|
||||
{formattedDate}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Line
|
||||
dataKey='winrate'
|
||||
type='natural'
|
||||
stroke='var(--color-emerald-500)'
|
||||
strokeWidth={2}
|
||||
dot={false}
|
||||
/>
|
||||
</LineChart>
|
||||
</ChartContainer>
|
||||
</CardContent>
|
||||
<CardFooter className='flex-col items-start gap-2 text-sm'>
|
||||
<div className='text-muted-foreground leading-none'>
|
||||
Showing rolling winrate over the last {gamesWindow} games
|
||||
</div>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
function calculateRollingWinrate(
|
||||
games: SelectGames[],
|
||||
windowSize: number
|
||||
): { date: Date; winrate: number }[] {
|
||||
if (games.length === 0 || windowSize <= 0) return []
|
||||
|
||||
// Use all games if windowSize is greater than the number of games
|
||||
const effectiveWindowSize = Math.min(windowSize, games.length)
|
||||
|
||||
const result: { date: Date; winrate: number }[] = []
|
||||
|
||||
// We need at least windowSize games to start calculating
|
||||
for (let i = effectiveWindowSize - 1; i < games.length; i++) {
|
||||
// Get the window of games
|
||||
const windowGames = games.slice(
|
||||
Math.max(0, i - effectiveWindowSize + 1),
|
||||
i + 1
|
||||
)
|
||||
|
||||
// Count wins in the window
|
||||
const wins = windowGames.filter((game) => game.result === 'win').length
|
||||
|
||||
// Calculate winrate as percentage
|
||||
const winrate = Math.round((wins / windowGames.length) * 100)
|
||||
|
||||
// Add data point
|
||||
if (games[i]) {
|
||||
result.push({
|
||||
date: games[i]!.gameTime,
|
||||
winrate: winrate,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
@@ -17,6 +17,7 @@ import { useState } from 'react'
|
||||
|
||||
import { GamesTable } from '@/app/(home)/players/[id]/_components/games-table'
|
||||
import { MmrTrendChart } from '@/app/(home)/players/[id]/_components/mmr-trend-chart'
|
||||
import { WinrateTrendChart } from '@/app/(home)/players/[id]/_components/winrate-trend-chart'
|
||||
import { OpponentsTable } from '@/app/(home)/players/[id]/_components/opponents-table'
|
||||
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
@@ -382,6 +383,7 @@ export function UserInfo() {
|
||||
<TabsTrigger value='matches'>Match History</TabsTrigger>
|
||||
<TabsTrigger value='opponents'>Opponents</TabsTrigger>
|
||||
<TabsTrigger value='mmr-trends'>MMR Trends</TabsTrigger>
|
||||
<TabsTrigger value='winrate-trends'>Winrate Trends</TabsTrigger>
|
||||
<TabsTrigger value='stats'>Statistics</TabsTrigger>
|
||||
<TabsTrigger value='achievements'>Achievements</TabsTrigger>
|
||||
</TabsList>
|
||||
@@ -443,6 +445,13 @@ export function UserInfo() {
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent value='winrate-trends' className='m-0'>
|
||||
<div className='overflow-hidden rounded-lg border'>
|
||||
<div className='overflow-x-auto'>
|
||||
<WinrateTrendChart games={games} />
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent value='stats' className='m-0'>
|
||||
<div className='grid grid-cols-1 gap-6 md:grid-cols-2'>
|
||||
{(rankedLeaderboard || lastRankedGame) && (
|
||||
|
||||
Reference in New Issue
Block a user