132 lines
5.1 KiB
TypeScript
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>
|
|
)
|
|
}
|