mirror of https://github.com/snachodog/mybuddy.git
Refactor reports graphs in to separate files.
This commit is contained in:
parent
ecfa974e20
commit
c5612b102d
|
@ -1,364 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from collections import OrderedDict
|
||||
|
||||
from django.db.models import Count, Case, When
|
||||
from django.db.models.functions import TruncDate
|
||||
from django.utils import timezone
|
||||
|
||||
import pandas as pd
|
||||
import plotly.offline as plotly
|
||||
import plotly.graph_objs as go
|
||||
|
||||
from core.models import DiaperChange, Feeding, Sleep, TummyTime
|
||||
from core.utils import duration_string, duration_parts
|
||||
|
||||
from . import utils
|
||||
|
||||
|
||||
def diaperchange_lifetimes(changes):
|
||||
"""Create a graph showing how long diapers last (time between changes).
|
||||
"""
|
||||
durations = []
|
||||
last_change = changes.first()
|
||||
for change in changes[1:]:
|
||||
duration = change.time - last_change.time
|
||||
if duration.seconds > 0:
|
||||
durations.append(duration)
|
||||
last_change = change
|
||||
|
||||
trace = go.Box(
|
||||
y=[round(d.seconds/3600, 2) for d in durations],
|
||||
name='Changes',
|
||||
jitter=0.3,
|
||||
pointpos=-1.8,
|
||||
boxpoints='all'
|
||||
)
|
||||
|
||||
layout_args = utils.default_graph_layout_options()
|
||||
layout_args['height'] = 800
|
||||
layout_args['title'] = '<b>Diaper Lifetimes</b>'
|
||||
layout_args['yaxis']['title'] = 'Time between changes (hours)'
|
||||
layout_args['yaxis']['zeroline'] = False
|
||||
layout_args['yaxis']['dtick'] = 1
|
||||
|
||||
fig = go.Figure({
|
||||
'data': [trace],
|
||||
'layout': go.Layout(**layout_args)
|
||||
})
|
||||
output = plotly.plot(fig, output_type='div', include_plotlyjs=False)
|
||||
return utils.split_graph_output(output)
|
||||
|
||||
|
||||
def diaperchange_types(changes):
|
||||
"""Create a graph showing types of totals for diaper changes.
|
||||
"""
|
||||
changes = changes.annotate(date=TruncDate('time'))\
|
||||
.values('date') \
|
||||
.annotate(wet_count=Count(Case(When(wet=True, then=1)))) \
|
||||
.annotate(solid_count=Count(Case(When(solid=True, then=1)))) \
|
||||
.annotate(total=Count('id')) \
|
||||
.order_by('-date')
|
||||
|
||||
solid_trace = go.Scatter(
|
||||
mode='markers',
|
||||
name='Solid',
|
||||
x=list(changes.values_list('date', flat=True)),
|
||||
y=list(changes.values_list('solid_count', flat=True)),
|
||||
)
|
||||
wet_trace = go.Scatter(
|
||||
mode='markers',
|
||||
name='Wet',
|
||||
x=list(changes.values_list('date', flat=True)),
|
||||
y=list(changes.values_list('wet_count', flat=True))
|
||||
)
|
||||
total_trace = go.Scatter(
|
||||
name='Total',
|
||||
x=list(changes.values_list('date', flat=True)),
|
||||
y=list(changes.values_list('total', flat=True))
|
||||
)
|
||||
|
||||
layout_args = utils.default_graph_layout_options()
|
||||
layout_args['barmode'] = 'stack'
|
||||
layout_args['title'] = '<b>Diaper Change Types</b>'
|
||||
layout_args['xaxis']['title'] = 'Date'
|
||||
layout_args['xaxis']['rangeselector'] = utils.rangeselector_date()
|
||||
layout_args['yaxis']['title'] = 'Number of changes'
|
||||
|
||||
fig = go.Figure({
|
||||
'data': [solid_trace, wet_trace, total_trace],
|
||||
'layout': go.Layout(**layout_args)
|
||||
})
|
||||
output = plotly.plot(fig, output_type='div', include_plotlyjs=False)
|
||||
return utils.split_graph_output(output)
|
||||
|
||||
|
||||
def sleep_totals(instances):
|
||||
"""Create a graph showing total time sleeping for each day.
|
||||
"""
|
||||
totals = {}
|
||||
for instance in instances:
|
||||
start = timezone.localtime(instance.start)
|
||||
end = timezone.localtime(instance.end)
|
||||
if start.date() not in totals.keys():
|
||||
totals[start.date()] = timezone.timedelta(seconds=0)
|
||||
if end.date() not in totals.keys():
|
||||
totals[end.date()] = timezone.timedelta(seconds=0)
|
||||
|
||||
# Account for dates crossing midnight.
|
||||
if start.date() != end.date():
|
||||
totals[start.date()] += end.replace(
|
||||
year=start.year, month=start.month, day=start.day,
|
||||
hour=23, minute=59, second=59) - start
|
||||
totals[end.date()] += end - start.replace(
|
||||
year=end.year, month=end.month, day=end.day, hour=0, minute=0,
|
||||
second=0)
|
||||
else:
|
||||
totals[start.date()] += instance.duration
|
||||
|
||||
trace = go.Bar(
|
||||
name='Total sleep',
|
||||
x=list(totals.keys()),
|
||||
y=[td.seconds/3600 for td in totals.values()],
|
||||
hoverinfo='text',
|
||||
textposition='outside',
|
||||
text=[_duration_string_short(td) for td in totals.values()]
|
||||
)
|
||||
|
||||
layout_args = utils.default_graph_layout_options()
|
||||
layout_args['barmode'] = 'stack'
|
||||
layout_args['title'] = '<b>Sleep Totals</b>'
|
||||
layout_args['xaxis']['title'] = 'Date'
|
||||
layout_args['xaxis']['rangeselector'] = utils.rangeselector_date()
|
||||
layout_args['yaxis']['title'] = 'Hours of sleep'
|
||||
|
||||
fig = go.Figure({
|
||||
'data': [trace],
|
||||
'layout': go.Layout(**layout_args)
|
||||
})
|
||||
output = plotly.plot(fig, output_type='div', include_plotlyjs=False)
|
||||
return utils.split_graph_output(output)
|
||||
|
||||
|
||||
def _duration_string_short(duration):
|
||||
"""Format a "short" duration string without seconds precision. This is
|
||||
intended to fit better in smaller spaces on a graph.
|
||||
"""
|
||||
h, m, s = duration_parts(duration)
|
||||
return '{}h{}m'.format(h, m)
|
||||
|
||||
|
||||
def sleep_pattern(instances):
|
||||
"""Create a graph showing blocked out periods of sleep during each day.
|
||||
"""
|
||||
# TODO: Simplify this using the bar charts "base" property.
|
||||
y_df = pd.DataFrame()
|
||||
text_df = pd.DataFrame()
|
||||
last_end_time = None
|
||||
adjustment = None
|
||||
df_index = 0
|
||||
for instance in instances:
|
||||
start_time = timezone.localtime(instance.start)
|
||||
end_time = timezone.localtime(instance.end)
|
||||
start_date = start_time.date().isoformat()
|
||||
duration = instance.duration
|
||||
|
||||
# Check if the previous entry crossed midnight (see below).
|
||||
if adjustment:
|
||||
# Fake (0) entry to keep the color switching logic working.
|
||||
df_index = _add_sleep_entry(
|
||||
y_df, text_df, 0, adjustment['column'], 0)
|
||||
# Real adjustment entry.
|
||||
df_index = _add_sleep_entry(
|
||||
y_df,
|
||||
text_df,
|
||||
df_index,
|
||||
adjustment['column'],
|
||||
adjustment['duration'].seconds/60,
|
||||
'Asleep {} ({} to {})'.format(
|
||||
duration_string(adjustment['duration']),
|
||||
adjustment['start_time'].strftime('%I:%M %p'),
|
||||
adjustment['end_time'].strftime('%I:%M %p')
|
||||
)
|
||||
)
|
||||
last_end_time = timezone.localtime(adjustment['end_time'])
|
||||
adjustment = None
|
||||
|
||||
# If the dates do not match, set up an adjustment for the next day.
|
||||
if end_time.date() != start_time.date():
|
||||
adj_start_time = end_time.replace(hour=0, minute=0, second=0)
|
||||
adjustment = {
|
||||
'column': end_time.date().isoformat(),
|
||||
'start_time': adj_start_time,
|
||||
'end_time': end_time,
|
||||
'duration': end_time - adj_start_time
|
||||
}
|
||||
|
||||
# Adjust end_time for the current entry.
|
||||
end_time = end_time.replace(
|
||||
year=start_time.year, month=start_time.month,
|
||||
day=start_time.day, hour=23, minute=59, second=0)
|
||||
duration = end_time - start_time
|
||||
|
||||
if start_date not in y_df:
|
||||
last_end_time = start_time.replace(hour=0, minute=0, second=0)
|
||||
|
||||
# Awake time.
|
||||
df_index = _add_sleep_entry(
|
||||
y_df,
|
||||
text_df,
|
||||
df_index,
|
||||
start_date,
|
||||
(start_time - last_end_time).seconds/60
|
||||
)
|
||||
|
||||
# Asleep time.
|
||||
df_index = _add_sleep_entry(
|
||||
y_df,
|
||||
text_df,
|
||||
df_index,
|
||||
start_date,
|
||||
duration.seconds/60,
|
||||
'Asleep {} ({} to {})'.format(
|
||||
duration_string(duration),
|
||||
start_time.strftime('%I:%M %p'),
|
||||
end_time.strftime('%I:%M %p')
|
||||
)
|
||||
)
|
||||
|
||||
last_end_time = end_time
|
||||
|
||||
dates = list(y_df)
|
||||
traces = []
|
||||
color = 'rgba(255, 255, 255, 0)'
|
||||
for index, row in y_df.iterrows():
|
||||
traces.append(go.Bar(
|
||||
x=dates,
|
||||
y=row,
|
||||
text=text_df.ix[index],
|
||||
hoverinfo='text',
|
||||
marker={'color': color},
|
||||
showlegend=False,
|
||||
))
|
||||
if color == 'rgba(255, 255, 255, 0)':
|
||||
color = 'rgb(35, 110, 150)'
|
||||
else:
|
||||
color = 'rgba(255, 255, 255, 0)'
|
||||
|
||||
layout_args = utils.default_graph_layout_options()
|
||||
layout_args['margin']['b'] = 100
|
||||
|
||||
layout_args['barmode'] = 'stack'
|
||||
layout_args['hovermode'] = 'closest'
|
||||
layout_args['title'] = '<b>Sleep Pattern</b>'
|
||||
layout_args['height'] = 800
|
||||
|
||||
layout_args['xaxis']['title'] = 'Date'
|
||||
layout_args['xaxis']['tickangle'] = -65
|
||||
layout_args['xaxis']['rangeselector'] = utils.rangeselector_date()
|
||||
|
||||
start = timezone.localtime().strptime('12:00 AM', '%I:%M %p')
|
||||
ticks = OrderedDict()
|
||||
ticks[0] = start.strftime('%I:%M %p')
|
||||
for i in range(30, 60*24, 30):
|
||||
ticks[i] = (start + timezone.timedelta(minutes=i)).strftime('%I:%M %p')
|
||||
|
||||
layout_args['yaxis']['title'] = 'Time of day'
|
||||
layout_args['yaxis']['rangemode'] = 'tozero'
|
||||
layout_args['yaxis']['tickmode'] = 'array'
|
||||
layout_args['yaxis']['tickvals'] = list(ticks.keys())
|
||||
layout_args['yaxis']['ticktext'] = list(ticks.values())
|
||||
layout_args['yaxis']['tickfont'] = {'size': 10}
|
||||
|
||||
fig = go.Figure({
|
||||
'data': traces,
|
||||
'layout': go.Layout(**layout_args)
|
||||
})
|
||||
output = plotly.plot(fig, output_type='div', include_plotlyjs=False)
|
||||
return utils.split_graph_output(output)
|
||||
|
||||
|
||||
def _add_sleep_entry(y_df, text_df, index, column, duration, text=''):
|
||||
"""Create a duration and text description entry in a DataFrame and return
|
||||
the next index on success.
|
||||
"""
|
||||
if column not in y_df:
|
||||
y_df.assign(**{column: 0 in range(0, len(y_df.index))})
|
||||
text_df.assign(**{column: 0 in range(0, len(text_df.index))})
|
||||
index = 0
|
||||
|
||||
y_df.set_value(index, column, duration)
|
||||
text_df.set_value(index, column, text)
|
||||
return index + 1
|
||||
|
||||
|
||||
def timeline(child, date):
|
||||
"""Create a time-sorted dictionary for all events for a child.
|
||||
"""
|
||||
min_date = date
|
||||
max_date = date.replace(hour=23, minute=59, second=59)
|
||||
events = []
|
||||
|
||||
instances = DiaperChange.objects.filter(child=child).filter(
|
||||
time__range=(min_date, max_date)).order_by('-time')
|
||||
for instance in instances:
|
||||
events.append({
|
||||
'time': timezone.localtime(instance.time),
|
||||
'event': '{} had a diaper change.'.format(child.first_name),
|
||||
'model_name': instance.model_name,
|
||||
})
|
||||
|
||||
instances = Feeding.objects.filter(child=child).filter(
|
||||
start__range=(min_date, max_date)).order_by('-start')
|
||||
for instance in instances:
|
||||
events.append({
|
||||
'time': timezone.localtime(instance.start),
|
||||
'event': '{} started feeding.'.format(instance.child.first_name),
|
||||
'model_name': instance.model_name,
|
||||
'type': 'start'
|
||||
})
|
||||
events.append({
|
||||
'time': timezone.localtime(instance.end),
|
||||
'event': '{} finished feeding.'.format(instance.child.first_name),
|
||||
'model_name': instance.model_name,
|
||||
'type': 'end'
|
||||
})
|
||||
|
||||
instances = Sleep.objects.filter(child=child).filter(
|
||||
start__range=(min_date, max_date)).order_by('-start')
|
||||
for instance in instances:
|
||||
events.append({
|
||||
'time': timezone.localtime(instance.start),
|
||||
'event': '{} fell asleep.'.format(instance.child.first_name),
|
||||
'model_name': instance.model_name,
|
||||
'type': 'start'
|
||||
})
|
||||
events.append({
|
||||
'time': timezone.localtime(instance.end),
|
||||
'event': '{} woke up.'.format(instance.child.first_name),
|
||||
'model_name': instance.model_name,
|
||||
'type': 'end'
|
||||
})
|
||||
|
||||
instances = TummyTime.objects.filter(child=child).filter(
|
||||
start__range=(min_date, max_date)).order_by('-start')
|
||||
for instance in instances:
|
||||
events.append({
|
||||
'time': timezone.localtime(instance.start),
|
||||
'event': '{} started tummy time!'.format(
|
||||
instance.child.first_name),
|
||||
'model_name': instance.model_name,
|
||||
'type': 'start'
|
||||
})
|
||||
events.append({
|
||||
'time': timezone.localtime(instance.end),
|
||||
'event': '{} finished tummy time.'.format(
|
||||
instance.child.first_name),
|
||||
'model_name': instance.model_name,
|
||||
'type': 'end'
|
||||
})
|
||||
|
||||
events.sort(key=lambda x: x['time'], reverse=True)
|
||||
|
||||
return events
|
|
@ -0,0 +1,5 @@
|
|||
from .diaperchange_lifetimes import diaperchange_lifetimes # NOQA
|
||||
from .diaperchange_types import diaperchange_types # NOQA
|
||||
from .sleep_pattern import sleep_pattern # NOQA
|
||||
from .sleep_totals import sleep_totals # NOQA
|
||||
from .timeline import timeline # NOQA
|
|
@ -0,0 +1,45 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import plotly.offline as plotly
|
||||
import plotly.graph_objs as go
|
||||
|
||||
from reports import utils
|
||||
|
||||
|
||||
def diaperchange_lifetimes(changes):
|
||||
"""
|
||||
Create a graph showing how long diapers last (time between 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')
|
||||
durations = []
|
||||
last_change = changes.first()
|
||||
for change in changes[1:]:
|
||||
duration = change.time - last_change.time
|
||||
if duration.seconds > 0:
|
||||
durations.append(duration)
|
||||
last_change = change
|
||||
|
||||
trace = go.Box(
|
||||
y=[round(d.seconds/3600, 2) for d in durations],
|
||||
name='Changes',
|
||||
jitter=0.3,
|
||||
pointpos=-1.8,
|
||||
boxpoints='all'
|
||||
)
|
||||
|
||||
layout_args = utils.default_graph_layout_options()
|
||||
layout_args['height'] = 800
|
||||
layout_args['title'] = '<b>Diaper Lifetimes</b>'
|
||||
layout_args['yaxis']['title'] = 'Time between changes (hours)'
|
||||
layout_args['yaxis']['zeroline'] = False
|
||||
layout_args['yaxis']['dtick'] = 1
|
||||
|
||||
fig = go.Figure({
|
||||
'data': [trace],
|
||||
'layout': go.Layout(**layout_args)
|
||||
})
|
||||
output = plotly.plot(fig, output_type='div', include_plotlyjs=False)
|
||||
return utils.split_graph_output(output)
|
|
@ -0,0 +1,56 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db.models import Count, Case, When
|
||||
from django.db.models.functions import TruncDate
|
||||
|
||||
import plotly.offline as plotly
|
||||
import plotly.graph_objs as go
|
||||
|
||||
from reports import utils
|
||||
|
||||
|
||||
def diaperchange_types(changes):
|
||||
"""
|
||||
Create a graph showing types of totals for diaper changes.
|
||||
:param changes: a QuerySet of Diaper Change instances.
|
||||
:returns: a tuple of the the graph's html and javascript.
|
||||
"""
|
||||
changes = changes.annotate(date=TruncDate('time'))\
|
||||
.values('date') \
|
||||
.annotate(wet_count=Count(Case(When(wet=True, then=1)))) \
|
||||
.annotate(solid_count=Count(Case(When(solid=True, then=1)))) \
|
||||
.annotate(total=Count('id')) \
|
||||
.order_by('-date')
|
||||
|
||||
solid_trace = go.Scatter(
|
||||
mode='markers',
|
||||
name='Solid',
|
||||
x=list(changes.values_list('date', flat=True)),
|
||||
y=list(changes.values_list('solid_count', flat=True)),
|
||||
)
|
||||
wet_trace = go.Scatter(
|
||||
mode='markers',
|
||||
name='Wet',
|
||||
x=list(changes.values_list('date', flat=True)),
|
||||
y=list(changes.values_list('wet_count', flat=True))
|
||||
)
|
||||
total_trace = go.Scatter(
|
||||
name='Total',
|
||||
x=list(changes.values_list('date', flat=True)),
|
||||
y=list(changes.values_list('total', flat=True))
|
||||
)
|
||||
|
||||
layout_args = utils.default_graph_layout_options()
|
||||
layout_args['barmode'] = 'stack'
|
||||
layout_args['title'] = '<b>Diaper Change Types</b>'
|
||||
layout_args['xaxis']['title'] = 'Date'
|
||||
layout_args['xaxis']['rangeselector'] = utils.rangeselector_date()
|
||||
layout_args['yaxis']['title'] = 'Number of changes'
|
||||
|
||||
fig = go.Figure({
|
||||
'data': [solid_trace, wet_trace, total_trace],
|
||||
'layout': go.Layout(**layout_args)
|
||||
})
|
||||
output = plotly.plot(fig, output_type='div', include_plotlyjs=False)
|
||||
return utils.split_graph_output(output)
|
|
@ -0,0 +1,169 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from collections import OrderedDict
|
||||
|
||||
from django.utils import timezone
|
||||
|
||||
import pandas as pd
|
||||
import plotly.offline as plotly
|
||||
import plotly.graph_objs as go
|
||||
|
||||
from core.utils import duration_string
|
||||
|
||||
from reports import utils
|
||||
|
||||
|
||||
def sleep_pattern(instances):
|
||||
"""
|
||||
Create a graph showing blocked out periods of sleep during each day.
|
||||
:param instances: a QuerySet of Sleep instances.
|
||||
:returns: a tuple of the the graph's html and javascript.
|
||||
"""
|
||||
# TODO: Simplify this using the bar charts "base" property.
|
||||
y_df = pd.DataFrame()
|
||||
text_df = pd.DataFrame()
|
||||
last_end_time = None
|
||||
adjustment = None
|
||||
df_index = 0
|
||||
for instance in instances:
|
||||
start_time = timezone.localtime(instance.start)
|
||||
end_time = timezone.localtime(instance.end)
|
||||
start_date = start_time.date().isoformat()
|
||||
duration = instance.duration
|
||||
|
||||
# Check if the previous entry crossed midnight (see below).
|
||||
if adjustment:
|
||||
# Fake (0) entry to keep the color switching logic working.
|
||||
df_index = _add_sleep_entry(
|
||||
y_df, text_df, 0, adjustment['column'], 0)
|
||||
# Real adjustment entry.
|
||||
df_index = _add_sleep_entry(
|
||||
y_df,
|
||||
text_df,
|
||||
df_index,
|
||||
adjustment['column'],
|
||||
adjustment['duration'].seconds/60,
|
||||
'Asleep {} ({} to {})'.format(
|
||||
duration_string(adjustment['duration']),
|
||||
adjustment['start_time'].strftime('%I:%M %p'),
|
||||
adjustment['end_time'].strftime('%I:%M %p')
|
||||
)
|
||||
)
|
||||
last_end_time = timezone.localtime(adjustment['end_time'])
|
||||
adjustment = None
|
||||
|
||||
# If the dates do not match, set up an adjustment for the next day.
|
||||
if end_time.date() != start_time.date():
|
||||
adj_start_time = end_time.replace(hour=0, minute=0, second=0)
|
||||
adjustment = {
|
||||
'column': end_time.date().isoformat(),
|
||||
'start_time': adj_start_time,
|
||||
'end_time': end_time,
|
||||
'duration': end_time - adj_start_time
|
||||
}
|
||||
|
||||
# Adjust end_time for the current entry.
|
||||
end_time = end_time.replace(
|
||||
year=start_time.year, month=start_time.month,
|
||||
day=start_time.day, hour=23, minute=59, second=0)
|
||||
duration = end_time - start_time
|
||||
|
||||
if start_date not in y_df:
|
||||
last_end_time = start_time.replace(hour=0, minute=0, second=0)
|
||||
|
||||
# Awake time.
|
||||
df_index = _add_sleep_entry(
|
||||
y_df,
|
||||
text_df,
|
||||
df_index,
|
||||
start_date,
|
||||
(start_time - last_end_time).seconds/60
|
||||
)
|
||||
|
||||
# Asleep time.
|
||||
df_index = _add_sleep_entry(
|
||||
y_df,
|
||||
text_df,
|
||||
df_index,
|
||||
start_date,
|
||||
duration.seconds/60,
|
||||
'Asleep {} ({} to {})'.format(
|
||||
duration_string(duration),
|
||||
start_time.strftime('%I:%M %p'),
|
||||
end_time.strftime('%I:%M %p')
|
||||
)
|
||||
)
|
||||
|
||||
last_end_time = end_time
|
||||
|
||||
dates = list(y_df)
|
||||
traces = []
|
||||
color = 'rgba(255, 255, 255, 0)'
|
||||
for index, row in y_df.iterrows():
|
||||
traces.append(go.Bar(
|
||||
x=dates,
|
||||
y=row,
|
||||
text=text_df.ix[index],
|
||||
hoverinfo='text',
|
||||
marker={'color': color},
|
||||
showlegend=False,
|
||||
))
|
||||
if color == 'rgba(255, 255, 255, 0)':
|
||||
color = 'rgb(35, 110, 150)'
|
||||
else:
|
||||
color = 'rgba(255, 255, 255, 0)'
|
||||
|
||||
layout_args = utils.default_graph_layout_options()
|
||||
layout_args['margin']['b'] = 100
|
||||
|
||||
layout_args['barmode'] = 'stack'
|
||||
layout_args['hovermode'] = 'closest'
|
||||
layout_args['title'] = '<b>Sleep Pattern</b>'
|
||||
layout_args['height'] = 800
|
||||
|
||||
layout_args['xaxis']['title'] = 'Date'
|
||||
layout_args['xaxis']['tickangle'] = -65
|
||||
layout_args['xaxis']['rangeselector'] = utils.rangeselector_date()
|
||||
|
||||
start = timezone.localtime().strptime('12:00 AM', '%I:%M %p')
|
||||
ticks = OrderedDict()
|
||||
ticks[0] = start.strftime('%I:%M %p')
|
||||
for i in range(30, 60*24, 30):
|
||||
ticks[i] = (start + timezone.timedelta(minutes=i)).strftime('%I:%M %p')
|
||||
|
||||
layout_args['yaxis']['title'] = 'Time of day'
|
||||
layout_args['yaxis']['rangemode'] = 'tozero'
|
||||
layout_args['yaxis']['tickmode'] = 'array'
|
||||
layout_args['yaxis']['tickvals'] = list(ticks.keys())
|
||||
layout_args['yaxis']['ticktext'] = list(ticks.values())
|
||||
layout_args['yaxis']['tickfont'] = {'size': 10}
|
||||
|
||||
fig = go.Figure({
|
||||
'data': traces,
|
||||
'layout': go.Layout(**layout_args)
|
||||
})
|
||||
output = plotly.plot(fig, output_type='div', include_plotlyjs=False)
|
||||
return utils.split_graph_output(output)
|
||||
|
||||
|
||||
def _add_sleep_entry(y_df, text_df, index, column, duration, text=''):
|
||||
"""
|
||||
Create a duration and text description entry in a DataFrame and return
|
||||
the next index on success.
|
||||
:param y_df: the y values DataFrame.
|
||||
:param text_df: the text values DataFrame.
|
||||
:param index: the index to target in both y_df and text_df.
|
||||
:param column: the column (date) to make the entry in.
|
||||
:param duration: the duration of the entry.
|
||||
:param text: text to go with the entry (displays on graph hover).
|
||||
:return: the next index of the DataFrames.
|
||||
"""
|
||||
if column not in y_df:
|
||||
y_df.assign(**{column: 0 in range(0, len(y_df.index))})
|
||||
text_df.assign(**{column: 0 in range(0, len(text_df.index))})
|
||||
index = 0
|
||||
|
||||
y_df.set_value(index, column, duration)
|
||||
text_df.set_value(index, column, text)
|
||||
return index + 1
|
|
@ -0,0 +1,71 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.utils import timezone
|
||||
|
||||
import plotly.offline as plotly
|
||||
import plotly.graph_objs as go
|
||||
|
||||
from core.utils import duration_parts
|
||||
|
||||
from reports import utils
|
||||
|
||||
|
||||
def sleep_totals(instances):
|
||||
"""
|
||||
Create a graph showing total time sleeping for each day.
|
||||
:param instances: a QuerySet of Sleep instances.
|
||||
:returns: a tuple of the the graph's html and javascript.
|
||||
"""
|
||||
totals = {}
|
||||
for instance in instances:
|
||||
start = timezone.localtime(instance.start)
|
||||
end = timezone.localtime(instance.end)
|
||||
if start.date() not in totals.keys():
|
||||
totals[start.date()] = timezone.timedelta(seconds=0)
|
||||
if end.date() not in totals.keys():
|
||||
totals[end.date()] = timezone.timedelta(seconds=0)
|
||||
|
||||
# Account for dates crossing midnight.
|
||||
if start.date() != end.date():
|
||||
totals[start.date()] += end.replace(
|
||||
year=start.year, month=start.month, day=start.day,
|
||||
hour=23, minute=59, second=59) - start
|
||||
totals[end.date()] += end - start.replace(
|
||||
year=end.year, month=end.month, day=end.day, hour=0, minute=0,
|
||||
second=0)
|
||||
else:
|
||||
totals[start.date()] += instance.duration
|
||||
|
||||
trace = go.Bar(
|
||||
name='Total sleep',
|
||||
x=list(totals.keys()),
|
||||
y=[td.seconds/3600 for td in totals.values()],
|
||||
hoverinfo='text',
|
||||
textposition='outside',
|
||||
text=[_duration_string_short(td) for td in totals.values()]
|
||||
)
|
||||
|
||||
layout_args = utils.default_graph_layout_options()
|
||||
layout_args['barmode'] = 'stack'
|
||||
layout_args['title'] = '<b>Sleep Totals</b>'
|
||||
layout_args['xaxis']['title'] = 'Date'
|
||||
layout_args['xaxis']['rangeselector'] = utils.rangeselector_date()
|
||||
layout_args['yaxis']['title'] = 'Hours of sleep'
|
||||
|
||||
fig = go.Figure({
|
||||
'data': [trace],
|
||||
'layout': go.Layout(**layout_args)
|
||||
})
|
||||
output = plotly.plot(fig, output_type='div', include_plotlyjs=False)
|
||||
return utils.split_graph_output(output)
|
||||
|
||||
|
||||
def _duration_string_short(duration):
|
||||
"""
|
||||
Format a "short" duration string without seconds precision. This is
|
||||
intended to fit better in smaller spaces on a graph.
|
||||
:returns: a string of the form XhXm.
|
||||
"""
|
||||
h, m, s = duration_parts(duration)
|
||||
return '{}h{}m'.format(h, m)
|
|
@ -0,0 +1,81 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.utils import timezone
|
||||
|
||||
from core.models import DiaperChange, Feeding, Sleep, TummyTime
|
||||
|
||||
|
||||
def timeline(child, date):
|
||||
"""
|
||||
Create a time-sorted dictionary for all events for a child.
|
||||
:param child: an instance of a Child.
|
||||
:param date: a DateTime instance for the day to be summarized.
|
||||
:returns: a list of the day's events.
|
||||
"""
|
||||
min_date = date
|
||||
max_date = date.replace(hour=23, minute=59, second=59)
|
||||
events = []
|
||||
|
||||
instances = DiaperChange.objects.filter(child=child).filter(
|
||||
time__range=(min_date, max_date)).order_by('-time')
|
||||
for instance in instances:
|
||||
events.append({
|
||||
'time': timezone.localtime(instance.time),
|
||||
'event': '{} had a diaper change.'.format(child.first_name),
|
||||
'model_name': instance.model_name,
|
||||
})
|
||||
|
||||
instances = Feeding.objects.filter(child=child).filter(
|
||||
start__range=(min_date, max_date)).order_by('-start')
|
||||
for instance in instances:
|
||||
events.append({
|
||||
'time': timezone.localtime(instance.start),
|
||||
'event': '{} started feeding.'.format(instance.child.first_name),
|
||||
'model_name': instance.model_name,
|
||||
'type': 'start'
|
||||
})
|
||||
events.append({
|
||||
'time': timezone.localtime(instance.end),
|
||||
'event': '{} finished feeding.'.format(instance.child.first_name),
|
||||
'model_name': instance.model_name,
|
||||
'type': 'end'
|
||||
})
|
||||
|
||||
instances = Sleep.objects.filter(child=child).filter(
|
||||
start__range=(min_date, max_date)).order_by('-start')
|
||||
for instance in instances:
|
||||
events.append({
|
||||
'time': timezone.localtime(instance.start),
|
||||
'event': '{} fell asleep.'.format(instance.child.first_name),
|
||||
'model_name': instance.model_name,
|
||||
'type': 'start'
|
||||
})
|
||||
events.append({
|
||||
'time': timezone.localtime(instance.end),
|
||||
'event': '{} woke up.'.format(instance.child.first_name),
|
||||
'model_name': instance.model_name,
|
||||
'type': 'end'
|
||||
})
|
||||
|
||||
instances = TummyTime.objects.filter(child=child).filter(
|
||||
start__range=(min_date, max_date)).order_by('-start')
|
||||
for instance in instances:
|
||||
events.append({
|
||||
'time': timezone.localtime(instance.start),
|
||||
'event': '{} started tummy time!'.format(
|
||||
instance.child.first_name),
|
||||
'model_name': instance.model_name,
|
||||
'type': 'start'
|
||||
})
|
||||
events.append({
|
||||
'time': timezone.localtime(instance.end),
|
||||
'event': '{} finished tummy time.'.format(
|
||||
instance.child.first_name),
|
||||
'model_name': instance.model_name,
|
||||
'type': 'end'
|
||||
})
|
||||
|
||||
events.sort(key=lambda x: x['time'], reverse=True)
|
||||
|
||||
return events
|
|
@ -3,7 +3,9 @@ from __future__ import unicode_literals
|
|||
|
||||
|
||||
def default_graph_layout_options():
|
||||
"""Default layout options for all graphs.
|
||||
"""
|
||||
Default layout options for all graphs.
|
||||
:returns: a dict of default options.
|
||||
"""
|
||||
return {
|
||||
'paper_bgcolor': 'rgb(52, 58, 64)',
|
||||
|
@ -36,7 +38,9 @@ def default_graph_layout_options():
|
|||
|
||||
|
||||
def rangeselector_date():
|
||||
"""Graph date range selectors settings for 1w, 2w, 1m, 3m, and all.
|
||||
"""
|
||||
Graph date range selectors settings for 1w, 2w, 1m, 3m, and all.
|
||||
:returns: a dict of settings for the selectors.
|
||||
"""
|
||||
return {
|
||||
'bgcolor': 'rgb(35, 149, 86)',
|
||||
|
@ -74,7 +78,10 @@ def rangeselector_date():
|
|||
|
||||
|
||||
def split_graph_output(output):
|
||||
"""Split out of a Plotly graph in to html and javascript.
|
||||
"""
|
||||
Split out of a Plotly graph in to html and javascript.
|
||||
:param output: a string of html and javascript comprising the graph.
|
||||
:returns: a tuple of the the graph's html and javascript.
|
||||
"""
|
||||
html, javascript = output.split('<script')
|
||||
javascript = '<script' + javascript
|
||||
|
|
Loading…
Reference in New Issue