Add font icons sets for labels.

This commit is contained in:
Timothy Allen 2018-01-05 08:37:04 +02:00
parent 6959174863
commit 7cdb61dae2
3 changed files with 107 additions and 48 deletions

BIN
fonts/OpenSansEmoji.ttf Normal file

Binary file not shown.

BIN
fonts/icogluco.ttf Normal file

Binary file not shown.

View File

@ -19,10 +19,14 @@ from matplotlib.backends.backend_pdf import PdfPages as FigurePDF
from matplotlib.collections import LineCollection from matplotlib.collections import LineCollection
from matplotlib.colors import ListedColormap, BoundaryNorm from matplotlib.colors import ListedColormap, BoundaryNorm
from matplotlib import dates as mdates from matplotlib import dates as mdates
from matplotlib import font_manager as fm
from matplotlib import image
from matplotlib.patches import Circle, PathPatch from matplotlib.patches import Circle, PathPatch
from matplotlib.path import Path from matplotlib.path import Path
from matplotlib import ticker as mticker from matplotlib import ticker as mticker
from matplotlib.offsetbox import (OffsetImage, AnnotationBbox)
import numpy as np import numpy as np
import os
import re import re
from scipy import interpolate from scipy import interpolate
from scipy.special import binom from scipy.special import binom
@ -72,7 +76,7 @@ def main():
# 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['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)
@ -96,11 +100,12 @@ def main():
rcParams['axes.titlesize'] = 12 rcParams['axes.titlesize'] = 12
rcParams['font.family'] = 'sans-serif' rcParams['font.family'] = 'sans-serif'
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']
''' Misuse the mathtext "mathcal" definition for the Unicode characters in Symbola '''
rcParams['mathtext.fontset'] = 'custom'
rcParams['mathtext.cal'] = 'Symbola'
rcParams['mathtext.default'] = 'regular' rcParams['mathtext.default'] = 'regular'
# Load custom fonts for the icon sets
if args.icons:
args.customfont = import_font('fonts/icogluco.ttf') # Works
#args.customfont = import_font('fonts/OpenSansEmoji.ttf') # Alternate working font
nrows = args.graphs_per_page nrows = args.graphs_per_page
ncols = 1 ncols = 1
@ -311,9 +316,6 @@ def generate_plot(data, ax=None, transforms={}, args=[], **plot_args):
''' Don't convert z to a numpy array if it has text in it ''' ''' Don't convert z to a numpy array if it has text in it '''
if len(z) > 0 and isinstance(z[0], (int, float)): if len(z) > 0 and isinstance(z[0], (int, float)):
z = np.asarray(z) z = np.asarray(z)
else:
z = np.asarray(z, dtype='unicode_')
''' Calculations the axis limits ''' ''' Calculations the axis limits '''
firstminute = mdates.num2date(x[0]).replace(hour=0, minute=0, second=0, microsecond=0) firstminute = mdates.num2date(x[0]).replace(hour=0, minute=0, second=0, microsecond=0)
@ -351,21 +353,21 @@ def generate_plot(data, ax=None, transforms={}, args=[], **plot_args):
ax.yaxis.set_ticks_position('none') ax.yaxis.set_ticks_position('none')
if 'maxmin' in transforms and transforms['maxmin'] is True: if 'maxmin' in transforms and transforms.get('maxmin') is True:
maxmin = True maxmin = True
else: else:
maxmin = False maxmin = False
''' Process points to apply smoothing and other fixups ''' ''' Process points to apply smoothing and other fixups '''
for transform in transforms: for transform in transforms:
if transform == 'linear' and transforms[transform] is True: if transform == 'linear' and transforms.get(transform) is True:
''' 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 x = np.linspace(x.min(), x.max(), 50) # 50 is number of points to make between x.max & x.min
y = f(x) y = f(x)
elif transform == 'spline' and transforms[transform] is True: elif transform == 'spline' and transforms.get(transform) is True:
''' Use SciPy's UnivariateSpline for transforming (s is transforming factor) ''' ''' Use SciPy's UnivariateSpline for transforming (s is transforming factor) '''
if args.units == UNIT_MMOLL: if args.units == UNIT_MMOLL:
s = 8 s = 8
@ -375,7 +377,7 @@ def generate_plot(data, ax=None, transforms={}, args=[], **plot_args):
curve = interpolate.UnivariateSpline(x=x, y=y, k=3, s=s) curve = interpolate.UnivariateSpline(x=x, y=y, k=3, s=s)
y = curve(x) y = curve(x)
elif transform == 'bezier' and transforms[transform] is True: elif transform == 'bezier' and transforms.get(transform) is True:
''' Create bezier function for transforming (s is transforming factor) ''' ''' Create bezier function for transforming (s is transforming factor) '''
def bezier(points, s=100): def bezier(points, s=100):
n = len(points) n = len(points)
@ -398,18 +400,18 @@ def generate_plot(data, ax=None, transforms={}, args=[], **plot_args):
(x, y) = (curve[:,0], curve[:,1]) (x, y) = (curve[:,0], curve[:,1])
''' Add the mean or median glucose and A1c values ''' ''' Add the mean or median glucose and A1c values '''
if transform == 'avgglucose' and isinstance(transforms[transform], (int, float)): if transform == 'avgglucose' and isinstance(transforms.get(transform), (int, float)):
if args.units == UNIT_MMOLL: if args.units == UNIT_MMOLL:
gmtext = 'Median glucose: %.1f%s' % (round(transforms['avgglucose'], 1), args.units) gmtext = 'Median glucose: %.1f%s' % (round(transforms.get('avgglucose'), 1), args.units)
else: else:
gmtext = 'Median glucose: %.0f%s' % (round(transforms['avgglucose'], 1), args.units) gmtext = 'Median glucose: %.0f%s' % (round(transforms.get('avgglucose'), 1), args.units)
ax.annotate(gmtext, fontsize=9, \ ax.annotate(gmtext, fontsize=9, \
xy=(0.95, 0.85), xycoords='axes fraction', verticalalignment='top', horizontalalignment='right', \ 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), \ zorder=40, bbox=dict(facecolor=GREEN, edgecolor='#009e73', alpha=0.7, pad=8), \
) )
if transform == 'avga1c' and isinstance(transforms[transform], (int, float)): if transform == 'avga1c' and isinstance(transforms.get(transform), (int, float)):
ax.annotate('Median HbA1c: %.1f%%' % round(transforms['avga1c'], 1), fontsize=9, \ ax.annotate('Median HbA1c: %.1f%%' % round(transforms.get('avga1c'), 1), fontsize=9, \
xy=(0.05, 0.85), xycoords='axes fraction', verticalalignment='top', horizontalalignment='left', \ 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), \ zorder=40, bbox=dict(facecolor=BOXYELLOW, edgecolor='#e69f00', alpha=0.7, pad=8), \
) )
@ -422,21 +424,56 @@ def generate_plot(data, ax=None, transforms={}, args=[], **plot_args):
else: else:
y_offset = convert_glucose_unit(6, UNIT_MMOLL) y_offset = convert_glucose_unit(6, UNIT_MMOLL)
if transform == 'label' and transforms[transform] is True: 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): for x_pos, y_pos, label in zip(x, y, z):
if len(label) > 0: if isinstance(label, dict) and len(label) > 0:
#print(label) 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. '''
if key == 'Insulin':
if isinstance(label.get(key), str):
symbol += '\N{SYRINGE}^{%s}' % label.get(key)
#symbol += '\N{PUSHPIN}^{%s}' % label.get(key)
#symbol += '\N{SYRINGE}'
else:
symbol += '\N{SYRINGE}'
elif key == 'Food':
symbol += '\N{GREEN APPLE}'
symbol += '$'
print(symbol)
ax.annotate( ax.annotate(
label, symbol,
xy=(x_pos, args.graph_max-y_offset), xy=(x_pos, args.graph_max-y_offset),
rotation=45, rotation=45,
zorder=25, zorder=25,
fontsize=10,
fontproperties=args.customfont,
) )
'''
if len(label) > 0:
if re.sub('F', '', label):
imagebox = OffsetImage(apple, axes=ax, zoom=.1, filternorm=None)
elif re.sub('I', '', label):
imagebox = OffsetImage(syringe, axes=ax, zoom=.1)
ab = AnnotationBbox(
imagebox,
xy=(x_pos, args.graph_max-y_offset),
frameon=False,
)
ab.zorder = 25
ax.add_artist(ab)
'''
''' Create a line coloured according to the list in transforms['color'] ''' ''' Create a line coloured according to the list in transforms['color'] '''
if transform == 'boundaries' and 'color' in transforms: if transform == 'boundaries' and 'color' in transforms:
cmap = ListedColormap(transforms['color']) cmap = ListedColormap(transforms.get('color'))
norm = BoundaryNorm(transforms['boundaries'], cmap.N) norm = BoundaryNorm(transforms.get('boundaries'), cmap.N)
''' create an array of points on the plot, and split into segments ''' ''' create an array of points on the plot, and split into segments '''
p = np.array([x, y]).T.reshape(-1, 1, 2) p = np.array([x, y]).T.reshape(-1, 1, 2)
segments = np.concatenate([p[:-1], p[1:]], axis=1) segments = np.concatenate([p[:-1], p[1:]], axis=1)
@ -448,7 +485,7 @@ def generate_plot(data, ax=None, transforms={}, args=[], **plot_args):
if 'boundaries' in transforms and 'color' in transforms: if 'boundaries' in transforms and 'color' in transforms:
ax.add_collection(lc) ax.add_collection(lc)
elif 'fill' in transforms and transforms['fill'] is True: elif 'fill' in transforms and transforms.get('fill') is True:
z = np.clip(y, None, args.high) z = np.clip(y, None, args.high)
ax.fill_between(x, y, z, interpolate=True, facecolor=YELLOW, alpha=0.7, zorder=12, **plot_args) ax.fill_between(x, y, z, interpolate=True, facecolor=YELLOW, alpha=0.7, zorder=12, **plot_args)
@ -463,6 +500,23 @@ def generate_plot(data, ax=None, transforms={}, args=[], **plot_args):
return ax return ax
def import_icon(filename):
basedir = os.path.dirname(os.path.abspath(__file__))
filepath = os.path.join(basedir, filename)
if not os.path.exists(filepath):
raise UserError("Image %s does not exist" % filepath)
icon = image.imread(filepath, format='png')
return icon
def import_font(fontname):
basedir = os.path.dirname(os.path.abspath(__file__))
fontdir = os.path.join(basedir, 'fonts')
fontpath = os.path.join(fontdir, fontname)
if not os.path.exists(fontpath):
raise UserError("Font %s does not exist" % fontpath)
prop = fm.FontProperties(fname=fontpath)
return prop
def parse_entry(data, icons, fmt='%Y-%m-%d %H:%M:%S'): def parse_entry(data, icons, fmt='%Y-%m-%d %H:%M:%S'):
''' Parse a row to create the icons and modify the timestamp ''' Parse a row to create the icons and modify the timestamp
@ -478,17 +532,24 @@ def parse_entry(data, icons, fmt='%Y-%m-%d %H:%M:%S'):
ValueError if an incorrectly-formatted date exists in data['timestamp'] ValueError if an incorrectly-formatted date exists in data['timestamp']
''' '''
if icons: if icons:
# Ignore comments that aren't relevant ''' Ignore comments that aren't relevant '''
rrelevant = re.compile('(Food|Rapid-acting insulin|Long-acting insulin)(?: \((.*?)\))', flags=re.IGNORECASE) rrelevant = re.compile('(Food|Rapid-acting insulin|Long-acting insulin)(?: \((.*?)\))', flags=re.IGNORECASE)
rduplicate = re.compile('.*?(\N{SYRINGE})') rduplicate = re.compile('^(I\$\^\{\d+\S?)(\}.*)$')
comment_parts = [] commentparts = {}
for comment_part in data.get('comment').split('; '): for part in data.get('comment').split('; '):
relevant = rrelevant.search(comment_part) relevant = rrelevant.search(part)
if relevant is not None: if relevant is not None:
ctype = relevant.group(1) ctype = relevant.group(1)
cvalue = relevant.group(2) cvalue = relevant.group(2)
cvalue = re.sub('(\d+)(\.\d+)?', '\g<1>', cvalue) #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))
except:
pass
cvalue = str(cvalue)
if re.search('Rapid', ctype) is not None: if re.search('Rapid', ctype) is not None:
cvalue += 'R' cvalue += 'R'
if re.search('Long', ctype) is not None: if re.search('Long', ctype) is not None:
@ -498,22 +559,20 @@ def parse_entry(data, icons, fmt='%Y-%m-%d %H:%M:%S'):
# unicode chacters that lack proper glyph names are massed together and printed # unicode chacters that lack proper glyph names are massed together and printed
# as the same character # as the same character
# XXX Alternatives include replacing the glyph with an image, or a Path # 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('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('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('Long-acting insulin', '$\mathcal{\N{SYRINGE}}^\mathrm{'+cvalue+'}$', ctype, flags=re.IGNORECASE)
#ctype = re.sub('Rapid-acting insulin', '$\mathcal{💉}$', ctype, flags=re.IGNORECASE) ctype = re.sub('Rapid-acting insulin', 'Insulin', ctype, flags=re.IGNORECASE)
#ctype = re.sub('Long-acting insulin', '$\mathcal{💉}$', ctype, flags=re.IGNORECASE) ctype = re.sub('Long-acting insulin', 'Insulin', ctype, flags=re.IGNORECASE)
idx = [i for i, x in enumerate(comment_parts) if rduplicate.search(x)] if ctype in commentparts:
if idx: commentparts[ctype] = commentparts[ctype] + '/' + cvalue
comment_parts[idx[0]] = re.sub('^(.*?\d+\S?)(.*)$', '\g<1>/'+cvalue+'\g<2>', comment_parts[idx[0]])
else: else:
comment_parts.append(ctype) commentparts[ctype] = cvalue
comment = ''.join(comment_parts) data['comment'] = commentparts
data['comment'] = comment
else: else:
data['comment'] = '' data['comment'] = {}
''' Convert timestamp to ISO8601 (by default, at least), and store datetime object ''' ''' Convert timestamp to ISO8601 (by default, at least), and store datetime object '''
try: try:
@ -655,14 +714,14 @@ def fill_gaps(rows, interval, maxinterval=dt.timedelta(days=1)):
continue continue
''' If the next row has a time gap, create new rows to insert ''' ''' If the next row has a time gap, create new rows to insert '''
if rows[i+1]['date'] - rows[i]['date'] > interval and \ if rows[i+1].get('date') - rows[i].get('date') > interval and \
rows[i+1]['date'] - rows[i]['date'] < maxinterval: rows[i+1].get('date') - rows[i].get('date') < maxinterval:
n = (rows[i+1]['date'] - rows[i]['date'])//interval n = (rows[i+1].get('date') - rows[i].get('date'))//interval
start = mdates.date2num(rows[i]['date']) start = mdates.date2num(rows[i].get('date'))
end = mdates.date2num(rows[i+1]['date']) end = mdates.date2num(rows[i+1].get('date'))
lower = rows[i]['value'] lower = rows[i].get('value')
upper = rows[i+1]['value'] upper = rows[i+1].get('value')
''' Calculate an range for each interval, assuming a straight line between the start and ''' Calculate an range for each interval, assuming a straight line between the start and
end of the gap. end of the gap.