Source code for rnets.addons.colorbar

# -*- coding: utf-8 -*-
"""This module aims to create an horizontal node colorbar that can be easily
attached to a dot graph. This node is created as a HTML table of 3 rows and
multiple columns. Each column contains the interpolation two color segment for
the given colorscheme.

Attributes:
    Attribute (type): Type synonym to define the attributes of an HTML table.
    OPEN_MARK (str): Open HTML character.
    CLOSE_MARK (str): Close HTML character.
    ATTR_SEPARATOR (str): HTML attribute separator character.
    ATTR_ASSIGN (str): HTML assignement character.
"""

from functools import partial
from itertools import pairwise, starmap
from typing import NamedTuple, Sequence, Callable

from rnets.dot import Edge, IDENT, ident_if, Node
from rnets.colors.utils import Color, rgb_to_hexstr

type Attribute = dict[str, str]

OPEN_MARK = '<'
CLOSE_MARK = '>'
ATTR_SEPARATOR = ' '
ATTR_ASSIGN = '='


[docs] class ColorbarCfg(NamedTuple): """Structure representing the colorbar configuration. Attributes: n_interp (int, optional): Number of color segments. Defaults to 100. width (int, optional): Width of each color segment, in HTML table units. Defaults to 1. height (int, optional): Height of the colorbar, in HTML table units. Defaults to 20. cellspacing (int, optional): Space between the different color segments in HTML table units. Keep a negative value to ensure uniform perception. Defaults to -1. node_name (str, optional): Name of the generated node. Defaults to Colorbar. fillcolor (:obj:`Color`, optional): Color of the node. Defaults to white. anchor_node (str or None): Node to anchor the colorbar. Defaults to None """ n_interp: int | None = 100 width: int = 1 height: int = 20 border: bool = False cellspacing: int = -1 node_name: str = "Colorbar" fillcolor: Color = (1., 1., 1.) anchor: str | None = None
[docs] class Item(NamedTuple): """HTML element. Attributes: name (str): Name of the HTML element. attributes (Attribute or None): Attributes of the HTML element. contains (Sequence of item or str or None): Items or values inside the HTML element. """ name: str attributes: Attribute | None contains: 'Sequence[Item] | str | None' def __str__(self): return item_to_str(self)
[docs] def attr_to_str( attr: Attribute , separator: str=ATTR_SEPARATOR , assign: str=ATTR_ASSIGN ) -> str: """Converts the given attribute into a str. Args: attr (`attribute`): Attribute to be converted. separator (str, optional): Separator between the different attribute options. Defaults to `ATTR_SEPARATOR`. assign (str, optional): Assignment operator. Defaults to `ATTR_ASSIGN`. Returns: str of the attribute with the given format: KEY:\"VALUE\" """ return separator.join(starmap( lambda k, v: f'{k}{assign}"{v}"' , attr.items()))
def match_contains( contains: Sequence[Item] | str | None ) -> str: match contains: case str(): return contains case None: return '' case _: return "\n{}\n".format( ident_if( '\n'.join(map(item_to_str, contains)) , IDENT , True))
[docs] def item_to_str( item: Item | None , separator: str = '\n' ) -> str: """Convert an item into a string. Args: item (:obj:`Item` or None): Item to be converted. separator (str): Separator between the different contains. Returns: str with the HTML form of the item. """ match item: case None: return '' case Item(): return ( OPEN_MARK + item.name + (f' {v}' if (v := attr_to_str(item.attributes)) else '') + CLOSE_MARK + match_contains(item.contains) + OPEN_MARK + '/' + item.name + CLOSE_MARK) case _: return ''
[docs] def build_node( item: Item , name: str , fillcolor: Color ) -> Node: """Given an :obj:`Item`, create a dot node using the str form of the item as a the node label. Args: item (:obj:`item`): Item that will be converted into a Node. name (str, optional): Name of the generated node. fillcolor (:obj:`Color`): Color of the node. Returns: :obj:`Node` built node using the str form of the item as the label. """ return Node( name , options={ "fillcolor": '"{}"'.format(rgb_to_hexstr(fillcolor, True)) , "label": OPEN_MARK + item_to_str(item) + CLOSE_MARK })
[docs] def build_tail( c_min: float , c_max: float ) -> Item: """Build the tail of the colorbar marking the starting, mid and final energy values. Always places 2 decimal values. Args: c_min (float): Minimum energy value. c_max (float): Maximum energy value. Returns: HTML row with the start, mid and end values of the energy. """ return Item("TR", {}, (Item("TD", {"COLSPAN": "100%"}, ( Item( "TABLE" , {"BORDER": "0", "CELLBORDER": "0", "CELLSPACING": "0", "WIDTH":"100%"} , (Item("TR", {}, ( Item("TD", {"ALIGN": "LEFT", "WIDTH": "33%"} , f"{c_min:.2f}") , Item("TD", {"ALIGN": "CENTER", "WIDTH": "34%"} , f"{(c_min + c_max)/2:.2f}") , Item("TD", {"ALIGN": "RIGHT", "WIDTH": "33%"} , f"{c_max:.2f}"))),)),)),))
[docs] def build_color_segment( cs: (Color, Color) , width: int , height: int ) -> Item: """Given a pair of :obj:`Color`, build a HTML cell containing an horizontal gradient. Args: cs (Color, Color): Color pair that will be interpolated in the segment. width (int): Width of the segment, in HTML table units. height (int): Height of the segment, in HTML table units. Returns: :obj:`Item` of the generated segment. """ return Item("TD" , {"BGCOLOR": f"{rgb_to_hexstr(cs[0])}:{rgb_to_hexstr(cs[1])}" , "WIDTH": str(width) , "HEIGHT": str(height)} , None)
def build_head( title: str , n_interp: int ) -> Item: return Item("TR", {}, ( Item("TD" , {"COLSPAN": str(n_interp)} , title),))
[docs] def build_body( color_fn: Callable[[float], Color] , n_interp: int , width: int , height: int ) -> Item: """Build the entire colorbar row. Args: color_fn (Callable[[float], Color]): Color interpolation function. n_interp (int): Number of columns. width (int): Width of the segment, in HTML table units. height (int): Height of the segment, in HTML table units. Returns: :obj:`Item` of the generated row. """ return (Item( "TR", {} , map( partial(build_color_segment, width=width, height=height) , pairwise(map( lambda x: color_fn(x / n_interp) , range(n_interp + 1)))),))
[docs] def build_colorbar( color_fn: Callable[[float], Color] , c_ran: tuple[float, float] | None , cfg: ColorbarCfg , title: str | None="Energy" ) -> Node: """Given a :obj:`ColorbarCfg` build the associated graphviz :obj:`Node`. Args: color_fn (:obj:`function`): Color interpolation function. It should take a float between [0,1] and return an :obj:`Color` c_ran (tuple of two floats, optional): Minimum and maximum energy values. Defaults to None cfg (:obj:`ColorbarCfg`): Configuration used to build the colorbar. title (str, optional): Title of the colorbar. Defaults to None. Returns: :obj:`Node` built from the given configuration. """ return build_node( Item( "TABLE" , {"BORDER": str(int(cfg.border)) , "CELLBORDER": "0" , "CELLSPACING": str(int(cfg.cellspacing))} , (build_head(title, cfg.n_interp) , build_body(color_fn, cfg.n_interp, cfg.width, cfg.height) , None if c_ran is None else build_tail(*c_ran))) , name=cfg.node_name , fillcolor=cfg.fillcolor)
def build_anchor( origin: Node , target: Node ) -> Edge: return Edge( origin=origin , target=target , direction="->" , options={"style": "invis"})