New statistic graph: Diaper change intervals

Fix formatting

Fix comment

Remove debug

Remove debug

Change naming of variable

Add new statistics graph: Feeding intervals

Fix naming

Add simple HTTP status code tests for new graphs

Remove number of feedings from graph

Remove obsolete yaxis2

Fix calculation of intervals and change scale to hours instead of minutes
This commit is contained in:
Marco H 2022-04-16 17:05:44 +02:00 committed by Christopher Charbonneau Wells
parent 8d8006ab71
commit 26e8a1f689
9 changed files with 225 additions and 1 deletions

View File

@ -2,8 +2,10 @@ from .bmi_change import bmi_change # NOQA
from .diaperchange_amounts import diaperchange_amounts # NOQA
from .diaperchange_lifetimes import diaperchange_lifetimes # NOQA
from .diaperchange_types import diaperchange_types # NOQA
from .diaperchange_intervals import diaperchange_intervals # NOQA
from .feeding_amounts import feeding_amounts # NOQA
from .feeding_duration import feeding_duration # NOQA
from .feeding_intervals import feeding_intervals # NOQA
from .head_circumference_change import head_circumference_change # NOQA
from .height_change import height_change # NOQA
from .pumping_amounts import pumping_amounts # NOQA

View File

@ -0,0 +1,93 @@
# -*- coding: utf-8 -*-
from django.db.models import Count, Case, When
from django.db.models.functions import TruncDate
from django.utils.translation import gettext as _
from django.utils.translation import get_language
import plotly.offline as plotly
import plotly.graph_objs as go
from core.utils import duration_parts
from reports import utils
def diaperchange_intervals(changes):
"""
Create a graph showing intervals of diaper changes.
:param changes: a QuerySet of Diaper Change instances.
:returns: a tuple of the the graph's html and javascript.
"""
changes = changes.order_by("time")
intervals = []
intervals_solid = []
intervals_wet = []
last_change = changes.first()
for change in changes[1:]:
interval = change.time - last_change.time
if interval.total_seconds() > 0:
intervals.append(interval)
if change.solid:
intervals_solid.append(interval)
if change.wet:
intervals_wet.append(interval)
last_change = change
trace_solid = go.Scatter(
name=_("Solid"),
line=dict(shape="spline"),
x=list(changes.values_list("time", flat=True)),
y=[i.total_seconds() / 3600 for i in intervals_solid],
hoverinfo="text",
text=[_duration_string_hms(i) for i in intervals_solid],
)
trace_wet = go.Scatter(
name=_("Wet"),
line=dict(shape="spline"),
x=list(changes.values_list("time", flat=True)),
y=[i.total_seconds() / 3600 for i in intervals_wet],
hoverinfo="text",
text=[_duration_string_hms(i) for i in intervals_wet],
)
trace_total = go.Scatter(
name=_("Total"),
line=dict(shape="spline"),
x=list(changes.values_list("time", flat=True)),
y=[i.total_seconds() / 3600 for i in intervals],
hoverinfo="text",
text=[_duration_string_hms(i) for i in intervals],
)
layout_args = utils.default_graph_layout_options()
layout_args["barmode"] = "stack"
layout_args["title"] = _("<b>Diaper Change Intervals</b>")
layout_args["xaxis"]["title"] = _("Date")
layout_args["xaxis"]["rangeselector"] = utils.rangeselector_date()
layout_args["yaxis"]["title"] = _("Interval (hours)")
fig = go.Figure(
{
"data": [trace_solid, trace_wet, trace_total],
"layout": go.Layout(**layout_args),
}
)
output = plotly.plot(
fig,
output_type="div",
include_plotlyjs=False,
config={"locale": get_language()},
)
return utils.split_graph_output(output)
def _duration_string_hms(duration):
"""
Format a duration string with hours, minutes and seconds. This is
intended to fit better in smaller spaces on a graph.
:returns: a string of the form Xm.
"""
h, m, s = duration_parts(duration)
return "{}h{}m{}s".format(h, m, s)

View File

@ -0,0 +1,58 @@
# -*- coding: utf-8 -*-
from django.db.models import Count, Sum
from django.db.models.functions import TruncDate
from django.utils.translation import gettext as _
import plotly.offline as plotly
import plotly.graph_objs as go
from core.utils import duration_parts
from reports import utils
def feeding_intervals(instances):
"""
Create a graph showing intervals of feeding instances over time.
:param instances: a QuerySet of Feeding instances.
:returns: a tuple of the the graph's html and javascript.
"""
totals = instances.annotate(count=Count("id")).order_by("start")
intervals = []
last_feeding = totals.first()
for feeding in totals[1:]:
interval = feeding.start - last_feeding.start
if interval.total_seconds() > 0:
intervals.append(interval)
last_feeding = feeding
trace_avg = go.Scatter(
name=_("Interval"),
line=dict(shape="spline"),
x=list(totals.values_list("start", flat=True)),
y=[i.total_seconds() / 3600 for i in intervals],
hoverinfo="text",
text=[_duration_string_hms(i) for i in intervals],
)
layout_args = utils.default_graph_layout_options()
layout_args["title"] = _("<b>Feeding intervals</b>")
layout_args["xaxis"]["title"] = _("Date")
layout_args["xaxis"]["rangeselector"] = utils.rangeselector_date()
layout_args["yaxis"]["title"] = _("Feeding interval (hours)")
fig = go.Figure({"data": [trace_avg], "layout": go.Layout(**layout_args)})
output = plotly.plot(fig, output_type="div", include_plotlyjs=False)
return utils.split_graph_output(output)
def _duration_string_hms(duration):
"""
Format a duration string with hours, minutes and seconds. This is
intended to fit better in smaller spaces on a graph.
:returns: a string of the form Xm.
"""
h, m, s = duration_parts(duration)
return "{}h{}m{}s".format(h, m, s)

View File

@ -0,0 +1,9 @@
{% extends 'reports/report_base.html' %}
{% load i18n %}
{% block title %}{% trans "Diaper Change Intervals" %} - {{ object }}{% endblock %}
{% block breadcrumbs %}
{{ block.super }}
<li class="breadcrumb-item active" aria-current="page">{% trans "Diaper Change Intervals" %}</li>
{% endblock %}

View File

@ -0,0 +1,9 @@
{% extends 'reports/report_base.html' %}
{% load i18n %}
{% block title %}{% trans "Feeding Intervals" %} - {{ object }}{% endblock %}
{% block breadcrumbs %}
{{ block.super }}
<li class="breadcrumb-item active" aria-current="page">{% trans "Feeding Intervals" %}</li>
{% endblock %}

View File

@ -20,10 +20,12 @@
<a href="{% url 'reports:report-diaperchange-amounts-child' object.slug %}" class="list-group-item list-group-item-action">{% trans "Diaper Change Amounts" %}</a>
<a href="{% url 'reports:report-diaperchange-types-child' object.slug %}" class="list-group-item list-group-item-action">{% trans "Diaper Change Types" %}</a>
<a href="{% url 'reports:report-diaperchange-lifetimes-child' object.slug %}" class="list-group-item list-group-item-action">{% trans "Diaper Lifetimes" %}</a>
<a href="{% url 'reports:report-diaperchange-intervals-child' object.slug %}" class="list-group-item list-group-item-action">{% trans "Diaper Intervals" %}</a>
<a href="{% url 'reports:report-feeding-amounts-child' object.slug %}" class="list-group-item list-group-item-action">{% trans "Feeding Amounts" %}</a>
<a href="{% url 'reports:report-feeding-duration-child' object.slug %}" class="list-group-item list-group-item-action">{% trans "Feeding Durations (Average)" %}</a>
<a href="{% url 'reports:report-head-circumference-change-child' object.slug %}" class="list-group-item list-group-item-action">{% trans "Head Circumference" %}</a>
<a href="{% url 'reports:report-height-change-child' object.slug %}" class="list-group-item list-group-item-action">{% trans "Height" %}</a>
<a href="{% url 'reports:report-feeding-intervals-child' object.slug %}" class="list-group-item list-group-item-action">{% trans "Feeding Intervals" %}</a>
<a href="{% url 'reports:report-pumping-amounts-child' object.slug %}" class="list-group-item list-group-item-action">{% trans "Pumping Amounts" %}</a>
<a href="{% url 'reports:report-sleep-pattern-child' object.slug %}" class="list-group-item list-group-item-action">{% trans "Sleep Pattern" %}</a>
<a href="{% url 'reports:report-sleep-totals-child' object.slug %}" class="list-group-item list-group-item-action">{% trans "Sleep Totals" %}</a>

View File

@ -46,11 +46,15 @@ class ViewsTestCase(TestCase):
self.assertEqual(page.status_code, 200)
page = self.c.get("{}/changes/types/".format(base_url))
self.assertEqual(page.status_code, 200)
page = self.c.get("{}/changes/intervals/".format(base_url))
self.assertEqual(page.status_code, 200)
page = self.c.get("{}/feeding/amounts/".format(base_url))
self.assertEqual(page.status_code, 200)
page = self.c.get("{}/feeding/duration/".format(base_url))
self.assertEqual(page.status_code, 200)
page = self.c.get("{}/feeding/intervals/".format(base_url))
self.assertEqual(page.status_code, 200)
page = self.c.get("{}/head-circumference/head-circumference/".format(base_url))
self.assertEqual(page.status_code, 200)

View File

@ -31,6 +31,11 @@ urlpatterns = [
views.DiaperChangeTypesChildReport.as_view(),
name="report-diaperchange-types-child",
),
path(
"children/<str:slug>/reports/changes/intervals/",
views.DiaperChangeIntervalsChildReport.as_view(),
name="report-diaperchange-intervals-child",
),
path(
"children/<str:slug>/reports/feeding/amounts/",
views.FeedingAmountsChildReport.as_view(),
@ -56,6 +61,11 @@ urlpatterns = [
views.PumpingAmounts.as_view(),
name="report-pumping-amounts-child",
),
path(
"children/<str:slug>/reports/feeding/intervals/",
views.FeedingIntervalsChildReport.as_view(),
name="report-feeding-intervals-child",
),
path(
"children/<str:slug>/reports/sleep/pattern/",
views.SleepPatternChildReport.as_view(),

View File

@ -80,7 +80,7 @@ class DiaperChangeTypesChildReport(PermissionRequiredMixin, DetailView):
model = models.Child
permission_required = ("core.view_child",)
template_name = "reports/diaperchange_types.html"
template_name = "reports/diaperchange_intervals.html"
def get_context_data(self, **kwargs):
context = super(DiaperChangeTypesChildReport, self).get_context_data(**kwargs)
@ -91,6 +91,26 @@ class DiaperChangeTypesChildReport(PermissionRequiredMixin, DetailView):
return context
class DiaperChangeIntervalsChildReport(PermissionRequiredMixin, DetailView):
"""
Graph of diaper change intervals.
"""
model = models.Child
permission_required = ("core.view_child",)
template_name = "reports/diaperchange_intervals.html"
def get_context_data(self, **kwargs):
context = super(DiaperChangeIntervalsChildReport, self).get_context_data(
**kwargs
)
child = context["object"]
changes = models.DiaperChange.objects.filter(child=child)
if changes:
context["html"], context["js"] = graphs.diaperchange_intervals(changes)
return context
class FeedingAmountsChildReport(PermissionRequiredMixin, DetailView):
"""
Graph of daily feeding amounts over time.
@ -137,6 +157,23 @@ class FeedingDurationChildReport(PermissionRequiredMixin, DetailView):
return context
class FeedingIntervalsChildReport(PermissionRequiredMixin, DetailView):
"""
Graph of diaper change intervals.
"""
model = models.Child
permission_required = ("core.view_child",)
template_name = "reports/feeding_intervals.html"
def get_context_data(self, **kwargs):
context = super(FeedingIntervalsChildReport, self).get_context_data(**kwargs)
child = context["object"]
instances = models.Feeding.objects.filter(child=child)
if instances:
context["html"], context["js"] = graphs.feeding_intervals(instances)
return context
class HeadCircumferenceChangeChildReport(PermissionRequiredMixin, DetailView):
"""
Graph of head circumference change over time.