Add font icons sets for labels.
This commit is contained in:
parent
6959174863
commit
7cdb61dae2
BIN
fonts/OpenSansEmoji.ttf
Normal file
BIN
fonts/OpenSansEmoji.ttf
Normal file
Binary file not shown.
BIN
fonts/icogluco.ttf
Normal file
BIN
fonts/icogluco.ttf
Normal file
Binary file not shown.
@ -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.
|
||||
|
Loading…
Reference in New Issue
Block a user