Spaces:
No application file
No application file
File size: 17,644 Bytes
b7731cd |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 |
# Copyright 2013 Leighton Pritchard. All rights reserved.
#
# This file is part of the Biopython distribution and governed by your
# choice of the "Biopython License Agreement" or the "BSD 3-Clause License".
# Please see the LICENSE file that should have been included as part of this
# package.
"""Classes and functions to visualise a KGML Pathway Map.
The KGML definition is as of release KGML v0.7.1
(http://www.kegg.jp/kegg/xml/docs/)
Classes:
"""
import os
import tempfile
from io import BytesIO
try:
from reportlab.lib import colors
from reportlab.pdfgen import canvas
except ImportError:
from Bio import MissingPythonDependencyError
raise MissingPythonDependencyError(
"Install reportlab if you want to use KGML_vis."
) from None
try:
from PIL import Image
except ImportError:
from Bio import MissingPythonDependencyError
raise MissingPythonDependencyError(
"Install pillow if you want to use KGML_vis."
) from None
from urllib.request import urlopen
from Bio.KEGG.KGML.KGML_pathway import Pathway
def darken(color, factor=0.7):
"""Return darkened color as a ReportLab RGB color.
Take a passed color and returns a Reportlab color that is darker by the
factor indicated in the parameter.
"""
newcol = color_to_reportlab(color)
for a in ["red", "green", "blue"]:
setattr(newcol, a, factor * getattr(newcol, a))
return newcol
def color_to_reportlab(color):
"""Return the passed color in Reportlab Color format.
We allow colors to be specified as hex values, tuples, or Reportlab Color
objects, and with or without an alpha channel. This function acts as a
Rosetta stone for conversion of those formats to a Reportlab Color
object, with alpha value.
Any other color specification is returned directly
"""
# Reportlab Color objects are in the format we want already
if isinstance(color, colors.Color):
return color
elif isinstance(color, str): # String implies hex color
if color.startswith("0x"): # Standardise to octothorpe
color.replace("0x", "#")
if len(color) == 7:
return colors.HexColor(color)
else:
try:
return colors.HexColor(color, hasAlpha=True)
except TypeError: # Catch pre-2.7 Reportlab
raise RuntimeError(
"Your reportlab seems to be too old, try 2.7 onwards"
) from None
elif isinstance(color, tuple): # Tuple implies RGB(alpha) tuple
return colors.Color(*color)
return color
def get_temp_imagefilename(url):
"""Return filename of temporary file containing downloaded image.
Create a new temporary file to hold the image file at the passed URL
and return the filename.
"""
img = urlopen(url).read()
im = Image.open(BytesIO(img))
# im.transpose(Image.FLIP_TOP_BOTTOM)
f = tempfile.NamedTemporaryFile(delete=False, suffix=".png")
fname = f.name
f.close()
im.save(fname, "PNG")
return fname
class KGMLCanvas:
"""Reportlab Canvas-based representation of a KGML pathway map."""
def __init__(
self,
pathway,
import_imagemap=False,
label_compounds=True,
label_orthologs=True,
label_reaction_entries=True,
label_maps=True,
show_maps=False,
fontname="Helvetica",
fontsize=6,
draw_relations=True,
show_orthologs=True,
show_compounds=True,
show_genes=True,
show_reaction_entries=True,
margins=(0.02, 0.02),
):
"""Initialize the class."""
self.pathway = pathway
self.show_maps = show_maps
self.show_orthologs = show_orthologs
self.show_compounds = show_compounds
self.show_genes = show_genes
self.show_reaction_entries = show_reaction_entries
self.label_compounds = label_compounds
self.label_orthologs = label_orthologs
self.label_reaction_entries = label_reaction_entries
self.label_maps = label_maps
self.fontname = fontname
self.fontsize = fontsize
self.draw_relations = draw_relations
self.non_reactant_transparency = 0.3
self.import_imagemap = import_imagemap # Import the map .png from URL
# percentage of canvas that will be margin in on either side in the
# X and Y directions
self.margins = margins
def draw(self, filename):
"""Add the map elements to the drawing."""
# Instantiate the drawing, first
# size x_max, y_max for now - we can add margins, later
if self.import_imagemap:
# We're drawing directly on the image, so we set the canvas to the
# same size as the image
if os.path.isfile(self.pathway.image):
imfilename = self.pathway.image
else:
imfilename = get_temp_imagefilename(self.pathway.image)
im = Image.open(imfilename)
cwidth, cheight = im.size
else:
# No image, so we set the canvas size to accommodate visible
# elements
cwidth, cheight = (self.pathway.bounds[1][0], self.pathway.bounds[1][1])
# Instantiate canvas
self.drawing = canvas.Canvas(
filename,
bottomup=0,
pagesize=(
cwidth * (1 + 2 * self.margins[0]),
cheight * (1 + 2 * self.margins[1]),
),
)
self.drawing.setFont(self.fontname, self.fontsize)
# Transform the canvas to add the margins
self.drawing.translate(
self.margins[0] * self.pathway.bounds[1][0],
self.margins[1] * self.pathway.bounds[1][1],
)
# Add the map image, if required
if self.import_imagemap:
self.drawing.saveState()
self.drawing.scale(1, -1)
self.drawing.translate(0, -cheight)
self.drawing.drawImage(imfilename, 0, 0)
self.drawing.restoreState()
# Add the reactions, compounds and maps
# Maps go on first, to be overlaid by more information.
# By default, they're slightly transparent.
if self.show_maps:
self.__add_maps()
if self.show_reaction_entries:
self.__add_reaction_entries()
if self.show_orthologs:
self.__add_orthologs()
if self.show_compounds:
self.__add_compounds()
if self.show_genes:
self.__add_genes()
# TODO: complete draw_relations code
# if self.draw_relations:
# self.__add_relations()
# Write the pathway map to PDF
self.drawing.save()
def __add_maps(self):
"""Add maps to the drawing of the map (PRIVATE).
We do this first, as they're regional labels to be overlaid by
information. Also, we want to set the color to something subtle.
We're using Hex colors because that's what KGML uses, and
Reportlab doesn't mind.
"""
for m in self.pathway.maps:
for g in m.graphics:
self.drawing.setStrokeColor("#888888")
self.drawing.setFillColor("#DDDDDD")
self.__add_graphics(g)
if self.label_maps:
self.drawing.setFillColor("#888888")
self.__add_labels(g)
def __add_graphics(self, graphics):
"""Add the passed graphics object to the map (PRIVATE).
Add text, add after the graphics object, for sane Z-ordering.
"""
if graphics.type == "line":
p = self.drawing.beginPath()
x, y = graphics.coords[0]
# There are optional settings for lines that aren't necessarily
# part of the KGML DTD
if graphics.width is not None:
self.drawing.setLineWidth(graphics.width)
else:
self.drawing.setLineWidth(1)
p.moveTo(x, y)
for (x, y) in graphics.coords:
p.lineTo(x, y)
self.drawing.drawPath(p)
self.drawing.setLineWidth(1) # Return to default
# KGML defines the (x, y) coordinates as the centre of the circle/
# rectangle/roundrectangle, but Reportlab uses the coordinates of the
# lower-left corner for rectangle/elif.
if graphics.type == "circle":
self.drawing.circle(
graphics.x, graphics.y, graphics.width * 0.5, stroke=1, fill=1
)
elif graphics.type == "roundrectangle":
self.drawing.roundRect(
graphics.x - graphics.width * 0.5,
graphics.y - graphics.height * 0.5,
graphics.width,
graphics.height,
min(graphics.width, graphics.height) * 0.1,
stroke=1,
fill=1,
)
elif graphics.type == "rectangle":
self.drawing.rect(
graphics.x - graphics.width * 0.5,
graphics.y - graphics.height * 0.5,
graphics.width,
graphics.height,
stroke=1,
fill=1,
)
def __add_labels(self, graphics):
"""Add labels for the passed graphics objects to the map (PRIVATE).
We don't check that the labels fit inside objects such as circles/
rectangles/roundrectangles.
"""
if graphics.type == "line":
# We use the midpoint of the line - sort of - we take the median
# line segment (list-wise, not in terms of length), and use the
# midpoint of that line. We could have other options here,
# maybe even parameterising it to a proportion of the total line
# length.
mid_idx = len(graphics.coords) * 0.5
if not int(mid_idx) == mid_idx:
idx1, idx2 = int(mid_idx - 0.5), int(mid_idx + 0.5)
else:
idx1, idx2 = int(mid_idx - 1), int(mid_idx)
x1, y1 = graphics.coords[idx1]
x2, y2 = graphics.coords[idx2]
x, y = 0.5 * (x1 + x2), 0.5 * (y1 + y2)
elif graphics.type == "circle":
x, y = graphics.x, graphics.y
elif graphics.type in ("rectangle", "roundrectangle"):
x, y = graphics.x, graphics.y
# How big so we want the text, and how many characters?
if graphics._parent.type == "map":
text = graphics.name
self.drawing.setFont(self.fontname, self.fontsize + 2)
elif len(graphics.name) < 15:
text = graphics.name
else:
text = graphics.name[:12] + "..."
self.drawing.drawCentredString(x, y, text)
self.drawing.setFont(self.fontname, self.fontsize)
def __add_orthologs(self):
"""Add 'ortholog' Entry elements to the drawing of the map (PRIVATE).
In KGML, these are typically line objects, so we render them
before the compound circles to cover the unsightly ends/junctions.
"""
for ortholog in self.pathway.orthologs:
for g in ortholog.graphics:
self.drawing.setStrokeColor(color_to_reportlab(g.fgcolor))
self.drawing.setFillColor(color_to_reportlab(g.bgcolor))
self.__add_graphics(g)
if self.label_orthologs:
# We want the label color to be slightly darker
# (where possible), so it can be read
self.drawing.setFillColor(darken(g.fgcolor))
self.__add_labels(g)
def __add_reaction_entries(self):
"""Add Entry elements for Reactions to the map drawing (PRIVATE).
In KGML, these are typically line objects, so we render them
before the compound circles to cover the unsightly ends/junctions
"""
for reaction in self.pathway.reaction_entries:
for g in reaction.graphics:
self.drawing.setStrokeColor(color_to_reportlab(g.fgcolor))
self.drawing.setFillColor(color_to_reportlab(g.bgcolor))
self.__add_graphics(g)
if self.label_reaction_entries:
# We want the label color to be slightly darker
# (where possible), so it can be read
self.drawing.setFillColor(darken(g.fgcolor))
self.__add_labels(g)
def __add_compounds(self):
"""Add compound elements to the drawing of the map (PRIVATE)."""
for compound in self.pathway.compounds:
for g in compound.graphics:
# Modify transparency of compounds that don't participate
# in reactions
fillcolor = color_to_reportlab(g.bgcolor)
if not compound.is_reactant:
fillcolor.alpha *= self.non_reactant_transparency
self.drawing.setStrokeColor(color_to_reportlab(g.fgcolor))
self.drawing.setFillColor(fillcolor)
self.__add_graphics(g)
if self.label_compounds:
if not compound.is_reactant:
t = 0.3
else:
t = 1
self.drawing.setFillColor(colors.Color(0.2, 0.2, 0.2, t))
self.__add_labels(g)
def __add_genes(self):
"""Add gene elements to the drawing of the map (PRIVATE)."""
for gene in self.pathway.genes:
for g in gene.graphics:
self.drawing.setStrokeColor(color_to_reportlab(g.fgcolor))
self.drawing.setFillColor(color_to_reportlab(g.bgcolor))
self.__add_graphics(g)
if self.label_compounds:
self.drawing.setFillColor(darken(g.fgcolor))
self.__add_labels(g)
def __add_relations(self):
"""Add relations to the map (PRIVATE).
This is tricky. There is no defined graphic in KGML for a
relation, and the corresponding entries are typically defined
as objects 'to be connected somehow'. KEGG uses KegSketch, which
is not public, and most third-party software draws straight line
arrows, with heads to indicate the appropriate direction
(at both ends for reversible reactions), using solid lines for
ECrel relation types, and dashed lines for maplink relation types.
The relation has:
- entry1: 'from' node
- entry2: 'to' node
- subtype: what the relation refers to
Typically we have entry1 = map/ortholog; entry2 = map/ortholog,
subtype = compound.
"""
# Dashed lines for maplinks, solid for everything else
for relation in list(self.pathway.relations):
if relation.type == "maplink":
self.drawing.setDash(6, 3)
else:
self.drawing.setDash()
for s in relation.subtypes:
subtype = self.pathway.entries[s[1]]
# Our aim is to draw an arrow from the entry1 object to the
# entry2 object, via the subtype object.
# 1) Entry 1 to subtype
self.__draw_arrow(relation.entry1, subtype)
# 2) subtype to Entry 2
self.__draw_arrow(subtype, relation.entry2)
def __draw_arrow(self, g_from, g_to):
"""Draw an arrow between given Entry objects (PRIVATE).
Draws an arrow from the g_from Entry object to the g_to
Entry object; both must have Graphics objects.
"""
# Centre and bound coordinates for the from and two objects
bounds_from, bounds_to = g_from.bounds, g_to.bounds
centre_from = (
0.5 * (bounds_from[0][0] + bounds_from[1][0]),
0.5 * (bounds_from[0][1] + bounds_from[1][1]),
)
centre_to = (
0.5 * (bounds_to[0][0] + bounds_to[1][0]),
0.5 * (bounds_to[0][1] + bounds_to[1][1]),
)
p = self.drawing.beginPath()
# print(True, g_from.name, g_to.name, bounds_to, bounds_from)
# If the 'from' and 'to' graphics are vertically-aligned, draw a line
# from the 'from' to the 'to' entity
if bounds_to[0][0] < centre_from[0] < bounds_to[1][0]:
# print(True, g_from.name, g_to.name, bounds_to, bounds_from)
if centre_to[1] > centre_from[1]: # to above from
p.moveTo(centre_from[0], bounds_from[1][1])
p.lineTo(centre_from[0], bounds_to[0][1])
# Draw arrow point - TODO
else: # to below from
p.moveTo(centre_from[0], bounds_from[0][1])
p.lineTo(centre_from[0], bounds_to[1][1])
# Draw arrow point - TODO
elif bounds_from[0][0] < centre_to[0] < bounds_from[1][0]:
# print(True, g_from.name, g_to.name, bounds_to, bounds_from)
if centre_to[1] > centre_from[1]: # to above from
p.moveTo(centre_to[0], bounds_from[1][1])
p.lineTo(centre_to[0], bounds_to[0][1])
# Draw arrow point - TODO
else: # to below from
p.moveTo(centre_to[0], bounds_from[0][1])
p.lineTo(centre_to[0], bounds_to[1][1])
# Draw arrow point - TODO
self.drawing.drawPath(p) # Draw arrow shaft
# print(g_from)
# print(bounds_from)
# print(g_to)
# print(bounds_to)
|