fediversion/frontend/components/songs/song-evolution-chart.tsx
fullsizemalt de2dd0a69d
Some checks failed
Deploy Fediversion / deploy (push) Failing after 1s
fix: ShowsPage pagination, strict mode, and component standardization
2025-12-31 02:07:44 -08:00

132 lines
5.1 KiB
TypeScript

"use client"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import {
ResponsiveContainer,
ScatterChart,
Scatter,
XAxis,
YAxis,
Tooltip,
CartesianGrid
} from "recharts"
import { format } from "date-fns"
import { Badge } from "@/components/ui/badge"
import { TrendingUp, Info } from "lucide-react"
interface Performance {
id: number
show_date: string
avg_rating: number
venue_name: string
venue_city: string
venue_state: string | null
show_id: number
}
interface SongEvolutionChartProps {
performances: Performance[]
title?: string
}
const CustomTooltip = ({ active, payload }: { active?: boolean; payload?: any[] }) => {
if (active && payload && payload.length) {
const data = payload[0].payload
return (
<Card className="p-3 shadow-xl text-sm min-w-[200px]">
<div className="font-semibold mb-1">
{format(new Date(data.date), "MMM d, yyyy")}
</div>
<div className="text-muted-foreground text-xs mb-2">
{data.venue}
</div>
<div className="flex items-center justify-between gap-4">
<span className="text-muted-foreground">Rating:</span>
<Badge variant="secondary" className="gap-1 bg-yellow-500/10 text-yellow-600 border-yellow-500/20">
Rating: {data.rating.toFixed(1)}
</Badge>
</div>
</Card>
)
}
return null
}
export function SongEvolutionChart({ performances, title = "Rating Evolution" }: SongEvolutionChartProps) {
// Filter out unrated performances to keep chart clean?
// Or keep them as 0? Usually 0 skews the chart. Let's filter > 0 for "Evolution of Quality".
const ratedPerfs = performances
.filter(p => p.avg_rating > 0)
.map(p => ({
id: p.id,
date: new Date(p.show_date).getTime(),
rating: p.avg_rating,
venue: `${p.venue_name}, ${p.venue_city}`,
fullDate: p.show_date
}))
.sort((a, b) => a.date - b.date)
if (ratedPerfs.length < 2) {
return null
}
// Calculate trend? Simple linear/avg for now.
const average = ratedPerfs.reduce((acc, curr) => acc + curr.rating, 0) / ratedPerfs.length
return (
<Card className="col-span-1 shadow-sm">
<CardHeader className="pb-2">
<div className="flex items-center justify-between">
<CardTitle className="text-base font-medium flex items-center gap-2">
<TrendingUp className="h-4 w-4 text-primary" />
{title}
</CardTitle>
{ratedPerfs.length > 5 && (
<div className="flex items-center gap-1 text-xs text-muted-foreground bg-muted/50 px-2 py-1 rounded">
<Info className="h-3 w-3" />
Avg: {average.toFixed(1)}
</div>
)}
</div>
</CardHeader>
<CardContent>
<div className="h-[250px] w-full mt-2">
<ResponsiveContainer width="100%" height="100%">
<ScatterChart margin={{ top: 10, right: 10, bottom: 20, left: -20 }}>
<CartesianGrid strokeDasharray="3 3" vertical={false} stroke="hsl(var(--border))" opacity={0.5} />
<XAxis
type="number"
dataKey="date"
domain={['dataMin', 'dataMax']}
tickFormatter={(unixTime) => format(new Date(unixTime), "yyyy")}
stroke="hsl(var(--muted-foreground))"
fontSize={12}
tickLine={false}
axisLine={false}
dy={10}
/>
<YAxis
type="number"
dataKey="rating"
domain={[0, 5]}
stroke="hsl(var(--muted-foreground))"
fontSize={12}
tickLine={false}
axisLine={false}
dx={-5}
/>
<Tooltip content={<CustomTooltip />} cursor={{ strokeDasharray: '3 3' }} />
<Scatter
name="Performance"
data={ratedPerfs}
fill="hsl(var(--primary))"
line={{ stroke: 'hsl(var(--primary))', strokeWidth: 2, strokeOpacity: 0.3 }}
shape="circle"
/>
</ScatterChart>
</ResponsiveContainer>
</div>
</CardContent>
</Card>
)
}