Skip to content

Commit

Permalink
feat: weight percentiles report
Browse files Browse the repository at this point in the history
  • Loading branch information
barakplasma committed Sep 20, 2023
1 parent b8b0f7f commit af1d179
Show file tree
Hide file tree
Showing 12 changed files with 3,922 additions and 16 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
# Generated by Django 4.2.5 on 2023-09-20 10:56

from django.db import migrations, models, transaction
from django.utils import dateparse
import csv


def add_to_ORM(WeightPercentile: models.Model, alias, filepath: str, sex: str):
with open(filepath) as csvfile:
weight_reader = csv.DictReader(csvfile)
data_list = []
for row in weight_reader:
data_list.append(
WeightPercentile(
age_in_days=dateparse.parse_duration(f'P{row["Age"]}D'),
sex=sex,
p3_weight=row["P3"],
p15_weight=row["P15"],
p50_weight=row["P50"],
p85_weight=row["P85"],
p97_weight=row["P97"],
)
)
WeightPercentile.objects.using(alias).bulk_create(data_list, batch_size=50)


def insert_weight_percentiles(apps, schema_editor):
WeightPercentile: models.Model = apps.get_model("core", "WeightPercentile")
db_alias = schema_editor.connection.alias
with transaction.atomic():
add_to_ORM(
WeightPercentile, db_alias, "core/migrations/weight_percentile_boys.csv", "boy"
)
with transaction.atomic():
add_to_ORM(
WeightPercentile, db_alias, "core/migrations/weight_percentile_girls.csv", "girl"
)

def remove_weight_percentiles(apps, schema_editor):
WeightPercentile: models.Model = apps.get_model("core", "WeightPercentile")
db_alias = schema_editor.connection.alias
with transaction.atomic():
WeightPercentile.objects.using(db_alias).all().delete()

class Migration(migrations.Migration):
dependencies = [
("core", "0029_alter_pumping_options_remove_pumping_time_and_more"),
]

operations = [
migrations.CreateModel(
name="WeightPercentile",
fields=[
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("age_in_days", models.DurationField()),
("p3_weight", models.FloatField()),
("p15_weight", models.FloatField()),
("p50_weight", models.FloatField()),
("p85_weight", models.FloatField()),
("p97_weight", models.FloatField()),
(
"sex",
models.CharField(
choices=[("girl", "Girl"), ("boy", "Boy")], max_length=255
),
),
],
),
migrations.AddConstraint(
model_name="weightpercentile",
constraint=models.UniqueConstraint(
fields=("age_in_days", "sex"), name="unique_age_sex"
),
),
migrations.RunPython(insert_weight_percentiles, remove_weight_percentiles),
]
1,858 changes: 1,858 additions & 0 deletions core/migrations/weight_percentile_boys.csv

Large diffs are not rendered by default.

1,858 changes: 1,858 additions & 0 deletions core/migrations/weight_percentile_girls.csv

Large diffs are not rendered by default.

28 changes: 28 additions & 0 deletions core/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -699,3 +699,31 @@ def __str__(self):

def clean(self):
validate_date(self.date, "date")


class WeightPercentile(models.Model):
model_name = "weight percentile"
age_in_days = models.DurationField(null=False)
p3_weight = models.FloatField(null=False)
p15_weight = models.FloatField(null=False)
p50_weight = models.FloatField(null=False)
p85_weight = models.FloatField(null=False)
p97_weight = models.FloatField(null=False)
sex = models.CharField(
null=False,
max_length=255,
choices=[
("girl", _("Girl")),
("boy", _("Boy")),
],
)

class Meta:
constraints = [
models.UniqueConstraint(
fields=["age_in_days", "sex"], name="unique_age_sex"
)
]

def __str__(self):
return f"Sex: {self.sex}, Age: {self.age_in_days} days, p3: {self.p3_weight} kg, p15: {self.p15_weight} kg, p50: {self.p50_weight} kg, p85: {self.p85_weight} kg, p97: {self.p97_weight} kg"
1 change: 1 addition & 0 deletions devenv.nix
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
pkgs.pipenv
pkgs.nodejs_18
pkgs.nodePackages.gulp
pkgs.sqlite
];

# https://devenv.sh/scripts/
Expand Down
8 changes: 4 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
"jquery": "^3.7.0",
"masonry-layout": "^4.2.2",
"npm-force-resolutions": "^0.0.10",
"plotly.js": "^2.24.3",
"plotly.js": "^2.26.0",
"pulltorefreshjs": "^0.1.22",
"pump": "^3.0.0",
"sass": "^1.63.6",
Expand Down
78 changes: 71 additions & 7 deletions reports/graphs/weight_change.py
Original file line number Diff line number Diff line change
@@ -1,34 +1,98 @@
# -*- coding: utf-8 -*-
from datetime import datetime
from django.utils.translation import gettext as _
from django.db.models.manager import BaseManager

import plotly.offline as plotly
import plotly.graph_objs as go

from reports import utils


def weight_change(objects):
def weight_change(
actual_weights: BaseManager, percentile_weights: BaseManager, birthday: datetime
):
"""
Create a graph showing weight over time.
:param objects: a QuerySet of Weight instances.
:param actual_weights: a QuerySet of Weight instances.
:param percentile_weights: a QuerySet of Weight Percentile instances.
:param birthday: a datetime of the child's birthday
:returns: a tuple of the the graph's html and javascript.
"""
objects = objects.order_by("-date")
actual_weights = actual_weights.order_by("-date")

trace = go.Scatter(
weighing_dates: list[datetime] = list(actual_weights.values_list("date", flat=True))
measured_weights = list(actual_weights.values_list("weight", flat=True))

actual_weights_trace = go.Scatter(
name=_("Weight"),
x=list(objects.values_list("date", flat=True)),
y=list(objects.values_list("weight", flat=True)),
x=weighing_dates,
y=measured_weights,
fill="tozeroy",
mode="lines+markers",
)

if percentile_weights:
dates = list(
map(
lambda timedelta: birthday + timedelta,
percentile_weights.values_list("age_in_days", flat=True),
)
)

percentile_weight_3_trace = go.Scatter(
name=_("P3"),
x=dates,
y=list(percentile_weights.values_list("p3_weight", flat=True)),
line={"color": "red"},
)
percentile_weight_15_trace = go.Scatter(
name=_("P15"),
x=dates,
y=list(percentile_weights.values_list("p15_weight", flat=True)),
line={"color": "orange"},
)
percentile_weight_50_trace = go.Scatter(
name=_("P50"),
x=dates,
y=list(percentile_weights.values_list("p50_weight", flat=True)),
line={"color": "green"},
)
percentile_weight_85_trace = go.Scatter(
name=_("P85"),
x=dates,
y=list(percentile_weights.values_list("p85_weight", flat=True)),
line={"color": "orange"},
)
percentile_weight_97_trace = go.Scatter(
name=_("P97"),
x=dates,
y=list(percentile_weights.values_list("p97_weight", flat=True)),
line={"color": "red"},
)

data = [
actual_weights_trace,
]
layout_args = utils.default_graph_layout_options()
layout_args["barmode"] = "stack"
layout_args["title"] = _("<b>Weight</b>")
layout_args["xaxis"]["title"] = _("Date")
layout_args["xaxis"]["rangeselector"] = utils.rangeselector_date()
layout_args["yaxis"]["title"] = _("Weight")
if percentile_weights:
layout_args["xaxis"]["range"] = [birthday, datetime.now()]
layout_args["yaxis"]["range"] = [0, max(measured_weights) * 1.5]
data.extend(
[
percentile_weight_97_trace,
percentile_weight_85_trace,
percentile_weight_50_trace,
percentile_weight_15_trace,
percentile_weight_3_trace,
]
)

fig = go.Figure({"data": [trace], "layout": go.Layout(**layout_args)})
fig = go.Figure({"data": data, "layout": go.Layout(**layout_args)})
output = plotly.plot(fig, output_type="div", include_plotlyjs=False)
return utils.split_graph_output(output)
2 changes: 2 additions & 0 deletions reports/templates/reports/report_list.html
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ <h1>Reports</h1>
<a href="{% url 'reports:report-temperature-change-child' object.slug %}" class="list-group-item list-group-item-action">{% trans "Temperature" %}</a>
<a href="{% url 'reports:report-tummy-time-duration-child' object.slug %}" class="list-group-item list-group-item-action">{% trans "Tummy Time Durations (Sum)" %}</a>
<a href="{% url 'reports:report-weight-change-child' object.slug %}" class="list-group-item list-group-item-action">{% trans "Weight" %}</a>
<a href="{% url 'reports:report-weight-change-child-sex' object.slug 'boy' %}" class="list-group-item list-group-item-action">{% trans "WHO Weight Percentiles for Boys in kg" %}</a>
<a href="{% url 'reports:report-weight-change-child-sex' object.slug 'girl' %}" class="list-group-item list-group-item-action">{% trans "WHO Weight Percentiles for Girls in kg" %}</a>
</div>
</div>
{% endblock %}
5 changes: 5 additions & 0 deletions reports/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,4 +81,9 @@
views.WeightChangeChildReport.as_view(),
name="report-weight-change-child",
),
path(
"children/<str:slug>/reports/weight/<sex>/",
views.WeightChangeChildReport.as_view(),
name="report-weight-change-child-sex",
),
]
12 changes: 9 additions & 3 deletions reports/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -295,7 +295,13 @@ class WeightChangeChildReport(PermissionRequiredMixin, DetailView):
def get_context_data(self, **kwargs):
context = super(WeightChangeChildReport, self).get_context_data(**kwargs)
child = context["object"]
objects = models.Weight.objects.filter(child=child)
if objects:
context["html"], context["js"] = graphs.weight_change(objects)
birthday = child.birth_date
actual_weights = models.Weight.objects.filter(child=child)
percentile_weights = models.WeightPercentile.objects.filter(
sex=self.kwargs.get("sex")
)
if actual_weights:
context["html"], context["js"] = graphs.weight_change(
actual_weights, percentile_weights, birthday
)
return context
2 changes: 1 addition & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ openpyxl==3.1.2
packaging==23.1 ; python_version >= '3.7'
pilkit==2.0
pillow===9.5.0
plotly==5.15.0
plotly==5.17.0
psycopg2-binary==2.9.6
pydantic==2.1.1 ; python_version >= '3.7'
pydantic-core==2.4.0 ; python_version >= '3.7'
Expand Down

0 comments on commit af1d179

Please sign in to comment.