diff --git a/reports/graphs.py b/reports/graphs.py index 754ed3da..27e888d9 100644 --- a/reports/graphs.py +++ b/reports/graphs.py @@ -5,13 +5,14 @@ from collections import OrderedDict from django.db.models import Count, Case, When from django.db.models.functions import TruncDate -from django.utils import timezone, duration +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, Sleep +from core.utils import duration_string from .utils import default_graph_layout_options, split_graph_output @@ -58,49 +59,81 @@ def diaperchange_types(child): def sleep_times(child): - # TODO: Handle dates crossing over midnight! + """Create a graph showing blocked out period of sleep during each day.""" instances = Sleep.objects.filter(child=child).order_by('start') 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'], + 'Asleep {} ({} to {})'.format( + duration_string(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).seconds/60 + } + + # Adjust end_time for the current entry. + end_time = end_time.replace( + day=start_time.day, hour=23, minute=59, second=0) + duration = end_time - start_time + if start_date not in y_df: - y_df.assign(**{start_date: 0 in range(0, len(y_df.index))}) - text_df.assign(**{start_date: 0 in range(0, len(y_df.index))}) last_end_time = start_time.replace(hour=0, minute=0, second=0) - df_index = 0 # Awake time. - y_df.set_value( - df_index, start_date, (start_time - last_end_time).seconds/60, - ) - text_df.set_value( + df_index = _add_sleep_entry( + y_df, + text_df, df_index, start_date, - 'Awake for from {} to {} ({})'.format( - last_end_time.strftime('%I:%M %p'), - start_time.strftime('%I:%M %p'), - duration.duration_string(start_time - last_end_time) - ) + (start_time - last_end_time).seconds/60 ) - df_index += 1 # Asleep time. - y_df.set_value(df_index, start_date, instance.duration.seconds/60) - text_df.set_value( + df_index = _add_sleep_entry( + y_df, + text_df, df_index, start_date, - 'Asleep for from {} to {} ({})'.format( + duration.seconds/60, + 'Asleep {} ({} to {})'.format( + duration_string(duration), start_time.strftime('%I:%M %p'), - end_time.strftime('%I:%M %p'), - duration.duration_string(instance.duration) + end_time.strftime('%I:%M %p') ) ) - df_index += 1 + last_end_time = end_time dates = list(y_df) @@ -151,3 +184,16 @@ def sleep_times(child): }) output = plotly.plot(fig, output_type='div', include_plotlyjs=False) return 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