Skip to content

Commit

Permalink
Move color and font options into new Look dataclass
Browse files Browse the repository at this point in the history
This solves the basic part of wireviz#225 - supporting options to specify
- Foreground/border color and text color in addition to bgcolor
- Font size is not requested in wireviz#225, but included as well
  • Loading branch information
kvid committed Oct 22, 2021
1 parent 40aed9e commit 251aab0
Show file tree
Hide file tree
Showing 3 changed files with 83 additions and 43 deletions.
77 changes: 59 additions & 18 deletions src/wireviz/DataClasses.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,19 @@
# -*- 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
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']
Expand All @@ -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
Expand All @@ -67,8 +108,8 @@ class Image:
src: str
scale: Optional[ImageScale] = None
# Attributes of the image cell <td> 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 <td> just below the image cell:
Expand Down
38 changes: 19 additions & 19 deletions src/wireviz/Harness.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,17 +97,16 @@ def create_graph(self) -> Graph:
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():
Expand Down Expand Up @@ -175,10 +174,11 @@ def create_graph(self) -> Graph:

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'
Expand Down Expand Up @@ -258,10 +258,11 @@ def create_graph(self) -> Graph:
wirehtml.append(f' <td><!-- {i}_out --></td>')
wirehtml.append(' </tr>')

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' <tr>')
wirehtml.append(f' <td colspan="3" border="0" cellspacing="0" cellpadding="0" port="w{i}" height="{(2 * len(bgcolors))}">')
wirehtml.append(' <table cellspacing="0" cellborder="0" border="0">')
# 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' <tr><td colspan="3" cellpadding="0" height="2" bgcolor="{bgcolor if bgcolor != "" else wv_colors.default_color}" border="0"></td></tr>')
wirehtml.append(' </table>')
Expand Down Expand Up @@ -301,10 +302,10 @@ def create_graph(self) -> Graph:
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' <tr><td colspan="3" cellpadding="0" {attributes} port="ws"></td></tr>')

wirehtml.append(' <tr><td>&nbsp;</td></tr>')
Expand All @@ -315,10 +316,10 @@ def create_graph(self) -> Graph:
# 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 ''
Expand Down Expand Up @@ -352,11 +353,10 @@ def create_graph(self) -> Graph:
to_string = ''
html = [row.replace(f'<!-- {connection.via_port}_out -->', 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):
Expand Down
11 changes: 5 additions & 6 deletions src/wireviz/wv_html.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,7 @@ def generate_html_output(filename: Union[str, Path], bom_list: List[List[str]],
file.write(' <meta charset="UTF-8">\n')
file.write(f' <meta name="generator" content="{APP_NAME} {__version__} - {APP_URL}">\n')
file.write(f' <title>{metadata["title"]}</title>\n')
file.write(f'</head><body style="font-family:{options.fontname};background-color:'
f'{wv_colors.translate_color(options.bgcolor, "HEX")}">\n')

file.write(f'</head><body style="{options.base.html_style()}">\n')
file.write(f'<h1>{metadata["title"]}</h1>\n')
description = metadata.get('description')
if description:
Expand All @@ -33,17 +31,18 @@ def generate_html_output(filename: Union[str, Path], bom_list: List[List[str]],

file.write('<h2>Bill of Materials</h2>\n')
listy = flatten2d(bom_list)
file.write('<table style="border:1px solid #000000; font-size: 14pt; border-spacing: 0px">\n')
border = options.base.html_style(color_prefix="border: 1px solid", include_all=False)
file.write(f'<table style="{border} border-spacing: 0px;">\n')
file.write(' <tr>\n')
for item in listy[0]:
file.write(f' <th style="text-align:left; border:1px solid #000000; padding: 8px">{item}</th>\n')
file.write(f' <th style="{border} padding: 8px; text-align:left;">{item}</th>\n')
file.write(' </tr>\n')
for row in listy[1:]:
file.write(' <tr>\n')
for i, item in enumerate(row):
item_str = item.replace('\u00b2', '&sup2;')
align = '; text-align:right' if listy[0][i] == 'Qty' else ''
file.write(f' <td style="border:1px solid #000000; padding: 4px{align}">{item_str}</td>\n')
file.write(f' <td style="{border} padding: 4px{align};">{item_str}</td>\n')
file.write(' </tr>\n')
file.write('</table>\n')

Expand Down

0 comments on commit 251aab0

Please sign in to comment.