forked from
chadtmiller.com/slices-teal-relay
Teal.fm frontend powered by slices.network
tealfm-slices.wisp.place
tealfm
slices
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}