Skip to content

Commit 948e26f

Browse files
committed
heatmap
1 parent 100b2d5 commit 948e26f

File tree

3 files changed

+121
-5
lines changed

3 files changed

+121
-5
lines changed

sitio/src/lib/HeatmapHours.svelte

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

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

Lines changed: 46 additions & 2 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,12 +24,15 @@ function getStartingFrom(query: string) {
2524
}
2625
}
2726

28-
export const load: PageServerLoad = async ({ params, url, setHeaders }) => {
27+
export const load: PageServerLoad = async ({ url, setHeaders }) => {
2928
const query =
3029
url.searchParams.get("q") ?? "date:" + dayjs().tz(tz).format("YYYY-MM-DD");
3130
const startingFrom = getStartingFrom(query);
3231
const endsAt = startingFrom.add(24, "hour");
3332

33+
const heatmapStart = dayjs().tz(tz).subtract(90, "day").startOf("day");
34+
const heatmapEnd = dayjs().tz(tz).endOf("day");
35+
3436
const t0 = performance.now();
3537
const [
3638
likedTweets,
@@ -40,6 +42,8 @@ export const load: PageServerLoad = async ({ params, url, setHeaders }) => {
4042
firstLikedTweet,
4143
monthData,
4244
hasNextMonth,
45+
heatLikes,
46+
heatRetweets,
4347
] = await Promise.all([
4448
db.query.likedTweets.findMany({
4549
columns: {
@@ -112,10 +116,49 @@ export const load: PageServerLoad = async ({ params, url, setHeaders }) => {
112116
startingFrom.add(1, "month").startOf("month").toDate(),
113117
),
114118
}),
119+
db.query.likedTweets.findMany({
120+
columns: { firstSeenAt: true },
121+
where: and(
122+
gte(schema.likedTweets.firstSeenAt, heatmapStart.toDate()),
123+
lte(schema.likedTweets.firstSeenAt, heatmapEnd.toDate()),
124+
likesCutoffSql,
125+
),
126+
orderBy: desc(schema.likedTweets.firstSeenAt),
127+
}),
128+
db.query.retweets.findMany({
129+
columns: { retweetAt: true },
130+
where: and(
131+
gte(schema.retweets.retweetAt, heatmapStart.toDate()),
132+
lte(schema.retweets.retweetAt, heatmapEnd.toDate()),
133+
),
134+
orderBy: desc(schema.retweets.retweetAt),
135+
}),
115136
]);
116137
const t1 = performance.now();
117138
console.log("queries", t1 - t0);
118139

140+
console.time("heatmap");
141+
const hourHeatmap: number[][] = Array.from({ length: 7 }, () =>
142+
Array(24).fill(0),
143+
);
144+
const addToHeat = (d: Date) => {
145+
// Assume fixed UTC-3 (America/Argentina/Buenos_Aires without DST)
146+
const utcHour = d.getUTCHours();
147+
let hour = utcHour - 3;
148+
let dow = d.getUTCDay(); // 0 = Sunday
149+
if (hour < 0) {
150+
hour += 24;
151+
dow = (dow + 6) % 7; // previous day
152+
} else if (hour >= 24) {
153+
hour -= 24;
154+
dow = (dow + 1) % 7; // next day (not expected with -3 but kept generic)
155+
}
156+
hourHeatmap[dow][hour]++;
157+
};
158+
heatLikes.forEach((t: { firstSeenAt: Date }) => addToHeat(t.firstSeenAt));
159+
heatRetweets.forEach((t: { retweetAt: Date }) => addToHeat(t.retweetAt));
160+
console.timeEnd("heatmap");
161+
119162
if (
120163
likedTweets.length === 0 &&
121164
retweets.length === 0 &&
@@ -145,5 +188,6 @@ export const load: PageServerLoad = async ({ params, url, setHeaders }) => {
145188
monthData,
146189
hasNextMonth: !!hasNextMonth,
147190
query,
191+
hourHeatmap,
148192
};
149193
};

sitio/src/routes/+page.svelte

Lines changed: 16 additions & 3 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
@@ -225,9 +226,9 @@
225226
{/if}
226227

227228
<section
228-
class="mx-auto flex w-full max-w-2xl flex-col gap-4 bg-neutral-100 p-4 dark:bg-neutral-800 md:rounded-lg"
229+
class="mx-auto flex w-full max-w-2xl flex-col gap-4 bg-neutral-100 p-4 md:rounded-lg dark:bg-neutral-800"
229230
>
230-
<h2 class=" my-2 text-center text-xl font-bold md:text-4xl">
231+
<h2 class="my-2 text-center text-xl font-bold md:text-4xl">
231232
Su actividad en {dayjs(data.start).isAfter(dayjs().startOf("month"))
232233
? "lo que va de"
233234
: ""}
@@ -246,6 +247,18 @@
246247
/>
247248
</section>
248249

250+
<section
251+
class="mx-auto w-full max-w-2xl bg-neutral-100 p-4 md:rounded-lg dark:bg-neutral-800"
252+
>
253+
<h2 class="mb-2 text-center text-xl font-bold md:text-3xl">
254+
¿Cuándo suele estar activo en Twitter?
255+
</h2>
256+
<p class="text-muted-foreground mb-4 text-center text-sm">
257+
Basado en retweets y likes de los últimos 90 días.
258+
</p>
259+
<HeatmapHours matrix={data.hourHeatmap} />
260+
</section>
261+
249262
<section class="mx-auto flex w-full max-w-[800px] flex-col py-8">
250263
<h2 class="mb-4 text-center text-2xl font-bold md:text-4xl">
251264
💞 Los favoritos de Milei 💞
@@ -308,7 +321,7 @@
308321
Como lo viste en la prensa
309322
</h2>
310323
<div
311-
class="mx-auto flex flex-col items-center justify-center gap-4 bg-neutral-100 p-2 dark:bg-neutral-800 md:mb-8 md:flex-row md:rounded-lg md:text-lg"
324+
class="mx-auto flex flex-col items-center justify-center gap-4 bg-neutral-100 p-2 md:mb-8 md:flex-row md:rounded-lg md:text-lg dark:bg-neutral-800"
312325
>
313326
<enhanced:img
314327
class="w-[300px] rounded-lg"

0 commit comments

Comments
 (0)