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.colors import ListedColormap, BoundaryNorm
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.path import Path
from matplotlib import ticker as mticker
from matplotlib.offsetbox import (OffsetImage, AnnotationBbox)
import numpy as np
import os
import re
from scipy import interpolate
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
# 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['value'] for l in rows]), 1)
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)
@ -96,11 +100,12 @@ def main():
rcParams['axes.titlesize'] = 12
rcParams['font.family'] = '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'
# 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
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 '''
if len(z) > 0 and isinstance(z[0], (int, float)):
z = np.asarray(z)
else:
z = np.asarray(z, dtype='unicode_')
''' Calculations the axis limits '''
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')
if 'maxmin' in transforms and transforms['maxmin'] is True:
if 'maxmin' in transforms and transforms.get('maxmin') is True:
maxmin = True
else:
maxmin = False
''' Process points to apply smoothing and other fixups '''
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 '''
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
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) '''
if args.units == UNIT_MMOLL:
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)
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) '''
def bezier(points, s=100):
n = len(points)
@ -398,18 +400,18 @@ def generate_plot(data, ax=None, transforms={}, args=[], **plot_args):
(x, y) = (curve[:,0], curve[:,1])
''' 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:
gmtext = 'Median glucose: %.1f%s' % (round(transforms['avgglucose'], 1), args.units)
gmtext = 'Median glucose: %.1f%s' % (round(transforms.get('avgglucose'), 1), args.units)
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, \
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[transform], (int, float)):
ax.annotate('Median HbA1c: %.1f%%' % round(transforms['avga1c'], 1), fontsize=9, \
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), \
)
@ -422,21 +424,56 @@ def generate_plot(data, ax=None, transforms={}, args=[], **plot_args):
else:
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):
if len(label) > 0:
#print(label)
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. '''
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(
label,
symbol,
xy=(x_pos, args.graph_max-y_offset),
rotation=45,
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'] '''
if transform == 'boundaries' and 'color' in transforms:
cmap = ListedColormap(transforms['color'])
norm = BoundaryNorm(transforms['boundaries'], cmap.N)
cmap = ListedColormap(transforms.get('color'))
norm = BoundaryNorm(transforms.get('boundaries'), cmap.N)
''' create an array of points on the plot, and split into segments '''
p = np.array([x, y]).T.reshape(-1, 1, 2)
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:
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)
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
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'):
''' 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']
'''
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)
rduplicate = re.compile('.*?(\N{SYRINGE})')
comment_parts = []
for comment_part in data.get('comment').split('; '):
relevant = rrelevant.search(comment_part)
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)
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:
cvalue += 'R'
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
# 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', '$\mathcal{💉}$', ctype, flags=re.IGNORECASE)
#ctype = re.sub('Long-acting insulin', '$\mathcal{💉}$', 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('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)
idx = [i for i, x in enumerate(comment_parts) if rduplicate.search(x)]
if idx:
comment_parts[idx[0]] = re.sub('^(.*?\d+\S?)(.*)$', '\g<1>/'+cvalue+'\g<2>', comment_parts[idx[0]])
if ctype in commentparts:
commentparts[ctype] = commentparts[ctype] + '/' + cvalue
else:
comment_parts.append(ctype)
commentparts[ctype] = cvalue
comment = ''.join(comment_parts)
data['comment'] = comment
data['comment'] = commentparts
else:
data['comment'] = ''
data['comment'] = {}
''' Convert timestamp to ISO8601 (by default, at least), and store datetime object '''
try:
@ -655,14 +714,14 @@ def fill_gaps(rows, interval, maxinterval=dt.timedelta(days=1)):
continue
''' If the next row has a time gap, create new rows to insert '''
if rows[i+1]['date'] - rows[i]['date'] > interval and \
rows[i+1]['date'] - rows[i]['date'] < maxinterval:
if rows[i+1].get('date') - rows[i].get('date') > interval and \
rows[i+1].get('date') - rows[i].get('date') < maxinterval:
n = (rows[i+1]['date'] - rows[i]['date'])//interval
start = mdates.date2num(rows[i]['date'])
end = mdates.date2num(rows[i+1]['date'])
lower = rows[i]['value']
upper = rows[i+1]['value']
n = (rows[i+1].get('date') - rows[i].get('date'))//interval
start = mdates.date2num(rows[i].get('date'))
end = mdates.date2num(rows[i+1].get('date'))
lower = rows[i].get('value')
upper = rows[i+1].get('value')
''' Calculate an range for each interval, assuming a straight line between the start and
end of the gap.