Implement statistics in Javascript only to soon replace this with HoloViews
This commit is contained in:
parent
6b1ff42f2f
commit
67a6be0e3c
7 changed files with 175 additions and 6 deletions
2
frontend/static/frontend/vendor/d3/d3.min.js
vendored
Normal file
2
frontend/static/frontend/vendor/d3/d3.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
|
@ -1,4 +1,4 @@
|
||||||
{% include "frontend/header_admin.html" with title=title %}
|
{% include "frontend/header_admin.html" with title=title styles=styles %}
|
||||||
{% include "frontend/sidebar.html" with title=title %}
|
{% include "frontend/sidebar.html" with title=title %}
|
||||||
|
|
||||||
<!-- Content Wrapper -->
|
<!-- Content Wrapper -->
|
||||||
|
@ -74,4 +74,4 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% include "frontend/footer.html" %}
|
{% include "frontend/footer.html" with scripts=scripts %}
|
18
mood/static/mood/statistics.css
Normal file
18
mood/static/mood/statistics.css
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
.line {
|
||||||
|
fill: none;
|
||||||
|
stroke: steelblue;
|
||||||
|
stroke-width: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.tooltip {
|
||||||
|
position: absolute;
|
||||||
|
text-align: center;
|
||||||
|
width: 120px;
|
||||||
|
height: 56px;
|
||||||
|
padding: 2px;
|
||||||
|
font: 12px sans-serif;
|
||||||
|
background: lightsteelblue;
|
||||||
|
border: 0px;
|
||||||
|
border-radius: 8px;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
95
mood/static/mood/statistics.js
Normal file
95
mood/static/mood/statistics.js
Normal file
|
@ -0,0 +1,95 @@
|
||||||
|
const queryString = window.location.search;
|
||||||
|
|
||||||
|
var margin = { top: 30, right: 120, bottom: 30, left: 50 },
|
||||||
|
width = 960 - margin.left - margin.right,
|
||||||
|
height = 500 - margin.top - margin.bottom,
|
||||||
|
tooltip = { width: 100, height: 100, x: 10, y: -30 };
|
||||||
|
|
||||||
|
var parseDate = d3.timeParse("%Y-%m-%d %H:%M"),
|
||||||
|
bisectDate = d3.bisector(function(d) { return d.date; }).left,
|
||||||
|
formatValue = d3.format(","),
|
||||||
|
dateFormatter = d3.timeFormat("%d.%m.%y %H:%M");
|
||||||
|
|
||||||
|
var x = d3.scaleTime()
|
||||||
|
.range([0, width]);
|
||||||
|
|
||||||
|
var y = d3.scaleLinear()
|
||||||
|
.range([height, 0]);
|
||||||
|
|
||||||
|
var valueline = d3.line()
|
||||||
|
.x(function(d) { return x(d.date); })
|
||||||
|
.y(function(d) { return y(d.value); });
|
||||||
|
|
||||||
|
var div = d3.select("body").append("div")
|
||||||
|
.attr("class", "tooltip")
|
||||||
|
.style("opacity", 0);
|
||||||
|
|
||||||
|
var xAxis = d3.axisBottom(x)
|
||||||
|
.tickFormat(dateFormatter);
|
||||||
|
|
||||||
|
var yAxis = d3.axisLeft(y)
|
||||||
|
.tickFormat(d3.format("s"))
|
||||||
|
|
||||||
|
var line = d3.line()
|
||||||
|
.x(function(d) { return x(d.date); })
|
||||||
|
.y(function(d) { return y(d.likes); });
|
||||||
|
|
||||||
|
var svg = d3.select("#my_dataviz")
|
||||||
|
.append("svg")
|
||||||
|
.attr("width", width + margin.left + margin.right)
|
||||||
|
.attr("height", height + margin.top + margin.bottom)
|
||||||
|
.append("g")
|
||||||
|
.attr("transform",
|
||||||
|
"translate(" + margin.left + "," + margin.top + ")");
|
||||||
|
|
||||||
|
d3.csv("csv/" + queryString).then(function(data) {
|
||||||
|
|
||||||
|
data.forEach(function(d) {
|
||||||
|
d.date = parseDate(d.date);
|
||||||
|
d.value = d.value;
|
||||||
|
});
|
||||||
|
|
||||||
|
data.sort(function(a, b) {
|
||||||
|
return a.date - b.date;
|
||||||
|
});
|
||||||
|
|
||||||
|
x.domain([data[0].date, data[data.length - 1].date]);
|
||||||
|
y.domain(d3.extent(data, function(d) { return d.value; }));
|
||||||
|
|
||||||
|
// add the valueline path.
|
||||||
|
svg.append("path")
|
||||||
|
.data([data])
|
||||||
|
.attr("class", "line")
|
||||||
|
.attr("d", valueline);
|
||||||
|
|
||||||
|
// add the dots with tooltips
|
||||||
|
svg.selectAll("dot")
|
||||||
|
.data(data)
|
||||||
|
.enter().append("circle")
|
||||||
|
.attr("r", 5)
|
||||||
|
.attr("cx", function(d) { return x(d.date); })
|
||||||
|
.attr("cy", function(d) { return y(d.value); })
|
||||||
|
.on("mouseover", function(event,d) {
|
||||||
|
div.transition()
|
||||||
|
.duration(200)
|
||||||
|
.style("opacity", .9);
|
||||||
|
div.html(dateFormatter(d.date) + "<br/>" + d.value)
|
||||||
|
.style("left", (event.pageX) + "px")
|
||||||
|
.style("top", (event.pageY - 28) + "px");
|
||||||
|
})
|
||||||
|
.on("mouseout", function(d) {
|
||||||
|
div.transition()
|
||||||
|
.duration(500)
|
||||||
|
.style("opacity", 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
// add the X Axis
|
||||||
|
svg.append("g")
|
||||||
|
.attr("transform", "translate(0," + height + ")")
|
||||||
|
.call(d3.axisBottom(x));
|
||||||
|
|
||||||
|
// add the Y Axis
|
||||||
|
svg.append("g")
|
||||||
|
.call(d3.axisLeft(y));
|
||||||
|
|
||||||
|
});
|
22
mood/templates/mood/statistics.html
Normal file
22
mood/templates/mood/statistics.html
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
{% extends "frontend/base.html" %}
|
||||||
|
{% block "content" %}
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
|
||||||
|
<!-- Entry details -->
|
||||||
|
<div class="col-xl-8 col-lg-7">
|
||||||
|
<div class="card shadow mb-4">
|
||||||
|
<!-- Card Header - Dropdown -->
|
||||||
|
<div class="card-header py-3 d-flex flex-row align-items-center justify-content-between">
|
||||||
|
<h6 class="m-0 font-weight-bold text-primary">Mood stats</h6>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<!-- Card Body -->
|
||||||
|
<div class="card-body">
|
||||||
|
<div id="my_dataviz"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% endblock %}
|
|
@ -1,4 +1,4 @@
|
||||||
from .views import StatusListView, StatusViewView, StatusDeleteView, StatusEditView, StatusCreateView, ActivityListView, ActivityEditView, ActivityCreateView, ActivityDeleteView, MoodListView, MoodEditView, NotificationCreateView, NotificationDeleteView, NotificationEditView, NotificationListView
|
from .views import StatusListView, StatusViewView, StatusDeleteView, StatusEditView, StatusCreateView, ActivityListView, ActivityEditView, ActivityCreateView, ActivityDeleteView, MoodListView, MoodEditView, NotificationCreateView, NotificationDeleteView, NotificationEditView, NotificationListView, MoodStatisticsView, MoodCSVView
|
||||||
|
|
||||||
from django.urls import path, include
|
from django.urls import path, include
|
||||||
|
|
||||||
|
@ -20,4 +20,6 @@ urlpatterns = [
|
||||||
path('notification/<int:id>/edit/', NotificationEditView.as_view(), name="notification_edit"),
|
path('notification/<int:id>/edit/', NotificationEditView.as_view(), name="notification_edit"),
|
||||||
path('notification/<int:id>/delete/', NotificationDeleteView.as_view(), name="notification_delete"),
|
path('notification/<int:id>/delete/', NotificationDeleteView.as_view(), name="notification_delete"),
|
||||||
path('notification/new/', NotificationCreateView.as_view(), name="notification_create"),
|
path('notification/new/', NotificationCreateView.as_view(), name="notification_create"),
|
||||||
|
path('statistics/', MoodStatisticsView.as_view(), name="statistics"),
|
||||||
|
path('statistics/csv/', MoodCSVView.as_view(), name="statistics_csv"),
|
||||||
]
|
]
|
|
@ -1,8 +1,9 @@
|
||||||
from django.views.generic import TemplateView, ListView, UpdateView, DetailView, CreateView, DeleteView
|
from django.views.generic import TemplateView, ListView, UpdateView, DetailView, CreateView, DeleteView, View
|
||||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||||
from django.shortcuts import get_object_or_404
|
from django.shortcuts import get_object_or_404
|
||||||
from django.urls import reverse_lazy
|
from django.urls import reverse_lazy
|
||||||
from django.http import HttpResponseRedirect
|
from django.http import HttpResponseRedirect, HttpResponse
|
||||||
|
from django.utils import timezone
|
||||||
|
|
||||||
from .models import Status, Activity, Mood, StatusMedia, StatusActivity
|
from .models import Status, Activity, Mood, StatusMedia, StatusActivity
|
||||||
from .forms import StatusForm
|
from .forms import StatusForm
|
||||||
|
@ -10,6 +11,8 @@ from .forms import StatusForm
|
||||||
from common.helpers import get_upload_path
|
from common.helpers import get_upload_path
|
||||||
from msgio.models import NotificationDailySchedule, Notification
|
from msgio.models import NotificationDailySchedule, Notification
|
||||||
|
|
||||||
|
from dateutil import relativedelta
|
||||||
|
|
||||||
class StatusListView(LoginRequiredMixin, ListView):
|
class StatusListView(LoginRequiredMixin, ListView):
|
||||||
template_name = "mood/status_list.html"
|
template_name = "mood/status_list.html"
|
||||||
model = Status
|
model = Status
|
||||||
|
@ -316,3 +319,30 @@ class NotificationDeleteView(LoginRequiredMixin, DeleteView):
|
||||||
|
|
||||||
def get_success_url(self):
|
def get_success_url(self):
|
||||||
return reverse_lazy("mood:notification_list")
|
return reverse_lazy("mood:notification_list")
|
||||||
|
|
||||||
|
class MoodStatisticsView(LoginRequiredMixin, TemplateView):
|
||||||
|
template_name = "mood/statistics.html"
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
context = super().get_context_data(**kwargs)
|
||||||
|
context["title"] = "Statistics"
|
||||||
|
context["scripts"] = ["frontend/vendor/d3/d3.min.js", "mood/statistics.js"]
|
||||||
|
context["styles"] = ["mood/statistics.css"]
|
||||||
|
return context
|
||||||
|
|
||||||
|
class MoodCSVView(LoginRequiredMixin, View):
|
||||||
|
def get(self, request, *args, **kwargs):
|
||||||
|
res = HttpResponse(content_type="text/csv")
|
||||||
|
res["content-disposition"] = 'filename="mood.csv"'
|
||||||
|
|
||||||
|
maxage = timezone.now()
|
||||||
|
minage = maxage - relativedelta.relativedelta(weeks=1)
|
||||||
|
|
||||||
|
output = "date,value"
|
||||||
|
|
||||||
|
for status in Status.objects.filter(user=request.user, timestamp__gte=minage, timestamp__lte=maxage):
|
||||||
|
date = status.timestamp.strftime("%Y-%m-%d %H:%M")
|
||||||
|
output += f"\n{date},{status.mood.value}"
|
||||||
|
|
||||||
|
res.write(output)
|
||||||
|
return res
|
Loading…
Reference in a new issue