Skip to content

Commit af14150

Browse files
committed
Improved ELO graph
1 parent ea6a59d commit af14150

File tree

6 files changed

+326
-121
lines changed

6 files changed

+326
-121
lines changed

app/components/elo-history-charts.tsx

Lines changed: 175 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -14,34 +14,139 @@ import {
1414
type TeamPlayerELOLog,
1515
} from '@prisma/client';
1616
import { BASE_ELO } from '~/utils/constants';
17+
import { useWindowSize } from '~/utils/hooks/use-window-size';
18+
import { useState } from 'react';
19+
import { ToggleSwitch } from '~/components/toggle-switch';
20+
21+
const formatSimpleDate = (date: string) => {
22+
return new Date(date).toLocaleString('no-NO', {
23+
month: '2-digit',
24+
day: '2-digit',
25+
});
26+
};
27+
28+
const formatDateWithTime = (date: string) => {
29+
return new Date(date).toLocaleString('no-NO', {
30+
year: 'numeric',
31+
month: '2-digit',
32+
day: '2-digit',
33+
hour: '2-digit',
34+
minute: '2-digit',
35+
});
36+
};
37+
38+
const formatDateOnly = (date: string) => {
39+
return new Date(date).toLocaleString('no-NO', {
40+
year: 'numeric',
41+
month: '2-digit',
42+
day: '2-digit',
43+
});
44+
};
45+
46+
const aggregateByDay = (data: any[]) => {
47+
if (data.length === 0) return [];
48+
49+
// First, group by day as before
50+
const groupedByDay = data.reduce((acc, item) => {
51+
const date = new Date(item.date);
52+
const dayKey = new Date(date.getFullYear(), date.getMonth(), date.getDate())
53+
.toISOString()
54+
.split('T')[0];
55+
56+
if (!acc[dayKey]) {
57+
acc[dayKey] = { date: dayKey, elos: [] };
58+
}
59+
acc[dayKey].elos.push(item.elo);
60+
return acc;
61+
}, {});
62+
63+
// Find start date and use today as end date
64+
const dates = Object.keys(groupedByDay).sort();
65+
const startDate = new Date(dates[0]);
66+
const endDate = new Date(); // Use today's date
67+
endDate.setHours(23, 59, 59, 999); // Set to end of today
68+
69+
// Fill in all days from start until today
70+
const filledData = [];
71+
let currentDate = new Date(startDate);
72+
let lastKnownElo = BASE_ELO;
73+
74+
while (currentDate <= endDate) {
75+
const dateKey = currentDate.toISOString().split('T')[0];
76+
const dayData = groupedByDay[dateKey];
77+
78+
if (dayData) {
79+
// Calculate average ELO for days with matches
80+
const avgElo = Math.round(
81+
dayData.elos.reduce((sum: number, elo: number) => sum + elo, 0) / dayData.elos.length
82+
);
83+
lastKnownElo = avgElo;
84+
filledData.push({
85+
date: dateKey,
86+
elo: avgElo,
87+
hasMatches: true,
88+
});
89+
} else {
90+
// Use last known ELO for days without matches
91+
filledData.push({
92+
date: dateKey,
93+
elo: lastKnownElo,
94+
hasMatches: false,
95+
});
96+
}
97+
98+
// Move to next day
99+
currentDate.setDate(currentDate.getDate() + 1);
100+
}
101+
102+
return filledData;
103+
};
17104

18105
interface Props {
19106
data: ELOLog[] | TeamELOLog[] | TeamPlayerELOLog[];
20107
}
21108

109+
const CustomTooltip = ({ active, payload, label, showDailyView }: any) => {
110+
if (active && payload && payload.length) {
111+
const date = payload[0].payload.date;
112+
const formattedDate = showDailyView
113+
? formatDateOnly(date)
114+
: formatDateWithTime(date);
115+
116+
return (
117+
<div className="bg-gray-800/95 border border-gray-700 rounded-lg p-2 shadow-lg text-sm sm:p-3 sm:text-base">
118+
<p className="text-xs text-gray-200 sm:text-sm">{formattedDate}</p>
119+
<p className="text-base font-semibold text-white sm:text-lg">
120+
ELO: {payload[0].value}
121+
</p>
122+
</div>
123+
);
124+
}
125+
return null;
126+
};
127+
22128
export const EloHistoryChart = ({ data }: Props) => {
129+
const [showDailyView, setShowDailyView] = useState(false);
130+
const { width } = useWindowSize();
131+
const isMobile = width ? width < 640 : false;
132+
133+
const processedData = showDailyView ? aggregateByDay(data) : data;
134+
23135
const massagedData =
24-
data.length > 0
136+
processedData.length > 0
25137
? [
26138
{
27139
date: new Date(
28-
new Date(data[0].date).getTime() - 5 * 60000
140+
new Date(processedData[0].date).getTime() - 5 * 60000
29141
).toISOString(),
30142
elo: BASE_ELO,
31143
},
32-
...data.map((dataPoint) => ({
33-
date: dataPoint.date,
34-
elo: dataPoint.elo,
35-
})),
144+
...processedData,
36145
].map((item) => ({
37-
date: new Date(item.date).toLocaleString('no-NO', {
38-
year: 'numeric',
39-
month: '2-digit',
40-
day: '2-digit',
41-
hour: '2-digit',
42-
minute: '2-digit',
43-
}),
146+
date: item.date,
147+
displayDate: formatSimpleDate(item.date.toString()),
44148
elo: item.elo,
149+
hasMatches: 'hasMatches' in item ? item.hasMatches : false,
45150
}))
46151
: [];
47152

@@ -50,21 +155,64 @@ export const EloHistoryChart = ({ data }: Props) => {
50155

51156
const yAxisDomain = [minElo - 50, maxElo + 50];
52157

158+
const axisColor = '#94a3b8'; // Tailwind gray-400 - darker than before
159+
53160
return (
54-
<ResponsiveContainer width="100%" height={300}>
55-
<LineChart data={massagedData}>
56-
<CartesianGrid strokeDasharray="3 3" />
57-
<XAxis dataKey="date" />
58-
<YAxis domain={yAxisDomain} />
59-
<Tooltip />
60-
<Legend />
61-
<Line
62-
type="monotone"
63-
dataKey="elo"
64-
stroke="#8884d8"
65-
activeDot={{ r: 8 }}
161+
<div className="space-y-4">
162+
<div className="flex justify-end">
163+
<ToggleSwitch
164+
checked={showDailyView}
165+
onChange={setShowDailyView}
166+
label="Vis ELO-endring per dag"
66167
/>
67-
</LineChart>
68-
</ResponsiveContainer>
168+
</div>
169+
<ResponsiveContainer width="100%" height={isMobile ? 200 : 300}>
170+
<LineChart data={massagedData}>
171+
<defs>
172+
<linearGradient id="eloColor" x1="0" y1="0" x2="0" y2="1">
173+
<stop offset="5%" stopColor="#3b82f6" stopOpacity={0.8}/>
174+
<stop offset="95%" stopColor="#3b82f6" stopOpacity={0}/>
175+
</linearGradient>
176+
</defs>
177+
<CartesianGrid
178+
strokeDasharray="3 3"
179+
stroke="rgba(255, 255, 255, 0.1)"
180+
vertical={false}
181+
/>
182+
<XAxis
183+
dataKey="displayDate"
184+
tick={{ fill: axisColor, fontSize: isMobile ? 10 : 12 }}
185+
tickLine={{ stroke: axisColor }}
186+
stroke={axisColor}
187+
angle={isMobile ? -45 : 0}
188+
textAnchor={isMobile ? 'end' : 'middle'}
189+
height={isMobile ? 60 : 30}
190+
/>
191+
<YAxis
192+
domain={yAxisDomain}
193+
tick={{ fill: axisColor, fontSize: isMobile ? 10 : 12 }}
194+
tickLine={{ stroke: axisColor }}
195+
stroke={axisColor}
196+
width={isMobile ? 30 : 40}
197+
/>
198+
<Tooltip
199+
content={(props) => <CustomTooltip {...props} showDailyView={showDailyView} />}
200+
wrapperStyle={{ zIndex: 1000 }}
201+
/>
202+
<Legend
203+
wrapperStyle={{ color: axisColor }}
204+
/>
205+
<Line
206+
type="monotone"
207+
dataKey="elo"
208+
stroke="#3b82f6"
209+
strokeWidth={2}
210+
dot={false}
211+
activeDot={{ r: 6, fill: '#3b82f6', stroke: '#fff' }}
212+
fill="url(#eloColor)"
213+
/>
214+
</LineChart>
215+
</ResponsiveContainer>
216+
</div>
69217
);
70218
};

app/components/toggle-switch.tsx

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
interface ToggleSwitchProps {
2+
checked: boolean;
3+
onChange: (checked: boolean) => void;
4+
label: string;
5+
}
6+
7+
export function ToggleSwitch({ checked, onChange, label }: ToggleSwitchProps) {
8+
return (
9+
<label className="flex items-center cursor-pointer gap-2">
10+
<div className="relative">
11+
<input
12+
type="checkbox"
13+
className="sr-only"
14+
checked={checked}
15+
onChange={(e) => onChange(e.target.checked)}
16+
/>
17+
<div className={`w-10 h-6 rounded-full shadow-inner ${
18+
checked ? 'bg-blue-500' : 'bg-gray-400'
19+
}`} />
20+
<div className={`absolute left-1 top-1 w-4 h-4 bg-white rounded-full transition-transform transform ${
21+
checked ? 'translate-x-4' : 'translate-x-0'
22+
}`} />
23+
</div>
24+
<span className="text-sm text-gray-700 dark:text-gray-300">{label}</span>
25+
</label>
26+
);
27+
}

app/routes/compare-players.$player1Id.$player2Id.tsx

Lines changed: 33 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -112,45 +112,40 @@ export default function Index() {
112112
</div>
113113

114114
{player1 && player2 && (
115-
<>
116-
<div className="container flex flex-col justify-center">
117-
<h2 className="mb-2 text-xl font-bold dark:text-green-200">
118-
Sammenligning {player1.name} vs {player2.name}
119-
</h2>
120-
<table
121-
className="mb-2 table-auto rounded-lg bg-blue-100
122-
p-4 text-lg text-black shadow-lg dark:bg-gray-700 dark:text-white"
123-
>
124-
<thead>
125-
<tr className="text-md">
126-
<th className="w-1/5 py-2"># kamper</th>
127-
<th className="w-1/5 py-2"># seiere</th>
128-
<th className="w-1/5 py-2"># tap</th>
129-
<th className="w-2/5 py-2">% overlegenhet</th>
130-
</tr>
131-
</thead>
132-
<tbody>
133-
<tr className="text-md">
134-
<td className="border py-2">
135-
{player1WinStats?.numberOfMatches}
136-
</td>
137-
<td className="border py-2">
138-
{player1WinStats?.numberOfMatchesWonByPlayer}
139-
</td>
140-
<td className="border py-2">
141-
{player1WinStats?.numberOfMatchesLostByPlayer}
142-
</td>
143-
<td className="border py-2">
144-
{player1WinStats?.winPercentage
145-
? player1WinStats.winPercentage.toFixed(2)
146-
: 0}
147-
%
148-
</td>
149-
</tr>
150-
</tbody>
151-
</table>
115+
<div className="container flex flex-col justify-center">
116+
<h2 className="mb-4 text-2xl font-bold text-gray-900 dark:text-white">
117+
Sammenligning {player1.name} vs {player2.name}
118+
</h2>
119+
<div className="grid grid-cols-4 gap-4 rounded-lg bg-white p-6 pr-8 shadow-lg dark:bg-gray-800">
120+
<div className="text-center">
121+
<div className="text-3xl font-bold text-blue-600 dark:text-blue-400">
122+
{player1WinStats?.numberOfMatches}
123+
</div>
124+
<div className="mt-1 text-sm text-gray-600 dark:text-gray-400">Kamper</div>
125+
</div>
126+
<div className="text-center">
127+
<div className="text-3xl font-bold text-green-600 dark:text-green-400">
128+
{player1WinStats?.numberOfMatchesWonByPlayer}
129+
</div>
130+
<div className="mt-1 text-sm text-gray-600 dark:text-gray-400">Seiere</div>
131+
</div>
132+
<div className="text-center">
133+
<div className="text-3xl font-bold text-red-600 dark:text-red-400">
134+
{player1WinStats?.numberOfMatchesLostByPlayer}
135+
</div>
136+
<div className="mt-1 text-sm text-gray-600 dark:text-gray-400">Tap</div>
137+
</div>
138+
<div className="text-center">
139+
<div className="text-3xl font-bold text-blue-600 dark:text-blue-400">
140+
{player1WinStats?.winPercentage
141+
? player1WinStats.winPercentage.toFixed(2)
142+
: 0}
143+
%
144+
</div>
145+
<div className="mt-1 text-sm text-gray-600 dark:text-gray-400">Overlegenhet</div>
146+
</div>
152147
</div>
153-
</>
148+
</div>
154149
)}
155150
</div>
156151
);

0 commit comments

Comments
 (0)