"""Cyclic reaction pathway figure generator - Rusty Shackleford 2018"""
import matplotlib
import matplotlib.patches as mpatches
import matplotlib.pyplot as plt
import math
import io
import base64
import numpy as np
import logging
import catacycle.drawing_helpers as dh
matplotlib.use('Agg')
log = logging.getLogger(__name__)
#log.setLevel(logging.DEBUG)
MAX_STEPS = 10
# define some default colors in case none are provided
fcolours = "#4286f4 #e2893b #de5eed #dd547d #4ee5ce #4286f4 #dd547d #4ee5ce #4286f4 #dd547d #4ee5ce".split()
rcolours = "#82abed #efb683 #edb2f4 #ef92ae #91f2e3 #82abed #ef92ae #91f2e3 #82abed #ef92ae #91f2e3".split()
incolours = fcolours
# scales the rates so they look nice in the cycle
scale = 0.5
radius = 3.0
######################################
# 1. For Drawing Cycle (Curved Arrows)
######################################
[docs]def draw(data=None, startrange=0.15, stoprange=0.85, f_format='svg', figsize=(8, 8), return_image=False):
# keep track of all paths to set the bounds of the canvas
paths = []
# set defaults and declare variables
img = io.BytesIO() # file-like object to hold image
# unpack data dictionary
forward_rates = data['forward_rates'][:data['num_steps']]
rev_rates = data['rev_rates'][:data['num_steps']]
fcolours = data['fcolours'][:data['num_steps']]
rcolours = data['rcolours'][:data['num_steps']]
is_incoming = data['is_incoming'][:data['num_steps']]
is_outgoing = data['is_outgoing'][:data['num_steps']]
gaps = data['gaps'][:data['num_steps']]
gap = float(data['gap'])
indgap = data['indgap']
thickness = data['multiplier']
startrange *= thickness
stoprange *= thickness
scale_type = data['scale_type']
swoop_width_scale = try_fallback(data, 'swoop_width_scale', 1.0)
swoop_radius_scale = try_fallback(data, 'swoop_radius_scale', 1.0)
swoop_sweep_scale = try_fallback(data, 'swoop_sweep_scale', 1.0)
rel_head_width = try_fallback(data, 'rel_head_width', 2.0)
rel_head_length_scaler = try_fallback(data, 'rel_head_length_scaler', 1.0)
swoop_head_length_scaler = try_fallback(data, 'swoop_head_length_scaler', 1.0)
swoop_start_angle_shift_multiplier = try_fallback(data, 'swoop_start_angle_shift_multiplier', 0.0)
edgecolor_f = fcolours #'k' 'none'
edgecolor_r = rcolours # 'k' 'none'
#edgecolor_swoops = ['none' for _ in range(len(fcolours))]
edgecolor_swoops = fcolours
flip = try_fallback(data, 'flip', False)
# Call Sofia's Scaler function, convert rates to arrow size
forward_rates, rev_rates, _ = scaler(forward_rates, rev_rates, startrange=startrange,
stoprange=stoprange, scale_type=scale_type)
# Figure initialization
fig = plt.figure(1, figsize=figsize)
ax = fig.add_subplot(111, autoscale_on=False) #, xlim=(-6.5, 6.5), ylim=(-6.5, 6.5))
plt.axis('off')
# Splitting circle by number of forward reactions
num_segments = len(forward_rates)
delta = 360.0/num_segments
# transforming rates to line widths
widths_f = [float(forward_rates[i]) * scale for i in range(num_segments)]
widths_r = [float(rev_rates[i]) * scale for i in range(num_segments)]
# Drawing outside and inside curves
for i in range(0, num_segments):
if not indgap:
gap = dh.ensure_valid_gap(delta, gap)
# starting and ending angle for each arrow (moving counterclockwise)
# gap/2 is added to center the gap at the top
# make sure the gap isnt so large that the arrow length becomes 0 or negative
theta1 = 90 - delta * (i + 1) + (gap / 2.0)
theta2 = 90 - (gap / 2.0) - delta * i
else:
g_1, g_0 = dh.ensure_valid_gaps(delta, gaps[(i+1)%len(gaps)], gaps[i])
gap = (g_1 + g_0) / 2 # keep an average gap for sizing the swoops
theta1 = 90 - delta * (i + 1) + (g_1 / 2.0)
theta2 = 90 - (g_0 / 2.0) - delta * i
rel_head_length = (0.06 + 0.015 * num_segments) * rel_head_length_scaler
if rev_rates[i] == 0: # draw an irreversible arrow
f_colour = fcolours[i]
arrow_path = dh.curved_arrow_single(theta1, theta2, radius, widths_f[i], origin=(0, 0),
rel_head_width=rel_head_width, rel_head_len=rel_head_length,
abs_head_len=None, reverse=False)
paths.append(arrow_path)
arrow_patch = mpatches.PathPatch(arrow_path, facecolor=f_colour, edgecolor=edgecolor_f[i])
ax.add_patch(arrow_patch)
else: # draw a reversible arrow
f_colour = fcolours[i]
r_colour = rcolours[i]
f_path, r_path = dh.curved_arrow_double(theta1, theta2, radius, widths_f[i], widths_r[i], origin=(0, 0),
rel_head_width=rel_head_width, rel_head_len=rel_head_length,
f_abs_head_len=None, r_abs_head_len=None, reverse=False)
paths += [f_path, r_path]
r_patch = mpatches.PathPatch(r_path, facecolor=r_colour, edgecolor=edgecolor_r[i])
f_patch = mpatches.PathPatch(f_path, facecolor=f_colour, edgecolor=edgecolor_f[i])
ax.add_patch(r_patch)
ax.add_patch(f_patch)
# input arrows/swoops
move_center_dist = 0
if rev_rates[i] != 0:
move_center_dist = widths_f[i] / 2
arrowhead_angle = math.radians(theta2 - theta1) * rel_head_length
central_angle = math.radians(theta1 + theta2) / 2 + arrowhead_angle / 2 # shifted to be in center of tail
swoop_width = widths_f[i] * swoop_width_scale # may need to scale
min_inner_rad = 0.1
swoop_radius = max([(radius - (num_segments * 0.25) - (gap * 0.015) - (thickness * 0.1) - 0.5) * 1.5 * swoop_radius_scale,
(swoop_width / 2 + min_inner_rad), (swoop_width / 2 + min_inner_rad) * 1.5 * swoop_radius_scale])
log.debug("Cycle Swoop Radius: {}".format(swoop_radius))
swoop_sweep_angle = 180 * swoop_sweep_scale
swoop_head_len = 0.3 / swoop_sweep_scale * swoop_head_length_scaler
shift = widths_f[i] / 2 - swoop_width / 2 # aligns swoop inner arc with cycle outer arc
swoop_start_angle = math.degrees(central_angle) + 90 + (180 - swoop_sweep_angle) / 2 + (swoop_sweep_angle / 2) * swoop_start_angle_shift_multiplier
swoop_end_angle = math.degrees(central_angle) + 270 - (180 - swoop_sweep_angle) / 2 + (swoop_sweep_angle / 2) * swoop_start_angle_shift_multiplier
dist_to_swoop_center = radius + shift + swoop_radius + move_center_dist
swoop_origin = (dist_to_swoop_center * math.cos(central_angle), dist_to_swoop_center * math.sin(central_angle))
if is_incoming[i] and is_outgoing[i]:
swoop_path = dh.curved_arrow_single(swoop_start_angle, swoop_end_angle, swoop_radius, swoop_width,
origin=swoop_origin, rel_head_width=rel_head_width,
rel_head_len=swoop_head_len, abs_head_len=swoop_head_len, reverse=True)
paths.append(swoop_path)
swoop_patch = mpatches.PathPatch(swoop_path, facecolor=f_colour, edgecolor=edgecolor_swoops[i])
ax.add_patch(swoop_patch)
elif is_outgoing[i]:
swoop_path = dh.curved_arrow_single(math.degrees(central_angle) + 180, swoop_end_angle, swoop_radius,
swoop_width, origin=swoop_origin, rel_head_width=rel_head_width,
rel_head_len=swoop_head_len, abs_head_len=swoop_head_len, reverse=True)
paths.append(swoop_path)
swoop_patch = mpatches.PathPatch(swoop_path, facecolor=f_colour, edgecolor=edgecolor_swoops[i])
ax.add_patch(swoop_patch)
elif is_incoming[i]:
swoop_path = dh.filled_circular_arc(swoop_start_angle, math.degrees(central_angle) + 180, swoop_radius,
swoop_width, origin=swoop_origin)
paths.append(swoop_path)
swoop_patch = mpatches.PathPatch(swoop_path, facecolor=f_colour, edgecolor=edgecolor_swoops[i])
ax.add_patch(swoop_patch)
dh.set_ax_lims(ax, paths)
if flip:
ax.invert_xaxis()
plt.draw()
# correct mimetype based on filetype (for displaying in browser)
if f_format == 'svg':
mimetype = 'image/svg+xml'
elif f_format == 'png':
mimetype = 'image/png'
elif f_format == 'jpg':
mimetype = 'image/jpg'
elif f_format == 'pdf':
mimetype = 'application/pdf'
elif f_format == 'eps':
mimetype = 'application/postscript'
else:
raise ValueError('Image format {} not supported.'.format(format))
plt.savefig(img, format=f_format, transparent=True)
plt.close()
img.seek(0)
if not return_image:
graph_url = base64.b64encode(img.getvalue()).decode()
return 'data:{};base64,{}'.format(mimetype, graph_url)
else:
return img, mimetype
###################################################
# 2. For Drawing Straight Arrows for Side Reactions
###################################################
[docs]def draw_straight(data, startrange=0.15, stoprange=0.85, f_format='svg', figsize=(8, 8), return_image=False):
# keep track of all paths to set the bounds of the canvas
paths = []
# set defaults and declare variables
img = io.BytesIO() # file-like object to hold image
# data is passed in as a python dictionary (which is collected from a web form)
forward_rates = data['forward_rates'][:data['num_steps']]
rev_rates = data['rev_rates'][:data['num_steps']]
rev_rate = data['r_rate_straight']
for_rate = data['f_rate_straight']
forward_rates.append(for_rate)
rev_rates.append(rev_rate)
gaps = data['gaps'][:data['num_steps']]
gap = float(data['gap'])
indgap = data['indgap']
fcolour = data['f_color_straight']
rcolour = data['r_color_straight']
is_incoming = data['incoming_straight']
is_outgoing = data['outgoing_straight']
thickness = data['multiplier']
startrange *= thickness
stoprange *= thickness
scale_type = data['scale_type']
swoop_width_scale = try_fallback(data, 'swoop_width_scale', 1.0)
swoop_radius_scale = try_fallback(data, 'swoop_radius_scale', 1.0)
swoop_sweep_scale = try_fallback(data, 'swoop_sweep_scale', 1.0)
rel_head_width = try_fallback(data, 'rel_head_width', 2.0)
rel_head_length_scaler = try_fallback(data, 'rel_head_length_scaler', 1.0)
swoop_head_length_scaler = try_fallback(data, 'swoop_head_length_scaler', 1.0)
swoop_start_angle_shift_multiplier = try_fallback(data, 'swoop_start_angle_shift_multiplier', 0.0)
edgecolor_f = fcolour # 'k' #'none'
edgecolor_r = rcolour # 'k' #'none'
edgecolor_swoop = fcolour # 'none'
flip = try_fallback(data, 'flip', False)
# Splitting circle by number of forward reactions
num_segments = len(forward_rates) - 1
delta = 360.0 / num_segments
# calculate the length of a segment and use the same length for a straight line to preserve scaling
if not indgap:
gap = dh.ensure_valid_gap(delta, gap)
else:
gaps = dh.ensure_all_valid_gaps(delta, gaps)
gap = sum(gaps) / len(gaps) # keep an average gap for sizing the swoops
theta1 = 90 - delta + (gap / 2.0)
theta2 = 90 - (gap / 2.0)
length = math.radians(theta2 - theta1) * radius
rel_head_length = (0.06 + 0.015 * num_segments) * rel_head_length_scaler
# Call Sofia's Scaler function, convert rates to arrow size
forward_rates, rev_rates, _ = scaler(forward_rates, rev_rates, startrange=startrange,
stoprange=stoprange, scale_type=scale_type)
# we only need the scaled rates for the straight arrow rxn
f_rate_scaled, r_rate_scaled = forward_rates[-1], rev_rates[-1]
f_width, r_width = scale * f_rate_scaled, scale * r_rate_scaled
# Figure initialization
fig = plt.figure(1, figsize=figsize)
ax = fig.add_subplot(111, autoscale_on=False, xlim=(-5, 5), ylim=(-5, 5))
plt.axis('off')
# add arrows to the axes
if rev_rate == 0: # draw an irreversible arrow
arrow_path = dh.straight_arrow_single(length, f_width, origin=(0, 0),
rel_head_width=rel_head_width, rel_head_len=rel_head_length,
abs_head_len=None, reverse=False)
paths.append(arrow_path)
arrow_patch = mpatches.PathPatch(arrow_path, facecolor=fcolour, edgecolor=edgecolor_f)
ax.add_patch(arrow_patch)
else: # draw a reversible arrow
f_path, r_path = dh.straight_arrow_double(length, f_width, r_width, origin=(0,0), rel_head_width=rel_head_width,
f_abs_head_len=None, r_abs_head_len=None, rel_head_len=rel_head_length,
reverse=False)
paths += [f_path, r_path]
r_patch = mpatches.PathPatch(r_path, facecolor=rcolour, edgecolor=edgecolor_r)
f_patch = mpatches.PathPatch(f_path, facecolor=fcolour, edgecolor=edgecolor_f)
ax.add_patch(r_patch)
ax.add_patch(f_patch)
# input arrows/swoops
move_center_y = 0
if rev_rate != 0:
move_center_y = f_width / 2
swoop_width = f_width * swoop_width_scale
min_inner_rad = 0.1
swoop_radius = max([(radius - (num_segments * 0.25) - (gap * 0.015) - (thickness * 0.1) - 0.5) * 1.5 * swoop_radius_scale,
(swoop_width / 2 + min_inner_rad), (swoop_width / 2 + min_inner_rad) * 1.5 * swoop_radius_scale])
log.debug("Straight Swoop Radius: {}".format(swoop_radius))
swoop_sweep_angle = 180 * swoop_sweep_scale
swoop_head_len = 0.3 / swoop_sweep_scale * swoop_head_length_scaler
shift = f_width / 2 - swoop_width / 2 # aligns swoop inner arc with cycle outer arc
swoop_start_angle = 180 + (180 - swoop_sweep_angle) / 2 + (swoop_sweep_angle / 2) * swoop_start_angle_shift_multiplier
swoop_end_angle = 360 - (180 - swoop_sweep_angle) / 2 + (swoop_sweep_angle / 2) * swoop_start_angle_shift_multiplier
swoop_origin = (-rel_head_length * length / 2, shift + swoop_radius + move_center_y)
if is_incoming and is_outgoing:
swoop_path = dh.curved_arrow_single(swoop_start_angle, swoop_end_angle, swoop_radius, swoop_width,
origin=swoop_origin, rel_head_width=rel_head_width,
rel_head_len=swoop_head_len, abs_head_len=swoop_head_len, reverse=True)
paths.append(swoop_path)
swoop_patch = mpatches.PathPatch(swoop_path, facecolor=fcolour, edgecolor=edgecolor_swoop)
ax.add_patch(swoop_patch)
elif is_outgoing:
swoop_path = dh.curved_arrow_single(270, swoop_end_angle, swoop_radius,
swoop_width, origin=swoop_origin, rel_head_width=rel_head_width,
rel_head_len=swoop_head_len, abs_head_len=swoop_head_len, reverse=True)
paths.append(swoop_path)
swoop_patch = mpatches.PathPatch(swoop_path, facecolor=fcolour, edgecolor=edgecolor_swoop)
ax.add_patch(swoop_patch)
elif is_incoming:
swoop_path = dh.filled_circular_arc(swoop_start_angle, 270, swoop_radius,
swoop_width, origin=swoop_origin)
paths.append(swoop_path)
swoop_patch = mpatches.PathPatch(swoop_path, facecolor=fcolour, edgecolor=edgecolor_swoop)
ax.add_patch(swoop_patch)
dh.set_ax_lims(ax, paths)
# if flip:
# ax.invert_xaxis()
# draw on the axes
plt.draw()
# correct mimetype based on filetype (for displaying in browser)
if f_format == 'svg':
mimetype = 'image/svg+xml'
elif f_format == 'png':
mimetype = 'image/png'
elif f_format == 'jpg':
mimetype = 'image/jpg'
elif f_format == 'pdf':
mimetype = 'application/pdf'
elif f_format == 'eps':
mimetype = 'application/postscript'
else:
raise ValueError('Image format {} not supported.'.format(format))
# save the figure to the temporary file-like object
plt.savefig(img, format=f_format, transparent=True)
plt.close()
img.seek(0)
if not return_image:
graph_url = base64.b64encode(img.getvalue()).decode()
return 'data:{};base64,{}'.format(mimetype, graph_url)
else:
return img, mimetype
########################################################################################################
########################################################################################################
[docs]def scaler(forward_rates, rev_rates, startrange=0.1, stoprange=0.8, scale_type='Linear'):
"""
Transforming rates to be within specified range defined by startrange and stoprange:
Can use linear or logarithmic scale, which is preserved when transforming the data.
:param forward_rates: a list of forward rates as floats or ints
:param rev_rates: a list of reverse rates as floats or ints
:param startrange: float, first number of the range you want output to take
:param stoprange: float, last number of range for output to take
:param scale_type: 'Linear', 'Logarithmic', or 'Preserve Multiples'.
:return: (forward_rates, rev_rates), a tuple of the original lists scaled properly
"""
if scale_type not in ['Linear', 'Logarithmic', 'Preserve Multiples']:
raise ValueError("scale_type must be Linear, Logarithmic, or Preserve Multiples")
forward_rates = np.array(forward_rates).astype(np.float)
rev_rates = np.array(rev_rates).astype(np.float)
log.debug("original forward: {}".format(forward_rates))
log.debug("original reverse: {}".format(rev_rates))
# make sure to only scale based on the non-zero elements. Zeros will not affect scaling
f_nonzero = np.nonzero(forward_rates)
r_nonzero = np.nonzero(rev_rates)
# scale logarithmically and then apply the transformation to be within specified bounds (leave zeros)
if scale_type == 'Logarithmic':
log.debug("logarithmic scale selected")
forward_rates[f_nonzero] = np.log10(forward_rates[f_nonzero])
rev_rates[r_nonzero] = np.log10(rev_rates[r_nonzero])
log.debug("post-log forward: {}".format(forward_rates))
log.debug("post-log reverse: {}".format(rev_rates))
f_min = np.min(forward_rates[f_nonzero])
f_max = forward_rates.max()
r_max = rev_rates.max()
if not any(rev_rates):
r_min = f_min
else:
r_min = np.min(rev_rates[r_nonzero])
maxima = max(f_max, r_max)
minima = min(f_min, r_min)
ranger = stoprange - startrange
if minima == maxima:
if scale_type == 'Preserve Multiples':
# forward_rates[f_nonzero] = stoprange / 2.0
# rev_rates[r_nonzero] = stoprange / 2.0
increments = ranger / 5 # for simple scaling only
log.debug("Increment: {}".format(increments))
forward_rates = increments * forward_rates
rev_rates = increments * rev_rates
else:
# if all rates are the same, just set them to be a medium value in the desired range
forward_rates[f_nonzero] = np.mean([stoprange, startrange])
rev_rates[r_nonzero] = np.mean([stoprange, startrange])
else:
if scale_type == 'Preserve Multiples':
# forward_rates = forward_rates / maxima * stoprange
# rev_rates = rev_rates / maxima * stoprange
increments = ranger / 5 # for simple scaling only
log.debug("Increment: {}".format(increments))
forward_rates = increments * forward_rates
rev_rates = increments * rev_rates
else:
log.debug("Max != Min")
# otherwise, scale and move the rates to be between the desired endpoints
forward_rates[f_nonzero] = ((forward_rates[f_nonzero] - minima) / (maxima - minima) * ranger + startrange)
rev_rates[r_nonzero] = ((rev_rates[r_nonzero] - minima) / (maxima - minima) * ranger + startrange)
forward_rates = forward_rates.tolist()
rev_rates = rev_rates.tolist()
log.debug("final forward: {}".format(forward_rates))
log.debug("final reverse: {}".format(rev_rates))
return forward_rates, rev_rates, maxima - minima
[docs]def try_fallback(dictionary, key, fb):
try:
return dictionary[key]
except KeyError:
return fb