Coverage for /home/deng/Projects/metatree_drawer/metatreedrawer/treeprofiler/layouts/profile_layouts.py: 15%
614 statements
« prev ^ index » next coverage.py v7.2.7, created at 2024-08-07 10:33 +0200
« prev ^ index » next coverage.py v7.2.7, created at 2024-08-07 10:33 +0200
1from io import StringIO
2from collections import OrderedDict, namedtuple
3import numpy as np
4import math
5import re
7from ete4.smartview import TreeStyle, NodeStyle, TreeLayout, PieChartFace
8from ete4.smartview import (RectFace, CircleFace, SeqMotifFace, TextFace, OutlineFace, \
9 SelectedFace, SelectedCircleFace, SelectedRectFace, LegendFace,
10 SeqFace, Face, AlignmentFace)
11from ete4.smartview.renderer.draw_helpers import draw_text, draw_line, draw_array
12from ete4 import SeqGroup
13from treeprofiler.layouts.general_layouts import get_piechartface, get_heatmapface
14from treeprofiler.src.utils import get_consensus_seq
17Box = namedtuple('Box', 'x y dx dy') # corner and size of a 2D shape
19profilecolors = {
20 'A':"#301515" ,
21 'R':"#145AFF" ,
22 'N':"#00DCDC" ,
23 'D':"#E60A0A" ,
24 'C':"#E6E600" ,
25 'Q':"#00DCDC" ,
26 'E':"#E60A0A" ,
27 'G':"#EBEBEB" ,
28 'H':"#8282D2" ,
29 'I':"#0F820F" ,
30 'S':"#FA9600" ,
31 'K':"#145AFF" ,
32 'M':"#E6E600" ,
33 'F':"#3232AA" ,
34 'P':"#DC9682" ,
35 'L':"#0F820F" ,
36 'T':"#FA9600" ,
37 'W':"#B45AB4" ,
38 'Z':"#FF69B4" ,
39 'V':"#0F820F" ,
40 'B':"#FF69B4" ,
41 'Y':"#3232AA" ,
42 'X':"#BEA06E",
43 # '.':"#FFFFFF",
44 '-':"#cccce8",
45 # '-': "#EBEBEB",
46 }
47gradientscolor = {
48 'z': '#D3D3D3', # absence lightgray
49 'x': '#000000', # black
50 '-': '#ffffff', # white
51 'a': '#ffede5', # lightest -> darkest reds (a->t)
52 'b': '#fee5d9', 'c': '#fedbcc', 'd': '#fdcdb9',
53 'e': '#fcbfa7', 'f': '#fcaf93', 'g': '#fca082',
54 'h': '#fc9070', 'i': '#fc8161', 'j': '#fb7252',
55 'k': '#f96044', 'l': '#f44f39', 'm': '#f03d2d',
56 'n': '#e32f27', 'o': '#d52221', 'p': '#c7171c',
57 'q': '#b81419', 'r': '#aa1016', 's': '#960b13',
58 't': '#7e0610'}
60# Draw categorical/numerical matrix as MSA using ProfileAlignmentFace
61class LayoutPropsMatrix(TreeLayout):
62 def __init__(self, name="Profile", matrix_type='categorical', alignment=None, \
63 matrix_props=None, width=None, profiles=None,
64 poswidth=20, height=20, column=0, range=None, \
65 summarize_inner_nodes=False, value_range=[], value_color={}, \
66 legend=True, active=True):
68 super().__init__(name, active=active)
69 self.alignment = SeqGroup(alignment) if alignment else None
70 self.matrix_type = matrix_type
71 self.matrix_props = matrix_props
72 self.profiles = profiles
74 if width:
75 self.width = width
76 else:
77 self.width = poswidth * len(matrix_props)
79 self.height = height
80 self.column = column
81 self.aligned_faces = True
83 self.length = len(next(self.alignment.iter_entries())[1]) if self.alignment else None
84 self.scale_range = range or (0, self.length)
85 self.value_range = value_range
86 self.value_color = value_color
88 self.summarize_inner_nodes = summarize_inner_nodes
89 self.legend = legend
91 def set_tree_style(self, tree, tree_style):
92 if self.length:
93 #face = TextScaleFace(width=self.width, scale_range=self.scale_range,
94 # headers=self.matrix_props, padding_y=0, rotation=270)
95 face = MatrixScaleFace(width=self.width, scale_range=(0, self.length), padding_y=0)
96 header = self.matrix_props[0]
97 title = TextFace(header, min_fsize=5, max_fsize=12,
98 padding_x=self.width/2, padding_y=2, width=self.width/2)
99 tree_style.aligned_panel_header.add_face(face, column=self.column)
100 tree_style.aligned_panel_header.add_face(title, column=self.column)
101 if self.matrix_type == 'categorical':
102 colormap = {value: profilecolors[letter] for value, letter in self.value_color.items()}
103 tree_style.add_legend(title=self.name,
104 variable='discrete',
105 colormap=colormap,
106 )
108 if self.matrix_type == 'numerical':
109 max_color = gradientscolor['t']
110 min_color = gradientscolor['a']
111 color_range = [max_color, min_color]
112 tree_style.add_legend(title=self.name,
113 variable='continuous',
114 value_range=self.value_range,
115 color_range=color_range,
116 )
117 def _get_seq(self, node):
118 if self.alignment:
119 return self.alignment.get_seq(node.name)
120 return node.props.get("seq", None)
122 def get_seq(self, node):
123 if node.is_leaf:
124 return self._get_seq(node)
125 if self.summarize_inner_nodes:
126 # TODO: summarize inner node's seq
127 matrix = ''
128 for leaf in node.leaves():
129 matrix += ">"+leaf.name+"\n"
130 matrix += self._get_seq(leaf)+"\n"
132 try:
133 if self.mode == "numerical":
134 consensus_seq = get_consensus_seq(matrix, 0.1)
135 elif self.mode == 'profiles':
136 consensus_seq = get_consensus_seq(matrix, 0.7)
137 else:
138 consensus_seq = get_consensus_seq(matrix, 0.7)
139 return str(consensus_seq)
140 except ValueError:
141 return None
142 else:
143 first_leaf = next(node.leaves())
144 return self._get_seq(first_leaf)
146 def set_node_style(self, node):
147 seq = self.get_seq(node)
148 if len(self.profiles) > 1:
149 poswidth = self.width / (len(self.profiles)-1 )
150 else:
151 poswidth = self.width
153 if seq:
154 seqFace = ProfileAlignmentFace(seq, gap_format=None, seqtype='aa',
155 seq_format=self.matrix_type, width=self.width, height=self.height,
156 poswidth=poswidth,
157 fgcolor='black', bgcolor='#bcc3d0', gapcolor='gray',
158 gap_linewidth=0.2,
159 max_fsize=12, ftype='sans-serif',
160 padding_x=0, padding_y=0)
161 node.add_face(seqFace, column=self.column, position='aligned',
162 collapsed_only=(not node.is_leaf))
164# Draw presence/absence matrix as MSA using ProfileAlignmentFace
165class LayoutProfile(TreeLayout):
166 def __init__(self, name="Profile", mode='profiles',
167 alignment=None, seq_format='profiles', profiles=None,
168 width=None, poswidth=20, height=20,
169 column=0, range=None, summarize_inner_nodes=False,
170 value_range=[], value_color={}, legend=True,
171 active=True):
172 super().__init__(name, active=active)
173 self.alignment = SeqGroup(alignment) if alignment else None
174 self.mode = mode
176 if width:
177 self.width = width
178 else:
179 self.width = poswidth * len(profiles)
180 #total_width = self.seqlength * self.poswidth
181 # if self.width:
182 # self.w_scale = self.width / total_width
183 # else:
184 # self.width = total_width
186 self.height = height
187 self.column = column
188 self.aligned_faces = True
189 self.seq_format = seq_format
190 self.profiles = profiles
192 self.length = len(next(self.alignment.iter_entries())[1]) if self.alignment else None
193 self.scale_range = range or (0, self.length)
194 self.value_range = value_range
195 self.value_color = value_color
197 self.summarize_inner_nodes = summarize_inner_nodes
198 self.legend = legend
200 def set_tree_style(self, tree, tree_style):
201 if self.length:
202 face = TextScaleFace(width=self.width, scale_range=self.scale_range,
203 headers=self.profiles, padding_y=0, rotation=270)
204 tree_style.aligned_panel_header.add_face(face, column=self.column)
206 if self.legend:
207 if self.mode == 'profiles':
208 color_dict = {}
209 for i in range(len(self.profiles)):
210 profile_val = self.profiles[i]
211 #profile_color = profilecolors[list(profilecolors.keys())[i % len(profilecolors)]]
212 color_dict[profile_val] = ''
214 tree_style.add_legend(title=self.name,
215 variable='discrete',
216 colormap=color_dict,
217 )
219 def _get_seq(self, node):
220 if self.alignment:
221 return self.alignment.get_seq(node.name)
222 return node.props.get("seq", None)
224 def get_seq(self, node):
225 if node.is_leaf:
226 return self._get_seq(node)
227 if self.summarize_inner_nodes:
228 # TODO: summarize inner node's seq
229 matrix = ''
230 for leaf in node.leaves():
231 matrix += ">"+leaf.name+"\n"
232 matrix += self._get_seq(leaf)+"\n"
234 try:
235 if self.mode == "numerical":
236 consensus_seq = get_consensus_seq(matrix, 0.1)
237 elif self.mode == 'profiles':
238 consensus_seq = get_consensus_seq(matrix, 0.7)
239 else:
240 consensus_seq = get_consensus_seq(matrix, 0.7)
241 return str(consensus_seq)
242 except ValueError:
243 return None
244 else:
245 first_leaf = next(node.leaves())
246 return self._get_seq(first_leaf)
248 def set_node_style(self, node):
250 seq = self.get_seq(node)
251 if len(self.profiles) > 1:
252 poswidth = self.width / (len(self.profiles)-1 )
253 else:
254 poswidth = self.width
256 if seq:
257 seqFace = ProfileAlignmentFace(seq, gap_format=None, seqtype='aa',
258 seq_format=self.seq_format, width=self.width, height=self.height,
259 poswidth=poswidth,
260 fgcolor='black', bgcolor='#bcc3d0', gapcolor='gray',
261 gap_linewidth=0.2,
262 max_fsize=12, ftype='sans-serif',
263 padding_x=0, padding_y=0)
264 node.add_face(seqFace, column=self.column, position='aligned',
265 collapsed_only=(not node.is_leaf))
267# Draw presence/absence, categorical/numerical matrix as drawing array using ProfileFace
268class LayoutPropsMatrixOld(TreeLayout):
269 def __init__(self, name="Profile", matrix=None, matrix_type='categorical', \
270 matrix_props=None, is_list=False, width=None, poswidth=20, height=20,
271 column=0, range=None, summarize_inner_nodes=False, value_range=[], \
272 value_color={}, legend=True, active=True):
273 super().__init__(name, active=active)
274 self.matrix = matrix
275 self.matrix_type = matrix_type
276 self.matrix_props = matrix_props
277 self.is_list = is_list
279 if width:
280 self.width = width
281 else:
282 self.width = poswidth * len(matrix_props)
284 self.height = height
285 self.column = column
286 self.aligned_faces = True
288 self.length = len(next((value for value in self.matrix.values() if value != [None]), None)) if any(value != [None] for value in self.matrix.values()) else 0
289 self.scale_range = range or (0, self.length)
290 self.value_range = value_range
291 self.value_color = value_color
293 self.summarize_inner_nodes = summarize_inner_nodes
294 self.legend = legend
296 def set_tree_style(self, tree, tree_style):
297 if self.length:
298 if self.is_list:
299 # first not None list to set the column
300 ncols = len(next((value for value in self.matrix.values() if value != [None]), None)) if any(value != [None] for value in self.matrix.values()) else 0
301 if ncols > 1:
302 total_width = self.width * (ncols-1)
303 else:
304 total_width = self.width
305 face = MatrixScaleFace(width=total_width, scale_range=(0, ncols), padding_y=0)
306 header = self.matrix_props
307 title = TextFace(header, min_fsize=5, max_fsize=12,
308 padding_x=0, padding_y=2, width=self.width)
309 tree_style.aligned_panel_header.add_face(face, column=self.column)
310 tree_style.aligned_panel_header.add_face(title, column=self.column)
312 else:
313 face = TextScaleFace(width=self.width, scale_range=self.scale_range,
314 headers=self.matrix_props, padding_y=0, rotation=270)
315 tree_style.aligned_panel_header.add_face(face, column=self.column)
317 if self.legend:
318 if self.matrix_type == 'numerical':
319 keys_list = list(self.value_color.keys())
320 middle_index = len(keys_list) // 2
321 middle_key = keys_list[middle_index]
322 middle_value = self.value_color[middle_key]
324 if self.value_range:
325 color_gradient = [
326 self.value_color[self.value_range[1]],
327 middle_value,
328 self.value_color[self.value_range[0]]
329 ]
330 tree_style.add_legend(title=self.name,
331 variable="continuous",
332 value_range=self.value_range,
333 color_range=color_gradient,
334 )
335 if self.matrix_type == 'categorical':
336 tree_style.add_legend(title=self.name,
337 variable='discrete',
338 colormap=self.value_color,
339 )
340 def _get_array(self, node):
341 if self.matrix:
342 return self.matrix.get(node.name)
344 # def get_array(self, node):
345 # if node.is_leaf:
346 # return self._get_array(node)
347 # else:
348 # first_leaf = next(node.leaves())
349 # return self._get_array(first_leaf)
351 def get_array(self, node):
352 if self.matrix.get(node.name):
353 return self.matrix.get(node.name)
354 else:
355 first_leaf = next(node.leaves())
356 return self._get_array(first_leaf)
358 def set_node_style(self, node):
359 array = self.get_array(node)
360 #array = self.get_array(node)
361 if array:
362 if not self.is_list:
363 if len(self.matrix_props) > 1:
364 poswidth = self.width / (len(self.matrix_props) - 1)
365 else:
366 poswidth = self.width
367 if array:
368 profileFace = ProfileFace(array, self.value_color, gap_format=None, \
369 seq_format=self.matrix_type, width=self.width, height=self.height, \
370 poswidth=poswidth, tooltip=True)
371 node.add_face(profileFace, column=self.column, position='aligned', \
372 collapsed_only=(not node.is_leaf))
373 else:
374 poswidth = self.width * len(array)
376 profileFace = ProfileFace(array, self.value_color, gap_format=None, \
377 seq_format=self.matrix_type, width=poswidth, height=self.height, \
378 poswidth=poswidth, tooltip=True)
379 node.add_face(profileFace, column=self.column, position='aligned', \
380 collapsed_only=(not node.is_leaf))
382class LayoutPropsMatrixBinary(TreeLayout):
383 def __init__(self, name="Binary_profiling", matrix=None,
384 matrix_props=None, is_list=False, width=None,
385 poswidth=20, height=20,
386 column=0, range=None, summarize_inner_nodes=False,
387 value_range=[],
388 value_color={}, legend=True, active=True):
389 super().__init__(name, active=active)
390 self.matrix = matrix
391 self.matrix_props = matrix_props
392 self.is_list = is_list
394 if width:
395 self.width = width
396 else:
397 self.width = poswidth * len(matrix_props)
399 self.height = height
400 self.column = column
401 self.aligned_faces = True
403 self.length = len(next((value for value in self.matrix.values() if value != [None]), None)) if any(value != [None] for value in self.matrix.values()) else 0
404 self.scale_range = range or (0, self.length)
405 self.value_range = value_range
406 self.value_color = value_color
408 self.summarize_inner_nodes = summarize_inner_nodes
409 self.legend = legend
411 def set_tree_style(self, tree, tree_style):
412 if self.length:
413 if self.is_list:
414 # first not None list to set the column
415 ncols = len(next((value for value in self.matrix.values() if value != [None]), None)) if any(value != [None] for value in self.matrix.values()) else 0
416 face = MatrixScaleFace(width=self.width, scale_range=(0, ncols), padding_y=0)
417 header = self.matrix_props[0]
418 title = TextFace(header, min_fsize=5, max_fsize=12,
419 padding_x=0, padding_y=2, width=self.width)
420 tree_style.aligned_panel_header.add_face(face, column=self.column)
421 tree_style.aligned_panel_header.add_face(title, column=self.column)
423 else:
424 face = TextScaleFace(width=self.width, scale_range=self.scale_range,
425 headers=self.matrix_props, padding_y=0, rotation=270)
426 tree_style.aligned_panel_header.add_face(face, column=self.column)
428 if self.legend:
429 keys_list = list(self.value_color.keys())
430 middle_index = len(keys_list) // 2
431 middle_key = keys_list[middle_index]
432 middle_value = self.value_color[middle_key]
434 if self.value_range:
435 color_gradient = [
436 self.value_color[self.value_range[1]],
437 middle_value,
438 self.value_color[self.value_range[0]]
439 ]
441 tree_style.add_legend(title=self.name,
442 variable='continuous',
443 value_range=self.value_range,
444 color_range=color_gradient
445 )
447 def _get_array(self, node):
448 if self.matrix:
449 return self.matrix.get(node.name)
451 # def get_array(self, node):
452 # if node.is_leaf:
453 # return self._get_array(node)
454 # else:
455 # first_leaf = next(node.leaves())
456 # return self._get_array(first_leaf)
458 def get_array(self, node):
459 if self.matrix.get(node.name):
460 return self.matrix.get(node.name)
461 else:
462 first_leaf = next(node.leaves())
463 return self._get_array(first_leaf)
465 def set_node_style(self, node):
466 array = self.get_array(node)
468 if len(self.matrix_props) > 1:
469 poswidth = self.width / (len(self.matrix_props)-1 )
470 else:
471 poswidth = self.width
473 if array:
474 profileFace = ProfileFace(array, self.value_color, gap_format=None, \
475 seq_format='numerical', width=self.width, height=self.height, \
476 poswidth=poswidth, tooltip=True)
477 node.add_face(profileFace, column=self.column, position='aligned', \
478 collapsed_only=(not node.is_leaf))
482#Faces
483class TextScaleFace(Face):
484 def __init__(self, name='', width=None, color='black',
485 scale_range=(0, 0), headers=None, tick_width=100, line_width=1,
486 formatter='%.0f',
487 min_fsize=10, max_fsize=15, ftype='sans-serif',
488 padding_x=0, padding_y=0, rotation=0):
490 Face.__init__(self, name=name,
491 padding_x=padding_x, padding_y=padding_y)
493 self.width = width
494 self.height = None
495 self.range = scale_range
496 self.headers = headers
498 self.color = color
499 self.min_fsize = min_fsize
500 self.max_fsize = max_fsize
501 self._fsize = max_fsize
502 self.ftype = ftype
503 self.formatter = formatter
504 self.rotation=rotation
506 self.tick_width = tick_width
507 self.line_width = line_width
509 self.vt_line_height = 10
511 def __name__(self):
512 return "ScaleFace"
514 def compute_bounding_box(self,
515 drawer,
516 point, size,
517 dx_to_closest_child,
518 bdx, bdy,
519 bdy0, bdy1,
520 pos, row,
521 n_row, n_col,
522 dx_before, dy_before):
524 if drawer.TYPE == 'circ':
525 pos = swap_pos(pos, point[1])
527 box = super().compute_bounding_box(
528 drawer,
529 point, size,
530 dx_to_closest_child,
531 bdx, bdy,
532 bdy0, bdy1,
533 pos, row,
534 n_row, n_col,
535 dx_before, dy_before)
537 x, y, _, dy = box
538 zx, zy = self.zoom
540 self.viewport = (drawer.viewport.x, drawer.viewport.x + drawer.viewport.dx)
542 self.height = (self.line_width + 10 + self.max_fsize) / zy
543 height = min(dy, self.height)
544 if pos == "aligned_bottom":
545 y = y + dy - height
547 self._box = Box(x, y, self.width / zx, height)
548 return self._box
550 def draw(self, drawer):
551 x0, y, _, dy = self._box
552 zx, zy = self.zoom
553 p1 = (x0, y + dy - 5 / zy)
554 p2 = (x0 + self.width, y + dy - self.vt_line_height / (2 * zy))
555 if drawer.TYPE == 'circ':
556 p1 = cartesian(p1)
557 p2 = cartesian(p2)
558 # yield draw_line(p1, p2, style={'stroke-width': self.line_width,
559 # 'stroke': self.color})
562 #nticks = round((self.width * zx) / self.tick_width)
564 if len(self.headers) > 1:
565 nticks = len(self.headers)
566 else:
567 nticks = 1
568 dx = self.width / nticks
569 range_factor = (self.range[1] - self.range[0]) / self.width
571 if self.viewport:
572 sm_start = round(max(self.viewport[0] - self.viewport_margin - x0, 0) / dx)
573 sm_end = nticks - round(max(x0 + self.width - (self.viewport[1] +
574 self.viewport_margin), 0) / dx)
575 else:
576 sm_start, sm_end = 0, nticks
578 for i in range(sm_start, sm_end + 1):
579 x = x0 + i * dx + dx/2
581 number = range_factor * i * dx
582 if number == 0:
583 text = "0"
584 else:
585 text = self.formatter % number if self.formatter else str(number)
587 #text = text.rstrip('0').rstrip('.') if '.' in text else text
588 try:
589 text = self.headers[i]
590 self.compute_fsize(self.tick_width / len(text), dy, zx, zy)
591 text_style = {
592 'max_fsize': self._fsize,
593 'text_anchor': 'left', # left, middle or right
594 'ftype': f'{self.ftype}, sans-serif', # default sans-serif
595 }
598 text_box = Box(x,
599 y,
600 # y + (dy - self._fsize / (zy * r)) / 2,
601 dx, dy)
602 yield draw_text(text_box, text, style=text_style,rotation=self.rotation)
604 p1 = (x, y + dy - self.vt_line_height / zy)
605 p2 = (x, y + dy)
607 yield draw_line(p1, p2, style={'stroke-width': self.line_width,
608 'stroke': self.color})
609 except IndexError:
610 break
612class ProfileAlignmentFace(Face):
613 def __init__(self, seq, bg=None,
614 gap_format='line', seqtype='aa', seq_format='profiles',
615 width=None, height=None, # max height
616 fgcolor='black', bgcolor='#bcc3d0', gapcolor='gray',
617 gap_linewidth=0.2,
618 max_fsize=12, ftype='sans-serif', poswidth=5,
619 padding_x=0, padding_y=0):
621 Face.__init__(self, padding_x=padding_x, padding_y=padding_y)
623 self.seq = seq
624 self.seqlength = len(self.seq)
625 self.seqtype = seqtype
627 self.autoformat = True # block if 1px contains > 1 tile
629 self.seq_format = seq_format
630 self.gap_format = gap_format
631 self.gap_linewidth = gap_linewidth
632 self.compress_gaps = False
634 self.poswidth = poswidth
635 self.w_scale = 1
636 self.width = width # sum of all regions' width if not provided
637 self.height = None # dynamically computed if not provided
639 total_width = self.seqlength * self.poswidth
640 if self.width:
641 self.w_scale = self.width / total_width
642 else:
643 self.width = total_width
644 self.bg = profilecolors
645 # self.fgcolor = fgcolor
646 # self.bgcolor = bgcolor
647 self.gapcolor = gapcolor
649 # Text
650 self.ftype = ftype
651 self._min_fsize = 8
652 self.max_fsize = max_fsize
653 self._fsize = None
655 self.blocks = []
656 self.build_blocks()
658 def __name__(self):
659 return "AlignmentFace"
661 def get_seq(self, start, end):
662 """Retrieves sequence given start, end"""
663 return self.seq[start:end]
665 def build_blocks(self):
666 pos = 0
667 for reg in re.split('([^-]+)', self.seq):
668 if reg:
669 if not reg.startswith("-"):
670 self.blocks.append([pos, pos + len(reg) - 1])
671 pos += len(reg)
673 self.blocks.sort()
675 def compute_bounding_box(self,
676 drawer,
677 point, size,
678 dx_to_closest_child,
679 bdx, bdy,
680 bdy0, bdy1,
681 pos, row,
682 n_row, n_col,
683 dx_before, dy_before):
685 if pos != 'branch_right' and not pos.startswith('aligned'):
686 raise InvalidUsage(f'Position {pos} not allowed for SeqMotifFace')
688 box = super().compute_bounding_box(
689 drawer,
690 point, size,
691 dx_to_closest_child,
692 bdx, bdy,
693 bdy0, bdy1,
694 pos, row,
695 n_row, n_col,
696 dx_before, dy_before)
698 x, y, _, dy = box
700 zx, zy = self.zoom
701 zx = 1 if drawer.TYPE != 'circ' else zx
703 # zx = drawer.zoom[0]
704 # self.zoom = (zx, zy)
706 if drawer.TYPE == "circ":
707 self.viewport = (0, drawer.viewport.dx)
708 else:
709 self.viewport = (drawer.viewport.x, drawer.viewport.x + drawer.viewport.dx)
711 self._box = Box(x, y, self.width / zx, dy)
712 return self._box
714 # def get_fsize(self, dx, dy, zx, zy, max_fsize=None):
715 # return min([dx * zx * CHAR_HEIGHT, abs(dy * zy), max_fsize or 4])
717 def draw(self, drawer):
718 def get_height(x, y):
719 r = (x or 1e-10) if drawer.TYPE == 'circ' else 1
720 default_h = dy * zy * r
721 h = min([self.height or default_h, default_h]) / zy
722 # h /= r
723 return y + (dy - h) / 2, h
725 # Only leaf/collapsed branch_right or aligned
726 x0, y, dx, dy = self._box
727 zx, zy = self.zoom
728 zx = drawer.zoom[0] if drawer.TYPE == 'circ' else zx
731 if self.gap_format in ["line", "-"]:
732 p1 = (x0, y + dy / 2)
733 p2 = (x0 + self.width, y + dy / 2)
734 if drawer.TYPE == 'circ':
735 p1 = cartesian(p1)
736 p2 = cartesian(p2)
737 yield draw_line(p1, p2, style={'stroke-width': self.gap_linewidth,
738 'stroke': self.gapcolor})
739 vx0, vx1 = self.viewport
740 too_small = (self.width * zx) / (self.seqlength) < 1
742 posw = self.poswidth * self.w_scale
743 viewport_start = vx0 - self.viewport_margin / zx
744 viewport_end = vx1 + self.viewport_margin / zx
745 sm_x = max(viewport_start - x0, 0)
746 sm_start = round(sm_x / posw)
747 w = self.seqlength * posw
748 sm_x0 = x0 if drawer.TYPE == "rect" else 0
749 sm_end = self.seqlength - round(max(sm_x0 + w - viewport_end, 0) / posw)
751 # if too_small:
752 # # for start, end in self.blocks:
753 # # if end >= sm_start and start <= sm_end:
754 # # bstart = max(sm_start, start)
755 # # bend = min(sm_end, end)
756 # # bx = x0 + bstart * posw
757 # # by, bh = get_height(bx, y)
758 # # box = Box(bx, by, (bend + 1 - bstart) * posw, bh)
760 # # yield [ "pixi-block", box ]
762 # # total position of columns
763 # seq = self.get_seq(sm_start, sm_end)
765 # # starting point
766 # sm_x = sm_x if drawer.TYPE == 'rect' else x0
767 # # get height and how high the box should be
768 # y, h = get_height(sm_x, y)
769 # # create a box
770 # sm_box = Box(sm_x+sm_x0, y, posw * len(seq), h)
772 # yield draw_array(sm_box,[gradientscolor[x] for x in seq])
773 if self.seq_format == "numerical":
774 seq = self.get_seq(sm_start, sm_end)
775 sm_x = sm_x if drawer.TYPE == 'rect' else x0
776 y, h = get_height(sm_x, y)
777 sm_box = Box(sm_x+sm_x0, y, posw * len(seq), h)
779 # fsize = self.get_fsize(dx / len(seq), dy, zx, zy, 20)
780 # style = {
781 # 'fill': "black",
782 # 'max_fsize': fsize,
783 # 'ftype': 'sans-serif', # default sans-serif
784 # }
786 yield [ f'pixi-gradients', sm_box, seq]
787 # yield draw_array(sm_box,[gradientscolor[x] for x in seq])
788 #yield draw_text(sm_box, for i in seq, "jjj", style=style)
790 elif self.seq_format == "categorical":
792 seq = self.get_seq(sm_start, sm_end)
793 sm_x = sm_x if drawer.TYPE == 'rect' else x0
794 y, h = get_height(sm_x, y)
795 sm_box = Box(sm_x+sm_x0, y, posw * len(seq), h)
796 # aa_type = "notext"
797 # yield [ f'pixi-aa_{aa_type}', sm_box, seq ]
798 yield draw_array(sm_box, [profilecolors[x] for x in seq])
800 else: # when is "profiles":
801 seq = self.get_seq(sm_start, sm_end)
802 sm_x = sm_x if drawer.TYPE == 'rect' else x0
803 y, h = get_height(sm_x, y)
804 sm_box = Box(sm_x+sm_x0, y, posw * len(seq), h)
806 if self.seq_format == 'profiles' or posw * zx < self._min_fsize:
807 aa_type = "notext"
808 tooltip = f'<p>{seq}</p>'
809 style = {
810 'fill': "black",
811 'max_fsize': 14,
812 'ftype': 'sans-serif', # default sans-serif
813 }
814 # yield draw_array(sm_box, [gradientscolor[x] for x in seq], tooltip=tooltip)
815 # yield [ f'pixi-aa_{aa_type}', sm_box, seq ]
816 yield [ f'pixi-gradients', sm_box, seq]
817 # else:
818 # aa_type = "text"
819 # yield [ f'pixi-aa_{aa_type}', sm_box, seq ]
820 # sm_x0 = sm_x0 + posw/2 - zx*2 # centering text in the middle of the box
821 # for i in range(len(seq)):
822 # sm_box = Box(sm_x+sm_x0+(posw * i), y, posw, h)
823 # yield draw_text(sm_box, seq[i], "jjj", style=style)
825class ProfileFace(Face):
826 def __init__(self, seq, value2color=None,
827 gap_format='line', seq_format='categorical', # profiles, numerical, categorical
828 width=None, height=None, # max height
829 gap_linewidth=0.2,
830 max_fsize=12, ftype='sans-serif', poswidth=5,
831 padding_x=0, padding_y=0, tooltip=True):
833 Face.__init__(self, padding_x=padding_x, padding_y=padding_y)
835 self.seq = seq
836 self.seqlength = len(self.seq)
837 self.value2color = value2color
838 self.absence_color = '#EBEBEB'
840 self.autoformat = True # block if 1px contains > 1 tile
842 self.seq_format = seq_format
843 self.gap_format = gap_format
844 self.gap_linewidth = gap_linewidth
845 self.compress_gaps = False
847 self.poswidth = poswidth
848 self.w_scale = 1
849 self.width = width # sum of all regions' width if not provided
850 self.height = None # dynamically computed if not provided
851 self.tooltip = tooltip
853 total_width = self.seqlength * self.poswidth
854 if self.width:
855 self.w_scale = self.width / total_width
856 else:
857 self.width = total_width
860 # Text
861 self.ftype = ftype
862 self._min_fsize = 8
863 self.max_fsize = max_fsize
864 self._fsize = None
866 self.blocks = []
867 self.build_blocks()
869 def __name__(self):
870 return "ProfileFace"
872 def get_seq(self, start, end):
873 """Retrieves sequence given start, end"""
874 return self.seq[start:end]
876 def build_blocks(self):
877 pos = 0
878 for reg in self.seq:
879 reg = str(reg)
880 if reg:
881 if not reg.startswith("-"):
882 self.blocks.append([pos, pos + len(reg) - 1])
883 pos += len(reg)
885 self.blocks.sort()
887 def compute_bounding_box(self,
888 drawer,
889 point, size,
890 dx_to_closest_child,
891 bdx, bdy,
892 bdy0, bdy1,
893 pos, row,
894 n_row, n_col,
895 dx_before, dy_before):
897 if pos != 'branch_right' and not pos.startswith('aligned'):
898 raise InvalidUsage(f'Position {pos} not allowed for Profile')
900 box = super().compute_bounding_box(
901 drawer,
902 point, size,
903 dx_to_closest_child,
904 bdx, bdy,
905 bdy0, bdy1,
906 pos, row,
907 n_row, n_col,
908 dx_before, dy_before)
910 x, y, _, dy = box
912 zx, zy = self.zoom
913 zx = 1 if drawer.TYPE != 'circ' else zx
915 # zx = drawer.zoom[0]
916 # self.zoom = (zx, zy)
918 if drawer.TYPE == "circ":
919 self.viewport = (0, drawer.viewport.dx)
920 else:
921 self.viewport = (drawer.viewport.x, drawer.viewport.x + drawer.viewport.dx)
923 self._box = Box(x, y, self.width / zx, dy)
924 return self._box
926 def get_rep_numbers(self, num_array, rep_num):
927 def find_closest(numbers, target):
928 # Find the number in 'numbers' that is closest to calculated target
929 return min(numbers, key=lambda x: abs(x - target))
931 # get the representative numbers from given array
932 seg_size = math.ceil(len(num_array) / rep_num)
933 rep_elements = []
935 for i in range(rep_num):
936 start_index = int(i * seg_size)
937 end_index = int((i + 1) * seg_size)
938 if i == rep_num - 1:
939 end_index = len(num_array)
941 segment = num_array[start_index:end_index]
942 if segment:
943 if len(segment) != 0:
944 segment_average = sum(segment) / len(segment)
945 else:
946 segment_average = 0
948 # Find the cloest number to the average
949 closest_number = find_closest(segment, segment_average)
950 rep_elements.append(closest_number)
951 return rep_elements
953 def draw(self, drawer):
954 def get_height(x, y):
955 r = (x or 1e-10) if drawer.TYPE == 'circ' else 1
956 default_h = dy * zy * r
957 h = min([self.height or default_h, default_h]) / zy
958 # h /= r
959 return y + (dy - h) / 2, h
961 # Only leaf/collapsed branch_right or aligned
962 x0, y, dx, dy = self._box
963 zx, zy = self.zoom
964 zx = drawer.zoom[0] if drawer.TYPE == 'circ' else zx
967 if self.gap_format in ["line", "-"]:
968 p1 = (x0, y + dy / 2)
969 p2 = (x0 + self.width, y + dy / 2)
970 if drawer.TYPE == 'circ':
971 p1 = cartesian(p1)
972 p2 = cartesian(p2)
973 yield draw_line(p1, p2, style={'stroke-width': self.gap_linewidth,
974 'stroke': self.gapcolor})
975 vx0, vx1 = self.viewport
976 too_small = (self.width * zx) / (self.seqlength) < 1
978 posw = self.poswidth * self.w_scale
979 viewport_start = vx0 - self.viewport_margin / zx
980 viewport_end = vx1 + self.viewport_margin / zx
981 sm_x = max(viewport_start - x0, 0)
982 sm_start = round(sm_x / posw)
983 w = self.seqlength * posw
984 sm_x0 = x0 if drawer.TYPE == "rect" else 0
985 sm_end = self.seqlength - round(max(sm_x0 + w - viewport_end, 0) / posw)
987 # total width of the matrix: self.width
988 # total number of column: self.seqlength
989 # at least 1px per column
991 if self.seq_format == "numerical":
992 seq = self.get_seq(sm_start, sm_end)
993 sm_x = sm_x if drawer.TYPE == 'rect' else x0
994 y, h = get_height(sm_x, y)
995 sm_box = Box(sm_x+sm_x0, y, posw * len(seq), h)
997 if too_small: # only happens in data-matrix visualization
998 #ncols_per_px = math.ceil(self.seqlength / (zx * sm_box.dx)) #jordi's idea
999 ncols_per_px = math.ceil(self.width * zx)
1000 rep_elements = self.get_rep_numbers(seq, ncols_per_px)
1001 if self.tooltip:
1002 tooltip = f'<p>{seq}</p>'
1003 else:
1004 tooltip = ''
1005 yield draw_array(sm_box, [self.value2color[x] if x is not None else self.absence_color for x in rep_elements], tooltip=tooltip)
1006 else:
1007 # fsize = self.get_fsize(dx / len(seq), dy, zx, zy, 20)
1008 # style = {
1009 # 'fill': "black",
1010 # 'max_fsize': fsize,
1011 # 'ftype': 'sans-serif', # default sans-serif
1012 # }
1013 if self.tooltip:
1014 tooltip = f'<p>{seq}</p>'
1015 else:
1016 tooltip = ''
1017 # yield draw_text(sm_box, for i in seq, "jjj", style=style)
1018 yield draw_array(sm_box, [self.value2color[x] if x is not None else self.absence_color for x in seq], tooltip=tooltip)
1021 if self.seq_format == "categorical":
1022 seq = self.get_seq(sm_start, sm_end)
1023 sm_x = sm_x if drawer.TYPE == 'rect' else x0
1024 y, h = get_height(sm_x, y)
1025 sm_box = Box(sm_x+sm_x0, y, posw * len(seq), h)
1026 #tooltip = f'<p>{seq}</p>'
1027 yield draw_array(sm_box, [self.value2color[x] if x is not None else self.absence_color for x in seq])
1029class MatrixScaleFace(Face):
1030 def __init__(self, name='', width=None, color='black',
1031 scale_range=(0, 0), tick_width=80, line_width=1,
1032 formatter='%.0f',
1033 min_fsize=6, max_fsize=12, ftype='sans-serif',
1034 padding_x=0, padding_y=0):
1036 Face.__init__(self, name=name,
1037 padding_x=padding_x, padding_y=padding_y)
1039 self.width = width
1040 self.height = None
1041 self.range = scale_range
1042 self.columns = scale_range[1]
1044 self.color = color
1045 self.min_fsize = min_fsize
1046 self.max_fsize = max_fsize
1047 self._fsize = max_fsize
1048 self.ftype = ftype
1049 self.formatter = formatter
1051 self.tick_width = tick_width
1052 self.line_width = line_width
1054 self.vt_line_height = 10
1056 def __name__(self):
1057 return "ScaleFace"
1059 def compute_bounding_box(self,
1060 drawer,
1061 point, size,
1062 dx_to_closest_child,
1063 bdx, bdy,
1064 bdy0, bdy1,
1065 pos, row,
1066 n_row, n_col,
1067 dx_before, dy_before):
1069 if drawer.TYPE == 'circ' and abs(point[1]) >= pi/2:
1070 pos = swap_pos(pos)
1072 box = super().compute_bounding_box(
1073 drawer,
1074 point, size,
1075 dx_to_closest_child,
1076 bdx, bdy,
1077 bdy0, bdy1,
1078 pos, row,
1079 n_row, n_col,
1080 dx_before, dy_before)
1082 x, y, _, dy = box
1083 zx, zy = self.zoom
1085 self.viewport = (drawer.viewport.x, drawer.viewport.x + drawer.viewport.dx)
1087 self.height = (self.line_width + 10 + self.max_fsize) / zy
1089 height = min(dy, self.height)
1091 if pos == "aligned_bottom":
1092 y = y + dy - height
1094 self._box = Box(x, y, self.width / zx, height)
1095 return self._box
1097 def draw(self, drawer):
1098 x0, y, _, dy = self._box
1099 zx, zy = self.zoom
1101 p1 = (x0, y + dy - 5 / zy)
1102 p2 = (x0 + self.width, y + dy - self.vt_line_height / (2 * zy))
1104 # count the middle point of each column
1105 if self.columns > 1:
1106 half_width_col = self.width / (self.columns-1) / 2
1107 p1 = (x0 + half_width_col, y + dy - 5 / zy)
1108 p2 = (x0 + (self.width + half_width_col), y + dy - self.vt_line_height / (2 * zy))
1110 if drawer.TYPE == 'circ':
1111 p1 = cartesian(p1)
1112 p2 = cartesian(p2)
1113 yield draw_line(p1, p2, style={'stroke-width': self.line_width,
1114 'stroke': self.color})
1115 else:
1116 half_width_col = self.width / 2
1121 #nticks = round((self.width * zx) / self.tick_width)
1122 if self.columns > 1:
1123 nticks = self.columns - 1
1124 else:
1125 nticks = 1
1128 dx = self.width / nticks
1129 range_factor = (self.range[1] - self.range[0]) / self.width
1131 if self.viewport:
1132 sm_start = round(max(self.viewport[0] - self.viewport_margin - x0, 0) / dx)
1133 sm_end = nticks - round(max(x0 + self.width - (self.viewport[1] +
1134 self.viewport_margin), 0) / dx)
1135 else:
1136 sm_start, sm_end = 0, nticks
1138 if self.columns > 1:
1139 for i in range(sm_start, sm_end + 1):
1141 x = x0 + i * dx
1143 # number = range_factor * i * dx
1145 # if number == 0:
1146 # text = "0"
1147 # else:
1148 # #actual_number = number + 1
1149 # text = self.formatter % number if self.formatter else str(number)
1151 # text = text.rstrip('0').rstrip('.') if '.' in text else text
1153 text = str(i+1)
1154 self.compute_fsize(self.tick_width / len(text), dy, zx, zy)
1155 text_style = {
1156 'max_fsize': self._fsize,
1157 'text_anchor': 'middle',
1158 'ftype': f'{self.ftype}, sans-serif', # default sans-serif
1159 }
1160 text_box = Box(x+ half_width_col,
1161 y,
1162 # y + (dy - self._fsize / (zy * r)) / 2,
1163 dx, dy)
1165 # column index starts from 1
1166 yield draw_text(text_box, text, style=text_style)
1168 # vertical line tick
1169 p1 = (x + half_width_col, y + dy - self.vt_line_height / zy)
1170 p2 = (x + half_width_col, y + dy)
1172 yield draw_line(p1, p2, style={'stroke-width': self.line_width,
1173 'stroke': self.color})
1174 else:
1175 x = x0
1176 text = str(1)
1177 self.compute_fsize(self.tick_width / len(text), dy, zx, zy)
1178 text_style = {
1179 'max_fsize': self._fsize,
1180 'text_anchor': 'middle',
1181 'ftype': f'{self.ftype}, sans-serif', # default sans-serif
1182 }
1183 text_box = Box(x+ half_width_col,
1184 y,
1185 # y + (dy - self._fsize / (zy * r)) / 2,
1186 dx, dy)
1188 # column index starts from 1
1189 yield draw_text(text_box, text, style=text_style)
1191 # vertical line tick
1192 p1 = (x + half_width_col, y + dy - self.vt_line_height / zy)
1193 p2 = (x + half_width_col, y + dy)
1195 yield draw_line(p1, p2, style={'stroke-width': self.line_width,
1196 'stroke': self.color})