Demo using Slices Network GraphQL Relay API to make a teal.fm client
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: fmTealAlphaFeedPlayAggregated( 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 - 75 ((d.count - minCount) / range) * chartHeight; 76 return `${x},${y}`; 77 }).join(" "); 78 79 // Generate area path 80 const areaPoints = [ 81 `${padding.left},${padding.top + chartHeight}`, 82 ...chartData.map((d, i) => { 83 const x = padding.left + (i / (chartData.length - 1)) * chartWidth; 84 const y = padding.top + chartHeight - 85 ((d.count - minCount) / range) * chartHeight; 86 return `${x},${y}`; 87 }), 88 `${padding.left + chartWidth},${padding.top + chartHeight}`, 89 ].join(" "); 90 91 return ( 92 <svg 93 viewBox={`0 0 ${width} ${height}`} 94 className="w-full h-full" 95 preserveAspectRatio="none" 96 > 97 {/* Area fill */} 98 <polygon 99 points={areaPoints} 100 fill="rgb(139 92 246 / 0.1)" 101 stroke="none" 102 /> 103 104 {/* Line */} 105 <polyline 106 points={points} 107 fill="none" 108 stroke="rgb(139 92 246)" 109 strokeWidth="1.5" 110 strokeLinecap="round" 111 strokeLinejoin="round" 112 /> 113 </svg> 114 ); 115}