Skip to content

Commit 54e72bf

Browse files
Add user activity heatmap component
Co-authored-by: git <[email protected]>
1 parent f9022e5 commit 54e72bf

File tree

3 files changed

+120
-3
lines changed

3 files changed

+120
-3
lines changed

sitio/src/lib/HeatmapHours.svelte

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
<script lang="ts">
2+
export let matrix: number[][]; // 7 x 24, dayjs().day() indexing (0=Sunday)
3+
export let title: string = "Horas típicas de actividad (UTC-3)";
4+
5+
const weekdays = [
6+
"Dom",
7+
"Lun",
8+
"Mar",
9+
"Mié",
10+
"Jue",
11+
"Vie",
12+
"Sáb",
13+
];
14+
15+
const hours = Array.from({ length: 24 }, (_, h) => h);
16+
17+
$: max = Math.max(0, ...matrix.flat());
18+
19+
function colorFor(value: number) {
20+
if (max === 0) return "#e5e7eb"; // neutral-200
21+
const t = value / max;
22+
// simple sequential palette from light to dark orange
23+
if (t === 0) return "#f3f4f6"; // neutral-100
24+
if (t < 0.2) return "#fde7d9";
25+
if (t < 0.4) return "#fdcdb3";
26+
if (t < 0.6) return "#fca36b";
27+
if (t < 0.8) return "#f77f00";
28+
return "#d65a00";
29+
}
30+
</script>
31+
32+
<div class="flex w-full flex-col gap-3">
33+
<div class="flex items-center justify-between">
34+
<h3 class="text-lg font-semibold">{title}</h3>
35+
<span class="text-xs text-muted-foreground">Zona horaria: UTC-3</span>
36+
</div>
37+
38+
<div class="overflow-x-auto">
39+
<div class="grid" style="grid-template-columns: 48px repeat(24, minmax(20px,1fr));">
40+
<div></div>
41+
{#each hours as h}
42+
<div class="text-center text-[10px] text-muted-foreground">{h}</div>
43+
{/each}
44+
45+
{#each matrix as row, dow}
46+
<div class="sticky left-0 z-10 bg-transparent pr-2 text-right text-xs">{weekdays[dow]}</div>
47+
{#each row as value}
48+
<div class="aspect-square border border-neutral-200 dark:border-neutral-700" style={`background:${colorFor(value)}`}
49+
title={`${weekdays[dow]} ${value} interacciones`}></div>
50+
{/each}
51+
{/each}
52+
</div>
53+
</div>
54+
55+
<div class="flex items-center gap-2 self-end text-xs text-muted-foreground">
56+
<span>menos</span>
57+
<div class="h-3 w-3" style="background:#f3f4f6"></div>
58+
<div class="h-3 w-3" style="background:#fde7d9"></div>
59+
<div class="h-3 w-3" style="background:#fdcdb3"></div>
60+
<div class="h-3 w-3" style="background:#fca36b"></div>
61+
<div class="h-3 w-3" style="background:#f77f00"></div>
62+
<div class="h-3 w-3" style="background:#d65a00"></div>
63+
<span>más</span>
64+
</div>
65+
</div>
66+

sitio/src/routes/+page.server.ts

Lines changed: 41 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import * as schema from "../schema";
44
import type { PageServerLoad } from "./$types";
55
import { dayjs, likesCutoffSql } from "$lib/consts";
66
import { error, redirect } from "@sveltejs/kit";
7-
import { getLastWeek } from "$lib/data-processing/weekly";
87
import { getStatsForDaysInTimePeriod } from "@/data-processing/days";
98

109
const tz = "America/Argentina/Buenos_Aires";
@@ -25,7 +24,7 @@ function getStartingFrom(query: string) {
2524
}
2625
}
2726

28-
export const load: PageServerLoad = async ({ params, url, setHeaders }) => {
27+
export const load = (async ({ url, setHeaders }) => {
2928
const query =
3029
url.searchParams.get("q") ?? "date:" + dayjs().tz(tz).format("YYYY-MM-DD");
3130
const startingFrom = getStartingFrom(query);
@@ -116,6 +115,44 @@ export const load: PageServerLoad = async ({ params, url, setHeaders }) => {
116115
const t1 = performance.now();
117116
console.log("queries", t1 - t0);
118117

118+
// Build 7x24 heatmap (UTC-3) over last 60 days using retweets and available likes
119+
const heatmapStart = dayjs().tz(tz).subtract(60, "day").startOf("day");
120+
const heatmapEnd = dayjs().tz(tz).endOf("day");
121+
const [heatLikes, heatRetweets]: [
122+
Array<{ firstSeenAt: Date }>,
123+
Array<{ retweetAt: Date }>,
124+
] = await Promise.all([
125+
db.query.likedTweets.findMany({
126+
columns: { firstSeenAt: true },
127+
where: and(
128+
gte(schema.likedTweets.firstSeenAt, heatmapStart.toDate()),
129+
lte(schema.likedTweets.firstSeenAt, heatmapEnd.toDate()),
130+
likesCutoffSql,
131+
),
132+
orderBy: desc(schema.likedTweets.firstSeenAt),
133+
}),
134+
db.query.retweets.findMany({
135+
columns: { retweetAt: true },
136+
where: and(
137+
gte(schema.retweets.retweetAt, heatmapStart.toDate()),
138+
lte(schema.retweets.retweetAt, heatmapEnd.toDate()),
139+
),
140+
orderBy: desc(schema.retweets.retweetAt),
141+
}),
142+
]);
143+
144+
const hourHeatmap: number[][] = Array.from({ length: 7 }, () =>
145+
Array(24).fill(0),
146+
);
147+
const addToHeat = (d: Date) => {
148+
const x = dayjs(d).tz(tz);
149+
const dow = x.day();
150+
const hour = x.hour();
151+
hourHeatmap[dow][hour]++;
152+
};
153+
heatLikes.forEach((t: { firstSeenAt: Date }) => addToHeat(t.firstSeenAt));
154+
heatRetweets.forEach((t: { retweetAt: Date }) => addToHeat(t.retweetAt));
155+
119156
if (
120157
likedTweets.length === 0 &&
121158
retweets.length === 0 &&
@@ -145,5 +182,6 @@ export const load: PageServerLoad = async ({ params, url, setHeaders }) => {
145182
monthData,
146183
hasNextMonth: !!hasNextMonth,
147184
query,
185+
hourHeatmap,
148186
};
149-
};
187+
}) satisfies PageServerLoad;

sitio/src/routes/+page.svelte

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
import { parseDate } from "@internationalized/date";
3737
import StatsCalendar from "@/StatsCalendar.svelte";
3838
import StatsCalendarNavigation from "@/StatsCalendarNavigation.svelte";
39+
import HeatmapHours from "$lib/HeatmapHours.svelte";
3940
4041
export let data: PageData;
4142
@@ -169,6 +170,18 @@
169170
</div>
170171
</section>
171172

173+
<section class="mx-auto w-full max-w-2xl">
174+
<div class="mx-2 my-4 rounded-lg bg-neutral-100 p-4 dark:bg-neutral-800">
175+
<h2 class="mb-2 text-center text-xl font-bold md:text-3xl">
176+
¿Cuándo suele estar activo en Twitter? (UTC-3)
177+
</h2>
178+
<p class="mb-4 text-center text-sm text-muted-foreground">
179+
Basado en retweets y likes de los últimos 60 días.
180+
</p>
181+
<HeatmapHours matrix={data.hourHeatmap} />
182+
</div>
183+
</section>
184+
172185
{#if dudoso}
173186
<section class="mx-auto w-full max-w-2xl">
174187
<p class="text-center text-sm">

0 commit comments

Comments
 (0)