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: {}))),
)