feat: interest over time graph (#127)

* api

* ui

* test

* add margin to prevent clipping
This commit is contained in:
Gabe Farrell 2026-01-12 16:20:31 -05:00 committed by GitHub
parent e45099c71a
commit 231eb1b0fb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 1097 additions and 4 deletions

162
db/queries/interest.sql Normal file
View file

@ -0,0 +1,162 @@
-- name: GetGroupedListensFromArtist :many
WITH artist_listens AS (
SELECT
l.listened_at
FROM listens l
JOIN tracks t ON t.id = l.track_id
JOIN artist_tracks at ON at.track_id = t.id
WHERE at.artist_id = $1
),
bounds AS (
SELECT
MIN(listened_at) AS start_time,
MAX(listened_at) AS end_time
FROM artist_listens
),
bucketed AS (
SELECT
LEAST(
sqlc.arg(bucket_count) - 1,
FLOOR(
(
EXTRACT(EPOCH FROM (al.listened_at - b.start_time))
/
NULLIF(EXTRACT(EPOCH FROM (b.end_time - b.start_time)), 0)
) * sqlc.arg(bucket_count)
)::int
) AS bucket_idx,
b.start_time,
b.end_time
FROM artist_listens al
CROSS JOIN bounds b
),
aggregated AS (
SELECT
start_time
+ (
bucket_idx * (end_time - start_time)
/ sqlc.arg(bucket_count)
) AS bucket_start,
start_time
+ (
(bucket_idx + 1) * (end_time - start_time)
/ sqlc.arg(bucket_count)
) AS bucket_end,
COUNT(*) AS listen_count
FROM bucketed
GROUP BY bucket_idx, start_time, end_time
)
SELECT
bucket_start::timestamptz,
bucket_end::timestamptz,
listen_count
FROM aggregated
ORDER BY bucket_start;
-- name: GetGroupedListensFromRelease :many
WITH artist_listens AS (
SELECT
l.listened_at
FROM listens l
JOIN tracks t ON t.id = l.track_id
WHERE t.release_id = $1
),
bounds AS (
SELECT
MIN(listened_at) AS start_time,
MAX(listened_at) AS end_time
FROM artist_listens
),
bucketed AS (
SELECT
LEAST(
sqlc.arg(bucket_count) - 1,
FLOOR(
(
EXTRACT(EPOCH FROM (al.listened_at - b.start_time))
/
NULLIF(EXTRACT(EPOCH FROM (b.end_time - b.start_time)), 0)
) * sqlc.arg(bucket_count)
)::int
) AS bucket_idx,
b.start_time,
b.end_time
FROM artist_listens al
CROSS JOIN bounds b
),
aggregated AS (
SELECT
start_time
+ (
bucket_idx * (end_time - start_time)
/ sqlc.arg(bucket_count)
) AS bucket_start,
start_time
+ (
(bucket_idx + 1) * (end_time - start_time)
/ sqlc.arg(bucket_count)
) AS bucket_end,
COUNT(*) AS listen_count
FROM bucketed
GROUP BY bucket_idx, start_time, end_time
)
SELECT
bucket_start::timestamptz,
bucket_end::timestamptz,
listen_count
FROM aggregated
ORDER BY bucket_start;
-- name: GetGroupedListensFromTrack :many
WITH artist_listens AS (
SELECT
l.listened_at
FROM listens l
JOIN tracks t ON t.id = l.track_id
WHERE t.id = $1
),
bounds AS (
SELECT
MIN(listened_at) AS start_time,
MAX(listened_at) AS end_time
FROM artist_listens
),
bucketed AS (
SELECT
LEAST(
sqlc.arg(bucket_count) - 1,
FLOOR(
(
EXTRACT(EPOCH FROM (al.listened_at - b.start_time))
/
NULLIF(EXTRACT(EPOCH FROM (b.end_time - b.start_time)), 0)
) * sqlc.arg(bucket_count)
)::int
) AS bucket_idx,
b.start_time,
b.end_time
FROM artist_listens al
CROSS JOIN bounds b
),
aggregated AS (
SELECT
start_time
+ (
bucket_idx * (end_time - start_time)
/ sqlc.arg(bucket_count)
) AS bucket_start,
start_time
+ (
(bucket_idx + 1) * (end_time - start_time)
/ sqlc.arg(bucket_count)
) AS bucket_end,
COUNT(*) AS listen_count
FROM bucketed
GROUP BY bucket_idx, start_time, end_time
)
SELECT
bucket_start::timestamptz,
bucket_end::timestamptz,
listen_count
FROM aggregated
ORDER BY bucket_start;