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:
parent
dfb68d4814
commit
9ad7fe7595
6 changed files with 146 additions and 29 deletions
|
@ -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"))
|
||||||
|
|
||||||
|
|
|
@ -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}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
1
mood/static/mood/dist/js/Tooltip.min.js
vendored
Normal file
1
mood/static/mood/dist/js/Tooltip.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
6
mood/static/mood/dist/js/popper.min.mjs
vendored
Normal file
6
mood/static/mood/dist/js/popper.min.mjs
vendored
Normal file
File diff suppressed because one or more lines are too long
12
mood/urls.py
12
mood/urls.py
|
@ -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(
|
||||||
|
|
|
@ -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))
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue