Skip to content

Commit a23b393

Browse files
Improved font styling for layout and alignment (#91)
* Improved font styling for layout and alignment * Update graphviz2drawio/mx/Node.py Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com> * Update graphviz2drawio/mx/MxGraph.py Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com> * refinements * formatting test --------- Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com>
1 parent 88d6437 commit a23b393

11 files changed

Lines changed: 62 additions & 67 deletions

File tree

README.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -98,14 +98,14 @@ pip install -r requirements.txt
9898
python -m graphviz2drawio test/directed/hello.gv.txt
9999
```
100100

101-
## Roadmap to 0.5
101+
## Roadmap
102102
- [ ] Migrate to uv/hatch for packaging and dep mgmt
103103
- [ ] Turn on master branch protection
104-
- [ ] Text alignment inside of shape
105-
- [ ] Port layout/orientation
106104
- [ ] Support for fill gradient
105+
- [ ] Support compatible [arrows](https://graphviz.org/docs/attr-types/arrowType/)
107106

108-
## License
107+
## Legal
109108

110109
© [Harold Martin](https://www.linkedin.com/in/harold-martin-98526971/) - released under [GPLv3](LICENSE.md)
111110

111+
diagrams.net is a trademark and draw.io is a registered trademark of JGraph Limited.

graphviz2drawio/models/SvgParser.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ def parse_nodes_edges_clusters(
3939
if title is None:
4040
raise MissingTitleError(g)
4141
if g.attrib["class"] == "node":
42-
nodes[title] = node_factory.from_svg(g)
42+
nodes[title] = node_factory.from_svg(g, labelloc="c")
4343
elif g.attrib["class"] == "edge":
4444
# We need to merge edges with the same source and target
4545
# GV represents multiple labels with multiple edges
@@ -52,6 +52,6 @@ def parse_nodes_edges_clusters(
5252
else:
5353
edges[edge.key_for_label] = edge
5454
elif g.attrib["class"] == "cluster":
55-
clusters[title] = node_factory.from_svg(g)
55+
clusters[title] = node_factory.from_svg(g, labelloc="t")
5656

5757
return nodes, list(edges.values()), clusters

graphviz2drawio/mx/Edge.py

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -115,10 +115,7 @@ def __repr__(self) -> str:
115115
)
116116

117117
def value_for_labels(self) -> str:
118-
text = ""
119-
for i, label in enumerate(self.labels):
120-
if i == 0:
121-
text += label.to_simple_value()
122-
else:
123-
text += f"<div>{label.to_simple_value()}</div>"
124-
return text
118+
return "".join(
119+
(label.get_mx_value() if i == 0 else f"<div>{label.get_mx_value()}</div>")
120+
for i, label in enumerate(self.labels)
121+
)

graphviz2drawio/mx/MxConst.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,9 @@
1818
NONE = "none"
1919
CURVED = "curved=1;"
2020
SHARP = "rounded=0;"
21+
22+
VERTICAL_ALIGN = {
23+
"t": "top",
24+
"b": "bottom",
25+
"c": "middle",
26+
}

graphviz2drawio/mx/MxGraph.py

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from graphviz2drawio.mx import MxConst
77
from graphviz2drawio.mx.Curve import Curve
88
from graphviz2drawio.mx.Edge import Edge
9+
from graphviz2drawio.mx.MxConst import VERTICAL_ALIGN
910
from graphviz2drawio.mx.Node import Node
1011
from graphviz2drawio.mx.Styles import Styles
1112

@@ -82,14 +83,16 @@ def add_node(self, node: Node) -> None:
8283

8384
attributes["image"] = image_data_for_path(image_path)
8485

86+
attributes["vertical_align"] = VERTICAL_ALIGN.get(node.labelloc, "middle")
87+
8588
style: str = style_for_shape.format(**attributes)
8689

8790
node_element = SubElement(
8891
self.root,
8992
MxConst.CELL,
9093
attrib={
9194
"id": node.sid,
92-
"value": node.text_to_mx_value(),
95+
"value": node.texts_to_mx_value(),
9396
"style": style,
9497
"parent": "1",
9598
"vertex": "1",
@@ -145,10 +148,6 @@ def add_mx_geo_with_points(element: Element, curve: Curve | None) -> None:
145148
},
146149
)
147150

148-
@staticmethod
149-
def x_y_strs(point: complex) -> tuple[str, str]:
150-
return str(int(point.real)), str(int(point.imag))
151-
152151
def value(self) -> str:
153152
indent(self.graph)
154153
return tostring(self.graph, encoding="unicode", xml_declaration=True)

graphviz2drawio/mx/Node.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ def __init__(
1313
fill: str | None,
1414
stroke: str | None,
1515
shape: str,
16+
labelloc: str,
1617
) -> None:
1718
super().__init__(sid, gid)
1819
self.rect = rect
@@ -21,15 +22,16 @@ def __init__(
2122
self.stroke = stroke
2223
self.label = None
2324
self.shape = shape
25+
self.labelloc = labelloc
2426

25-
def text_to_mx_value(self) -> str:
27+
def texts_to_mx_value(self) -> str:
2628
value = ""
2729
last_text = len(self.texts) - 1
2830
for i, t in enumerate(self.texts):
29-
style = t.get_mx_style()
30-
value += f"<p style='{style}'>{t.text}</p>"
31+
value += f"<div>{t.get_mx_value()}</div>" if i != 0 else t.get_mx_value()
3132
if i != last_text:
3233
value += "<hr size='1'/>"
34+
3335
return value
3436

3537
def __repr__(self) -> str:

graphviz2drawio/mx/NodeFactory.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ def __init__(self, coords: CoordsTranslate) -> None:
1515
super().__init__()
1616
self.coords = coords
1717

18-
def from_svg(self, g: Element) -> Node:
18+
def from_svg(self, g: Element, labelloc: str) -> Node:
1919
texts = self._extract_texts(g)
2020
rect = None
2121
fill = None
@@ -56,6 +56,7 @@ def from_svg(self, g: Element) -> Node:
5656
fill=fill,
5757
stroke=stroke,
5858
shape=shape,
59+
labelloc=labelloc,
5960
)
6061

6162
@staticmethod

graphviz2drawio/mx/Styles.py

Lines changed: 10 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,14 @@
55

66
# Make this subclass StrEnum when dropping Py 3.10 support
77
class Styles(Enum):
8-
NODE = "verticalAlign=top;align=left;overflow=fill;html=1;rounded=0;shadow=0;comic=0;labelBackgroundColor=none;strokeColor={stroke};strokeWidth=1;fillColor={fill};"
8+
NODE = "verticalAlign={vertical_align};html=1;rounded=0;labelBackgroundColor=none;strokeColor={stroke};strokeWidth=1;fillColor={fill};"
99
EDGE = "html=1;endArrow={end_arrow};dashed={dashed};endFill={end_fill};startArrow={start_arrow};startFill={start_fill};fillColor={stroke};strokeColor={stroke};"
1010
EDGE_LABEL = (
1111
"edgeLabel;html=1;align=center;verticalAlign=bottom;resizable=0;points=[];"
1212
)
1313
EDGE_INVIS = "rounded=1;html=1;exitX={exit_x:.3g};exitY={exit_y:.3g};jettySize=auto;curved={curved};endArrow={end_arrow};dashed={dashed};endFill={end_fill};"
1414

15-
TEXT = "margin:0px;text-align:{align};{margin};font-size:{size}px;font-family:{family};color:{color};"
15+
TEXT_VALUE = "{open_tags}<font style='font-size: {size}px;' face='{family}' color='{color}'>{text}</font>{close_tags}"
1616

1717
ELLIPSE = "ellipse;" + NODE
1818
CIRCLE = "ellipse;aspect=fixed;" + NODE
@@ -28,12 +28,10 @@ class Styles(Enum):
2828
PARALLELOGRAM = "shape=parallelogram;perimeter=parallelogramPerimeter;" + NODE
2929
HOUSE = "shape=offPageConnector;direction=west;" + NODE
3030
PENTAGON = "shape=mxgraph.basic.pentagon;" + NODE
31-
OCTAGON = (
32-
"shape=mxgraph.basic.octagon2;align=center;verticalAlign=middle;dx=15;" + NODE
33-
)
31+
OCTAGON = "shape=mxgraph.basic.octagon2;align=center;dx=15;" + NODE
3432
DOUBLE_CIRCLE = "ellipse;shape=doubleEllipse;aspect=fixed;" + NODE
3533
DOUBLE_OCTAGON = (
36-
"shape=image;html=1;verticalAlign=middle;verticalLabelPosition=middle;imageAspect=0;aspect=fixed;image=https://cdn4.iconfinder.com/data/icons/feather/24/octagon-128.png;labelPosition=center;align=center;"
34+
"shape=image;html=1;verticalLabelPosition=middle;imageAspect=0;aspect=fixed;image=https://cdn4.iconfinder.com/data/icons/feather/24/octagon-128.png;labelPosition=center;align=center;"
3735
+ NODE
3836
)
3937
INV_TRIANGLE = "triangle;direction=south;" + NODE
@@ -43,32 +41,30 @@ class Styles(Enum):
4341
INV_HOUSE = "shape=offPageConnector;direction=east;" + NODE
4442
SQUARE = "aspect=fixed;" + NODE
4543
STAR = (
46-
"shape=mxgraph.basic.star;labelPosition=center;align=center;verticalLabelPosition=middle;verticalAlign=middle;"
44+
"shape=mxgraph.basic.star;labelPosition=center;align=center;verticalLabelPosition=middle;"
4745
+ NODE
4846
)
4947
UNDERLINE = "line;strokeWidth=2;verticalAlign=bottom;labelPosition=center;verticalLabelPosition=top;align=center;"
5048
CYLINDER = "shape=cylinder;boundedLbl=1;backgroundOutline=1;" + NODE
5149
NOTE = "shape=note;backgroundOutline=1;" + NODE
5250
TAB = "shape=folder;tabWidth=40;tabHeight=14;tabPosition=left;" + NODE
5351
FOLDER = (
54-
"shape=mxgraph.office.concepts.folder;outlineConnect=0;align=center;verticalLabelPosition=middle;verticalAlign=middle;labelPosition=center;shadow=0;dashed=0;"
52+
"shape=mxgraph.office.concepts.folder;outlineConnect=0;align=center;verticalLabelPosition=middle;labelPosition=center;shadow=0;dashed=0;"
5553
+ NODE
5654
)
5755
CUBE = "shape=cube;boundedLbl=1;backgroundOutline=1;" + NODE
58-
COMPONENT = (
59-
"shape=component;align=center;spacingLeft=36;verticalAlign=bottom;" + NODE
60-
)
56+
COMPONENT = "shape=component;align=center;spacingLeft=36;" + NODE
6157
RPROMOTER = (
62-
"shape=mxgraph.arrows2.bendArrow;dy=15;dx=38;notch=0;arrowHead=55;rounded=0;shadow=0;dashed=0;align=center;verticalAlign=middle;"
58+
"shape=mxgraph.arrows2.bendArrow;dy=15;dx=38;notch=0;arrowHead=55;rounded=0;shadow=0;dashed=0;align=center;"
6359
+ NODE
6460
)
6561
LPROMOTER = "flipH=1;" + RPROMOTER
6662
CDS = (
67-
"shape=mxgraph.arrows2.arrow;dy=0;dx=10;notch=0;shadow=0;dashed=0;align=center;verticalAlign=middle;"
63+
"shape=mxgraph.arrows2.arrow;dy=0;dx=10;notch=0;shadow=0;dashed=0;align=center;"
6864
+ NODE
6965
)
7066
RARROW = (
71-
"shape=mxgraph.arrows2.arrow;dy=0.6;dx=40;align=center;labelPosition=center;notch=0;strokeWidth=2;verticalLabelPosition=middle;verticalAlign=middle;"
67+
"shape=mxgraph.arrows2.arrow;dy=0.6;dx=40;align=center;labelPosition=center;notch=0;strokeWidth=2;verticalLabelPosition=middle;"
7268
+ NODE
7369
)
7470
LARROW = "flipH=1;" + RARROW

graphviz2drawio/mx/Text.py

Lines changed: 15 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
from xml.etree.ElementTree import Element
22

3-
from graphviz2drawio.models import DotAttr
43
from graphviz2drawio.mx import MxConst
54

65
from ..models.Errors import MissingTextError
@@ -11,55 +10,48 @@ class Text:
1110
def __init__(
1211
self,
1312
text: str,
14-
anchor: str | None,
1513
family: str,
1614
size: float,
1715
color: str | None,
1816
*,
1917
bold: bool,
2018
italic: bool,
2119
) -> None:
22-
self.anchor = anchor
2320
self.family = family
2421
self.size = size
2522
self.text = text
2623
self.color = color
2724
self.bold = bold
2825
self.italic = italic
2926

30-
def get_mx_style(self) -> str:
31-
align = MxConst.CENTER if self.anchor == DotAttr.MIDDLE else MxConst.START
32-
margin = (
33-
"margin-top:4px" if self.anchor == DotAttr.MIDDLE else "margin-left:4px"
34-
)
35-
rescaled_size = 10.0 * (self.size / 14.0)
36-
return Styles.TEXT.format(
37-
align=align,
38-
margin=margin,
39-
size=rescaled_size,
27+
def get_mx_value(self) -> str:
28+
open_tags = ""
29+
close_tags = ""
30+
if self.bold:
31+
open_tags = "<b>"
32+
close_tags = "</b>"
33+
if self.italic:
34+
open_tags = "<i>" + open_tags
35+
close_tags += "</i>"
36+
return Styles.TEXT_VALUE.format(
37+
text=self.text,
38+
size=self.size or "14",
4039
family=self.family or MxConst.DEFAULT_FONT_FAMILY,
4140
color=self.color or MxConst.DEFAULT_FONT_COLOR,
41+
open_tags=open_tags,
42+
close_tags=close_tags,
4243
)
4344

44-
def to_simple_value(self) -> str:
45-
text = self.text
46-
if self.bold:
47-
text = f"<b>{text}</b>"
48-
if self.italic:
49-
text = f"<i>{text}</i>"
50-
return text
51-
5245
@staticmethod
5346
def from_svg(t: Element) -> "Text":
5447
text = t.text
5548
if text is None:
5649
raise MissingTextError(t)
5750
return Text(
5851
text=text.replace("<", "&lt;").replace(">", "&gt;"),
59-
anchor=t.attrib.get("text-anchor", None),
6052
family=t.attrib.get("font-family", MxConst.DEFAULT_FONT_FAMILY),
6153
size=float(t.attrib.get("font-size", MxConst.DEFAULT_TEXT_SIZE)),
62-
color=t.attrib.get("fill", None),
54+
color=t.attrib.get("fill", MxConst.DEFAULT_FONT_COLOR),
6355
bold=t.get("font-weight", None) == "bold",
6456
italic=t.get("font-style", None) == "italic",
6557
)

test/directed/psg.gv.txt

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,3 @@
1-
##"I made a program to generate dot files representing the LR(0) state graph along with computed LALR(1) lookahead for an arbitrary context-free grammar, to make the diagrams I used in this article: http://blog.lab49.com/archives/2471. The program also highlights errant nodes in red if the grammar would produce a shift/reduce or reduce/reduce conflict -- you may be able to go to http://kthielen.dnsalias.com:8082/ to produce a graph more to your liking". Contributed by Kalani Thielen.
2-
3-
##Command to get the layout: "dot -Gsize=10,15 -Tpng thisfile > thisfile.png"
4-
51
digraph g {
62
graph [fontsize=30 labelloc="t" label="" splines=true overlap=false rankdir = "LR"];
73
ratio = auto;

0 commit comments

Comments
 (0)