Teal.fm frontend powered by slices.network tealfm-slices.wisp.place
tealfm slices
at main 3.3 kB view raw
1import { graphql, useFragment } from "react-relay"; 2import { useMemo } from "react"; 3import type { ScrobbleChart_data$key } from "./__generated__/ScrobbleChart_data.graphql"; 4 5interface ScrobbleChartProps { 6 queryRef: ScrobbleChart_data$key; 7} 8 9export default function ScrobbleChart({ queryRef }: ScrobbleChartProps) { 10 const data = useFragment( 11 graphql` 12 fragment ScrobbleChart_data on Query { 13 chartData: fmTealAlphaFeedPlaysAggregated( 14 groupBy: [{ field: playedTime, interval: day }] 15 where: $chartWhere 16 limit: 90 17 ) { 18 playedTime 19 count 20 } 21 } 22 `, 23 queryRef 24 ); 25 26 const chartData = useMemo(() => { 27 if (!data?.chartData) return []; 28 29 // Convert aggregated data to chart format 30 const aggregated = data.chartData.map((item) => { 31 // playedTime comes back as '2025-08-03 00:00:00', extract just the date part 32 const date = item.playedTime ? item.playedTime.split(' ')[0] : ""; 33 return { 34 date, 35 count: item.count, 36 }; 37 }).sort((a, b) => a.date.localeCompare(b.date)); 38 39 // Fill in missing days with zero counts 40 const now = new Date(); 41 now.setHours(0, 0, 0, 0); 42 const filledData = []; 43 44 for (let i = 89; i >= 0; i--) { 45 const date = new Date(now); 46 date.setDate(date.getDate() - i); 47 const dateStr = date.toISOString().split("T")[0]; 48 49 const existing = aggregated.find((d) => d.date === dateStr); 50 filledData.push({ 51 date: dateStr, 52 count: existing ? existing.count : 0, 53 }); 54 } 55 56 return filledData; 57 }, [data?.chartData]); 58 59 if (!chartData || chartData.length === 0) return null; 60 61 const width = 1000; 62 const height = 100; 63 const padding = { top: 0, right: 0, bottom: 0, left: 0 }; 64 const chartWidth = width - padding.left - padding.right; 65 const chartHeight = height - padding.top - padding.bottom; 66 67 const maxCount = Math.max(...chartData.map((d) => d.count)); 68 const minCount = Math.min(...chartData.map((d) => d.count)); 69 const range = maxCount - minCount || 1; 70 71 // Generate points for the line 72 const points = chartData.map((d, i) => { 73 const x = padding.left + (i / (chartData.length - 1)) * chartWidth; 74 const y = padding.top + chartHeight - ((d.count - minCount) / range) * chartHeight; 75 return `${x},${y}`; 76 }).join(" "); 77 78 // Generate area path 79 const areaPoints = [ 80 `${padding.left},${padding.top + chartHeight}`, 81 ...chartData.map((d, i) => { 82 const x = padding.left + (i / (chartData.length - 1)) * chartWidth; 83 const y = padding.top + chartHeight - ((d.count - minCount) / range) * chartHeight; 84 return `${x},${y}`; 85 }), 86 `${padding.left + chartWidth},${padding.top + chartHeight}`, 87 ].join(" "); 88 89 return ( 90 <svg 91 viewBox={`0 0 ${width} ${height}`} 92 className="w-full h-full" 93 preserveAspectRatio="none" 94 > 95 {/* Area fill */} 96 <polygon 97 points={areaPoints} 98 fill="rgb(139 92 246 / 0.1)" 99 stroke="none" 100 /> 101 102 {/* Line */} 103 <polyline 104 points={points} 105 fill="none" 106 stroke="rgb(139 92 246)" 107 strokeWidth="1.5" 108 strokeLinecap="round" 109 strokeLinejoin="round" 110 /> 111 </svg> 112 ); 113}