elmeg-demo/frontend/components/songs/song-evolution-chart.tsx
fullsizemalt cb91b5ad6d
Some checks are pending
Deploy Elmeg / deploy (push) Waiting to run
feat(frontend): Added Recharts and Song Evolution Chart
2025-12-21 02:26:18 -08:00

136 lines
5.3 KiB
TypeScript

"use client"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import {
ResponsiveContainer,
ScatterChart,
Scatter,
XAxis,
YAxis,
ZAxis,
Tooltip,
CartesianGrid,
TooltipProps
} 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 }: TooltipProps<number, string>) => {
if (active && payload && payload.length) {
const data = payload[0].payload
return (
<div className="bg-background border rounded-lg shadow-xl p-3 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">
{data.rating.toFixed(1)}
</Badge>
</div>
</div>
)
}
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
const latest = ratedPerfs[ratedPerfs.length - 1].rating
const isTrendingUp = latest >= average
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>
)
}