Source code for ete4.smartview.layout

"""
Definition of the basic elements for a tree representation (Layout and
Decoration), extra labels (Label), and the default tree style.

The valid keys for a tree style are:

- shape
- radius
- angle-start
- angle-end
- angle-span
- node-height-min
- content-height-min
- collapsed
- show-popup-props
- hide-popup-props
- is-leaf-fn
- box
- dot
- hz-line
- vt-line
- aliases

Some properties will be used directly by the backend: shape,
node-height-min, content-height-min, radius, angle-start, angle-end,
angle-span, show-popup-props, hide-popup-props, is-leaf-fn.

Others  will be controlled by the css class of the element in the frontend:
box, dot, hz-line, vt-line.

And the "aliases" part will tell the frontend which styles are referenced.

Example of a tree style in use::

  my_tree_style = {
      'shape': 'circular',  # or 'rectangular'
      'radius': 5,
      'angle-start': -180,
      'angle-end': 180,  # alternatively we can give 'angle-span'
      'node-height-min': 10,
      'content-height-min': 5,
      'collapsed': {'shape': 'outline', 'fill-opacity': 0.8},
      'show-popup-props': None,  # all defined properties
      'hide-popup-props': ['support'],  # except support
      'is-leaf-fn': lambda node: node.level > 4,
      'box': {'fill': 'green', 'opacity': 0.1, 'stroke': 'blue'},
      'dot': {'shape': 'hexagon', 'fill': 'red'},
      'hz-line': {'stroke-width': 2},
      'vt-line': {'stroke': '#ffff00'},
      'aliases': {
          'support': {'fill': 'green'},  # changes the default one
          'my-leaf': {'fill': 'blue', 'font-weight': 'bold'},
      },
  }

  layout = Layout(name='Example layout', draw_tree=my_tree_style)
"""

from collections import namedtuple
from dataclasses import dataclass, field
from functools import lru_cache
import inspect
import copy

from .faces import Face, PropFace, TextFace


# Layouts have all the information needed to represent a tree.
#

[docs] class Layout: """ A complete specification of how to represent a tree. Layouts have a name and two functions providing the style and decorations of the full tree and the visible nodes. When exploring a tree, layouts compose. Using several layouts will add extra graphic representations, and/or overwrite some styles from previous layouts. """
[docs] def __init__(self, name, draw_tree=None, draw_node=None, cache_size=None, active=True): """ :param name: String identifying the layout (to select in the gui, etc.) :param draw_tree: Function specifying tree style and decorations. :param draw_node: Function specifying node style and decorations. :param cache_size: Number of elements that draw_node() will memorize (useful values are None for infinite cache, and 0 for no cache). :param active: If True, the layout is used immediately when exploring. """ self.cache_size = cache_size # used to cache functions in the setters # Name. This is mainly to activate/deactivate the layout in the gui. assert type(name) is str self.name = name # Tree representation (style and decorations). self.draw_tree = draw_tree # Node representation (style and decorations). self.draw_node = draw_node # Set if the layout should be initially active in the gui. self.active = active # TODO: Find a better place for this
@property def draw_tree(self): return self._draw_tree @draw_tree.setter def draw_tree(self, value): if value is None: self._draw_tree = lambda tree: [DEFAULT_TREE_STYLE] elif type(value) is dict: self._draw_tree = lambda tree: [DEFAULT_TREE_STYLE, value] elif callable(value): @lru_cache(maxsize=self.cache_size) def cached_draw_tree(tree): return [DEFAULT_TREE_STYLE] + to_elements(value(tree)) self._draw_tree = cached_draw_tree else: raise ValueError('draw_tree can be either a dict or a function') @property def draw_node(self): return self._draw_node @draw_node.setter def draw_node(self, value): assert value is None or callable(value) if value is None: self._draw_node = lambda node, collapsed: [] return f = value # nicer name, since it is a function # We use an auxiliary function to cache its results. arity = len(inspect.signature(f).parameters) if arity == 1: # f(node) (unspecified what to do with collapsed) @lru_cache(maxsize=self.cache_size) def cached_draw_node(node, collapsed): if not collapsed: return to_elements(f(node)) # get just for the node else: return [x for n in collapsed # get from all siblings for x in to_elements(f(n))] elif arity == 2: # f(node, collapsed) (fully specified) @lru_cache(maxsize=self.cache_size) def cached_draw_node(node, collapsed): return to_elements(f(node, collapsed)) else: raise ValueError('draw_node can have only 1 or 2 arguments.') self._draw_node = cached_draw_node # use the auxiliary caching function
[docs] def to_elements(xs): """Return a list of the elements of iterable xs as Decorations/dicts.""" # Normally xs is already a list of decorations/dicts. if xs is None: # but xs can be None (a draw_node() didn't return anything) return [] if type(xs) is dict: return [xs] if not hasattr(xs, '__iter__'): # or it can be a single element return [xs if type(xs) is Decoration else Decoration(xs)] # Return elements, wrapped as Decorations if they need it. return [x if type(x) in [Decoration, dict] else Decoration(x) for x in xs]
DEFAULT_TREE_STYLE = { # the default style of a tree 'show-popup-props': ['dist', 'support'], 'aliases': { # to name styles that can be referenced in draw_node 'dist': {'fill': '#888'}, 'support': {'fill': '#f88'}, # a light red } }
[docs] def update_style(style, style_new): """Update the style dictionary merging properly with style_new.""" subdicts = {k for k in style_new if type(style_new[k]) is dict and type(style.get(k)) is dict} style.update((k, copy.deepcopy(v)) for k, v in style_new.items() if k not in subdicts) for k in subdicts: update_style(style[k], style_new[k])
[docs] @dataclass class Decoration: """ A decoration is a face with a position ("top", "bottom", "right", etc.), a column (an integer used for relative order with other faces in the same position), and an anchor point (to fine-tune the position of things like texts within them). """ face: Face position: str column: int anchor: tuple
[docs] def __init__(self, face, position='top', column=0, anchor=None): self.face = TextFace(face) if type(face) is str else face self.position = position self.column = column self.anchor = anchor or default_anchors[position]
default_anchors = {'top': (-1, 1), # left, bottom 'bottom': (-1, -1), # left, top 'right': (-1, 0), # left, middle 'left': ( 1, 0), # right, middle 'aligned': (-1, 0), # left, middle 'header': (-1, 1), # (unused for the moment) 'footer': (-1, 1)} # (unused for the moment) # The default layout.
[docs] def default_draw_node(node, collapsed): if not collapsed: face_dist = PropFace('dist', '%.2g', style='dist') yield Decoration(face_dist, position='top') face_support = PropFace('support', '%.2g', style='support') yield Decoration(face_support, position='bottom') if node.is_leaf or collapsed: yield Decoration(PropFace('name'), position='right')
BASIC_LAYOUT = Layout(name='basic', draw_node=default_draw_node) # Description of a label that we want to add to the representation of a node. Label = namedtuple('Label', 'code style node_type position column anchor fs_max')