updating the playtime frontend with claude only code to replace the one graph with a dumb-bell graph, having each individual map show its players at beginning versus end and also the overall impact of maps + their popularity
This commit is contained in:
@@ -11,6 +11,8 @@ interface MapPoint {
|
||||
map_start_dt: string;
|
||||
map_end_dt: string | null;
|
||||
total_players: number;
|
||||
start_players: number;
|
||||
end_players: number;
|
||||
}
|
||||
|
||||
interface PlayerChip {
|
||||
@@ -120,22 +122,22 @@ export default function MapPopulationChart() {
|
||||
const visiblePoints = points.slice(viewStartIdx, viewEndIdx);
|
||||
const plotWidth = Math.max(0, width - PAD_LEFT - PAD_RIGHT);
|
||||
const plotHeight = HEIGHT - PAD_TOP - PAD_BOTTOM;
|
||||
const maxValue = Math.max(1, ...visiblePoints.map((p) => p.total_players));
|
||||
// Scale is driven by the dumbbell endpoints (start/end concurrent
|
||||
// population), not total_players — total_players is cumulative reach
|
||||
// across the whole session and would otherwise dwarf the actual
|
||||
// population swings the chart exists to show.
|
||||
const maxValue = Math.max(1, ...visiblePoints.flatMap((p) => [p.start_players, p.end_players]));
|
||||
const axisMax = Math.ceil((maxValue * 1.15) / 5) * 5 || 5;
|
||||
|
||||
const slotWidth = visiblePoints.length > 0 ? plotWidth / visiblePoints.length : 0;
|
||||
const barWidth = Math.max(2, slotWidth * 0.6);
|
||||
|
||||
function barX(i: number) {
|
||||
return PAD_LEFT + i * slotWidth + (slotWidth - barWidth) / 2;
|
||||
return PAD_LEFT + i * slotWidth + slotWidth / 2;
|
||||
}
|
||||
function barY(value: number) {
|
||||
const h = (value / axisMax) * plotHeight;
|
||||
return PAD_TOP + (plotHeight - h);
|
||||
}
|
||||
function barHeight(value: number) {
|
||||
return Math.max(value > 0 ? 2 : 0, (value / axisMax) * plotHeight);
|
||||
}
|
||||
function indexForX(x: number): number {
|
||||
if (slotWidth === 0) return 0;
|
||||
return Math.max(0, Math.min(visiblePoints.length - 1, Math.floor((x - PAD_LEFT) / slotWidth)));
|
||||
@@ -236,6 +238,11 @@ export default function MapPopulationChart() {
|
||||
{visiblePoints.length} of {points.length}
|
||||
{reachedStart ? '' : '+'} maps
|
||||
</h2>
|
||||
<p className="text-xs text-ink-faint mt-1">
|
||||
Each line shows a map's population at the moment it started (grey dot) versus when it ended
|
||||
(green = grew, red = shrank) — so a decline shows up on the map where it actually happened,
|
||||
not the one that started low right after.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between gap-4 mb-2">
|
||||
@@ -244,7 +251,7 @@ export default function MapPopulationChart() {
|
||||
<path d="M9 9l6 6M9 15l6-6" strokeLinecap="round" />
|
||||
<circle cx="12" cy="12" r="9" />
|
||||
</svg>
|
||||
Scroll to zoom, drag to pan (all the way back to 2015), click a bar for who was online
|
||||
Scroll to zoom, drag to pan (all the way back to 2015), click a line for who was online
|
||||
{fetchingMore && <span className="text-ink-faint">— loading more…</span>}
|
||||
</div>
|
||||
{isZoomed && (
|
||||
@@ -287,13 +294,6 @@ export default function MapPopulationChart() {
|
||||
}
|
||||
}}
|
||||
>
|
||||
<defs>
|
||||
<linearGradient id="mapBarGradient" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="0%" stopColor="#5EE596" />
|
||||
<stop offset="100%" stopColor="#2A9C5C" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
|
||||
{yTicks.map((v) => {
|
||||
const y = barY(v);
|
||||
return (
|
||||
@@ -306,19 +306,31 @@ export default function MapPopulationChart() {
|
||||
);
|
||||
})}
|
||||
|
||||
{visiblePoints.map((p, i) => (
|
||||
<rect
|
||||
key={p.map_id}
|
||||
x={barX(i)}
|
||||
y={barY(p.total_players)}
|
||||
width={barWidth}
|
||||
height={barHeight(p.total_players)}
|
||||
rx={3}
|
||||
fill="url(#mapBarGradient)"
|
||||
fillOpacity={hoverIndex === i ? 1 : 0.8}
|
||||
style={hoverIndex === i ? { filter: 'drop-shadow(0 0 6px rgba(63, 211, 122, 0.55))' } : undefined}
|
||||
/>
|
||||
))}
|
||||
{visiblePoints.map((p, i) => {
|
||||
const x = barX(i);
|
||||
const yStart = barY(p.start_players);
|
||||
const yEnd = barY(p.end_players);
|
||||
const grew = p.end_players > p.start_players;
|
||||
const shrank = p.end_players < p.start_players;
|
||||
const endColor = grew ? '#3FD37A' : shrank ? '#E0665C' : '#8A9691';
|
||||
const active = hoverIndex === i;
|
||||
return (
|
||||
<g key={p.map_id} opacity={active ? 1 : 0.85}>
|
||||
<line
|
||||
x1={x}
|
||||
y1={yStart}
|
||||
x2={x}
|
||||
y2={yEnd}
|
||||
stroke={endColor}
|
||||
strokeWidth={active ? 2.5 : 1.75}
|
||||
strokeLinecap="round"
|
||||
style={active ? { filter: 'drop-shadow(0 0 5px rgba(63, 211, 122, 0.45))' } : undefined}
|
||||
/>
|
||||
<circle cx={x} cy={yStart} r={active ? 4 : 3} fill="#8A9691" />
|
||||
<circle cx={x} cy={yEnd} r={active ? 5 : 3.5} fill={endColor} />
|
||||
</g>
|
||||
);
|
||||
})}
|
||||
|
||||
<line
|
||||
x1={PAD_LEFT}
|
||||
@@ -333,8 +345,12 @@ export default function MapPopulationChart() {
|
||||
<div
|
||||
className="absolute pointer-events-none"
|
||||
style={{
|
||||
left: Math.min(width - 200, Math.max(0, barX(hoverIndex) - 60)),
|
||||
top: barY(visiblePoints[hoverIndex].total_players) - 70,
|
||||
left: Math.min(width - 210, Math.max(0, barX(hoverIndex) - 60)),
|
||||
top:
|
||||
Math.min(
|
||||
barY(visiblePoints[hoverIndex].start_players),
|
||||
barY(visiblePoints[hoverIndex].end_players),
|
||||
) - 92,
|
||||
background: '#121715',
|
||||
border: '1px solid #1F2723',
|
||||
borderRadius: 6,
|
||||
@@ -347,8 +363,21 @@ export default function MapPopulationChart() {
|
||||
<div style={{ color: '#8A9691' }}>
|
||||
{new Date(visiblePoints[hoverIndex].map_start_dt.replace(' ', 'T')).toLocaleString()}
|
||||
</div>
|
||||
<div style={{ color: '#3FD37A' }}>
|
||||
{visiblePoints[hoverIndex].total_players} total players during this session
|
||||
<div style={{ color: '#E4EBE7', marginTop: 2 }}>
|
||||
{visiblePoints[hoverIndex].start_players} → {visiblePoints[hoverIndex].end_players} players
|
||||
{(() => {
|
||||
const delta = visiblePoints[hoverIndex].end_players - visiblePoints[hoverIndex].start_players;
|
||||
const color = delta > 0 ? '#3FD37A' : delta < 0 ? '#E0665C' : '#8A9691';
|
||||
return (
|
||||
<span style={{ color, marginLeft: 6 }}>
|
||||
({delta > 0 ? '+' : ''}
|
||||
{delta})
|
||||
</span>
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
<div style={{ color: '#8A9691', marginTop: 2 }}>
|
||||
{visiblePoints[hoverIndex].total_players} distinct players during this session (reach)
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user