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 { GamesTable } from '@/app/(home)/players/[id]/_components/games-table'
|
||||||
import { MmrTrendChart } from '@/app/(home)/players/[id]/_components/mmr-trend-chart'
|
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 { OpponentsTable } from '@/app/(home)/players/[id]/_components/opponents-table'
|
||||||
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
|
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
|
||||||
import { Badge } from '@/components/ui/badge'
|
import { Badge } from '@/components/ui/badge'
|
||||||
@@ -382,6 +383,7 @@ export function UserInfo() {
|
|||||||
<TabsTrigger value='matches'>Match History</TabsTrigger>
|
<TabsTrigger value='matches'>Match History</TabsTrigger>
|
||||||
<TabsTrigger value='opponents'>Opponents</TabsTrigger>
|
<TabsTrigger value='opponents'>Opponents</TabsTrigger>
|
||||||
<TabsTrigger value='mmr-trends'>MMR Trends</TabsTrigger>
|
<TabsTrigger value='mmr-trends'>MMR Trends</TabsTrigger>
|
||||||
|
<TabsTrigger value='winrate-trends'>Winrate Trends</TabsTrigger>
|
||||||
<TabsTrigger value='stats'>Statistics</TabsTrigger>
|
<TabsTrigger value='stats'>Statistics</TabsTrigger>
|
||||||
<TabsTrigger value='achievements'>Achievements</TabsTrigger>
|
<TabsTrigger value='achievements'>Achievements</TabsTrigger>
|
||||||
</TabsList>
|
</TabsList>
|
||||||
@@ -443,6 +445,13 @@ export function UserInfo() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
</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'>
|
<TabsContent value='stats' className='m-0'>
|
||||||
<div className='grid grid-cols-1 gap-6 md:grid-cols-2'>
|
<div className='grid grid-cols-1 gap-6 md:grid-cols-2'>
|
||||||
{(rankedLeaderboard || lastRankedGame) && (
|
{(rankedLeaderboard || lastRankedGame) && (
|
||||||
|
|||||||
Reference in New Issue
Block a user