Source code for treevalue.tree.tree.graph

from functools import lru_cache
from typing import Type, Callable, Union, Optional, Tuple

from graphviz import Digraph, nohtml
from hbutils.reflection import post_process, dynamic_call, freduce

from .tree import TreeValue
from ...utils import build_graph, Color
from ...utils.tree import SUFFIXED_TAG


def _min_distance(l_, n):
    list_ = sorted(l_)
    return min([abs(a - b) for a, b in zip(list_, list_[1:] + [list_[0] + n])])


_FIRST_DIS_CNT = 2


def _dis_ratios(n, s, t):
    l_ = [(i * t + s) % n for i in range(n)]
    _dis = [_min_distance(l_[:i], n) for i in range(_FIRST_DIS_CNT, n)]
    _exp = [n // i for i in range(_FIRST_DIS_CNT, n)]
    _ratios = [e / d for d, e in zip(_dis, _exp)]
    return max(_ratios), tuple(_ratios)


@lru_cache()
def _gcd(x, y):
    if y == 0:
        return x
    else:
        return _gcd(y, x % y)


@lru_cache()
def _best_t_for_n_s(n, s):
    _final_mean, _final_t = None, None
    for _current_t in range(1, (n + 1) // 2 + 1):
        if _gcd(_current_t, n) == 1:
            _current_mean = _dis_ratios(n, s, _current_t)
            if _final_mean is None or _current_mean < _final_mean:
                _final_mean, _final_t = _current_mean, _current_t

    return n, _final_t, _final_mean


@lru_cache()
def _hue_id_mapping(n, s):
    _, t, _ = _best_t_for_n_s(n, s)
    return [(t * i + s) % n for i in range(n)]


@lru_cache()
def _base_hue(n, s, i):
    return (_hue_id_mapping(n, s)[i] + 0.5) / n


@lru_cache()
def _all_color(n, s, i):
    h = _base_hue(n, s, i)

    # line color
    _line = Color.from_hsv(h, 0.8, 0.6, alpha=0.8)

    # key font color
    _key = Color.from_hsv(h, 1.0, 0.45, alpha=1.0)

    # border color
    _border_1 = Color.from_hsv(h, 0.65, 0.7, alpha=0.6)
    _border_2 = Color.from_hsv(h, 0.65, 0.7, alpha=0.0)

    # shape color
    _shape = Color.from_hsv(h, 0.55, 1.0, alpha=0.6)

    # data font color
    _data = Color.from_hsv(h, 1.0, 0.2, alpha=1.0)

    return _line, _key, _border_1, _border_2, _shape, _data


@freduce(init=lambda: (lambda: {}))
def _dict_call_merge(d1, d2):
    d1 = dynamic_call(d1)
    d2 = dynamic_call(d2)

    def _new_func(*args, **kwargs):
        _r1 = d1(*args, **kwargs)
        _r2 = d2(*args, **kwargs)

        _return = dict(_r1)
        _return.update(_r2)
        return _return

    return _new_func


@dynamic_call
def _node_id(current):
    return 'node_%x' % (id(current._detach()))


@dynamic_call
def _default_value_id(_, parent, current_path, parent_path):
    return 'value__%s__%s' % (_node_id(parent, parent_path), current_path[-1])


@post_process(lambda f: dynamic_call(f) if f is not None else None)
def _dup_value_func(dup_value):
    if dup_value:
        if isinstance(dup_value, (type, tuple)):
            _dup_value = lambda v: id(v) if isinstance(v, dup_value) else None
        elif hasattr(dup_value, '__call__'):
            _dup_value = dup_value
        else:
            _dup_value = lambda v: id(v)
        _id_getter = dynamic_call(_dup_value)

        def _new_func(current, parent, current_path, parent_path):
            _id = _id_getter(current, parent, current_path, parent_path)
            if not _id:
                return _default_value_id(current, parent, current_path, parent_path)
            elif isinstance(_id, int):
                return 'value_%x' % (_id,)
            else:
                return 'value_%s' % (_id,)

        return _new_func
    else:
        return None


_GENERIC_N = 36
_GENERIC_S = _GENERIC_N // 3


[docs]def graphics(*trees, title: Optional[str] = None, cfg: Optional[dict] = None, dup_value: Union[bool, Callable, type, Tuple[Type, ...]] = False, repr_gen: Optional[Callable] = None, node_cfg_gen: Optional[Callable] = None, edge_cfg_gen: Optional[Callable] = None) -> Digraph: """ Overview: Draw graph by tree values. Multiple tree values is supported. Args: - trees: Given tree values, tuples of `Tuple[TreeValue, str]` or tree values are both accepted. - title (:obj:`Optional[str]`): Title of the graph. - cfg (:obj:`Optional[dict]`): Configuration of the graph. - dup_value (:obj:`Union[bool, Callable, type, Tuple[Type, ...]]`): Value duplicator, \ set `True` to make value with same id use the same node in graph, \ you can also define your own node id algorithm by this argument. \ Default is `False` which means do not use value duplicator. - repr_gen (:obj:`Optional[Callable]`): Representation format generator, \ default is `None` which means using `repr` function. - node_cfg_gen (:obj:`Optional[Callable]`): Node configuration generator, \ default is `None` which means no configuration. - edge_cfg_gen (:obj:`Optional[Callable]`): Edge configuration generator, \ default is `None` which means no configuration. Returns: - graph (:obj:`Digraph`): Generated graph of tree values. """ def _node_tag(current, parent, current_path, parent_path, is_node): if is_node: return _node_id(current, current_path) else: return dynamic_call( _dup_value_func(dup_value) or _default_value_id )(current, parent, current_path, parent_path) setattr(_node_tag, SUFFIXED_TAG, True) _line_color = lambda i: _all_color(_GENERIC_N, _GENERIC_S, i)[0] _key_font_color = lambda i: _all_color(_GENERIC_N, _GENERIC_S, i)[1] _border_color = lambda i, is_node: _all_color(_GENERIC_N, _GENERIC_S, i)[2 if is_node else 3] _shape_color = lambda i: _all_color(_GENERIC_N, _GENERIC_S, i)[4] _data_font_color = lambda i: _all_color(_GENERIC_N, _GENERIC_S, i)[5] return build_graph( *trees, node_id_gen=_node_tag, graph_title=title or "<untitled>", graph_cfg=cfg or {}, repr_gen=repr_gen or (lambda x: nohtml(repr(x))), iter_gen=lambda n: iter(n.items()) if isinstance(n, TreeValue) else None, node_cfg_gen=_dict_call_merge(lambda n, p, np, pp, is_node, is_root, root: { 'fillcolor': _shape_color(root[2]), 'color': _border_color(root[2], is_node), 'fontcolor': _data_font_color(root[2]), 'style': 'filled', 'shape': 'diamond' if is_root else ('ellipse' if is_node else 'box'), 'penwidth': 3 if is_root else 1.5, 'fontname': "Times-Roman bold" if is_node else "Times-Roman", }, (node_cfg_gen or (lambda: {}))), edge_cfg_gen=_dict_call_merge(lambda n, p, np, pp, is_node, root: { 'arrowhead': 'vee' if is_node else 'dot', 'arrowsize': 1.0 if is_node else 0.5, 'color': _line_color(root[2]), 'fontcolor': _key_font_color(root[2]), 'fontname': "Times-Roman bold", }, (edge_cfg_gen or (lambda: {}))), )