Cleanup documentation.
This commit is contained in:
parent
4d77fac610
commit
9f3b597f55
@ -5,17 +5,20 @@ __author__ = 'Timothy Allen'
|
||||
__email__ = 'tim@treehouse.org.za'
|
||||
__license__ = 'MIT'
|
||||
|
||||
''' Included are the Noto Sans and IcoGluco font sets.
|
||||
Noto Sans is licensed under the SIL Open Font License version 1.1
|
||||
''' Included are the OpenSans and IcoGluco font sets.
|
||||
IcoGluco contains fonts from Noto Sans, which is licensed under the
|
||||
SIL Open Font License version 1.1
|
||||
<http://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=OFL>,
|
||||
IcoGluco contains fonts from Noto Sans, as well as
|
||||
green apple character from Vectors Market <https://www.flaticon.com/authors/vectors-market>,
|
||||
as well as a green apple character from
|
||||
Vectors Market <https://www.flaticon.com/authors/vectors-market>,
|
||||
licensed under Creative Commons BY 3.0, <http://creativecommons.org/licenses/by/3.0/>, and
|
||||
syringe and pushpin characters from FreePik, <http://www.freepik.com>,
|
||||
licensed under Creative Commons BY 3.0, <http://creativecommons.org/licenses/by/3.0/>
|
||||
licensed under Creative Commons BY 3.0, <http://creativecommons.org/licenses/by/3.0/>.
|
||||
'''
|
||||
|
||||
# TODO: weekly graph with each day's figures as a different-coloured line
|
||||
# TODO: Split each type of charts into a separate function and offer a means
|
||||
# of selecting which charts to generate
|
||||
|
||||
import argparse
|
||||
import csv
|
||||
@ -80,6 +83,10 @@ def main():
|
||||
for row in rows:
|
||||
row = parse_entry(row, args.icons)
|
||||
|
||||
''' Fill in gaps that might exist in the data, in order to smooth the curves and fills '''
|
||||
''' We're using 8 minute gaps in order to have more accurate fills '''
|
||||
rows = fill_gaps(rows, interval=dt.timedelta(minutes=10))
|
||||
|
||||
''' If we're on the default values for units, highs and lows, check that the average
|
||||
value is under 35 (assuming that average mmol/L < 35 and average mg/dL > 35) '''
|
||||
if args.units == UNIT_MMOLL and (args.high == DEFAULT_HIGH or args.low == DEFAULT_LOW):
|
||||
@ -92,16 +99,6 @@ def main():
|
||||
args.graph_max = GRAPH_MAX_MGDL
|
||||
args.graph_min = GRAPH_MIN_MGDL
|
||||
|
||||
''' Fill in gaps that might exist in the data, in order to smooth the curves and fills '''
|
||||
''' We're using 8 minute gaps in order to have more accurate fills '''
|
||||
rows = fill_gaps(rows, interval=dt.timedelta(minutes=10))
|
||||
|
||||
''' Calculate the days and weeks in which we are interested '''
|
||||
''' Note that trim_weeks should be adjusted based on the interval passed to fill_gaps() '''
|
||||
(days, weeks) = list_days_and_weeks(rows, trim_weeks=300)
|
||||
totalweeks = sum([len(weeks[y]) for y in weeks])
|
||||
totaldays = len(days)
|
||||
|
||||
''' Set some defaults '''
|
||||
rcParams['font.size'] = 8
|
||||
rcParams['axes.titlesize'] = 12
|
||||
@ -116,15 +113,21 @@ def main():
|
||||
custom icons on IcoMoon, works around this. '''
|
||||
if args.icons:
|
||||
args.customfont = import_font('fonts/icogluco.ttf')
|
||||
#args.customfont = import_font('fonts/OpenSansEmoji.ttf') # Alternate working font
|
||||
#args.customfont = import_font('fonts/OpenSansEmoji.ttf') # Alternate font
|
||||
|
||||
''' Calculate the days and weeks in which we are interested '''
|
||||
''' Note that trim_weeks should be adjusted based on the interval passed to fill_gaps() '''
|
||||
(days, weeks) = list_days_and_weeks(rows, trim_weeks=300)
|
||||
totalweeks = sum([len(weeks[y]) for y in weeks])
|
||||
totaldays = len(days)
|
||||
|
||||
nrows = args.graphs_per_page
|
||||
ncols = 1
|
||||
plotnum = 1
|
||||
with FigurePDF(args.output_file) as pdf:
|
||||
|
||||
''' Overall averages for all data by hour '''
|
||||
title = 'Overall Average Daily Glucose Summary'
|
||||
''' Overall averages for all data by hour of the day '''
|
||||
title = 'Overall Average Glucose Summary'
|
||||
|
||||
data = {}
|
||||
for row in rows:
|
||||
@ -144,7 +147,7 @@ def main():
|
||||
'min' : intervals.get(i).get('min'),
|
||||
}
|
||||
|
||||
''' Calculate the mean and median blood glucose levels for the day '''
|
||||
''' Calculate the mean and median blood glucose and HbA1c levels '''
|
||||
(g_mean, g_median, a_mean, a_median) = calculate_averages(data, args)
|
||||
|
||||
figure = Figure(figsize=args.pagesize)
|
||||
@ -155,9 +158,10 @@ def main():
|
||||
figure.set_tight_layout({'pad':3})
|
||||
|
||||
''' Draw the target range '''
|
||||
ax.axhspan(args.low, args.high, facecolor='#0072b2', edgecolor='#a8a8a8', alpha=0.2, zorder=5)
|
||||
ax.axhspan(args.low, args.high, facecolor='#0072b2', edgecolor='#a8a8a8', alpha=0.1, zorder=5)
|
||||
|
||||
''' The maxmined curve of maximum and minimum values '''
|
||||
''' The maxmin curve (maximum and minimum values for each 15 minute
|
||||
period of the data set, by day) '''
|
||||
generate_plot(intervaldata,
|
||||
ax=ax,
|
||||
transforms={'spline':False, 'maxmin':True},
|
||||
@ -166,6 +170,8 @@ def main():
|
||||
alpha=0.5,
|
||||
)
|
||||
|
||||
''' The graph with a bezier curve applied, and a boundary transform to change line colour
|
||||
above and below the target values '''
|
||||
generate_plot(data,
|
||||
ax=ax,
|
||||
transforms={'bezier':True, 'avga1c':a_median, \
|
||||
@ -178,7 +184,8 @@ def main():
|
||||
pdf.savefig(figure)
|
||||
ax.clear()
|
||||
|
||||
''' Overall averages for a week by hour '''
|
||||
|
||||
''' Overall averages for a week by hour of the dday '''
|
||||
cnt = 0
|
||||
for year in reversed(sorted(weeks.keys())):
|
||||
for week in reversed(sorted(weeks[year].keys())):
|
||||
@ -187,7 +194,7 @@ def main():
|
||||
monday = time + dt.timedelta(days=-time.weekday(), weeks=week-1)
|
||||
sunday = monday + dt.timedelta(days=6)
|
||||
period = monday.strftime('%A, %-d %B %Y') + ' to ' + sunday.strftime('%A, %-d %B %Y');
|
||||
title = 'Average Daily Glucose for ' + period
|
||||
title = 'Average Glucose for ' + period
|
||||
|
||||
weekrows = []
|
||||
for row in rows:
|
||||
@ -204,6 +211,8 @@ def main():
|
||||
'comment' : row.get('comment'),
|
||||
}
|
||||
|
||||
''' Calculate the maximum and minimum value for each 15-minute period
|
||||
of the day, across the week '''
|
||||
intervals = calculate_max_min(weekrows)
|
||||
intervaldata = {}
|
||||
for i in intervals:
|
||||
@ -213,7 +222,7 @@ def main():
|
||||
'min' : intervals.get(i).get('min'),
|
||||
}
|
||||
|
||||
''' Calculate the mean and median blood glucose levels for the day '''
|
||||
''' Calculate the mean and median blood glucose levels for the week '''
|
||||
(g_mean, g_median, a_mean, a_median) = calculate_averages(data, args)
|
||||
|
||||
if cnt % nrows == 0:
|
||||
@ -226,7 +235,7 @@ def main():
|
||||
figure.set_tight_layout({'pad':3})
|
||||
|
||||
''' Draw the target range '''
|
||||
ax.axhspan(args.low, args.high, facecolor='#0072b2', edgecolor='#a8a8a8', alpha=0.2, zorder=5)
|
||||
ax.axhspan(args.low, args.high, facecolor='#0072b2', edgecolor='#a8a8a8', alpha=0.1, zorder=5)
|
||||
|
||||
''' The maxmined curve of maximum and minimum values '''
|
||||
generate_plot(intervaldata,
|
||||
@ -237,6 +246,8 @@ def main():
|
||||
alpha=0.5,
|
||||
)
|
||||
|
||||
''' The graph with a bezier curve applied, and a boundary transform to change line colour
|
||||
above and below the target values '''
|
||||
generate_plot(data,
|
||||
ax=ax,
|
||||
transforms={'bezier':True, \
|
||||
@ -280,19 +291,20 @@ def main():
|
||||
''' Draw the target range '''
|
||||
ax.axhspan(args.low, args.high, facecolor='#0072b2', edgecolor='#a8a8a8', alpha=0.2, zorder=5)
|
||||
|
||||
''' Draw graph with a spline tranform and labels '''
|
||||
generate_plot(data,
|
||||
ax=ax,
|
||||
transforms={'spline':True, 'label':True, 'avgglucose':g_median, 'avga1c':a_median},
|
||||
args=args,
|
||||
color=BLUE,
|
||||
)
|
||||
|
||||
)
|
||||
''' For max higher than target high '''
|
||||
''' Fill the chart with colour when line is higher or lower than target range '''
|
||||
generate_plot(data,
|
||||
ax=ax,
|
||||
transforms={'spline':True, 'fill':True},
|
||||
args=args,
|
||||
)
|
||||
)
|
||||
|
||||
''' Save the graph to the output PDF if we're at the end of the page '''
|
||||
if (cnt + 1) % nrows == 0 or (cnt + 1) == totaldays:
|
||||
@ -367,7 +379,7 @@ def generate_plot(data, ax=None, transforms={}, args=[], **plot_args):
|
||||
else:
|
||||
maxmin = False
|
||||
|
||||
''' Process points to apply smoothing and other fixups '''
|
||||
''' Transform points to apply smoothing and other fixups '''
|
||||
for transform in transforms:
|
||||
if transform == 'linear' and transforms.get(transform) is True:
|
||||
''' Use SciPy's interp1d for linear transforming '''
|
||||
@ -379,10 +391,11 @@ def generate_plot(data, ax=None, transforms={}, args=[], **plot_args):
|
||||
|
||||
elif transform == 'spline' and transforms.get(transform) is True:
|
||||
''' Use SciPy's UnivariateSpline for transforming (s is transforming factor) '''
|
||||
''' An s of 8 (mmol/L) or 200 (mg/dL) was chosen by experimentation! '''
|
||||
if args.units == UNIT_MMOLL:
|
||||
s = 8
|
||||
else:
|
||||
s = convert_glucose_unit(12, UNIT_MMOLL)
|
||||
s = 200
|
||||
if not maxmin:
|
||||
curve = interpolate.UnivariateSpline(x=x, y=y, k=3, s=s)
|
||||
y = curve(x)
|
||||
@ -399,7 +412,7 @@ def generate_plot(data, ax=None, transforms={}, args=[], **plot_args):
|
||||
|
||||
''' The binomial calculation for the bezier curve overflows with arrays of 1020 or more elements,
|
||||
For large arrays, get a smaller slice of the full array.
|
||||
Do this by removing every nth element from the array '''
|
||||
Do this by removing every nth element from the array. '''
|
||||
n = 5
|
||||
while len(x) > 1000:
|
||||
x = np.delete(x, np.arange(0, len(x), n), axis=0)
|
||||
@ -409,21 +422,22 @@ def generate_plot(data, ax=None, transforms={}, args=[], **plot_args):
|
||||
curve = np.array([c for _, c in bezier(np.array([x,y]).T, 250)])
|
||||
(x, y) = (curve[:,0], curve[:,1])
|
||||
|
||||
''' Add the mean or median glucose and A1c values '''
|
||||
''' Add the mean or median glucose and A1c values in an annotation box '''
|
||||
if transform == 'avgglucose' and isinstance(transforms.get(transform), (int, float)):
|
||||
if args.units == UNIT_MMOLL:
|
||||
gmtext = 'Median glucose: %.1f%s' % (round(transforms.get('avgglucose'), 1), args.units)
|
||||
else:
|
||||
gmtext = 'Median glucose: %.0f%s' % (round(transforms.get('avgglucose'), 1), args.units)
|
||||
|
||||
ax.annotate(gmtext, fontsize=9, \
|
||||
xy=(0.95, 0.85), xycoords='axes fraction', verticalalignment='top', horizontalalignment='right', \
|
||||
zorder=40, bbox=dict(facecolor=GREEN, edgecolor='#009e73', alpha=0.7, pad=8), \
|
||||
)
|
||||
ax.annotate(gmtext, fontsize=9, xy=(0.95, 0.85),
|
||||
xycoords='axes fraction', verticalalignment='top', horizontalalignment='right',
|
||||
zorder=40, bbox=dict(facecolor=GREEN, edgecolor='#009e73', alpha=0.7, pad=8),
|
||||
)
|
||||
if transform == 'avga1c' and isinstance(transforms.get(transform), (int, float)):
|
||||
ax.annotate('Median HbA1c: %.1f%%' % round(transforms.get('avga1c'), 1), fontsize=9, \
|
||||
xy=(0.05, 0.85), xycoords='axes fraction', verticalalignment='top', horizontalalignment='left', \
|
||||
zorder=40, bbox=dict(facecolor=BOXYELLOW, edgecolor='#e69f00', alpha=0.7, pad=8), \
|
||||
ax.annotate('Median HbA1c: %.1f%%' % round(transforms.get('avga1c'), 1), fontsize=9,
|
||||
xy=(0.05, 0.85), xycoords='axes fraction',
|
||||
verticalalignment='top', horizontalalignment='left',
|
||||
zorder=40, bbox=dict(facecolor=BOXYELLOW, edgecolor='#e69f00', alpha=0.7, pad=8),
|
||||
)
|
||||
|
||||
if args.units == UNIT_MMOLL:
|
||||
@ -434,14 +448,13 @@ def generate_plot(data, ax=None, transforms={}, args=[], **plot_args):
|
||||
if transform == 'label' and transforms.get(transform) is True and args.icons is True:
|
||||
for x_pos, y_pos, label in zip(x, y, z):
|
||||
if isinstance(label, dict) and len(label) > 0:
|
||||
symbol = ''
|
||||
symbol = '$'
|
||||
for key in label:
|
||||
''' In the included IcoGluco font use for args.customfont,
|
||||
\N{SYRINGE} is a straight syringe (from FreePik),
|
||||
\N{PUSHPIN} is a an angled syringe (from FreePik),
|
||||
\N{DAGGER} is unused,
|
||||
\N{GREEN APPLE} is an apple (from Vectors Market. '''
|
||||
\N{SYRINGE} is a straight syringe (modified from FreePik) for rotated labels,
|
||||
\N{PUSHPIN} is a an angled syringe (from FreePik) for horizontal labels,
|
||||
\N{DAGGER} is unused (reserved a different syringe icon),
|
||||
\N{GREEN APPLE} is an apple (from Vectors Market). '''
|
||||
if key == 'Insulin':
|
||||
if isinstance(label.get(key), str):
|
||||
symbol += '\N{SYRINGE}^{%s}' % label.get(key)
|
||||
@ -452,13 +465,9 @@ def generate_plot(data, ax=None, transforms={}, args=[], **plot_args):
|
||||
elif key == 'Food':
|
||||
symbol += '\N{GREEN APPLE}'
|
||||
symbol += '$'
|
||||
ax.annotate(
|
||||
symbol,
|
||||
xy=(x_pos, args.graph_max-y_offset),
|
||||
rotation=45,
|
||||
zorder=25,
|
||||
fontsize=10,
|
||||
fontproperties=args.customfont,
|
||||
ax.annotate(symbol, xy=(x_pos, args.graph_max-y_offset),
|
||||
rotation=45, zorder=25, fontsize=10,
|
||||
fontproperties=args.customfont,
|
||||
)
|
||||
|
||||
''' Create a line coloured according to the list in transforms['color'] '''
|
||||
@ -493,8 +502,8 @@ def generate_plot(data, ax=None, transforms={}, args=[], **plot_args):
|
||||
|
||||
def import_font(fontname):
|
||||
''' Turns a relative font path into a matplotlib font property. '''
|
||||
basedir = os.path.dirname(os.path.abspath(__file__))
|
||||
fontpath = os.path.join(basedir, fontname)
|
||||
basedir = os.path.dirname(os.path.abspath(__file__))
|
||||
fontpath = os.path.join(basedir, fontname)
|
||||
if not os.path.exists(fontpath):
|
||||
raise UserError("Font %s does not exist" % fontpath)
|
||||
prop = fm.FontProperties(fname=fontpath)
|
||||
@ -516,13 +525,13 @@ def parse_entry(data, icons, fmt='%Y-%m-%d %H:%M:%S'):
|
||||
'''
|
||||
if icons:
|
||||
''' Ignore comments that aren't relevant '''
|
||||
rrelevant = re.compile('(Food|Rapid-acting insulin|Long-acting insulin)(?: \((.*?)\))', flags=re.IGNORECASE)
|
||||
rduplicate = re.compile('^(I\$\^\{\d+\S?)(\}.*)$')
|
||||
rrelevant = re.compile('(Food|Rapid-acting insulin|Long-acting insulin)(?: \((.*?)\))', flags=re.IGNORECASE)
|
||||
rduplicate = re.compile('^(I\$\^\{\d+\S?)(\}.*)$')
|
||||
commentparts = {}
|
||||
for part in data.get('comment').split('; '):
|
||||
relevant = rrelevant.search(part)
|
||||
if relevant is not None:
|
||||
ctype = relevant.group(1)
|
||||
ctype = relevant.group(1)
|
||||
cvalue = relevant.group(2)
|
||||
|
||||
''' Convert floating point-style strings (2.0) to integer-style strings (2) '''
|
||||
@ -828,6 +837,9 @@ def convert_glucose_unit(value, from_unit, to_unit=None):
|
||||
|
||||
Raises:
|
||||
exceptions.InvalidGlucoseUnit: If the parameters are incorrect.
|
||||
Note that this is defined by the main glucometerutils package, from which
|
||||
this function is duplicated, and is not a valid exception for this script.
|
||||
So let's hope it doesn't get triggered!
|
||||
"""
|
||||
if from_unit not in VALID_UNITS:
|
||||
raise exceptions.InvalidGlucoseUnit(from_unit)
|
||||
|
Loading…
Reference in New Issue
Block a user