feat: Enhances heatmap with tooltips and new JSON endpoint

Adds additional scripts for tooltips and legends in the heatmap,
enhancing data visualization and usability.

Refactors dashboard logic to dynamically fetch and render mood
data, allowing for more customized color scales and average
mood calculations.

Introduces a new API endpoint to provide mood value details,
necessary for proper heatmap rendering.

Improves the handling of mood data to compute averages for each
day, allowing for richer insights into mood patterns.
This commit is contained in:
Kumi 2024-11-28 15:01:13 +01:00
parent dfb68d4814
commit 9ad7fe7595
Signed by: kumi
GPG key ID: ECBCC9082395383F
6 changed files with 146 additions and 29 deletions

View file

@ -34,6 +34,9 @@ mood_section = DashboardSection("Moods", "mood/dashboard_section.html")
mood_section.add_script(static("mood/dist/js/d3.v7.min.js")) mood_section.add_script(static("mood/dist/js/d3.v7.min.js"))
mood_section.add_script(static("mood/dist/js/cal-heatmap.min.js")) mood_section.add_script(static("mood/dist/js/cal-heatmap.min.js"))
mood_section.add_script(static("mood/dist/js/popper.min.mjs"))
mood_section.add_script(static("mood/dist/js/Tooltip.min.js"))
mood_section.add_script(static("mood/dist/js/Legend.min.js"))
mood_section.add_script(static("mood/dashboard.js")) mood_section.add_script(static("mood/dashboard.js"))

View file

@ -1,14 +1,44 @@
const cal = new CalHeatmap(); const cal = new CalHeatmap();
cal.paint({
fetch("/mood/statistics/heatmap/values/")
.then((response) => response.json())
.then((data) => {
var moodOptions = data;
const start = new Date(new Date().setFullYear(new Date().getFullYear() - 1));
const end = new Date();
var domain = Object.keys(moodOptions);
const range = ["#ffffd4"].concat(domain.map((key) => moodOptions[key]["color"])).concat(["#000000"]);
domain = [0].concat(domain).concat([Infinity]);
fetch("/mood/statistics/heatmap/?start=" + start.toISOString().split("T")[0] + "&end=" + end.toISOString().split("T")[0])
.then((response) => response.json())
.then((data) => {
cal.paint({
itemSelector: "#mood-count-heatmap", itemSelector: "#mood-count-heatmap",
data: { data: {
source: source:
"/mood/statistics/heatmap/?start={{start=YYYY-MM-DD}}&end={{end=YYYY-MM-DD}}", data,
x: "date", x: "date",
y: "value", y: d => {
if (d.average) {
const key = Object.keys(moodOptions).reduce((a, b) => Math.abs(moodOptions[a] - d.average) < Math.abs(moodOptions[b] - d.average) ? a : b);
return key;
}
return 0;
},
value: "count",
},
scale: {
color: {
domain: domain,
type: "ordinal",
range: range,
}
}, },
date: { date: {
start: new Date(new Date().setFullYear(new Date().getFullYear() - 1)), // Start from one year ago start: start, // Start from one year ago
}, },
range: 13, // Display 13 months so that the current month is included range: 13, // Display 13 months so that the current month is included
domain: { domain: {
@ -24,4 +54,34 @@ cal.paint({
height: 10, height: 10,
}, },
highlight: [new Date()], highlight: [new Date()],
}); },
[
[
Tooltip, {
enabled: true,
text: function (timestamp, value, dayjsDate) {
const date_str = dayjsDate.format("YYYY-MM-DD");
const obj = data.find(o => o["date"] === date_str);
if (!obj) {
return `${date_str}<br>Mood Count: 0<br>Average Mood: N/A`
}
const average = obj["average"];
if (!average) {
return `${date_str}<br>Mood Count: ${obj.count}<br>Average Mood: N/A`
}
const key = Object.keys(moodOptions).reduce((a, b) => Math.abs(moodOptions[a] - average) < Math.abs(moodOptions[b] - average) ? a : b);
const mood = moodOptions[key];
return `${date_str}<br>Mood Count: ${value}<br>Average Mood: ${mood.name}`
}
}
]
]
);
});
});

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -22,6 +22,7 @@ from .views import (
ActivityPlotView, ActivityPlotView,
ActivityPiesView, ActivityPiesView,
MoodCountHeatmapJSONView, MoodCountHeatmapJSONView,
MoodHeatmapValuesJSONView,
) )
from django.urls import path from django.urls import path
@ -62,7 +63,16 @@ urlpatterns = [
), ),
path("statistics/", MoodStatisticsView.as_view(), name="statistics"), path("statistics/", MoodStatisticsView.as_view(), name="statistics"),
path("statistics/csv/", MoodCSVView.as_view(), name="statistics_csv"), path("statistics/csv/", MoodCSVView.as_view(), name="statistics_csv"),
path("statistics/heatmap/", MoodCountHeatmapJSONView.as_view(), name="statistics_heatmap"), path(
"statistics/heatmap/",
MoodCountHeatmapJSONView.as_view(),
name="statistics_heatmap",
),
path(
"statistics/heatmap/values/",
MoodHeatmapValuesJSONView.as_view(),
name="statistics_heatmap_values",
),
path("statistics/plot/", MoodPlotView.as_view(), name="statistics_plot"), path("statistics/plot/", MoodPlotView.as_view(), name="statistics_plot"),
path("statistics/pies/", MoodPiesView.as_view(), name="statistics_pies"), path("statistics/pies/", MoodPiesView.as_view(), name="statistics_pies"),
path( path(

View file

@ -569,9 +569,46 @@ class MoodCountHeatmapJSONView(LoginRequiredMixin, View):
for entry in data: for entry in data:
date = entry.timestamp.strftime("%Y-%m-%d") date = entry.timestamp.strftime("%Y-%m-%d")
output[date] = output.get(date, 0) + 1
output = [{"date": key, "value": value} for key, value in output.items()] if "date" not in output:
output[date] = {"count": 0, "total": 0}
if entry.mood:
output[date]["total"] += entry.mood.value
output[date]["count"] += 1
output = [
{
"date": key,
"count": value["count"],
"average": (
(value["total"] / value["count"]) if value["count"] > 0 else 0
),
}
for key, value in output.items()
]
res.write(json.dumps(output))
return res
class MoodHeatmapValuesJSONView(LoginRequiredMixin, View):
"""Returns a JSON object with the available mood values.
This is used to display the correct colors in the heatmap.
"""
def get(self, request, *args, **kwargs):
res = HttpResponse(content_type="application/json")
data = Mood.objects.filter(user=request.user)
output = {
entry.value: {"name": entry.name, "icon": entry.icon, "color": entry.color}
for entry in data
}
res.write(json.dumps(output)) res.write(json.dumps(output))