From 251aab08ff18ebdd2b6dcd1a5c7210ceff4a8947 Mon Sep 17 00:00:00 2001 From: KV Date: Sun, 25 Apr 2021 07:37:01 +0200 Subject: [PATCH] Move color and font options into new Look dataclass This solves the basic part of #225 - supporting options to specify - Foreground/border color and text color in addition to bgcolor - Font size is not requested in #225, but included as well --- src/wireviz/DataClasses.py | 77 +++++++++++++++++++++++++++++--------- src/wireviz/Harness.py | 38 +++++++++---------- src/wireviz/wv_html.py | 11 +++--- 3 files changed, 83 insertions(+), 43 deletions(-) diff --git a/src/wireviz/DataClasses.py b/src/wireviz/DataClasses.py index 6b7462a..be76617 100644 --- a/src/wireviz/DataClasses.py +++ b/src/wireviz/DataClasses.py @@ -1,11 +1,11 @@ # -*- coding: utf-8 -*- from typing import Dict, List, Optional, Tuple, Union -from dataclasses import dataclass, field, InitVar +from dataclasses import asdict, dataclass, field, InitVar from pathlib import Path from wireviz.wv_helper import int2tuple, aspect_ratio -from wireviz.wv_colors import Color, Colors, ColorMode, ColorScheme, COLOR_CODES +from wireviz.wv_colors import Color, Colors, ColorMode, ColorScheme, COLOR_CODES, translate_color # Each type alias have their legal values described in comments - validation might be implemented in the future @@ -13,6 +13,7 @@ PlainText = str # Text not containing HTML tags nor newlines Hypertext = str # Text possibly including HTML hyperlinks that are removed in all outputs except HTML output MultilineHypertext = str # Hypertext possibly also including newlines to break lines in diagram output Designator = PlainText # Case insensitive unique name of connector or cable +Points = float # Size in points = 1/72 inch # Literal type aliases below are commented to avoid requiring python 3.8 ConnectorMultiplier = PlainText # = Literal['pincount', 'populated'] @@ -32,26 +33,66 @@ class Metadata(dict): pass +@dataclass +class Look: + """Colors and font that defines how an element should look like.""" + color: Optional[Color] = None + bgcolor: Optional[Color] = None + fontcolor: Optional[Color] = None + fontname: Optional[PlainText] = None + fontsize: Optional[Points] = None + + def _2dict(self) -> dict: + """Return dict of strings with color values translated to hex.""" + return { + k:translate_color(v, "hex") if 'color' in k else str(v) for k,v in asdict(self).items() + } + + def graph_args(self) -> dict: + """Return dict with arguments to a dot graph.""" + return {k:v for k,v in self._2dict().items() if k != 'color'} + + def node_args(self) -> dict: + """Return dict with arguments to a dot node with filled style.""" + return {k.replace('bg', 'fill'):v for k,v in self._2dict().items()} + + def html_style(self, color_prefix: Optional[str] = None, include_all: bool = True) -> str: + """Return HTML style value containing all non-empty option values.""" + translated = Look(**self._2dict()) + return ' '.join(value for value in ( + f'{color_prefix} {translated.color};' if self.color and color_prefix else None, + f'background-color: {translated.bgcolor};' if self.bgcolor and include_all else None, + f'color: {translated.fontcolor};' if self.fontcolor and include_all else None, + f'font-family: {self.fontname};' if self.fontname and include_all else None, + f'font-size: {self.fontsize}pt;' if self.fontsize and include_all else None, + ) if value) + +DEFAULT_LOOK = Look( + color = 'BK', + bgcolor = 'WH', + fontcolor = 'BK', + fontname = 'arial', + fontsize = 14, +) + + @dataclass class Options: - fontname: PlainText = 'arial' - bgcolor: Color = 'WH' - bgcolor_node: Optional[Color] = 'WH' - bgcolor_connector: Optional[Color] = None - bgcolor_cable: Optional[Color] = None - bgcolor_bundle: Optional[Color] = None + base: Look = field(default_factory=dict) + node: Look = field(default_factory=dict) + connector: Look = field(default_factory=dict) + cable: Look = field(default_factory=dict) + bundle: Look = field(default_factory=dict) color_mode: ColorMode = 'SHORT' mini_bom_mode: bool = True def __post_init__(self): - if not self.bgcolor_node: - self.bgcolor_node = self.bgcolor - if not self.bgcolor_connector: - self.bgcolor_connector = self.bgcolor_node - if not self.bgcolor_cable: - self.bgcolor_cable = self.bgcolor_node - if not self.bgcolor_bundle: - self.bgcolor_bundle = self.bgcolor_cable + # Build initialization dicts with default values followed by dict entries from YAML input. + self.base = Look(**{**asdict(DEFAULT_LOOK), **self.base}) + self.node = Look(**{**asdict(self.base), **self.node}) + self.connector = Look(**{**asdict(self.node), **self.connector}) + self.cable = Look(**{**asdict(self.node), **self.cable}) + self.bundle = Look(**{**asdict(self.cable), **self.bundle}) @dataclass @@ -67,8 +108,8 @@ class Image: src: str scale: Optional[ImageScale] = None # Attributes of the image cell containing the image: - width: Optional[int] = None - height: Optional[int] = None + width: Optional[Points] = None + height: Optional[Points] = None fixedsize: Optional[bool] = None bgcolor: Optional[Color] = None # Contents of the text cell just below the image cell: diff --git a/src/wireviz/Harness.py b/src/wireviz/Harness.py index 2f9eb64..3e63974 100644 --- a/src/wireviz/Harness.py +++ b/src/wireviz/Harness.py @@ -97,17 +97,16 @@ class Harness: dot.body.append(f'// {APP_URL}') dot.attr('graph', rankdir='LR', ranksep='2', - bgcolor=wv_colors.translate_color(self.options.bgcolor, "HEX"), nodesep='0.33', - fontname=self.options.fontname) - dot.attr('node', - shape='none', - width='0', height='0', margin='0', # Actual size of the node is entirely determined by the label. + **self.options.base.graph_args()) + dot.attr('node', shape='none', style='filled', - fillcolor=wv_colors.translate_color(self.options.bgcolor_node, "HEX"), - fontname=self.options.fontname) + width='0', height='0', margin='0', # Actual size of the node is entirely determined by the label. + **self.options.node.node_args()) dot.attr('edge', style='bold', - fontname=self.options.fontname) + **self.options.base.node_args()) + + wire_border_hex = wv_colors.get_color_hex(self.options.base.color)[0] # prepare ports on connectors depending on which side they will connect for _, cable in self.cables.items(): @@ -175,10 +174,11 @@ class Harness: html = '\n'.join(html) dot.node(connector.name, label=f'<\n{html}\n>', shape='box', style='filled', - fillcolor=translate_color(self.options.bgcolor_connector, "HEX")) + **self.options.connector.node_args()) if len(connector.loops) > 0: - dot.attr('edge', color='#000000:#ffffff:#000000') + # TODO: Use self.options.wire.color and self.options.wire.bgcolor here? + dot.attr('edge', color=f'{wire_border_hex}:#ffffff:{wire_border_hex}') if connector.ports_left: loop_side = 'l' loop_dir = 'w' @@ -258,10 +258,11 @@ class Harness: wirehtml.append(f' ') wirehtml.append(' ') - bgcolors = ['#000000'] + get_color_hex(connection_color, pad=pad) + ['#000000'] + bgcolors = [wire_border_hex] + get_color_hex(connection_color, pad=pad) + [wire_border_hex] wirehtml.append(f' ') wirehtml.append(f' ') wirehtml.append(' ') + # TODO: Reverse curved wire colors instead? Test also with empty wire colors! wv_colors.default_color ?? for j, bgcolor in enumerate(bgcolors[::-1]): # Reverse to match the curved wires when more than 2 colors wirehtml.append(f' ') wirehtml.append('
') @@ -301,10 +302,10 @@ class Harness: if isinstance(cable.shield, str): # shield is shown with specified color and black borders shield_color_hex = wv_colors.get_color_hex(cable.shield)[0] - attributes = f'height="6" bgcolor="{shield_color_hex}" border="2" sides="tb"' + attributes = f'height="6" bgcolor="{shield_color_hex}" color="{wire_border_hex}" border="2" sides="tb"' else: # shield is shown as a thin black wire - attributes = f'height="2" bgcolor="#000000" border="0"' + attributes = f'height="2" bgcolor="{wire_border_hex}" border="0"' wirehtml.append(f' ') wirehtml.append('  ') @@ -315,10 +316,10 @@ class Harness: # connections for connection in cable.connections: if isinstance(connection.via_port, int): # check if it's an actual wire and not a shield - dot.attr('edge', color=':'.join(['#000000'] + wv_colors.get_color_hex(cable.colors[connection.via_port - 1], pad=pad) + ['#000000'])) + dot.attr('edge', color=':'.join([wire_border_hex] + wv_colors.get_color_hex(cable.colors[connection.via_port - 1], pad=pad) + [wire_border_hex])) else: # it's a shield connection # shield is shown with specified color and black borders, or as a thin black wire otherwise - dot.attr('edge', color=':'.join(['#000000', shield_color_hex, '#000000']) if isinstance(cable.shield, str) else '#000000') + dot.attr('edge', color=':'.join([wire_border_hex, shield_color_hex, wire_border_hex]) if isinstance(cable.shield, str) else wire_border_hex) if connection.from_port is not None: # connect to left from_connector = self.connectors[connection.from_name] from_port = f':p{connection.from_port+1}r' if from_connector.style != 'simple' else '' @@ -352,11 +353,10 @@ class Harness: to_string = '' html = [row.replace(f'', to_string) for row in html] - style, bgcolor = ('filled,dashed', self.options.bgcolor_bundle) if cable.category == 'bundle' else \ - ('filled', self.options.bgcolor_cable) + style, options = ('filled,dashed', self.options.bundle) if cable.category == 'bundle' else \ + ('filled', self.options.cable) html = '\n'.join(html) - dot.node(cable.name, label=f'<\n{html}\n>', shape='box', - style=style, fillcolor=translate_color(bgcolor, "HEX")) + dot.node(cable.name, label=f'<\n{html}\n>', shape='box', style=style, **options.node_args()) def typecheck(name: str, value: Any, expect: type) -> None: if not isinstance(value, expect): diff --git a/src/wireviz/wv_html.py b/src/wireviz/wv_html.py index 0b81974..287e613 100644 --- a/src/wireviz/wv_html.py +++ b/src/wireviz/wv_html.py @@ -15,9 +15,7 @@ def generate_html_output(filename: Union[str, Path], bom_list: List[List[str]], file.write(' \n') file.write(f' \n') file.write(f' {metadata["title"]}\n') - file.write(f'\n') - + file.write(f'\n') file.write(f'

{metadata["title"]}

\n') description = metadata.get('description') if description: @@ -33,17 +31,18 @@ def generate_html_output(filename: Union[str, Path], bom_list: List[List[str]], file.write('

Bill of Materials

\n') listy = flatten2d(bom_list) - file.write('\n') + border = options.base.html_style(color_prefix="border: 1px solid", include_all=False) + file.write(f'
\n') file.write(' \n') for item in listy[0]: - file.write(f' \n') + file.write(f' \n') file.write(' \n') for row in listy[1:]: file.write(' \n') for i, item in enumerate(row): item_str = item.replace('\u00b2', '²') align = '; text-align:right' if listy[0][i] == 'Qty' else '' - file.write(f' \n') + file.write(f' \n') file.write(' \n') file.write('
{item}{item}
{item_str}{item_str}
\n')