Skip to content

Commit 608a2ea

Browse files
authored
Merge pull request #17 from hbmartin/edge-layout
Calculate SVG curves to layout edges
2 parents d1a58e7 + c676a5f commit 608a2ea

18 files changed

Lines changed: 256 additions & 76 deletions

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,9 @@ Temporary Items
135135

136136
# Generated
137137
test/**/*.xml
138+
/*.gv
139+
/*.dot
140+
/*.xml
138141

139142
# PyCharm
140143
.idea/workspace.xml

.idea/dictionaries/haroldmartin.xml

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

README.md

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,16 +11,21 @@ Convert graphviz (dot) files into draw.io (mxGraph) format
1111

1212
graphviz2drawio requires [Python 3](https://www.python.org/downloads/) and [Graphviz](https://www.graphviz.org/download/)
1313

14-
On Mac OS these can be installed with [Homebrew](https://brew.sh/):
14+
* On Mac OS these can be installed with [Homebrew](https://brew.sh/):
1515

1616
```
1717
brew update; brew install python3 graphviz
1818
```
19+
* On Ubuntu / Debian based Linux, install graphviz using:
1920

20-
### Installation
21+
```
22+
sudo apt install graphviz graphviz-dev
23+
```
24+
25+
### Installation / Upgrade
2126

2227
```
23-
pip3 install graphviz2drawio
28+
pip3 install graphviz2drawio --upgrade
2429
```
2530
## Usage
2631
Run the conversion app on your graphviz file
@@ -37,10 +42,10 @@ from graphviz2drawio import graphviz2drawio
3742
xml = graphviz2drawio.convert(graph_to_convert)
3843
print(xml)
3944
```
40-
graph_to_convert can be any of a file path, file handle, string of dot language, or PyGraphviz.AGraph object
45+
where `graph_to_convert` can be any of a file path, file handle, string of dot language, or PyGraphviz.AGraph object
4146

4247
## Limitations
43-
Current alpha release may not correctly convert all dot commands. PLEASE [open an issue](https://github.com/hbmartin/graphviz2drawio/issues) with your dot file to report conversion problems or visual errors.
48+
Please [open an issue](https://github.com/hbmartin/graphviz2drawio/issues) with your dot file to report crashes or incorrectect conversions.
4449

4550
## Built With
4651

@@ -59,6 +64,7 @@ This project makes strict use of Black for code formatting. See the [Black page]
5964
## Authors
6065

6166
* [Harold Martin](https://www.linkedin.com/in/harold-martin-98526971/) - harold.martin at gmail
67+
* Jonah Caplan
6268

6369
## License
6470

graphviz2drawio/models/CoordsTranslate.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@ def __init__(self, x, y):
33
self.x = x
44
self.y = y
55

6+
def complex_translate(self, cnum):
7+
return complex(cnum.real + self.x, cnum.imag + self.y)
8+
69
def translate(self, x, y):
710
return float(x) + self.x, float(y) + self.y
811

graphviz2drawio/models/Rect.py

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,34 @@ def __init__(self, x, y, width, height):
77
self.bottom = y + height
88
self.right = x + width
99

10-
def to_dict_int(self):
10+
def x_ratio(self, search):
11+
if search < self.x:
12+
return 0
13+
elif search > self.x + self.width:
14+
return 1
15+
else:
16+
ratio = (search - self.x) / self.width
17+
return self._approx(ratio, 0.5, 0.1)
18+
19+
def y_ratio(self, search):
20+
if search < self.y:
21+
return 0
22+
elif search > self.y + self.height:
23+
return 1
24+
else:
25+
ratio = (search - self.y) / self.height
26+
return self._approx(ratio, 0.5, 0.1)
27+
28+
@staticmethod
29+
def _approx(value, center, delta):
30+
if abs(value - center) < delta:
31+
return center
32+
return value
33+
34+
def to_dict_str(self):
1135
return {
12-
"x": str(int(self.x)),
13-
"y": str(int(self.y)),
14-
"width": str(int(self.width)),
15-
"height": str(int(self.height)),
36+
"x": str(self.x),
37+
"y": str(self.y),
38+
"width": str(self.width),
39+
"height": str(self.height),
1640
}
Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,22 @@
11
from xml.etree import ElementTree
22
from . import SVG
33
from .CoordsTranslate import CoordsTranslate
4-
from graphviz2drawio.mx.Edge import Edge
4+
from graphviz2drawio.mx.EdgeFactory import EdgeFactory
55
from graphviz2drawio.mx.NodeFactory import NodeFactory
66
from collections import OrderedDict
77

88

99
class SvgParser:
1010
def __init__(self, svg_data):
11-
super(SvgParser, self).__init__()
1211
self.svg_data = svg_data
1312

1413
def get_nodes_and_edges(self):
1514
root = ElementTree.fromstring(self.svg_data)[0]
1615

1716
coords = CoordsTranslate.from_svg_transform(root.attrib["transform"])
1817
node_factory = NodeFactory(coords)
18+
edge_factory = EdgeFactory(coords)
19+
1920
nodes = OrderedDict()
2021
edges = []
2122

@@ -25,6 +26,6 @@ def get_nodes_and_edges(self):
2526
if g.attrib["class"] == "node":
2627
nodes[title] = node_factory.from_svg(g)
2728
elif g.attrib["class"] == "edge":
28-
edges.append(Edge.from_svg(g))
29+
edges.append(edge_factory.from_svg(g))
2930

3031
return nodes, edges

graphviz2drawio/mx/Curve.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
from . import LinearRegression
2+
3+
linear_min_r2 = 0.9
4+
5+
6+
class Curve:
7+
def __init__(self, start, end, cb):
8+
"""Takes complex numbers for start, end, and list of 4 Bezier control points"""
9+
self.start = start
10+
self.end = end
11+
assert cb is None or len(cb) == 4
12+
self.cb = cb
13+
14+
def __str__(self):
15+
control = "[" + (str(self.cb) if self.cb is not None else "None") + "]"
16+
return str(self.start) + ", " + control + ", " + str(self.end)
17+
18+
@staticmethod
19+
def is_linear(points, threshold=linear_min_r2):
20+
"""
21+
Returns a boolean indicating whether a list of complex points is linear.
22+
23+
Takes a list of complex points and optional minimum R**2 threshold for linear regression.
24+
"""
25+
r2 = LinearRegression.coefficients(points)[2]
26+
return r2 > threshold
27+
28+
def cubic_bezier_coordinates(self, t):
29+
"""
30+
Returns a complex number representing the point along the cubic bezier curve.
31+
32+
Takes parametric parameter t where 0 <= t <= 1
33+
"""
34+
x = Curve._cubic_bezier(self._cb("real"), t)
35+
y = Curve._cubic_bezier(self._cb("imag"), t)
36+
return complex(x, y)
37+
38+
def _cb(self, prop):
39+
return [getattr(x, prop) for x in self.cb]
40+
41+
@staticmethod
42+
def _cubic_bezier(p, t):
43+
"""
44+
Returns a float representing the point along the cubic bezier curve in the given dimension.
45+
46+
Takes ordered list of 4 control points [P0, P1, P2, P3] and parametric parameter t where 0 <= t <= 1
47+
48+
implements explicit form of https://en.wikipedia.org/wiki/B%C3%A9zier_curve#Cubic_B%C3%A9zier_curves
49+
"""
50+
assert 0 <= t <= 1
51+
return (
52+
(((1.0 - t) ** 3) * p[0])
53+
+ (3.0 * t * ((1.0 - t) ** 2) * p[1])
54+
+ (3.0 * (t ** 2) * (1.0 - t) * p[2])
55+
+ ((t ** 3) * p[3])
56+
)

graphviz2drawio/mx/CurveFactory.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
from svg.path import CubicBezier
2+
from svg.path import parse_path
3+
from .Curve import Curve
4+
5+
6+
class CurveFactory:
7+
def __init__(self, coords):
8+
super(CurveFactory, self).__init__()
9+
self.coords = coords
10+
11+
def from_svg(self, svg_path):
12+
path = parse_path(svg_path)
13+
start = self.coords.complex_translate(path[0].start)
14+
end = self.coords.complex_translate(path[1].end)
15+
cb = None
16+
if isinstance(path[1], CubicBezier):
17+
# TODO: needs to account for multiple bezier in path
18+
points = [path[1].start, path[1].control1, path[1].control2, path[1].end]
19+
if not Curve.is_linear(points):
20+
cb = [self.coords.complex_translate(p) for p in points]
21+
return Curve(start=start, end=end, cb=cb)

graphviz2drawio/mx/Edge.py

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,12 @@
1-
from graphviz2drawio.models import SVG
21
from .GraphObj import GraphObj
32

43

54
class Edge(GraphObj):
6-
def __init__(self, sid, gid, fr, to):
5+
def __init__(self, sid, gid, fr, to, curve):
76
super(Edge, self).__init__(sid, gid)
87
self.fr = fr
98
self.to = to
9+
self.curve = curve
1010
self.style = None
1111
self.dir = None
1212
self.arrowtail = None
13-
14-
@staticmethod
15-
def from_svg(g):
16-
gid = SVG.get_title(g).replace("--", "->")
17-
fr, to = gid.split("->")
18-
return Edge(sid=g.attrib["id"], gid=gid, fr=fr, to=to)

graphviz2drawio/mx/EdgeFactory.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
from graphviz2drawio.models import SVG
2+
from .CurveFactory import CurveFactory
3+
from .Edge import Edge
4+
5+
6+
class EdgeFactory:
7+
def __init__(self, coords):
8+
super(EdgeFactory, self).__init__()
9+
self.curve_factory = CurveFactory(coords)
10+
11+
def from_svg(self, g):
12+
gid = SVG.get_title(g).replace("--", "->")
13+
fr, to = gid.split("->")
14+
curve = None
15+
if SVG.has(g, "path"):
16+
path = SVG.get_first(g, "path")
17+
if "d" in path.attrib:
18+
curve = self.curve_factory.from_svg(path.attrib["d"])
19+
return Edge(sid=g.attrib["id"], gid=gid, fr=fr, to=to, curve=curve)

0 commit comments

Comments
 (0)