Coverage for /home/deng/Projects/ete4/hackathon/ete4/ete4/treeview/main.py: 1%
295 statements
« prev ^ index » next coverage.py v7.2.7, created at 2024-03-21 09:19 +0100
« prev ^ index » next coverage.py v7.2.7, created at 2024-03-21 09:19 +0100
1import re
2import types
4from .qt import *
6from ..utils import SVG_COLORS, COLOR_SCHEMES
8import time
9def tracktime(f):
10 def a_wrapper_accepting_arguments(*args, **kargs):
11 t1 = time.time()
12 r = f(*args, **kargs)
13 print(" -> TIME:", f.__name__, time.time() - t1)
14 return r
15 return a_wrapper_accepting_arguments
18_LINE_TYPE_CHECKER = lambda x: x in (0, 1, 2)
19_SIZE_CHECKER = lambda x: isinstance(x, int)
20_COLOR_MATCH = re.compile(r"^#[A-Fa-f\d]{6}$")
21_COLOR_CHECKER = lambda x: x.lower() in SVG_COLORS or re.match(_COLOR_MATCH, x)
22_NODE_TYPE_CHECKER = lambda x: x in ["sphere", "circle", "square"]
23_BOOL_CHECKER = lambda x: isinstance(x, bool) or x in (0, 1)
25FACE_POSITIONS = {"branch-right", "branch-top", "branch-bottom", "float", "float-behind", "aligned"}
27__all__ = ["NodeStyle", "TreeStyle", "FaceContainer", "_leaf", "add_face_to_node", "COLOR_SCHEMES"]
29NODE_STYLE_DEFAULT = [
30 ["fgcolor", "#0030c1", _COLOR_CHECKER],
31 ["bgcolor", "#FFFFFF", _COLOR_CHECKER],
32 #["node_bgcolor", "#FFFFFF", _COLOR_CHECKER],
33 #["partition_bgcolor","#FFFFFF", _COLOR_CHECKER],
34 #["faces_bgcolor", "#FFFFFF", _COLOR_CHECKER],
35 ["vt_line_color", "#000000", _COLOR_CHECKER],
36 ["hz_line_color", "#000000", _COLOR_CHECKER],
37 ["hz_line_type", 0, _LINE_TYPE_CHECKER], # 0 solid, 1 dashed, 2 dotted
38 ["vt_line_type", 0, _LINE_TYPE_CHECKER], # 0 solid, 1 dashed, 2 dotted
39 ["size", 3, _SIZE_CHECKER], # node circle size
40 ["shape", "circle", _NODE_TYPE_CHECKER],
41 ["draw_descendants", True, _BOOL_CHECKER],
42 ["hz_line_width", 0, _SIZE_CHECKER],
43 ["vt_line_width", 0, _SIZE_CHECKER]
44]
46TREE_STYLE_CHECKER = {
47 "mode": lambda x: x.lower() in ["c", "r"],
48}
50# _faces and faces are registered to allow deepcopy to work on nodes
51VALID_NODE_STYLE_KEYS = {i[0] for i in NODE_STYLE_DEFAULT} | {"_faces"}
53class _Border:
54 def __init__(self):
55 self.width = None
56 self.type = 0
57 self.color = None
59 def apply(self, item):
60 if self.width is not None:
61 r = item.boundingRect()
62 border = QGraphicsRectItem(r)
63 border.setParentItem(item)
64 if self.color:
65 pen = QPen(QColor(self.color))
66 else:
67 pen = QPen(Qt.PenStyle.NoPen)
68 set_pen_style(pen, self.type)
69 pen.setWidth(self.width)
70 pen.setCapStyle(Qt.PenCapStyle.FlatCap)
71 border.setPen(pen)
72 return border
73 else:
74 return None
76class _Background:
77 """
78 Background of the object.
79 """
81 def __init__(self, color=None):
82 """
83 :param color: color code as RGB or from :data:`SVG_COLORS`.
84 """
85 self.color = color
87 def apply(self, item):
88 if self.color:
89 r = item.boundingRect()
90 bg = QGraphicsRectItem(r)
91 bg.setParentItem(item)
92 pen = QPen(QColor(self.color))
93 brush = QBrush(QColor(self.color))
94 bg.setPen(pen)
95 bg.setBrush(brush)
96 bg.setFlag(QGraphicsItem.GraphicsItemFlag.ItemStacksBehindParent)
97 return bg
98 else:
99 return None
102class _ActionDelegator:
103 """ Used to associate GUI Functions to nodes and faces """
105 def get_delegate(self):
106 return self._delegate
108 def set_delegate(self, delegate):
109 if hasattr(delegate, "init"):
110 delegate.init(self)
112 for attr in dir(delegate):
113 if not attr.startswith("_") and attr != "init" :
114 fn = getattr(delegate, attr)
115 setattr(self, attr, types.MethodType(fn, self))
116 self._delegate = delegate
118 delegate = property(get_delegate, set_delegate)
120 def __init__(self):
121 self._delegate = None
123class NodeStyle(dict):
124 """Dictionary with all valid node graphical attributes."""
126 def __init__(self, *args, **kargs):
127 """NodeStyle constructor.
129 :param #0030c1 fgcolor: RGB code or name in :data:`SVG_COLORS`
130 :param #FFFFFF bgcolor: RGB code or name in :data:`SVG_COLORS`
131 :param #FFFFFF node_bgcolor: RGB code or name in :data:`SVG_COLORS`
132 :param #FFFFFF partition_bgcolor: RGB code or name in :data:`SVG_COLORS`
133 :param #FFFFFF faces_bgcolor: RGB code or name in :data:`SVG_COLORS`
134 :param #000000 vt_line_color: RGB code or name in :data:`SVG_COLORS`
135 :param #000000 hz_line_color: RGB code or name in :data:`SVG_COLORS`
136 :param 0 hz_line_type: integer number
137 :param 0 vt_line_type: integer number
138 :param 3 size: integer number
139 :param "circle" shape: "circle", "square" or "sphere"
140 :param True draw_descendants: Mark an internal node as a leaf.
141 :param 0 hz_line_width: Integer number representing the
142 width of the line in pixels. A line width of zero
143 indicates a cosmetic pen. This means that the pen width is
144 always drawn one pixel wide, independent of the
145 transformation set on the painter.
146 :param 0 vt_line_width: Integer number representing the
147 width of the line in pixels. A line width of zero
148 indicates a cosmetic pen. This means that the pen width is
149 always drawn one pixel wide, independent of the
150 transformation set on the painter.
151 """
152 super().__init__(*args, **kargs)
153 self.init()
155 def init(self):
156 for key, dvalue, checker in NODE_STYLE_DEFAULT:
157 if key not in self:
158 self[key] = dvalue
159 elif not checker(self[key]):
160 raise ValueError("'%s' attribute in node style has not a valid value: %s" %
161 (key, self[key]))
163 def __setitem__(self, i, v):
164 if i not in VALID_NODE_STYLE_KEYS:
165 raise ValueError("'%s' is not a valid keyword for a NodeStyle instance" % i)
167 super().__setitem__(i, v)
170class TreeStyle:
171 """Image properties used to render a tree.
173 **-- About tree design --**
175 :param None layout_fn: Layout function used to dynamically control
176 the aspect of nodes. Valid values are: None or a pointer to a method,
177 function, etc.
179 **-- About tree shape --**
181 :param "r" mode: Valid modes are 'c'(ircular) or 'r'(ectangular).
182 :param 0 orientation: If 0, tree is drawn from left-to-right. If
183 1, tree is drawn from right-to-left. This property only makes
184 sense when "r" mode is used.
185 :param 0 rotation: Tree figure will be rotate X degrees (clock-wise
186 rotation).
187 :param 1 min_leaf_separation: Min separation, in pixels, between
188 two adjacent branches
189 :param 0 branch_vertical_margin: Leaf branch separation margin, in
190 pixels. This will add a separation of X pixels between adjacent
191 leaf branches. In practice, increasing this value work as
192 increasing Y axis scale.
193 :param 0 arc_start: When circular trees are drawn, this defines the
194 starting angle (in degrees) from which leaves are distributed
195 (clock-wise) around the total arc span (0 = 3 o'clock).
196 :param 359 arc_span: Total arc used to draw circular trees (in
197 degrees).
198 :param 0 margin_left: Left tree image margin, in pixels.
199 :param 0 margin_right: Right tree image margin, in pixels.
200 :param 0 margin_top: Top tree image margin, in pixels.
201 :param 0 margin_bottom: Bottom tree image margin, in pixels.
203 **-- About Tree branches --**
205 :param None scale: Scale used to draw branch lengths. If None, it will
206 be automatically calculated.
207 :param "mid" optimal_scale_level: Two levels of automatic branch
208 scale detection are available: :attr:`"mid"` and
209 :attr:`"full"`. In :attr:`full` mode, branch scale will me
210 adjusted to fully avoid dotted lines in the tree image. In other
211 words, scale will be increased until the extra space necessary
212 to allocated all branch-top/bottom faces and branch-right faces
213 (in circular mode) is covered by real branches. Note, however,
214 that the optimal scale in trees with very unbalanced branch
215 lengths might be huge. If :attr:`"mid"` mode is selected (as it is by default),
216 optimal scale will only satisfy the space necessary to allocate
217 branch-right faces in circular trees. Some dotted lines
218 (artificial branch offsets) will still appear when
219 branch-top/bottom faces are larger than branch length. Note that
220 both options apply only when :attr:`scale` is set to None
221 (automatic).
222 :param 0.25 root_opening_factor: (from 0 to 1). It defines how much the center of
223 a circular tree could be opened when adjusting optimal scale, referred
224 to the total tree length. By default (0.25), a blank space up to 4
225 times smaller than the tree width could be used to calculate the
226 optimal tree scale. A 0 value would mean that root node should
227 always be tightly adjusted to the center of the tree.
228 :param True complete_branch_lines_when_necessary: True or False.
229 Draws an extra line (dotted by default) to complete branch
230 lengths when the space to cover is larger than the branch
231 itself.
232 :param False pack_leaves: If True, in circular layouts pull leaf
233 nodes closer to center while avoiding collisions.
234 :param 2 extra_branch_line_type: 0=solid, 1=dashed, 2=dotted
235 :param "gray" extra_branch_line_color: RGB code or name in
236 :data:`SVG_COLORS`
237 :param False force_topology: Convert tree branches to a fixed
238 length, thus allowing to observe the topology of tight nodes
239 :param False draw_guiding_lines: Draw guidelines from leaf nodes
240 to aligned faces
241 :param 2 guiding_lines_type: 0=solid, 1=dashed, 2=dotted.
242 :param "gray" guiding_lines_color: RGB code or name in :data:`SVG_COLORS`
244 **-- About node faces --**
246 :param False allow_face_overlap: If True, node faces are not taken
247 into account to scale circular tree images, just like many other
248 visualization programs. Overlapping among branch elements (such
249 as node labels) will be therefore ignored, and tree size
250 will be a lot smaller. Note that in most cases, manual setting
251 of tree scale will be also necessary.
252 :param True draw_aligned_faces_as_table: Aligned faces will be
253 drawn as a table, considering all columns in all node faces.
254 :param True children_faces_on_top: When floating faces from
255 different nodes overlap, children faces are drawn on top of
256 parent faces. This can be reversed by setting this attribute
257 to false.
259 **-- Addons --**
261 :param False show_border: Draw a border around the whole tree
262 :param True show_scale: Include the scale legend in the tree
263 image
264 :param None scale_length: Scale length to be used as reference
265 scale bar when visualizing tree. None = automatically adjusted.
266 :param False show_leaf_name: Automatically adds a text Face to
267 leaf nodes showing their names
268 :param False show_branch_length: Automatically adds branch
269 length information on top of branches
270 :param False show_branch_support: Automatically adds branch
271 support text in the bottom of tree branches
273 **-- Tree surroundings --**
275 The following options are actually Face containers, so graphical
276 elements can be added just as it is done with nodes. In example,
277 to add tree legend::
279 TreeStyle.legend.add_face(CircleFace(10, "red"), column=0)
280 TreeStyle.legend.add_face(TextFace("0.5 support"), column=1)
282 :param aligned_header: a :class:`FaceContainer` aligned to the end
283 of the tree and placed at the top part.
284 :param aligned_foot: a :class:`FaceContainer` aligned to the end
285 of the tree and placed at the bottom part.
286 :param legend: a :class:`FaceContainer` with an arbitrary number of faces
287 representing the legend of the figure.
288 :param 4 legend_position=4: TopLeft corner if 1, TopRight
289 if 2, BottomLeft if 3, BottomRight if 4
290 :param title: A Face container that can be used as tree title
292 """
294 def set_layout_fn(self, layout):
295 self._layout_handler = []
296 if type(layout) not in set([list, set, tuple, frozenset]):
297 layout = [layout]
299 for ly in layout:
300 # Validates layout function
301 if callable(ly) is True or ly is None:
302 self._layout_handler.append(ly)
303 else:
304 from . import layouts
305 try:
306 self._layout_handler.append(getattr(layouts, ly))
307 except Exception as e:
308 print(e)
309 raise ValueError ("Required layout is not a function pointer nor a valid layout name.")
311 def get_layout_fn(self):
312 return self._layout_handler
314 layout_fn = property(get_layout_fn, set_layout_fn)
316 def __init__(self):
317 # :::::::::::::::::::::::::
318 # TREE SHAPE AND SIZE
319 # :::::::::::::::::::::::::
321 # Valid modes are : "c" or "r"
322 self.mode = "r"
324 # Applies only for circular mode. It prevents aligned faces to
325 # overlap each other by increasing the radius.
326 self.allow_face_overlap = False
328 # Layout function used to dynamically control the aspect of
329 # nodes
330 self._layout_handler = []
332 # 0= tree is drawn from left-to-right 1= tree is drawn from
333 # right-to-left. This property only has sense when "r" mode
334 # is used.
335 self.orientation = 0
337 # Tree rotation in degrees (clock-wise rotation)
338 self.rotation = 0
340 # Scale used to convert branch lengths to pixels. If 'None',
341 # the scale will be automatically calculated.
342 self.scale = None
344 # How much the center of a circular tree can be opened,
345 # referred to the total tree length.
346 self.root_opening_factor = 0.25
348 # mid, or full
349 self.optimal_scale_level = "mid"
351 # Min separation, in pixels, between to adjacent branches
352 self.min_leaf_separation = 1
354 # Leaf branch separation margin, in pixels. This will add a
355 # separation of X pixels between adjacent leaf branches. In
356 # practice this produces a Y-zoom in.
357 self.branch_vertical_margin = 0
359 # When circular trees are drawn, this defines the starting
360 # angle (in degrees) from which leaves are distributed
361 # (clock-wise) around the total arc. 0 = 3 o'clock
362 self.arc_start = 0
364 # Total arc used to draw circular trees (in degrees)
365 self.arc_span = 359
367 # Margins around tree picture
368 self.margin_left = 1
369 self.margin_right = 1
370 self.margin_top = 1
371 self.margin_bottom = 1
373 # :::::::::::::::::::::::::
374 # TREE BRANCHES
375 # :::::::::::::::::::::::::
377 # When top-branch and bottom-branch faces are larger than
378 # branch length, branch line can be completed. Also, when
379 # circular trees are drawn,
380 self.complete_branch_lines_when_necessary = True
381 self.pack_leaves = False
382 self.extra_branch_line_type = 2 # 0 solid, 1 dashed, 2 dotted
383 self.extra_branch_line_color = "gray"
385 # Convert tree branches to a fixed length, thus allowing to
386 # observe the topology of tight nodes
387 self.force_topology = False
389 # Draw guidelines from leaf nodes to aligned faces
390 self.draw_guiding_lines = False
392 # Format and color for the guiding lines
393 self.guiding_lines_type = 2 # 0 solid, 1 dashed, 2 dotted
394 self.guiding_lines_color = "gray"
396 # :::::::::::::::::::::::::
397 # FACES
398 # :::::::::::::::::::::::::
400 # Aligned faces will be drawn as a table, considering all
401 # columns in all node faces.
402 self.draw_aligned_faces_as_table = True
403 self.aligned_table_style = 0 # 0 = full grid (rows and
404 # columns), 1 = semigrid ( rows
405 # are merged )
407 # When floating faces from different nodes overlap, children
408 # faces are drawn on top of parent faces. This can be reversed
409 # by setting this attribute to false.
410 self.children_faces_on_top = True
412 # :::::::::::::::::::::::::
413 # Addons
414 # :::::::::::::::::::::::::
416 # Draw a border around the whole tree
417 self.show_border = False
419 # Draw the scale
420 self.show_scale = True
421 self.scale_length = None
423 # Initialize aligned face headers
424 self.aligned_header = FaceContainer()
425 self.aligned_foot = FaceContainer()
427 self.show_leaf_name = True
428 self.show_branch_length = False
429 self.show_branch_support = False
431 self.legend = FaceContainer()
432 self.legend_position = 2
435 self.title = FaceContainer()
436 self.tree_width = 180
437 # PRIVATE values
438 self._scale = None
440 self.__closed__ = 1
443 def __setattr__(self, attr, val):
444 if hasattr(self, attr) or not getattr(self, "__closed__", 0):
445 if TREE_STYLE_CHECKER.get(attr, lambda x: True)(val):
446 object.__setattr__(self, attr, val)
447 else:
448 raise ValueError("[%s] wrong type" % attr)
449 else:
450 raise ValueError("[%s] option is not supported" % attr)
452class _FaceAreas:
453 def __init__(self):
454 for a in FACE_POSITIONS:
455 setattr(self, a, FaceContainer())
457 def __setattr__(self, attr, val):
458 if attr not in FACE_POSITIONS:
459 raise AttributeError("Face area [%s] not in %s" %(attr, FACE_POSITIONS) )
460 return super(_FaceAreas, self).__setattr__(attr, val)
462 def __getattr__(self, attr):
463 if attr not in FACE_POSITIONS:
464 raise AttributeError("Face area [%s] not in %s" %(attr, FACE_POSITIONS) )
465 return super(_FaceAreas, self).__getattr__(attr)
467class FaceContainer(dict):
468 """
469 .. versionadded:: 2.1
471 Use this object to create a grid of faces. You can add faces to different columns.
472 """
473 def add_face(self, face, column):
474 """
475 add the face **face** to the specified **column**
476 """
477 self.setdefault(int(column), []).append(face)
479def _leaf(node):
480 collapsed = hasattr(node, "_img_style") and not node.img_style["draw_descendants"]
481 return collapsed or node.is_leaf
483def add_face_to_node(face, node, column, aligned=False, position="branch-right"):
484 """
485 .. currentmodule:: ete3.treeview.faces
487 Adds a Face to a given node.
489 :argument face: A :class:`Face` instance
491 .. currentmodule:: ete3
493 :argument node: a tree node instance (:class:`Tree`, :class:`PhyloTree`, etc.)
494 :argument column: An integer number starting from 0
495 :argument "branch-right" position: Possible values are
496 "branch-right", "branch-top", "branch-bottom", "float", "float-behind" and "aligned".
497 """
499 ## ADD HERE SOME TYPE CHECK FOR node and face
501 # to stay 2.0 compatible
502 if aligned == True:
503 position = "aligned"
505 if node.props.get("_temp_faces", None):
506 getattr(node.props["_temp_faces"], position).add_face(face, column)
507 else:
508 raise Exception("This function can only be called within a layout function. Use node.add_face() instead")
511def set_pen_style(pen, line_style):
512 if line_style == 0:
513 pen.setStyle(Qt.PenStyle.SolidLine)
514 elif line_style == 1:
515 pen.setStyle(Qt.PenStyle.DashLine)
516 elif line_style == 2:
517 pen.setStyle(Qt.PenStyle.DotLine)
520def save(scene, imgName, w=None, h=None, dpi=90,\
521 take_region=False, units="px"):
522 ipython_inline = False
523 if imgName == "%%inline":
524 ipython_inline = True
525 ext = "PNG"
526 elif imgName == "%%inlineSVG":
527 ipython_inline = True
528 ext = "SVG"
529 elif imgName.startswith("%%return"):
530 try:
531 ext = imgName.split(".")[1].upper()
532 except IndexError:
533 ext = 'SVG'
534 imgName = '%%return'
535 else:
536 ext = imgName.split(".")[-1].upper()
538 main_rect = scene.sceneRect()
539 aspect_ratio = main_rect.height() / main_rect.width()
541 # auto adjust size
542 if not w and not h:
543 units = "px"
544 w = main_rect.width()
545 h = main_rect.height()
546 ratio_mode = Qt.AspectRatioMode.KeepAspectRatio
547 elif w and h:
548 ratio_mode = Qt.AspectRatioMode.IgnoreAspectRatio
549 elif h is None :
550 h = w * aspect_ratio
551 ratio_mode = Qt.AspectRatioMode.KeepAspectRatio
552 elif w is None:
553 w = h / aspect_ratio
554 ratio_mode = Qt.AspectRatioMode.KeepAspectRatio
556 # Adjust to resolution
557 if units == "mm":
558 if w:
559 w = w * 0.0393700787 * dpi
560 if h:
561 h = h * 0.0393700787 * dpi
562 elif units == "in":
563 if w:
564 w = w * dpi
565 if h:
566 h = h * dpi
567 elif units == "px":
568 pass
569 else:
570 raise Exception("wrong unit format")
572 x_scale, y_scale = w/main_rect.width(), h/main_rect.height()
574 if ext == "SVG":
575 svg = QSvgGenerator()
576 targetRect = QRectF(0, 0, w, h)
577 svg.setSize(QSize(int(w), int(h)))
578 svg.setViewBox(targetRect)
579 svg.setTitle("Generated with ETE http://etetoolkit.org")
580 svg.setDescription("Generated with ETE http://etetoolkit.org")
582 if imgName == '%%return':
583 ba = QByteArray()
584 buf = QBuffer(ba)
585 buf.open(QIODevice.WriteOnly)
586 svg.setOutputDevice(buf)
587 else:
588 svg.setFileName(imgName)
590 pp = QPainter()
591 pp.begin(svg)
592 scene.render(pp, targetRect, scene.sceneRect(), ratio_mode)
593 pp.end()
594 if imgName == '%%return':
595 compatible_code = str(ba)
596 print('from memory')
597 else:
598 with open(imgName) as f:
599 compatible_code = f.read()
601 # Fix a very annoying problem with Radial gradients in
602 # inkscape and browsers...
603 compatible_code = compatible_code.replace("xml:id=", "id=")
604 compatible_code = re.sub(r'font-size="(\d+)"', 'font-size="\\1pt"', compatible_code)
605 compatible_code = compatible_code.replace('\n', ' ')
606 compatible_code = re.sub(r'<g [^>]+>\s*</g>', '', compatible_code)
607 # End of fix
608 if ipython_inline:
609 from IPython.core.display import SVG
610 return SVG(compatible_code)
612 elif imgName == '%%return':
613 return x_scale, y_scale, compatible_code
614 else:
615 with open(imgName, "w") as f:
616 f.write(compatible_code)
618 elif ext == "PDF":
619 format = QPrinter.OutputFormat.PdfFormat
621 printer = QPrinter(QPrinter.PrinterMode.HighResolution)
622 printer.setResolution(dpi)
623 printer.setOutputFormat(format)
624 printer.setPageSize(QPageSize(QPageSize.PageSizeId.A4))
626 printer.setFullPage(True);
627 printer.setOutputFileName(imgName);
628 pp = QPainter(printer)
629 targetRect = QRectF(0, 0 , w, h)
630 scene.render(pp, targetRect, scene.sceneRect(), ratio_mode)
631 else:
632 targetRect = QRectF(0, 0, w, h)
633 ii= QImage(int(w), int(h), QImage.Format.Format_ARGB32)
634 ii.fill(QColor(Qt.GlobalColor.white).rgb())
635 ii.setDotsPerMeterX(int(dpi / 0.0254)) # Convert inches to meters
636 ii.setDotsPerMeterY(int(dpi / 0.0254))
637 pp = QPainter(ii)
638 pp.setRenderHint(QPainter.RenderHint.Antialiasing)
639 pp.setRenderHint(QPainter.RenderHint.TextAntialiasing)
640 pp.setRenderHint(QPainter.RenderHint.SmoothPixmapTransform)
642 scene.render(pp, targetRect, scene.sceneRect(), ratio_mode)
643 pp.end()
644 if ipython_inline:
645 ba = QByteArray()
646 buf = QBuffer(ba)
647 buf.open(QIODevice.OpenModeFlag.WriteOnly)
648 ii.save(buf, "PNG")
649 from IPython.core.display import Image
650 return Image(ba.data())
651 elif imgName == '%%return':
652 ba = QByteArray()
653 buf = QBuffer(ba)
654 buf.open(QIODevice.WriteOnly)
655 ii.save(buf, "PNG")
656 return x_scale, y_scale, ba.toBase64()
657 else:
658 ii.save(imgName)
660 return w/main_rect.width(), h/main_rect.height()