[trends] Add new time of day trend
All checks were successful
build / test (push) Successful in 2m4s

This commit is contained in:
2026-06-23 22:21:18 -04:00
parent ee59cde882
commit 327ba94c63
7 changed files with 178 additions and 0 deletions

View File

@ -604,6 +604,17 @@ independent of the email flow it was originally creatdd for
** TODO [#B] Is there way to create unique slugs for media instances :media_types:
** DONE [#B] Time of Day Categories trend :trends:
:PROPERTIES:
:ID: 6598074f-2290-46db-967b-29f45d30be29
:END:
*** Description
Added a "Time of Day Categories" trend that groups scrobbles for Books, Trails,
Birding Locations, and Board Games into Early Bird (5-10:59am), Day Jay (11am-6:59pm),
and Night Owl (7pm-4:59am) buckets. Shows both overall and per-media-type breakdowns.
* Version 58.1 [1/1]
** DONE [#B] Add auto genre tagging for papers :books:papers:metadata:
:PROPERTIES:

View File

@ -0,0 +1,65 @@
<div class="row">
<div class="col-12">
{% if data.total and data.total > 0 %}
<h5>Overall</h5>
<div class="table-responsive mb-4">
<table class="table table-striped table-sm">
<thead>
<tr>
<th>Category</th>
<th class="text-end">Scrobbles</th>
<th class="text-end">%</th>
</tr>
</thead>
<tbody>
{% for slug, info in data.categories.items %}
<tr>
<td>{{ info.label }}</td>
<td class="text-end">{{ info.count }}</td>
<td class="text-end">{{ info.pct }}%</td>
</tr>
{% endfor %}
<tr class="table-secondary">
<td><strong>Total</strong></td>
<td class="text-end"><strong>{{ data.total }}</strong></td>
<td class="text-end"></td>
</tr>
</tbody>
</table>
</div>
<h5>By Media Type</h5>
{% for mt, mt_data in data.by_media_type.items %}
<h6 class="mt-3">{{ mt }}</h6>
<div class="table-responsive">
<table class="table table-striped table-sm">
<thead>
<tr>
<th>Category</th>
<th class="text-end">Scrobbles</th>
<th class="text-end">%</th>
</tr>
</thead>
<tbody>
{% for slug, info in mt_data.categories.items %}
<tr>
<td>{{ info.label }}</td>
<td class="text-end">{{ info.count }}</td>
<td class="text-end">{{ info.pct }}%</td>
</tr>
{% endfor %}
<tr class="table-secondary">
<td><strong>Total</strong></td>
<td class="text-end"><strong>{{ mt_data.total }}</strong></td>
<td class="text-end"></td>
</tr>
</tbody>
</table>
</div>
{% endfor %}
{% else %}
<p class="text-muted">No data found for Books, Trails, Birding Locations, or Board Games in this period.</p>
{% endif %}
</div>
</div>

View File

@ -55,6 +55,9 @@
{% elif trend.slug == "reading-pace-vs-activity" %}
{% include "trends/_reading_pace.html" %}
{% elif trend.slug == "time-of-day-categories" %}
{% include "trends/_time_of_day_categories.html" %}
{% elif trend.slug == "trending-up" %}
{% include "trends/_trending_up.html" %}

View File

@ -15,6 +15,7 @@ from trends.trends.mood import (
compute_mood_weather,
)
from trends.trends.reading import compute_reading_pace_vs_activity
from trends.trends.time_of_day import compute_time_of_day_categories
from trends.trends.trending import compute_trending_up
TREND_REGISTRY = {}
@ -44,5 +45,8 @@ compute_peak_hours = register("peak-hours")(compute_peak_hours)
compute_reading_pace_vs_activity = register("reading-pace-vs-activity")(
compute_reading_pace_vs_activity
)
compute_time_of_day_categories = register("time-of-day-categories")(
compute_time_of_day_categories
)
compute_trending_up = register("trending-up")(compute_trending_up)
compute_weekly_rhythm = register("weekly-rhythm")(compute_weekly_rhythm)

View File

@ -0,0 +1,89 @@
from collections import OrderedDict
from django.db.models import Count, Q
from django.db.models.functions import Extract
from scrobbles.models import Scrobble
TARGET_MEDIA_TYPES = ["Book", "Trail", "BirdingLocation", "BoardGame"]
CATEGORIES = OrderedDict(
[
("early_bird", {"label": "Early Bird", "hours": {5, 6, 7, 8, 9, 10}}),
("day_jay", {"label": "Day Jay", "hours": {11, 12, 13, 14, 15, 16, 17, 18}}),
("night_owl", {"label": "Night Owl", "hours": {19, 20, 21, 22, 23, 0, 1, 2, 3, 4}}),
]
)
def _categorize_hour(hour):
for slug, cat in CATEGORIES.items():
if hour in cat["hours"]:
return slug
return None
def compute_time_of_day_categories(user, period="last_30"):
from trends.utils import get_date_range
start, end = get_date_range(period)
filters = Q(user=user, media_type__in=TARGET_MEDIA_TYPES, timestamp__isnull=False)
if start:
filters &= Q(timestamp__gte=start)
if end:
filters &= Q(timestamp__lte=end)
qs = (
Scrobble.objects.filter(filters)
.annotate(hour=Extract("timestamp", "hour"))
.values("media_type", "hour")
.annotate(count=Count("id"))
.order_by("media_type", "hour")
)
raw = {}
for row in qs:
mt = row["media_type"]
raw.setdefault(mt, {})[row["hour"]] = row["count"]
by_media_type = {}
grand_totals = {"early_bird": 0, "day_jay": 0, "night_owl": 0}
grand_total = 0
for mt in TARGET_MEDIA_TYPES:
mt_data = raw.get(mt, {})
cat_counts = {"early_bird": 0, "day_jay": 0, "night_owl": 0}
mt_total = 0
for hour, count in mt_data.items():
slug = _categorize_hour(hour)
if slug:
cat_counts[slug] += count
mt_total += count
by_media_type[mt] = {
"total": mt_total,
"categories": {},
}
for slug in CATEGORIES:
c = cat_counts[slug]
by_media_type[mt]["categories"][slug] = {
"count": c,
"pct": round((c / mt_total * 100), 1) if mt_total else 0,
"label": CATEGORIES[slug]["label"],
}
grand_totals[slug] += c
grand_total += mt_total
categories = {}
for slug in CATEGORIES:
c = grand_totals[slug]
categories[slug] = {
"count": c,
"pct": round((c / grand_total * 100), 1) if grand_total else 0,
"label": CATEGORIES[slug]["label"],
}
return {
"categories": categories,
"total": grand_total,
"by_media_type": by_media_type,
}

View File

@ -25,6 +25,7 @@ TIME_BOUND_TRENDS = {
"mood-weather",
"peak-hours",
"reading-pace-vs-activity",
"time-of-day-categories",
"trending-up",
"weekly-rhythm",
}

View File

@ -55,6 +55,11 @@ TREND_METADATA = {
"description": "Compare how long you read per session with and without concurrent music.",
"icon": "📊",
},
"time-of-day-categories": {
"title": "Time of Day Categories",
"description": "Are you an early bird, day jay, or night owl? Categorized by Books, Trails, Birding Locations, and Board Games.",
"icon": "🦉",
},
"trending-up": {
"title": "Trending Media Types",
"description": "Which media types have you been consuming more or less of recently?",