glucose360 0.0.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
glucose360/plots.py ADDED
@@ -0,0 +1,494 @@
1
+ import pandas as pd
2
+ import numpy as np
3
+ import src.glucose360.preprocessing as pp
4
+ import configparser
5
+ import json
6
+
7
+ import plotly.express as px
8
+ from plotly.subplots import make_subplots
9
+ import plotly.graph_objects as go
10
+
11
+ import os
12
+
13
+ from src.glucose360.features import percent_time_in_range, percent_time_in_tight_range, mean, CV, GMI
14
+
15
+ dir_path = os.path.dirname(os.path.realpath(__file__))
16
+ config_path = os.path.join(dir_path, "config.ini")
17
+ config = configparser.ConfigParser()
18
+ config.read(config_path)
19
+ ID = config['variables']['id']
20
+ GLUCOSE = config['variables']['glucose']
21
+ TIME = config['variables']['time']
22
+ BEFORE = config['variables']['before']
23
+ AFTER = config['variables']['after']
24
+ TYPE = config['variables']['type']
25
+ DESCRIPTION = config['variables']['description']
26
+
27
+ def daily_plot_all(
28
+ df: pd.DataFrame,
29
+ events: pd.DataFrame = None,
30
+ save: str = None,
31
+ height: int = 2000
32
+ ):
33
+ """Graphs daily plots for all of the patients in the given DataFrame.
34
+ Also saves these plots as PDFs and HTMLs if passed a valid path.
35
+
36
+ :param df: the DataFrame (following package guidelines) containing the CGM data to plot
37
+ :type df: 'pandas.DataFrame'
38
+ :param events: a DataFrame containing any events to mark on the daily plots, defaults to None
39
+ :type events: 'pandas.DataFrame', optional
40
+ :param save: path of the location where the saved PDF and HTML versions of the plots are saved, defaults to None
41
+ :type save: str, optional
42
+ :param height: the height (in pixels) of the resulting plot(s), defaults to 2000
43
+ :type height: int, optional
44
+ """
45
+ for id, data in df.groupby(ID):
46
+ daily_plot(data, id, height, events, save)
47
+
48
+ def daily_plot(
49
+ df: pd.DataFrame,
50
+ id: str,
51
+ height: int = 2000,
52
+ events: pd.DataFrame = None,
53
+ save: bool = False,
54
+ app: bool = False
55
+ ):
56
+ """Graphs a daily (time-series) plot for the given patient within the given DataFrame.
57
+ Also saves this plot as both a PDF and HTML file if passed a valid path.
58
+
59
+ :param df: the DataFrame (following package guidelines) containing the CGM data to plot
60
+ :type df: 'pandas.DataFrame'
61
+ :param id: the identification of the patient whose CGM data to plot within the given DataFrame
62
+ :type id: str
63
+ :param events: a DataFrame containing any events to mark on the daily plots, defaults to None
64
+ :type events: 'pandas.DataFrame', optional
65
+ :param save: path of the location where the saved PDF and HTML versions of the plot are saved, defaults to None
66
+ :type save: str, optional
67
+ :param height: the height (in pixels) of the resulting plot, defaults to 2000
68
+ :type height: int, optional
69
+ :param app: boolean indicating whether to return the Plotly figure instead of rendering it (used mainly within the web application), defaults to False
70
+ :type app: bool, optional
71
+ :return: None if app is False, otherwise the Plotly figure
72
+ :rtype: 'plotly.graph_objects.Figure' | None
73
+ """
74
+ show_events = not (events is None or events[events[ID] == id].empty)
75
+ data = df.loc[id].copy()
76
+ data[TIME] = pd.to_datetime(data[TIME])
77
+ data["Day"] = data[TIME].dt.date
78
+
79
+ days = data["Day"].unique().astype(str).tolist()
80
+ offset = pd.Timedelta(hours=1, minutes=30)
81
+
82
+ rendered_types = []
83
+ if show_events:
84
+ event_types = events[TYPE].unique()
85
+ with open('event_colors.json') as colors_file:
86
+ color_dict = json.load(colors_file)
87
+ colors = list(color_dict.values())
88
+ color_map = {event_type: colors[i] for i, event_type in enumerate(event_types)}
89
+
90
+ fig = make_subplots(rows=len(days), cols=2 if show_events else 1, column_widths=[0.66,0.34] if show_events else None,
91
+ specs=[[{"type":"scatter"}, {"type":"table"}] for _ in range(len(days))] if show_events else None,
92
+ horizontal_spacing=0.01 if show_events else None,
93
+ vertical_spacing=0.05 if show_events else None)
94
+
95
+ num_events = 0
96
+
97
+ for idx, (day, dataset) in enumerate(data.groupby("Day"), start=1):
98
+ fig.add_trace(go.Scatter(x=dataset[TIME],y=dataset[GLUCOSE],mode='lines+markers',name=str(day), showlegend=False), row=idx, col=1)
99
+ fig.update_xaxes(range=[pd.Timestamp(day) - offset, pd.Timestamp(day) + pd.Timedelta(days=1) + offset], row=idx, col=1, tickfont_size=20, titlefont_size=35)
100
+ fig.update_yaxes(title_text="Glucose Value (mg/dL)", tickfont_size=20, titlefont_size=35, row=idx, col=1)
101
+
102
+ if show_events:
103
+ day_events = events[(events[TIME].dt.date == day) & (events[ID] == id)].sort_values(TIME)
104
+ num_events = max(num_events, day_events.shape[0])
105
+ if not day_events.empty:
106
+ table_body = [day_events[TIME].dt.time.astype(str).tolist(), day_events[DESCRIPTION].tolist()]
107
+ fig.add_trace(
108
+ go.Table(
109
+ columnwidth=[25,25],
110
+ header=dict(values = [["<b>Time</b>"], ["<b>Description</b>"]],font=dict(size=22)),
111
+ cells=dict(values=table_body,align=['left', 'left'],font=dict(size=20))
112
+ ), row=idx, col=2
113
+ )
114
+
115
+ for _, row in day_events.drop_duplicates(subset=[TIME, TYPE]).iterrows():
116
+ already_rendered = (row[TYPE] in rendered_types)
117
+ if (not already_rendered): rendered_types.append(row[TYPE])
118
+ fig.add_shape(go.layout.Shape(
119
+ type="line", yref="y domain",
120
+ x0=pd.to_datetime(row[TIME]), y0=0,
121
+ x1=pd.to_datetime(row[TIME]), y1=1,
122
+ line_color=color_map[row[TYPE]], line_dash="dash",
123
+ name=row[TYPE], legendgroup=row[TYPE], showlegend=(not already_rendered)), row=idx, col=1)
124
+
125
+ fig.update_yaxes(range=[min(np.min(data[GLUCOSE]), 60) - 10, max(np.max(data[GLUCOSE]), 180) + 10])
126
+ fig.update_layout(title=f"Daily Plot for {id}", height=height, showlegend=False, titlefont_size=40)
127
+
128
+ if save:
129
+ path = os.path.join(save, f"{id}_daily_plot")
130
+ fig.write_image(path + ".pdf", width=1500, height=height)
131
+ fig.write_image(path + ".png", width=1500, height=height)
132
+ fig.write_image(path + ".jpeg", width=1500, height=height)
133
+ fig.write_html(path +".html")
134
+
135
+ if app: return fig
136
+ fig.show()
137
+
138
+ def event_plot_all(df: pd.DataFrame, id: str, events: pd.DataFrame, type: str, save: bool = False):
139
+ """Graphs all event plots of a certain type for the given patient within the given DataFrame.
140
+
141
+ :param df: the DataFrame (following package guidelines) containing the CGM data to plot
142
+ :type df: 'pandas.DataFrame'
143
+ :param id: the identification of the patient whose CGM data to plot within the given DataFrame
144
+ :type id: str
145
+ :param events: a DataFrame containing events to be plotted, defaults to None
146
+ :type events: 'pandas.DataFrame'
147
+ :param type: the type of events to plot
148
+ :type type: str
149
+ """
150
+ relevant_events = events[(events[ID] == id) & (events[TYPE] == type)]
151
+ for index, event in relevant_events.iterrows():
152
+ event_plot(df, id, event, relevant_events, save=save)
153
+
154
+ def event_plot(df: pd.DataFrame, id: str, event: pd.Series, events: pd.DataFrame = None, save: bool = False, app: bool = False):
155
+ """Graphs an event plot for the given patient within the given DataFrame.
156
+
157
+ :param df: the DataFrame (following package guidelines) containing the CGM data to plot
158
+ :type df: 'pandas.DataFrame'
159
+ :param id: the identification of the patient whose CGM data to plot within the given DataFrame
160
+ :type id: str
161
+ :param event: the event to be displayed
162
+ :type event: 'pandas.Series'
163
+ :param events: a DataFrame containing any extra events to be marked within the event plot, defaults to None
164
+ :type events: 'pandas.DataFrame', optional
165
+ :param app: boolean indicating whether to return the Plotly figure instead of rendering it (used mainly within the web application), defaults to False
166
+ :type app: bool, optional
167
+ :return: None if app is False, otherwise the Plotly figure
168
+ :rtype: 'plotly.graph_objects.Figure' | None
169
+ """
170
+ if event[ID] != id: raise Exception("Given event does not match the 'id' given.")
171
+ data = df.loc[id].copy()
172
+ event[TIME] = pd.to_datetime(event[TIME])
173
+ before = event[TIME] - pd.Timedelta(minutes=event[BEFORE])
174
+ after = event[TIME] + pd.Timedelta(minutes=event[AFTER])
175
+
176
+ data["Day"] = data[TIME].dt.date
177
+ subplot_figs = [go.Scatter(x=dataset[TIME],y=dataset[GLUCOSE],mode='lines+markers', name=str(day)) for day, dataset in data.groupby("Day")]
178
+ fig = go.Figure(data=subplot_figs, layout=go.Layout(title=f"Event Plot for {id}", titlefont_size=40, legend=dict(font=dict(size=20))))
179
+
180
+ event_data = events[events[ID] == id] if events is not None else pd.DataFrame()
181
+ if not event_data.empty: create_event_lines(fig, event_data)
182
+
183
+ fig.update_xaxes(type="date", range=[before, after], tickfont_size=20, titlefont_size=35)
184
+ fig.update_yaxes(title_text="Glucose Value (mg/dL)", range=[85, 170], tickfont_size=20, titlefont_size=35)
185
+
186
+ if save:
187
+ path = os.path.join(save, f"{id}_event_plot")
188
+ fig.write_image(path + ".png", width=1500, height=1000)
189
+ fig.write_html(path +".html")
190
+
191
+ if app: return fig
192
+ fig.show()
193
+
194
+ def create_event_lines(fig: go.Figure, events: pd.DataFrame):
195
+ """Marks vertical lines within the given plotly Figure for the given events
196
+
197
+ :param fig: the plotly figure to mark vertical lines in
198
+ :type fig: plotly.graph_objects.Figure
199
+ :param events: Pandas DataFrame containing vevents to mark
200
+ :type events: pandas.DataFrame''
201
+ """
202
+ event_types = events[TYPE].unique()
203
+ events[TIME] = pd.to_datetime(events[TIME])
204
+ with open('event_colors.json') as colors_file:
205
+ color_dict = json.load(colors_file)
206
+ colors = list(color_dict.values())
207
+ color_map = {event_type: colors[i] for i, event_type in enumerate(event_types)}
208
+
209
+ rendered_types = []
210
+ for event in events.itertuples():
211
+ time = getattr(event, TIME)
212
+ type = getattr(event, TYPE)
213
+ already_rendered = (type in rendered_types)
214
+ if (not already_rendered): rendered_types.append(type)
215
+ fig.add_vline(x=time, line_width=3, line_dash="dash", line_color=color_map[type], name=type, legendgroup=type, showlegend=(not already_rendered))
216
+
217
+ def weekly_plot_all(df: pd.DataFrame, save: str = None, height: int = 1000):
218
+ """Graphs weekly plots for all of the patients within the given DataFrame.
219
+ Also saves these plots as PDF and HTML files if passed a valid path.
220
+
221
+ :param df: the DataFrame (following package guidelines) containing the CGM data to plot
222
+ :type df: 'pandas.DataFrame'
223
+ :param save: path of the location where the saved PDF and HTML versions of the plots are saved, defaults to None
224
+ :type save: str, optional
225
+ :param height: the height (in pixels) of the resulting plot(s), defaults to 1000
226
+ :type height: int, optional
227
+ """
228
+ for id, data in df.groupby(ID):
229
+ weekly_plot(data, id, height)
230
+
231
+ def weekly_plot(df: pd.DataFrame, id: str, save: str = None, height: int = 1000, app = False):
232
+ """Graphs a weekly (time-series) plot for the given patient within the given DataFrame.
233
+ Also saves this plot as a PDF and HTML if passed a valid path.
234
+
235
+ :param df: the DataFrame (following package guidelines) containing the CGM data to plot
236
+ :type df: 'pandas.DataFrame'
237
+ :param id: the identification of the patient whose CGM data to plot within the given DataFrame
238
+ :type id: str
239
+ :param save: path of the location where the saved PDF and HTML versions of the plots are saved, defaults to None
240
+ :type save: str, optional
241
+ :param height: the height (in pixels) of the resulting plot, defaults to 1000
242
+ :type height: int, optional
243
+ :param app: boolean indicating whether to return the Plotly figure instead of rendering it (used mainly within the web application), defaults to False
244
+ :type app: bool, optional
245
+ :return: None if app is False, otherwise the Plotly figure
246
+ :rtype: 'plotly.graph_objects.Figure' | None
247
+ """
248
+ data = df.loc[id].reset_index().copy()
249
+ data.set_index(TIME, inplace=True)
250
+
251
+ weekly_data = data.groupby(pd.Grouper(freq='W'))
252
+ weekly_dfs = [group for _, group in weekly_data]
253
+
254
+ fig = make_subplots(rows=len(weekly_dfs), cols=1)
255
+ for week_index in range(len(weekly_dfs)):
256
+ week = weekly_dfs[week_index].reset_index()
257
+ fig.add_trace(
258
+ go.Scatter(
259
+ x=week[TIME],
260
+ y=week[GLUCOSE],
261
+ mode='lines+markers',
262
+ ), row=(week_index+1), col=1
263
+ )
264
+ fig.update_yaxes(title_text="Glucose Value (mg/dL)", row=(week_index+1), col=1)
265
+
266
+ if len(weekly_dfs) > 1:
267
+ offset_before = pd.Timedelta(hours=10)
268
+ offset_after = pd.Timedelta(hours=10)
269
+ first_end = pd.Timestamp(weekly_dfs[1].reset_index()[TIME].dt.date.iloc[0])
270
+ first_start = first_end - pd.Timedelta(weeks=1)
271
+ last_start = pd.Timestamp(weekly_dfs[-1].reset_index()[TIME].dt.date.iloc[0])
272
+ last_end = last_start + pd.Timedelta(weeks=1)
273
+ fig.update_xaxes(range=[first_start - offset_before, first_end + offset_after], row=1, col=1)
274
+ fig.update_xaxes(range=[last_start - offset_before, last_end + offset_after], row=len(weekly_dfs), col=1)
275
+ fig.update_yaxes(range=[min(np.min(data[GLUCOSE]), 60) - 10, max(np.max(data[GLUCOSE]), 180) + 10], tickfont_size=20, titlefont_size=15)
276
+ fig.update_xaxes(tickformat="%B %d, %Y <br> (%a)", tickfont_size=15, titlefont_size=30)
277
+
278
+ fig.update_layout(title=f"Weekly Plot for {id}", titlefont_size=30, height=height, showlegend=False)
279
+
280
+ if save:
281
+ path = os.path.join(save, f"{id}_weekly_plot")
282
+ fig.write_image(path + ".pdf", width=1500, height=height)
283
+ fig.write_image(path + ".png", width=1500, height=height)
284
+ fig.write_image(path + ".jpeg", width=1500, height=height)
285
+ fig.write_html(path +".html")
286
+
287
+ if app: return fig
288
+ fig.show()
289
+
290
+ def spaghetti_plot_all(df: pd.DataFrame, chunk_day: bool = False, save: str = None, height: int = 600):
291
+ """Graphs spaghetti plots for all patients within the given DataFrame.
292
+ Also saves these plots as PDF and HTML files if passed a valid path.
293
+
294
+ :param df: the DataFrame (following package guidelines) containing the CGM data to plot
295
+ :type df: 'pandas.DataFrame'
296
+ :param chunk_day: boolean indicating whether to create separate subplots based on whether the data occurred on a weekday or during the weekend
297
+ :type chunk_day: bool, optional
298
+ :param save: path of the location where the saved PDF and HTML versions of the plots are saved, defaults to None
299
+ :type save: str, optional
300
+ :param height: the height (in pixels) of the resulting plot(s), defaults to 600
301
+ :type height: int, optional
302
+ """
303
+ for id, data in df.groupby(ID):
304
+ spaghetti_plot(df=data, id=id, chunk_day=chunk_day, save=save, height=height)
305
+
306
+ def spaghetti_plot(df: pd.DataFrame, id: str, chunk_day: bool = False, save: str = None, height: int = 600, app=False):
307
+ """Graphs a spaghetti plot for the given patient within the given DataFrame.
308
+ Also saves this plot as both a PDF and HTML file if passed a valid path.
309
+
310
+ :param df: the DataFrame (following package guidelines) containing the CGM data to plot
311
+ :type df: 'pandas.DataFrame'
312
+ :param id: the identification of the patient whose CGM data to plot within the given DataFrame
313
+ :type id: str
314
+ :param chunk_day: boolean indicating whether to create separate subplots based on whether the data occurred on a weekday or during the weekend
315
+ :type chunk_day: bool, optional
316
+ :param save: path of the location where the saved PDF and HTML versions of the plots are saved, defaults to None
317
+ :type save: str, optional
318
+ :param height: the height (in pixels) of the resulting plot(s), defaults to 600
319
+ :type height: int, optional
320
+ :param app: boolean indicating whether to return the Plotly figure instead of rendering it (used mainly within the web application), defaults to False
321
+ :type app: bool, optional
322
+ :return: None if app is False, otherwise the Plotly figure
323
+ :rtype: 'plotly.graph_objects.Figure' | None
324
+ """
325
+ data = df.loc[id].copy()
326
+ data["Day"] = data[TIME].dt.date
327
+ times = data[TIME] - data[TIME].dt.normalize()
328
+ data["Time"] = (pd.to_datetime(["1/1/1970" for i in range(data[TIME].size)]) + times)
329
+ data.sort_values(by=[TIME], inplace=True)
330
+
331
+ fig = px.line(data, x="Time", y=GLUCOSE, color="Day", title=f"Spaghetti Plot for {id}", height=height, facet_col="Day Chunking" if chunk_day else None)
332
+ fig.update_xaxes(tickformat="%H:%M:%S", title_text="Time", tickfont_size=20, titlefont_size=35) # shows only the times for the x-axis
333
+ fig.update_yaxes(title_text="Glucose Value (mg/dL)", tickfont_size=20, titlefont_size=35, row=1, col=1)
334
+ fig.update_annotations(font_size=35)
335
+ fig.update_layout(title=dict(font=dict(size=40)), legend=dict(font=dict(size=30)))
336
+
337
+ if save:
338
+ path = os.path.join(save, f"{id}_spaghetti_plot")
339
+ fig.write_image(path + ".pdf", width=1500, height=height)
340
+ fig.write_image(path + ".png", width=1500, height=height)
341
+ fig.write_image(path + ".jpeg", width=1500, height=height)
342
+ fig.write_html(path +".html")
343
+
344
+ if app: return fig
345
+ fig.show()
346
+
347
+ def AGP_plot_all(df: pd.DataFrame, height: int = 600, save: str = None):
348
+ """Graphs AGP-report style plots for all of the patients within the given DataFrame.
349
+ Also saves these plots as both PDF and HTML files if passed a valid path.
350
+
351
+ :param df: the DataFrame (following package guidelines) containing the CGM data to plot
352
+ :type df: 'pandas.DataFrame'
353
+ :param save: path of the location where the saved PDF and HTML versions of the plot are saved, defaults to None
354
+ :type save: str, optional
355
+ :param height: the height (in pixels) of the resulting plot, defaults to 600
356
+ :type height: int, optional
357
+ """
358
+ for id, data in df.groupby(ID):
359
+ AGP_plot(data, id, height)
360
+
361
+
362
+ def AGP_plot(df: pd.DataFrame, id: str, save: str = None, height: int = 600, app=False):
363
+ """Graphs AGP-report style plots for the given patient within the given DataFrame.
364
+ Also saves this plot as both a PDF and HTML file if passed a valid path.
365
+
366
+ :param df: the DataFrame (following package guidelines) containing the CGM data to plot
367
+ :type df: 'pandas.DataFrame'
368
+ :param id: the identification of the patient whose CGM data to plot within the given DataFrame
369
+ :type id: str
370
+ :param save: path of the location where the saved PDF and HTML versions of the plot are saved, defaults to None
371
+ :type save: str, optional
372
+ :param height: the height (in pixels) of the resulting plot, defaults to 600
373
+ :type height: int, optional
374
+ :param app: boolean indicating whether to return the Plotly figure instead of rendering it (used mainly within the web application), defaults to False
375
+ :type app: bool, optional
376
+ :return: None if app is False, otherwise the Plotly figure
377
+ :rtype: 'plotly.graph_objects.Figure' | None
378
+ """
379
+ config.read('config.ini')
380
+ interval = int(config["variables"]["interval"])
381
+ if interval > 5:
382
+ raise Exception("Data needs to have measurement intervals at most 5 minutes long")
383
+
384
+ data = df.loc[id].copy()
385
+ data.reset_index(inplace=True)
386
+
387
+ data[[TIME, GLUCOSE, ID]] = pp._resample_data(data[[TIME, GLUCOSE, ID]])
388
+ times = data[TIME] - data[TIME].dt.normalize()
389
+ # need to be in a DateTime format so plots can tell how to scale the x axis labels below
390
+ data["Time"] = (pd.to_datetime(["1/1/1970" for i in range(data[TIME].size)]) + times)
391
+
392
+ data.set_index("Time", inplace=True)
393
+
394
+ agp_data = pd.DataFrame()
395
+ for time, measurements in data.groupby("Time"):
396
+ metrics = {
397
+ "Time": time,
398
+ "5th": measurements[GLUCOSE].quantile(0.05),
399
+ "25th": measurements[GLUCOSE].quantile(0.25),
400
+ "Median": measurements[GLUCOSE].median(),
401
+ "75th": measurements[GLUCOSE].quantile(0.75),
402
+ "95th": measurements[GLUCOSE].quantile(0.95),
403
+ }
404
+ agp_data = pd.concat([agp_data, pd.DataFrame.from_records([metrics])])
405
+
406
+ agp_data.sort_values(by=["Time"], inplace=True)
407
+
408
+ fig = go.Figure()
409
+ fig.add_trace(go.Scatter(name="5th", x=agp_data["Time"], y=agp_data["5th"], line=dict(color="#869FCE")))
410
+ fig.add_trace(go.Scatter(name="25th", x=agp_data["Time"], y=agp_data["25th"], fill="tonexty", fillcolor="#C9D4E9", line=dict(color="#97A8CB")))
411
+ fig.add_trace(go.Scatter(name="Median",x=agp_data["Time"], y=agp_data["Median"], fill="tonexty", fillcolor="#97A8CB", line=dict(color="#183260")))
412
+ fig.add_trace(go.Scatter(name="75th", x=agp_data["Time"], y=agp_data["75th"], fill="tonexty", fillcolor="#97A8CB", line=dict(color="#97A8CB")))
413
+ fig.add_trace(go.Scatter(name="95th", x=agp_data["Time"], y=agp_data["95th"], fill="tonexty", fillcolor="#C9D4E9", line=dict(color="#869FCE")))
414
+
415
+ fig.add_hline(y=70, line_color="lime")
416
+ fig.add_hline(y=140, line_color="lime")
417
+ fig.add_hline(y=180, line_color="green")
418
+ fig.update_layout(title={"text": f"AGP Plot for {id}", "font": {"size":30}}, height=height, yaxis_range = [35,405], legend=dict(font=dict(size=30)))
419
+ fig.update_xaxes(tickformat="%H:%M:%S", title_text="Time", tickfont_size=20, titlefont_size=25) # shows only the times for the x-axis
420
+ fig.update_yaxes(title_text="Glucose Value (mg/dL)", tickfont_size=20, titlefont_size=25)
421
+
422
+ if app: return fig
423
+ fig.show()
424
+
425
+ def AGP_report(df: pd.DataFrame, id: str, path: str = None):
426
+ """Creates an AGP-report for the given patient within the given DataFrame.
427
+ Also saves this plot as a PDF file if passed a valid path.
428
+
429
+ :param df: the DataFrame (following package guidelines) containing the CGM data to report on
430
+ :type df: 'pandas.DataFrame'
431
+ :param id: the identification of the patient whose CGM data to report on
432
+ :type id: str
433
+ :param path: path of the location where the saved PDF version of the plot is saved, defaults to None
434
+ :type path: str, optional
435
+ :return: the AGP-report in string form if path is False, otherwise None
436
+ :rtype: str | None
437
+ """
438
+ fig = make_subplots(rows = 1, cols = 2, specs=[[{"type": "table"}, {"type": "bar"}]])
439
+
440
+ patient_data = df.loc[id]
441
+ TIR = {"< 54 mg/dL": percent_time_in_range(patient_data, 0, 53),
442
+ "54 - 69 mg/dL": percent_time_in_range(patient_data, 54, 69),
443
+ "70 - 140 mg/dL": percent_time_in_tight_range(patient_data),
444
+ "141 - 180 mg/dL": percent_time_in_range(patient_data, 141, 180),
445
+ "181 - 250 mg/dL": percent_time_in_range(patient_data, 181, 250),
446
+ "> 250 mg/dL": percent_time_in_range(patient_data, 251, 400)}
447
+
448
+ COLORS = {"< 54 mg/dL": "rgba(151,34,35,255)",
449
+ "54 - 69 mg/dL": "rgba(229,43,23,255)",
450
+ "70 - 140 mg/dL": "#00ff00",
451
+ "141 - 180 mg/dL": "rgba(82,173,79,255)",
452
+ "181 - 250 mg/dL": "rgba(250,192,3,255)",
453
+ "> 250 mg/dL": "rgba(241,136,64,255)"}
454
+
455
+ for key, value in TIR.items():
456
+ fig.add_trace(go.Bar(name=key, x=["TIR"], y=[value], marker=dict(color=COLORS[key]), text=[round(value, 2)], textposition="inside"), row=1, col=2)
457
+ fig.update_layout(barmode='stack', height=600, font=dict(size=20))
458
+
459
+ ave_glucose = mean(patient_data)
460
+ gmi = GMI(patient_data)
461
+ cv = CV(patient_data)
462
+
463
+ days = patient_data[TIME].dt.date
464
+ table_body = [["Test Patient ID:", f"{len(days.unique())} Days:", "Average Glucose (mg/dL):", "Glucose Management Indicator:", "Glucose Variability:"],
465
+ [id, f"{days.iloc[0]} to {days.iloc[-1]}", str(round(ave_glucose, 2)), str(round(gmi, 2)), str(round(cv, 2))]]
466
+ fig.add_trace(go.Table(cells=dict(values=table_body,align=['left', 'center'],font=dict(size=25),height=50)), row=1, col=1)
467
+
468
+ agp = AGP_plot(df, id, app=True);
469
+ weekly = weekly_plot(df, id, height=500, app=True);
470
+
471
+ header_html = fig.to_html(full_html=False, include_plotlyjs="cdn")
472
+ agp_html = agp.to_html(full_html=False, include_plotlyjs=False)
473
+ weekly_html = weekly.to_html(full_html=False, include_plotlyjs=False)
474
+
475
+ html_template = f"""
476
+ <!DOCTYPE html>
477
+ <html>
478
+ <head>
479
+ <title>AGP Report for {id}</title>
480
+ </head>
481
+ <body>
482
+ <h1> AGP Report: Continuous Glucose Monitoring </h1>
483
+ {header_html}
484
+ {agp_html}
485
+ {weekly_html}
486
+ </body>
487
+ </html>
488
+ """
489
+
490
+ if path:
491
+ with open(os.path.join(path, f"{id}_AGP_Report.html"), "w") as f:
492
+ f.write(html_template)
493
+
494
+ return html_template