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.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.
|
||||||
|
Loading…
Reference in New Issue
Block a user