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' | {item} | \n')
+ file.write(f' {item} | \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' | {item_str} | \n')
+ file.write(f' {item_str} | \n')
file.write('
\n')
file.write('
\n')