Final cleanup.
This commit is contained in:
parent
b070cc51ce
commit
4d77fac610
@ -1,6 +1,9 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
''' Utility to convert data from a glucometer into charts. '''
|
''' 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.
|
''' Included are the Noto Sans and IcoGluco font sets.
|
||||||
Noto Sans is licensed under the SIL Open Font License version 1.1
|
Noto Sans is licensed under the SIL Open Font License version 1.1
|
||||||
@ -12,11 +15,6 @@
|
|||||||
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/>
|
||||||
'''
|
'''
|
||||||
|
|
||||||
__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
|
# TODO: weekly graph with each day's figures as a different-coloured line
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
@ -40,30 +38,32 @@ import re
|
|||||||
from scipy import interpolate
|
from scipy import interpolate
|
||||||
from scipy.special import binom
|
from scipy.special import binom
|
||||||
import sys
|
import sys
|
||||||
import pprint
|
|
||||||
|
|
||||||
# Constants for units
|
''' Constants for units '''
|
||||||
UNIT_MGDL = 'mg/dL'
|
UNIT_MGDL = 'mg/dL'
|
||||||
UNIT_MMOLL = 'mmol/L'
|
UNIT_MMOLL = 'mmol/L'
|
||||||
VALID_UNITS = [UNIT_MGDL, UNIT_MMOLL]
|
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
|
INTERVAL = 15
|
||||||
# Maximum gluclose value to display (TODO: mmol/mg)
|
''' Set the default high and low in mmol/L; it will be reset to mg/dL if neccessary '''
|
||||||
GRAPH_MAX = 21
|
|
||||||
GRAPH_MIN = 1
|
|
||||||
DEFAULT_HIGH = 8
|
DEFAULT_HIGH = 8
|
||||||
DEFAULT_LOW = 4
|
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'
|
RED = '#d71920'
|
||||||
# Colour for above-target maxmins
|
'''' Colour for above-target maxmins '''
|
||||||
YELLOW = '#f1b80e'
|
YELLOW = '#f1b80e'
|
||||||
# Colour for graph lines
|
''' Colour for graph lines '''
|
||||||
BLUE = '#02538f'
|
BLUE = '#02538f'
|
||||||
# Colour for median glucose box
|
''' Colour for median glucose box '''
|
||||||
GREEN = '#009e73'
|
GREEN = '#009e73'
|
||||||
# Colour for median A1c box
|
''' Colour for median A1c box '''
|
||||||
BOXYELLOW = '#e69f00'
|
BOXYELLOW = '#e69f00'
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
@ -71,8 +71,6 @@ def main():
|
|||||||
raise Exception(
|
raise Exception(
|
||||||
'Unsupported Python version, please use at least Python 3.2')
|
'Unsupported Python version, please use at least Python 3.2')
|
||||||
|
|
||||||
pp = pprint.PrettyPrinter(depth=6)
|
|
||||||
|
|
||||||
args = parse_arguments()
|
args = parse_arguments()
|
||||||
|
|
||||||
''' This could be done directly from glucometerutils instead of via CSV '''
|
''' This could be done directly from glucometerutils instead of via CSV '''
|
||||||
@ -82,17 +80,17 @@ def main():
|
|||||||
for row in rows:
|
for row in rows:
|
||||||
row = parse_entry(row, args.icons)
|
row = parse_entry(row, args.icons)
|
||||||
|
|
||||||
# If we're on the default values for units, highs and lows, check that the average
|
''' 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)
|
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):
|
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)
|
mean = round(np.mean([l.get('value') for l in rows]), 1)
|
||||||
if mean > 35:
|
if mean > 35:
|
||||||
args.units = UNIT_MGDL
|
args.units = UNIT_MGDL
|
||||||
args.high = convert_glucose_unit(args.high, UNIT_MMOLL)
|
args.high = convert_glucose_unit(args.high, UNIT_MMOLL)
|
||||||
args.low = convert_glucose_unit(args.low, UNIT_MMOLL)
|
args.low = convert_glucose_unit(args.low, UNIT_MMOLL)
|
||||||
args.graph_max = convert_glucose_unit(args.graph_max, UNIT_MMOLL)
|
''' Manually specify max and min for mg/dL '''
|
||||||
args.graph_min = convert_glucose_unit(args.graph_min, UNIT_MMOLL)
|
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 '''
|
''' 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 '''
|
''' 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['font.sans-serif'] = ['Calibri','Verdana','Geneva','Arial','Helvetica','DejaVu Sans','Bitstream Vera Sans','sans-serif']
|
||||||
rcParams['mathtext.default'] = 'regular'
|
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:
|
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
|
#args.customfont = import_font('fonts/OpenSansEmoji.ttf') # Alternate working font
|
||||||
|
|
||||||
nrows = args.graphs_per_page
|
nrows = args.graphs_per_page
|
||||||
@ -302,18 +304,16 @@ def main():
|
|||||||
|
|
||||||
|
|
||||||
def generate_plot(data, ax=None, transforms={}, args=[], **plot_args):
|
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())
|
(x, y, z, p, q) = (list(), list(), list(), list(), list())
|
||||||
for (key, value) in sorted(data.items()):
|
for (key, value) in sorted(data.items()):
|
||||||
# Time
|
''' Time '''
|
||||||
a = key
|
a = key
|
||||||
if 'maxmin' in transforms:
|
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')
|
b = value.get('max')
|
||||||
c = value.get('min')
|
c = value.get('min')
|
||||||
else:
|
else:
|
||||||
# Glucose and comment
|
''' Glucose and comment '''
|
||||||
b = value.get('value')
|
b = value.get('value')
|
||||||
c = value.get('comment', '')
|
c = value.get('comment', '')
|
||||||
x.append(a)
|
x.append(a)
|
||||||
@ -342,7 +342,7 @@ def generate_plot(data, ax=None, transforms={}, args=[], **plot_args):
|
|||||||
if args.units == UNIT_MMOLL:
|
if args.units == UNIT_MMOLL:
|
||||||
y_tick_freq = 2
|
y_tick_freq = 2
|
||||||
else:
|
else:
|
||||||
y_tick_freq = convert_glucose_unit(2, UNIT_MMOLL)
|
y_tick_freq = 50
|
||||||
|
|
||||||
''' Formatting for axis labels, using date calculations from above '''
|
''' Formatting for axis labels, using date calculations from above '''
|
||||||
ax.set_xlabel('Time', fontsize=9)
|
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 '''
|
''' Use SciPy's interp1d for linear transforming '''
|
||||||
if not maxmin:
|
if not maxmin:
|
||||||
f = interpolate.interp1d(x, y, kind='linear')
|
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)
|
y = f(x)
|
||||||
|
|
||||||
elif transform == 'spline' and transforms.get(transform) is True:
|
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), \
|
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:
|
if args.units == UNIT_MMOLL:
|
||||||
y_offset = 6
|
y_offset = 6
|
||||||
else:
|
else:
|
||||||
@ -454,7 +452,6 @@ def generate_plot(data, ax=None, transforms={}, args=[], **plot_args):
|
|||||||
elif key == 'Food':
|
elif key == 'Food':
|
||||||
symbol += '\N{GREEN APPLE}'
|
symbol += '\N{GREEN APPLE}'
|
||||||
symbol += '$'
|
symbol += '$'
|
||||||
print(symbol)
|
|
||||||
ax.annotate(
|
ax.annotate(
|
||||||
symbol,
|
symbol,
|
||||||
xy=(x_pos, args.graph_max-y_offset),
|
xy=(x_pos, args.graph_max-y_offset),
|
||||||
@ -495,9 +492,9 @@ def generate_plot(data, ax=None, transforms={}, args=[], **plot_args):
|
|||||||
return ax
|
return ax
|
||||||
|
|
||||||
def import_font(fontname):
|
def import_font(fontname):
|
||||||
|
''' Turns a relative font path into a matplotlib font property. '''
|
||||||
basedir = os.path.dirname(os.path.abspath(__file__))
|
basedir = os.path.dirname(os.path.abspath(__file__))
|
||||||
fontdir = os.path.join(basedir, 'fonts')
|
fontpath = os.path.join(basedir, fontname)
|
||||||
fontpath = os.path.join(fontdir, fontname)
|
|
||||||
if not os.path.exists(fontpath):
|
if not os.path.exists(fontpath):
|
||||||
raise UserError("Font %s does not exist" % fontpath)
|
raise UserError("Font %s does not exist" % fontpath)
|
||||||
prop = fm.FontProperties(fname=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)
|
ctype = relevant.group(1)
|
||||||
cvalue = relevant.group(2)
|
cvalue = relevant.group(2)
|
||||||
|
|
||||||
#cvalue = re.sub('(\d+)(\.\d+)?', '\g<1>', cvalue)
|
|
||||||
''' Convert floating point-style strings (2.0) to integer-style strings (2) '''
|
''' Convert floating point-style strings (2.0) to integer-style strings (2) '''
|
||||||
try:
|
try:
|
||||||
cvalue = int(float(cvalue))
|
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:
|
if re.search('Long', ctype) is not None:
|
||||||
cvalue += 'L'
|
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('Rapid-acting insulin', 'Insulin', ctype, flags=re.IGNORECASE)
|
||||||
ctype = re.sub('Long-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.pagesize = verify_pagesize(args.pagesize)
|
||||||
args.units = verify_units(args.units, args.high, args.low)
|
args.units = verify_units(args.units, args.high, args.low)
|
||||||
if args.units == UNIT_MMOLL:
|
if args.units == UNIT_MMOLL:
|
||||||
args.graph_max = GRAPH_MAX
|
args.graph_max = GRAPH_MAX_MMOLL
|
||||||
args.graph_min = GRAPH_MIN
|
args.graph_min = GRAPH_MIN_MMOLL
|
||||||
else:
|
else:
|
||||||
args.graph_max = convert_glucose_unit(GRAPH_MAX, UNIT_MMOLL)
|
args.graph_max = GRAPH_MAX_MGDL
|
||||||
args.graph_min = convert_glucose_unit(GRAPH_MIN, UNIT_MMOLL)
|
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 '''
|
''' Ensure we have a valid number of graphs_per_page '''
|
||||||
if not isinstance(args.graphs_per_page, int) or args.graphs_per_page < 1:
|
if not isinstance(args.graphs_per_page, int) or args.graphs_per_page < 1:
|
||||||
|
Loading…
Reference in New Issue
Block a user