Cleanup documentation.

This commit is contained in:
Timothy Allen 2018-01-05 17:29:59 +02:00
parent 4d77fac610
commit 9f3b597f55
1 changed files with 68 additions and 56 deletions

View File

@ -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)