# -*- coding: utf-8 -*- import arrow import copy import datetime from dateutil import parser import math import dash import dash_bootstrap_components as dbc import dash_core_components as dcc import dash_html_components as html from dash.dependencies import Input, Output import plotly.figure_factory as ff import plotly.graph_objs as go from plotly.subplots import make_subplots from data_handler import DataHandler app = dash.Dash(__name__, external_stylesheets=[dbc.themes.BOOTSTRAP]) app.config["suppress_callback_exceptions"] = True data_handler = DataHandler() DAILY_TAP = "daily" WEEKLY_TAP = "weekly" MONTHLY_TAP = "monthly" QUATERLY_TAP = "quarterly" tab_list = [ DAILY_TAP, WEEKLY_TAP ] task_categories = [ "Article", "Blog", "Book", "Develop", "Exercise", "Hobby", "Management", "Meeting", "MOOC", "Planning", "Research", "Review", "Seminar", "Think", ] # the style arguments for the sidebar. We use position:fixed and a fixed width SIDEBAR_STYLE = { "position": "fixed", "top": 0, "left": 0, "bottom": 0, "width": "16rem", "padding": "2rem 1rem", } # the styles for the main content position it to the right of the sidebar and # add some padding. CONTENT_STYLE = { "margin-left": "17rem", "padding": "2rem 1rem", "background-color": "#f4f7fc", } sidebar = html.Div( [ html.A( html.H2("Quantified Self", className="display-5"), href="/", style={"text-decoration": "none"}, ), html.Hr(), html.P("BeHappy Project", className="lead"), dbc.Nav( [ dbc.NavLink(tab.capitalize(), href=f"/{tab}", id=f"{tab}-link") for tab in tab_list ], vertical=True, pills=True, ), # Interval dcc.Interval( id="interval-component-10min", interval=10 * 60 * 1000, # every 10 mins n_intervals=0, ), ], style=SIDEBAR_STYLE, ) content = html.Div(id="page-content", style=CONTENT_STYLE) app.layout = html.Div([dcc.Location(id="url"), sidebar, content]) # this callback uses the current pathname to set the active state of the # corresponding nav link to true, allowing users to tell see page they are on @app.callback( [Output(f"{tab}-link", "active") for tab in tab_list], [Input("url", "pathname")] ) def toggle_active_links(pathname): if pathname == "/": return [False] * len(tab_list) return [pathname == f"/{tab}" for tab in tab_list] def make_card_html(body, title_text=None, size=12, thresholds=[]): background_color = "white" value = None if type(body[0]) == html.P and body[0].children: value = int(body[0].children) if value is not None and len(thresholds) == 2: good_threshold, bad_threshold = thresholds if value >= good_threshold: background_color = "palegreen" elif value < bad_threshold: background_color = "lightsalmon" card_html = html.Div( [ html.Div( [html.Div(body, className="card-body", style={"background-color": background_color})], className="card mb-3 text-center", ) ], className=f"col-sm-{size}", ) if title_text is not None: title = html.H6(title_text, className="card-title") card_html.children[0].children[0].children.insert(0, title) return card_html @app.callback(Output("page-content", "children"), [Input("url", "pathname")]) def render_page_content(pathname): if pathname == "/": # Dashboard Tab # Daily Dashboard daily_task_hour_card = make_card_html( [ html.P(id="daily_task_hour_card"), ], title_text="Task Hour", size=2, ) daily_sleep_hour_card = make_card_html( [html.P(id="daily_sleep_hour_card")], title_text="Sleep Hour", size=2, ) daily_remain_hour_card = make_card_html( [html.P(id="daily_remain_hour_card")], title_text="Remain Hour", size=2 ) daily_exercise_card = make_card_html( [html.P(id="daily_exercise_card")], title_text="Exercise", size=2 ) daily_diary_card = make_card_html([html.P(id="daily_diary_card")], title_text="Diary", size=2) daily_bat_card = make_card_html([html.P(id="daily_bat_card")], title_text="BAT", size=2) # Weekly Dashboard weekly_task_total_hour_card = make_card_html([html.P(id="weekly_task_total_hour_card")], title_text="Total Hour", size=3 ) weekly_exercise_card = make_card_html( [html.P(id="weekly_exercise_count_card")], title_text="Exercise Count", size=3 ) weekly_diary_card = make_card_html( [html.P(id="weekly_diary_count_card")], title_text="Diary Count", size=3 ) weekly_bat_card = make_card_html( [html.P(id="weekly_bat_count_card")], title_text="BAT Count", size=3 ) dashboard_div = html.Div([], className="container-fluid row") dashboard_div.children.extend([ html.H2("Daily", className="col-sm-12"), html.Hr(), daily_task_hour_card, daily_sleep_hour_card, daily_remain_hour_card, daily_exercise_card, daily_diary_card, daily_bat_card, ]) dashboard_div.children.extend([ html.H2("Weekly", className="col-sm-12"), html.Hr(), weekly_task_total_hour_card, weekly_exercise_card, weekly_diary_card, weekly_bat_card, ]) # Weekly Task Category for task_category in task_categories: dashboard_div.children.append( make_card_html( [html.P(id=f"weekly_{task_category.lower()}_card")], title_text=task_category, size=2, thresholds=[2, 4] ) ) return dashboard_div elif pathname == f"/{DAILY_TAP}": # Daily - Schedule daily_schedule_card = make_card_html( [ html.Div( [ html.Span( "Date:", style={"font-size": "1.5em", "margin-right": "5px"} ), dcc.DatePickerSingle( id="date-picker-daily-schedule", date=arrow.now().datetime, max_date_allowed=arrow.now().datetime, ), ], style={"text-align": "center"}, ), dcc.Graph(id="live-daily-schedule"), ] ) # Daily - Calendar Heatmap daily_cal_heatmap_card = make_card_html( [ html.Div( [ html.Span( "DateRange:", style={"font-size": "1.5em", "margin-right": "5px"}, ), dcc.DatePickerRange( id="date-picker-range-calendar-heatmap", first_day_of_week=1, # Monday start_date=arrow.now().shift(days=-30).datetime, end_date=arrow.now().datetime, max_date_allowed=arrow.now().datetime, ), ], style={"text-align": "center"}, ), dcc.Graph(id="live-calendar-heatmap"), ] ) # Daily - Summary Chart daily_summary_card = make_card_html( [ dcc.Graph(id="live-daily-chart"), ] ) return html.Div( [ daily_schedule_card, daily_cal_heatmap_card, daily_summary_card, ], className="container-fluid row", ) elif pathname == f"/{WEEKLY_TAP}": # Weekly Task - Stack Bar Chart weekly_task_card = make_card_html( [ html.Div( [ html.Span( "DateRange:", style={"font-size": "1.5em", "margin-right": "5px"}, ), dcc.DatePickerRange( id="date-picker-range-weekly", first_day_of_week=1, # Monday start_date=arrow.now().shift(days=-30).datetime, end_date=arrow.now().datetime, max_date_allowed=arrow.now().datetime, ), ], style={"text-align": "center"}, ), dcc.Graph(id="live-weekly-stack-reports"), dcc.Graph(id="live-weekly-pie-reports"), ] ) return html.Div( [ weekly_task_card, ], className="container-fluid row", ) elif pathname == "tab-2": return html.Div([html.H3("Tab content 2")]) @app.callback( Output("live-daily-schedule", "figure"), [ Input("interval-component-10min", "n_intervals"), Input("date-picker-daily-schedule", "date"), ], ) def make_daily_schedule_fig(n, date): timedelta = arrow.now() - arrow.get(date, tzinfo="Asia/Seoul") days_diff = timedelta.days if days_diff < 0: pass # Can't handle future!" elif days_diff == 0: record_data = data_handler.read_record(redownload=True) else: record_data = data_handler.read_record(days=-days_diff) activity_data = record_data["activity"] task_data = activity_data["task"] toggl_projects = [data["project"] for data in task_data] colors = {} for data in task_data: colors[data["project"]] = data["color"] base_date = date tomorrow_base_date = arrow.get(base_date).shift(days=+1).format("YYYY-MM-DD") df = [ # Labeling scores dict(Task=5, Start=base_date, Finish=base_date, Resource=toggl_projects[0]), dict(Task=4, Start=base_date, Finish=base_date, Resource=toggl_projects[0]), dict(Task=3, Start=base_date, Finish=base_date, Resource=toggl_projects[0]), dict(Task=2, Start=base_date, Finish=base_date, Resource=toggl_projects[0]), dict(Task=1, Start=base_date, Finish=base_date, Resource=toggl_projects[0]), ] # Labeling projects for project in toggl_projects: df.append( dict( Task=1, Start=tomorrow_base_date, Finish=tomorrow_base_date, Resource=project, ) ) for data in task_data: task = { "Task": data.get("score", 3), "Start": arrow.get(data["start_time"]).format("YYYY-MM-DD HH:mm:ss"), "Finish": arrow.get(data["end_time"]).format("YYYY-MM-DD HH:mm:ss"), "Resource": data["project"], "Description": data["description"], } df.append(task) fig = ff.create_gantt( df, colors=colors, index_col="Resource", title="Daily Schedule", group_tasks=True, show_colorbar=True, bar_width=0.3, showgrid_x=True, showgrid_y=True, width=1000, height=600, ) happy_data = activity_data["happy"] if len(happy_data) > 0: xs = [arrow.get(d["time"]).format("YYYY-MM-DD HH:mm:ss") for d in happy_data] ys = [d["score"] - 1 for d in happy_data] scatter_trace = dict( type="scatter", mode="markers", marker=dict(size=10, color="#439C59", line=dict(width=2)), name="Happy", x=xs, y=ys, ) fig.add_trace(scatter_trace) # Annotations annotations = [] for index, d in enumerate(fig["data"]): if d["text"] is None: continue data_count = len(d["x"]) for i in range(0, data_count, 2): text = d["text"][i] if text is None: continue start_date = d["x"][i] end_date = d["x"][i + 1] start_score = d["y"][i] end_score = d["y"][i + 1] if start_date == end_date or start_score != end_score: continue description = d["text"][i] project_names = list(colors.keys()) project_name = "Empty" for p_name in project_names: if description.startswith(p_name): project_name = p_name break if type(start_date) != datetime.datetime: start_date = parser.parse(start_date) if type(end_date) != datetime.datetime: end_date = parser.parse(end_date) up_ays = [-50, -90, -70, -110] down_ays = [50, 90, 70, 110] if start_score > 2: # large than 3 ays = up_ays else: ays = down_ays ay = ays[index % len(ays)] annotations.append( go.layout.Annotation( x=start_date + (end_date - start_date) / 2, y=start_score, xref="x", yref="y", text=description, font=dict(family="Courier New, monospace", size=12, color="#fff"), bgcolor=colors.get(project_name, "#DEDEDE"), bordercolor="#666", borderpad=2, arrowhead=7, ax=0, ay=ay, opacity=0.7, ) ) fig.update_layout(annotations=annotations) return fig def get_base_of_range(start_date, end_date, weekday_value=0): # if start_date.weekday() != weekday_value: # start_date = start_date.shift(days=-(start_date.weekday() - weekday_value)) if end_date.weekday() != weekday_value: end_date = end_date.shift(days=+(abs(end_date.weekday() - weekday_value))) base_dates = [] for r in arrow.Arrow.range("day", start_date, end_date): if r.weekday() == weekday_value: base_dates.append(r) return base_dates @app.callback( Output("live-weekly-stack-reports", "figure"), [ Input("interval-component-10min", "n_intervals"), Input("date-picker-range-weekly", "start_date"), Input("date-picker-range-weekly", "end_date"), ], ) def make_stacked_bar_fig(n, start_date, end_date): start_date = arrow.get(start_date) end_date = arrow.get(end_date) categories = [ "Article", "Blog", "Book", "Develop", "Exercise", "Hobby", "Management", "Meeting", "MOOC", "Planning", "Research", "Review", "Seminar", "Think", "Empty", ] task_reports = {} colors = {"Empty": "#DEDEDE"} WEEKDAY_SUNDAY = 6 sunday_dates = get_base_of_range(start_date, end_date, weekday_value=WEEKDAY_SUNDAY) for c in categories: task_reports[c] = [0] * len(sunday_dates) weekly_index = 0 for r in arrow.Arrow.range("day", start_date, end_date): offset_day = (arrow.now() - r).days record_data = data_handler.read_record(days=-offset_day) for weekly_index, base_date in enumerate(sunday_dates): days_diff = (base_date - r).days if days_diff < 7 and days_diff >= 0: break activity_data = record_data.get("activity", {}) task_data = activity_data.get("task", []) for t in task_data: project = t["project"] duration = (arrow.get(t["end_time"]) - arrow.get(t["start_time"])).seconds duration_hours = round(duration / 60 / 60, 1) task_reports[project][weekly_index] += duration_hours # Color if project not in colors: colors[project] = t["color"] data = [] for category, task_report in task_reports.items(): differ_with_last_weeks = [f"{task_report[0]} (0)"] for i in range(1, len(task_report)): last_week_task_time = task_report[i - 1] task_time = task_report[i] differ_time = round(task_time - last_week_task_time, 2) plus_and_minus = "+" if differ_time < 0: plus_and_minus = "" differ_with_last_weeks.append( f"{round(task_time, 2)} ({plus_and_minus}{differ_time})" ) data.append( go.Bar( x=sunday_dates, y=task_report, name=category, marker=dict( color=colors.get(category, "#DEDEDE"), line=dict(color="#222", width=1), ), hovertext=differ_with_last_weeks, opacity=0.8, ) ) layout = go.Layout( autosize=True, barmode="stack", title="Weekly Task Report (Stack Bar)" ) fig = go.Figure(data=data, layout=layout) return fig @app.callback( Output("live-weekly-pie-reports", "figure"), [ Input("interval-component-10min", "n_intervals"), Input("date-picker-range-weekly", "start_date"), Input("date-picker-range-weekly", "end_date"), ], ) def make_pie_chart_fig(n, start_date, end_date): start_date = arrow.get(start_date) end_date = arrow.get(end_date) categories = copy.deepcopy(task_categories) categories.append("Empty") task_reports = {} colors = {"Empty": "#DEDEDE"} WEEKDAY_SUNDAY = 6 sunday_dates = get_base_of_range(start_date, end_date, weekday_value=WEEKDAY_SUNDAY) for c in categories: task_reports[c] = [0] * len(sunday_dates) weekly_index = 0 for r in arrow.Arrow.range("day", start_date, end_date): offset_day = (arrow.now() - r).days record_data = data_handler.read_record(days=-offset_day) for weekly_index, base_date in enumerate(sunday_dates): days_diff = (base_date - r).days if days_diff < 7 and days_diff >= 0: break activity_data = record_data.get("activity", {}) task_data = activity_data.get("task", []) for t in task_data: project = t["project"] duration = (arrow.get(t["end_time"]) - arrow.get(t["start_time"])).seconds duration_hours = round(duration / 60 / 60, 1) task_reports[project][weekly_index] += duration_hours # Color if project not in colors: colors[project] = t["color"] pie_chart_count = weekly_index + 1 COL_COUNT = 4 ROW_COUNT = math.ceil(pie_chart_count / COL_COUNT) pie_values = [] for i in range(pie_chart_count): pie_values.append([]) subplots_specs = [] for r in range(ROW_COUNT): row_specs = [] for c in range(COL_COUNT): row_specs.append({"type": "domain"}) subplots_specs.append(row_specs) fig = make_subplots(rows=ROW_COUNT, cols=COL_COUNT, specs=subplots_specs) pie_colors = [] for category, task_values in task_reports.items(): for i, v in enumerate(task_values): pie_values[i].append(v) pie_colors.append(colors.get(category, "#DEDEDE")) for i, pie_value in enumerate(pie_values): col_index = int((i % COL_COUNT)) + 1 row_index = int((i / COL_COUNT)) + 1 fig.add_trace( go.Pie( labels=categories, values=pie_value, name=sunday_dates[i].format("MMM D"), ), row=row_index, col=col_index, ) # Use `hole` to create a donut-like pie chart fig.update_traces( hole=.3, hoverinfo="label+percent+name", marker={"colors": pie_colors} ) return fig def make_weekly_dates(N): format_date_list = [] now = arrow.now() for i in range(-(N - 1), 1, 1): date = now.shift(days=i) format_date_list.append(date.format("YYYY-MM-DD")) return format_date_list @app.callback( Output("live-daily-chart", "figure"), [Input("interval-component-10min", "n_intervals")] ) def make_scatter_line_fig(n): week_days = 7 weekly_data = [] for i in range(-(week_days - 1), 1): record_data = data_handler.read_record(days=i) if "summary" not in record_data or "total" not in record_data["summary"]: record_data = data_handler.read_record(days=i, redownload=True) weekly_data.append(record_data) dates = make_weekly_dates(week_days) def get_score(data, category): summary = data.get("summary", {}) return summary.get(category, 0) attention_scores = [get_score(data, "attention") for data in weekly_data] happy_scores = [get_score(data, "happy") for data in weekly_data] productive_scores = [get_score(data, "productive") for data in weekly_data] sleep_scores = [get_score(data, "sleep") for data in weekly_data] repeat_task_scores = [get_score(data, "repeat_task") for data in weekly_data] total_scores = [get_score(data, "total") for data in weekly_data] names = ["attention", "happy", "productive", "sleep", "repeat_task", "total"] ys = [ attention_scores, happy_scores, productive_scores, sleep_scores, repeat_task_scores, total_scores, ] # Create traces data = [] for name, y in zip(names, ys): data.append(go.Scatter(x=dates, y=y, mode="lines+markers", name=name)) layout = go.Layout(autosize=True, title="Summary Chart") fig = go.Figure(data=data, layout=layout) return fig @app.callback( Output("live-calendar-heatmap", "figure"), [ Input("interval-component-10min", "n_intervals"), Input("date-picker-range-calendar-heatmap", "start_date"), Input("date-picker-range-calendar-heatmap", "end_date"), ], ) def make_calendar_heatmap_fig(n, start_date, end_date): start_date = arrow.get(start_date) end_date = arrow.get(end_date) categories = ["BAT", "Diary", "Exercise"] dates = [] z = [] for _ in categories: z.append([]) for r in arrow.Arrow.range("day", start_date, end_date): offset_day = (arrow.now() - r).days record_data = data_handler.read_record(days=-offset_day) summary = record_data.get("summary", {}) for i, category in enumerate(categories): do_category = summary.get(f"do_{category.lower()}", False) z[i].append(int(do_category)) dates.append(r.format("YYYY-MM-DD")) categories.append("All") z_do_all = [] for i in range(len(dates)): do_all = 0 for item in z: do_all += item[i] z_do_all.append(do_all) z.append(z_do_all) fig = go.Figure( data=go.Heatmap( z=z, text=z, x=dates, y=categories, colorscale=[[0, "#FFFFFF"], [1, "#19410a"]], xgap=7, ygap=7, ) ) fig.update_layout( title="BAT, Diary, Exercise per day", height=300, xaxis={ "tickformat": "%a-%m-%d", "tickangle": 75, "showticklabels": True, "dtick": 86400000.0 * 1, # 1 day }, ) return fig @app.callback( [ Output(component_id='daily_task_hour_card', component_property='children'), Output(component_id='daily_sleep_hour_card', component_property='children'), Output(component_id='daily_remain_hour_card', component_property='children'), Output(component_id='daily_exercise_card', component_property='children'), Output(component_id='daily_diary_card', component_property='children'), Output(component_id='daily_bat_card', component_property='children'), ], [Input(component_id='interval-component-10min', component_property='n_intervals')] ) def update_daily(n): today_record_data = data_handler.read_record(redownload=True) activity_data = today_record_data.get("activity", {}) task_hour = 0 today_tasks = activity_data.get("task", []) for t in today_tasks: duration = (arrow.get(t["end_time"]) - arrow.get(t["start_time"])).seconds duration_hours = duration / 60 / 60 task_hour += duration_hours task_hour = round(task_hour, 1) sleep_hour = 0 today_sleep = activity_data.get("sleep", []) for s in today_sleep: if s["is_main"] is True: start_time = arrow.get(s["start_time"]) end_time = arrow.get(s["end_time"]) sleep_hour = (end_time - start_time).seconds sleep_hour = sleep_hour / 60 / 60 remain_hour = 24 - task_hour - sleep_hour today_summary = today_record_data.get("summary", {}) exercise = "X" if "do_exercise" in today_summary and today_summary["do_exercise"]: exercise= "O" diary = "X" if "do_diary" in today_summary and today_summary["do_diary"]: diary= "O" bat = "X" if "do_bat" in today_summary and today_summary["do_bat"]: bat= "O" return round(task_hour, 1), round(sleep_hour, 1), round(remain_hour, 1), exercise, diary, bat @app.callback( [ Output(component_id='weekly_bat_count_card', component_property='children'), Output(component_id='weekly_diary_count_card', component_property='children'), Output(component_id='weekly_exercise_count_card', component_property='children'), ], [Input(component_id='interval-component-10min', component_property='n_intervals')] ) def update_weekly(n): end_date = arrow.now() start_date = end_date.shift(days=-end_date.weekday()) WEEKDAY_SUNDAY = 6 sunday_dates = get_base_of_range(start_date, end_date, weekday_value=WEEKDAY_SUNDAY) bat_count = 0 diary_count = 0 exercise_count = 0 weekly_index = 0 for r in arrow.Arrow.range("day", start_date, end_date): offset_day = (arrow.now() - r).days record_data = data_handler.read_record(days=-offset_day) summary_data = record_data["summary"] if summary_data["do_bat"] is True: bat_count += 1 if summary_data["do_diary"] is True: diary_count += 1 if summary_data["do_exercise"] is True: exercise_count += 1 for weekly_index, base_date in enumerate(sunday_dates): days_diff = (base_date - r).days if days_diff < 7 and days_diff >= 0: break return bat_count, diary_count, exercise_count @app.callback( [Output(component_id=f"weekly_{task_category.lower()}_card", component_property='children') for task_category in task_categories] + [ Output(component_id='weekly_task_total_hour_card', component_property='children'), ], [Input(component_id='interval-component-10min', component_property='n_intervals')] ) def update_weekly_task_category(n): task_reports = {} end_date = arrow.now() start_date = end_date.shift(days=-end_date.weekday()) WEEKDAY_SUNDAY = 6 sunday_dates = get_base_of_range(start_date, end_date, weekday_value=WEEKDAY_SUNDAY) categories = copy.deepcopy(task_categories) categories.append("Empty") for c in categories: task_reports[c] = [0] * len(sunday_dates) weekly_index = 0 for r in arrow.Arrow.range("day", start_date, end_date): offset_day = (arrow.now() - r).days record_data = data_handler.read_record(days=-offset_day) for weekly_index, base_date in enumerate(sunday_dates): days_diff = (base_date - r).days if days_diff < 7 and days_diff >= 0: break activity_data = record_data.get("activity", {}) task_data = activity_data.get("task", []) for t in task_data: project = t["project"] duration = (arrow.get(t["end_time"]) - arrow.get(t["start_time"])).seconds duration_hours = round(duration / 60 / 60, 1) task_reports[project][weekly_index] += duration_hours results = [] for task_category in task_categories: results.append(task_reports[task_category][0]) total_hour = round(sum(results), 1) results.append(total_hour) return results if __name__ == "__main__": app.run_server(debug=True, host="0.0.0.0", port=8000)