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

1from io import StringIO 

2from collections import OrderedDict, namedtuple 

3import numpy as np 

4import math 

5import re 

6 

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 

15 

16 

17Box = namedtuple('Box', 'x y dx dy') # corner and size of a 2D shape 

18 

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'} 

59 

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): 

67 

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 

73 

74 if width: 

75 self.width = width 

76 else: 

77 self.width = poswidth * len(matrix_props) 

78 

79 self.height = height 

80 self.column = column 

81 self.aligned_faces = True 

82 

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 

87 

88 self.summarize_inner_nodes = summarize_inner_nodes 

89 self.legend = legend 

90 

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 ) 

107 

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) 

121 

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" 

131 

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) 

145 

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 

152 

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)) 

163 

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 

175 

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 

185 

186 self.height = height 

187 self.column = column 

188 self.aligned_faces = True 

189 self.seq_format = seq_format 

190 self.profiles = profiles 

191 

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 

196 

197 self.summarize_inner_nodes = summarize_inner_nodes 

198 self.legend = legend 

199 

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) 

205 

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] = '' 

213 

214 tree_style.add_legend(title=self.name, 

215 variable='discrete', 

216 colormap=color_dict, 

217 ) 

218 

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) 

223 

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" 

233 

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) 

247 

248 def set_node_style(self, node): 

249 

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 

255 

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)) 

266 

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 

278 

279 if width: 

280 self.width = width 

281 else: 

282 self.width = poswidth * len(matrix_props) 

283 

284 self.height = height 

285 self.column = column 

286 self.aligned_faces = True 

287 

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 

292 

293 self.summarize_inner_nodes = summarize_inner_nodes 

294 self.legend = legend 

295 

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) 

311 

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) 

316 

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] 

323 

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) 

343 

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) 

350 

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) 

357 

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) 

375 

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)) 

381 

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 

393 

394 if width: 

395 self.width = width 

396 else: 

397 self.width = poswidth * len(matrix_props) 

398 

399 self.height = height 

400 self.column = column 

401 self.aligned_faces = True 

402 

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 

407 

408 self.summarize_inner_nodes = summarize_inner_nodes 

409 self.legend = legend 

410 

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) 

422 

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) 

427 

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] 

433 

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 ] 

440 

441 tree_style.add_legend(title=self.name, 

442 variable='continuous', 

443 value_range=self.value_range, 

444 color_range=color_gradient 

445 ) 

446 

447 def _get_array(self, node): 

448 if self.matrix: 

449 return self.matrix.get(node.name) 

450 

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) 

457 

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) 

464 

465 def set_node_style(self, node): 

466 array = self.get_array(node) 

467 

468 if len(self.matrix_props) > 1: 

469 poswidth = self.width / (len(self.matrix_props)-1 ) 

470 else: 

471 poswidth = self.width 

472 

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)) 

479 

480 

481 

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): 

489 

490 Face.__init__(self, name=name, 

491 padding_x=padding_x, padding_y=padding_y) 

492 

493 self.width = width 

494 self.height = None 

495 self.range = scale_range 

496 self.headers = headers 

497 

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 

505 

506 self.tick_width = tick_width 

507 self.line_width = line_width 

508 

509 self.vt_line_height = 10 

510 

511 def __name__(self): 

512 return "ScaleFace" 

513 

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): 

523 

524 if drawer.TYPE == 'circ': 

525 pos = swap_pos(pos, point[1]) 

526 

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) 

536 

537 x, y, _, dy = box 

538 zx, zy = self.zoom 

539 

540 self.viewport = (drawer.viewport.x, drawer.viewport.x + drawer.viewport.dx) 

541 

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 

546 

547 self._box = Box(x, y, self.width / zx, height) 

548 return self._box 

549 

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}) 

560 

561 

562 #nticks = round((self.width * zx) / self.tick_width) 

563 

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 

570 

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 

577 

578 for i in range(sm_start, sm_end + 1): 

579 x = x0 + i * dx + dx/2 

580 

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) 

586 

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 } 

596 

597 

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) 

603 

604 p1 = (x, y + dy - self.vt_line_height / zy) 

605 p2 = (x, y + dy) 

606 

607 yield draw_line(p1, p2, style={'stroke-width': self.line_width, 

608 'stroke': self.color}) 

609 except IndexError: 

610 break 

611 

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): 

620 

621 Face.__init__(self, padding_x=padding_x, padding_y=padding_y) 

622 

623 self.seq = seq 

624 self.seqlength = len(self.seq) 

625 self.seqtype = seqtype 

626 

627 self.autoformat = True # block if 1px contains > 1 tile 

628 

629 self.seq_format = seq_format 

630 self.gap_format = gap_format 

631 self.gap_linewidth = gap_linewidth 

632 self.compress_gaps = False 

633 

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 

638 

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 

648 

649 # Text 

650 self.ftype = ftype 

651 self._min_fsize = 8 

652 self.max_fsize = max_fsize 

653 self._fsize = None 

654 

655 self.blocks = [] 

656 self.build_blocks() 

657 

658 def __name__(self): 

659 return "AlignmentFace" 

660 

661 def get_seq(self, start, end): 

662 """Retrieves sequence given start, end""" 

663 return self.seq[start:end] 

664 

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) 

672 

673 self.blocks.sort() 

674 

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): 

684 

685 if pos != 'branch_right' and not pos.startswith('aligned'): 

686 raise InvalidUsage(f'Position {pos} not allowed for SeqMotifFace') 

687 

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) 

697 

698 x, y, _, dy = box 

699 

700 zx, zy = self.zoom 

701 zx = 1 if drawer.TYPE != 'circ' else zx 

702 

703 # zx = drawer.zoom[0] 

704 # self.zoom = (zx, zy) 

705 

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) 

710 

711 self._box = Box(x, y, self.width / zx, dy) 

712 return self._box 

713 

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]) 

716 

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 

724 

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 

729 

730 

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 

741 

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) 

750 

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) 

759 

760 # # yield [ "pixi-block", box ] 

761 

762 # # total position of columns 

763 # seq = self.get_seq(sm_start, sm_end) 

764 

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) 

771 

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) 

778 

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 # } 

785 

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) 

789 

790 elif self.seq_format == "categorical": 

791 

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]) 

799 

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) 

805 

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) 

824 

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): 

832 

833 Face.__init__(self, padding_x=padding_x, padding_y=padding_y) 

834 

835 self.seq = seq 

836 self.seqlength = len(self.seq) 

837 self.value2color = value2color 

838 self.absence_color = '#EBEBEB' 

839 

840 self.autoformat = True # block if 1px contains > 1 tile 

841 

842 self.seq_format = seq_format 

843 self.gap_format = gap_format 

844 self.gap_linewidth = gap_linewidth 

845 self.compress_gaps = False 

846 

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 

852 

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 

858 

859 

860 # Text 

861 self.ftype = ftype 

862 self._min_fsize = 8 

863 self.max_fsize = max_fsize 

864 self._fsize = None 

865 

866 self.blocks = [] 

867 self.build_blocks() 

868 

869 def __name__(self): 

870 return "ProfileFace" 

871 

872 def get_seq(self, start, end): 

873 """Retrieves sequence given start, end""" 

874 return self.seq[start:end] 

875 

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) 

884 

885 self.blocks.sort() 

886 

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): 

896 

897 if pos != 'branch_right' and not pos.startswith('aligned'): 

898 raise InvalidUsage(f'Position {pos} not allowed for Profile') 

899 

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) 

909 

910 x, y, _, dy = box 

911 

912 zx, zy = self.zoom 

913 zx = 1 if drawer.TYPE != 'circ' else zx 

914 

915 # zx = drawer.zoom[0] 

916 # self.zoom = (zx, zy) 

917 

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) 

922 

923 self._box = Box(x, y, self.width / zx, dy) 

924 return self._box 

925 

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)) 

930 

931 # get the representative numbers from given array 

932 seg_size = math.ceil(len(num_array) / rep_num) 

933 rep_elements = [] 

934 

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) 

940 

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 

947 

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 

952 

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 

960 

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 

965 

966 

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 

977 

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) 

986 

987 # total width of the matrix: self.width 

988 # total number of column: self.seqlength 

989 # at least 1px per column 

990 

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) 

996 

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) 

1019 

1020 

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]) 

1028 

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): 

1035 

1036 Face.__init__(self, name=name, 

1037 padding_x=padding_x, padding_y=padding_y) 

1038 

1039 self.width = width 

1040 self.height = None 

1041 self.range = scale_range 

1042 self.columns = scale_range[1] 

1043 

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 

1050 

1051 self.tick_width = tick_width 

1052 self.line_width = line_width 

1053 

1054 self.vt_line_height = 10 

1055 

1056 def __name__(self): 

1057 return "ScaleFace" 

1058 

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): 

1068 

1069 if drawer.TYPE == 'circ' and abs(point[1]) >= pi/2: 

1070 pos = swap_pos(pos) 

1071 

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) 

1081 

1082 x, y, _, dy = box 

1083 zx, zy = self.zoom 

1084 

1085 self.viewport = (drawer.viewport.x, drawer.viewport.x + drawer.viewport.dx) 

1086 

1087 self.height = (self.line_width + 10 + self.max_fsize) / zy 

1088 

1089 height = min(dy, self.height) 

1090 

1091 if pos == "aligned_bottom": 

1092 y = y + dy - height 

1093 

1094 self._box = Box(x, y, self.width / zx, height) 

1095 return self._box 

1096 

1097 def draw(self, drawer): 

1098 x0, y, _, dy = self._box 

1099 zx, zy = self.zoom 

1100 

1101 p1 = (x0, y + dy - 5 / zy) 

1102 p2 = (x0 + self.width, y + dy - self.vt_line_height / (2 * zy)) 

1103 

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)) 

1109 

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 

1117 

1118 

1119 

1120 

1121 #nticks = round((self.width * zx) / self.tick_width) 

1122 if self.columns > 1: 

1123 nticks = self.columns - 1 

1124 else: 

1125 nticks = 1 

1126 

1127 

1128 dx = self.width / nticks 

1129 range_factor = (self.range[1] - self.range[0]) / self.width 

1130 

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 

1137 

1138 if self.columns > 1: 

1139 for i in range(sm_start, sm_end + 1): 

1140 

1141 x = x0 + i * dx 

1142 

1143 # number = range_factor * i * dx 

1144 

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) 

1150 

1151 # text = text.rstrip('0').rstrip('.') if '.' in text else text 

1152 

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) 

1164 

1165 # column index starts from 1 

1166 yield draw_text(text_box, text, style=text_style) 

1167 

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) 

1171 

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) 

1187 

1188 # column index starts from 1 

1189 yield draw_text(text_box, text, style=text_style) 

1190 

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) 

1194 

1195 yield draw_line(p1, p2, style={'stroke-width': self.line_width, 

1196 'stroke': self.color})