diff --git a/glucometer_graphs.py b/glucometer_graphs.py index dff5b07..722ea12 100755 --- a/glucometer_graphs.py +++ b/glucometer_graphs.py @@ -1,6 +1,9 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- ''' Utility to convert data from a glucometer into charts. ''' +__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 @@ -12,11 +15,6 @@ licensed under Creative Commons BY 3.0, ''' -__author__ = 'Timothy Allen' -__email__ = 'tim@treehouse.org.za' -__license__ = 'MIT' - -# TODO: comments -- unicode/images/np.array # TODO: weekly graph with each day's figures as a different-coloured line import argparse @@ -40,30 +38,32 @@ import re from scipy import interpolate from scipy.special import binom import sys -import pprint -# Constants for units +''' Constants for units ''' UNIT_MGDL = 'mg/dL' UNIT_MMOLL = 'mmol/L' VALID_UNITS = [UNIT_MGDL, UNIT_MMOLL] -# When averaging, set the period to this number of minutes +''' When averaging, set the period to this number of minutes ''' INTERVAL = 15 -# Maximum gluclose value to display (TODO: mmol/mg) -GRAPH_MAX = 21 -GRAPH_MIN = 1 +''' Set the default high and low in mmol/L; it will be reset to mg/dL if neccessary ''' DEFAULT_HIGH = 8 DEFAULT_LOW = 4 +''' Maximum glucose value to display ''' +GRAPH_MAX_MMOLL = 21 +GRAPH_MIN_MMOLL = 0 +GRAPH_MAX_MGDL = 400 +GRAPH_MIN_MGDL = 0 -# Colour for below-target maxmins +''' Colour for below-target maxmins ''' RED = '#d71920' -# Colour for above-target maxmins +'''' Colour for above-target maxmins ''' YELLOW = '#f1b80e' -# Colour for graph lines +''' Colour for graph lines ''' BLUE = '#02538f' -# Colour for median glucose box +''' Colour for median glucose box ''' GREEN = '#009e73' -# Colour for median A1c box +''' Colour for median A1c box ''' BOXYELLOW = '#e69f00' def main(): @@ -71,8 +71,6 @@ def main(): raise Exception( 'Unsupported Python version, please use at least Python 3.2') - pp = pprint.PrettyPrinter(depth=6) - args = parse_arguments() ''' This could be done directly from glucometerutils instead of via CSV ''' @@ -82,17 +80,17 @@ def main(): for row in rows: row = parse_entry(row, args.icons) - # 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 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): mean = round(np.mean([l.get('value') for l in rows]), 1) if mean > 35: args.units = UNIT_MGDL args.high = convert_glucose_unit(args.high, UNIT_MMOLL) args.low = convert_glucose_unit(args.low, UNIT_MMOLL) - args.graph_max = convert_glucose_unit(args.graph_max, UNIT_MMOLL) - args.graph_min = convert_glucose_unit(args.graph_min, UNIT_MMOLL) - + ''' Manually specify max and min for mg/dL ''' + 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 ''' @@ -111,9 +109,13 @@ def main(): rcParams['font.sans-serif'] = ['Calibri','Verdana','Geneva','Arial','Helvetica','DejaVu Sans','Bitstream Vera Sans','sans-serif'] rcParams['mathtext.default'] = 'regular' - # Load custom fonts for the icon sets + ''' Load custom fonts for the icon sets + At present, backend_pdf does not parse unicode correctly, and unicode + characters from many fonts that lack proper glyph names are massed together + and printed as the same character. The IcoGluco font, generated from Noto Sans and + custom icons on IcoMoon, works around this. ''' if args.icons: - args.customfont = import_font('fonts/icogluco.ttf') # Works + args.customfont = import_font('fonts/icogluco.ttf') #args.customfont = import_font('fonts/OpenSansEmoji.ttf') # Alternate working font nrows = args.graphs_per_page @@ -302,18 +304,16 @@ def main(): def generate_plot(data, ax=None, transforms={}, args=[], **plot_args): - pp = pprint.PrettyPrinter(depth=6) - (x, y, z, p, q) = (list(), list(), list(), list(), list()) for (key, value) in sorted(data.items()): - # Time + ''' Time ''' a = key if 'maxmin' in transforms: - # If a max and a min exists, initialise them to y and z + ''' If a max and a min exists, initialise them to y and z ''' b = value.get('max') c = value.get('min') else: - # Glucose and comment + ''' Glucose and comment ''' b = value.get('value') c = value.get('comment', '') x.append(a) @@ -342,7 +342,7 @@ def generate_plot(data, ax=None, transforms={}, args=[], **plot_args): if args.units == UNIT_MMOLL: y_tick_freq = 2 else: - y_tick_freq = convert_glucose_unit(2, UNIT_MMOLL) + y_tick_freq = 50 ''' Formatting for axis labels, using date calculations from above ''' ax.set_xlabel('Time', fontsize=9) @@ -373,7 +373,8 @@ def generate_plot(data, ax=None, transforms={}, args=[], **plot_args): ''' Use SciPy's interp1d for linear transforming ''' if not maxmin: f = interpolate.interp1d(x, y, kind='linear') - x = np.linspace(x.min(), x.max(), 50) # 50 is number of points to make between x.max & x.min + ''' 50 is number of points to make between x.max & x.min ''' + x = np.linspace(x.min(), x.max(), 50) y = f(x) elif transform == 'spline' and transforms.get(transform) is True: @@ -425,9 +426,6 @@ def generate_plot(data, ax=None, transforms={}, args=[], **plot_args): zorder=40, bbox=dict(facecolor=BOXYELLOW, edgecolor='#e69f00', alpha=0.7, pad=8), \ ) - # XXX At present, backend_pdf does not parse unicode correctly, and all recent - # unicode chacters that lack proper glyph names are massed together and printed - # as the same character if args.units == UNIT_MMOLL: y_offset = 6 else: @@ -454,7 +452,6 @@ def generate_plot(data, ax=None, transforms={}, args=[], **plot_args): elif key == 'Food': symbol += '\N{GREEN APPLE}' symbol += '$' - print(symbol) ax.annotate( symbol, xy=(x_pos, args.graph_max-y_offset), @@ -495,9 +492,9 @@ def generate_plot(data, ax=None, transforms={}, args=[], **plot_args): return ax def import_font(fontname): + ''' Turns a relative font path into a matplotlib font property. ''' basedir = os.path.dirname(os.path.abspath(__file__)) - fontdir = os.path.join(basedir, 'fonts') - fontpath = os.path.join(fontdir, fontname) + 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) @@ -528,7 +525,6 @@ def parse_entry(data, icons, fmt='%Y-%m-%d %H:%M:%S'): ctype = relevant.group(1) cvalue = relevant.group(2) - #cvalue = re.sub('(\d+)(\.\d+)?', '\g<1>', cvalue) ''' Convert floating point-style strings (2.0) to integer-style strings (2) ''' try: cvalue = int(float(cvalue)) @@ -541,13 +537,6 @@ def parse_entry(data, icons, fmt='%Y-%m-%d %H:%M:%S'): if re.search('Long', ctype) is not None: cvalue += 'L' - # XXX At present, backend_pdf does not parse unicode correctly, and all recent - # unicode chacters that lack proper glyph names are massed together and printed - # as the same character - # XXX Alternatives include replacing the glyph with an image, or a Path - #ctype = re.sub('Food', '$\mathcal{\N{GREEN APPLE}}$', ctype, flags=re.IGNORECASE) - #ctype = re.sub('Rapid-acting insulin', '$\mathcal{\N{SYRINGE}}^\mathrm{'+cvalue+'}$', ctype, flags=re.IGNORECASE) - #ctype = re.sub('Long-acting insulin', '$\mathcal{\N{SYRINGE}}^\mathrm{'+cvalue+'}$', ctype, flags=re.IGNORECASE) ctype = re.sub('Rapid-acting insulin', 'Insulin', ctype, flags=re.IGNORECASE) ctype = re.sub('Long-acting insulin', 'Insulin', ctype, flags=re.IGNORECASE) @@ -800,11 +789,15 @@ def parse_arguments(): args.pagesize = verify_pagesize(args.pagesize) args.units = verify_units(args.units, args.high, args.low) if args.units == UNIT_MMOLL: - args.graph_max = GRAPH_MAX - args.graph_min = GRAPH_MIN + args.graph_max = GRAPH_MAX_MMOLL + args.graph_min = GRAPH_MIN_MMOLL else: - args.graph_max = convert_glucose_unit(GRAPH_MAX, UNIT_MMOLL) - args.graph_min = convert_glucose_unit(GRAPH_MIN, UNIT_MMOLL) + args.graph_max = GRAPH_MAX_MGDL + args.graph_min = GRAPH_MIN_MGDL + ''' If the user specified the units but not the high or low targets, set them now ''' + if args.high == DEFAULT_HIGH or args.low == DEFAULT_LOW: + args.high = convert_glucose_unit(args.high, UNIT_MMOLL) + args.low = convert_glucose_unit(args.low, UNIT_MMOLL) ''' Ensure we have a valid number of graphs_per_page ''' if not isinstance(args.graphs_per_page, int) or args.graphs_per_page < 1: