├── LICENSE ├── README.md ├── examples ├── binary-tree │ ├── Makefile │ └── main.go ├── clean.sh ├── linked-list │ ├── Makefile │ └── main.go ├── linked-list2 │ ├── Makefile │ └── main.go ├── run.sh └── web-links │ ├── README.md │ ├── example.png │ └── main.go └── graphviz ├── attributes.go ├── graphviz.go └── graphviz_test.go /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Mohsen Zohrevandi 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GoGraphviz 2 | 3 | Package graphviz provides an easy-to-use API for visualizing graphs using Graphviz (http://graphviz.org). 4 | It can generate graph descriptions in DOT language which can be made into a picture using various Graphviz 5 | tools such as dot, neato, ... 6 | 7 | See [documentation](https://godoc.org/github.com/mzohreva/GoGraphviz/graphviz) on godoc.org. 8 | 9 | See [examples](https://github.com/mzohreva/GoGraphviz/tree/master/examples) folder for usage examples. 10 | -------------------------------------------------------------------------------- /examples/binary-tree/Makefile: -------------------------------------------------------------------------------- 1 | BIN = binary-tree 2 | 3 | $(BIN): main.go ../../graphviz/*.go 4 | go build 5 | 6 | run: $(BIN) 7 | ./$(BIN) | dot -Tpng -o $(BIN).png 8 | 9 | clean: 10 | go clean 11 | rm -f $(BIN).png 12 | -------------------------------------------------------------------------------- /examples/binary-tree/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/mzohreva/GoGraphviz/graphviz" 8 | ) 9 | 10 | func main() { 11 | arr := []int{1, 3, 4, 5, 9, 11, 15, 23} 12 | tree := newBinaryTree(arr, 0, len(arr)-1) 13 | G := visuzlizeBinaryTree(tree) 14 | G.GenerateDOT(os.Stdout) 15 | } 16 | 17 | func visuzlizeBinaryTree(root *binaryTree) *graphviz.Graph { 18 | G := &graphviz.Graph{} 19 | addSubTree(root, G) 20 | G.DefaultNodeAttribute(graphviz.Shape, graphviz.ShapeCircle) 21 | G.GraphAttribute(graphviz.NodeSep, "0.3") 22 | G.MakeDirected() 23 | return G 24 | } 25 | 26 | func addSubTree(root *binaryTree, G *graphviz.Graph) int { 27 | if root == nil { 28 | null := G.AddNode("") 29 | G.NodeAttribute(null, graphviz.Shape, graphviz.ShapePoint) 30 | return null 31 | } 32 | rootNode := G.AddNode(fmt.Sprint(root.value)) 33 | leftNode := addSubTree(root.left, G) 34 | rightNode := addSubTree(root.right, G) 35 | G.AddEdge(rootNode, leftNode, "") 36 | G.AddEdge(rootNode, rightNode, "") 37 | return rootNode 38 | } 39 | 40 | type binaryTree struct { 41 | value int 42 | left, right *binaryTree 43 | } 44 | 45 | func newBinaryTree(arr []int, start, end int) *binaryTree { 46 | if start == end { 47 | return &binaryTree{arr[start], nil, nil} 48 | } 49 | if start > end { 50 | return nil 51 | } 52 | mid := (start + end) / 2 53 | root := &binaryTree{arr[mid], nil, nil} 54 | root.left = newBinaryTree(arr, start, mid-1) 55 | root.right = newBinaryTree(arr, mid+1, end) 56 | return root 57 | } 58 | -------------------------------------------------------------------------------- /examples/clean.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | cwd=$(pwd) 4 | 5 | cd $cwd/linked-list && make clean 6 | cd $cwd/binary-tree && make clean 7 | cd $cwd/linked-list2 && make clean 8 | -------------------------------------------------------------------------------- /examples/linked-list/Makefile: -------------------------------------------------------------------------------- 1 | BIN = linked-list 2 | 3 | $(BIN): main.go ../../graphviz/*.go 4 | go build 5 | 6 | run: $(BIN) 7 | ./$(BIN) | dot -Tpng -o $(BIN).png 8 | 9 | clean: 10 | go clean 11 | rm -f $(BIN).png 12 | -------------------------------------------------------------------------------- /examples/linked-list/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/mzohreva/GoGraphviz/graphviz" 7 | ) 8 | 9 | func main() { 10 | G := graphviz.Graph{} 11 | G.MakeDirected() 12 | n1 := G.AddNode("Hello") 13 | n2 := G.AddNode("World") 14 | n3 := G.AddNode("Hi") 15 | n4 := G.AddNode("NULL") 16 | G.AddEdge(n1, n2, "next") 17 | G.AddEdge(n2, n3, "next") 18 | G.AddEdge(n3, n4, "next") 19 | G.MakeSameRank(n1, n2, n3, n4) 20 | 21 | G.GraphAttribute(graphviz.NodeSep, "0.5") 22 | 23 | G.DefaultNodeAttribute(graphviz.Shape, graphviz.ShapeBox) 24 | G.DefaultNodeAttribute(graphviz.FontName, "Courier") 25 | G.DefaultNodeAttribute(graphviz.FontSize, "14") 26 | G.DefaultNodeAttribute(graphviz.Style, graphviz.StyleFilled+","+graphviz.StyleRounded) 27 | G.DefaultNodeAttribute(graphviz.FillColor, "yellow") 28 | 29 | G.NodeAttribute(n4, graphviz.Shape, graphviz.ShapeCircle) 30 | G.NodeAttribute(n4, graphviz.Style, graphviz.StyleDashed) 31 | 32 | G.DefaultEdgeAttribute(graphviz.FontName, "Courier") 33 | G.DefaultEdgeAttribute(graphviz.FontSize, "12") 34 | 35 | G.GenerateDOT(os.Stdout) 36 | } 37 | -------------------------------------------------------------------------------- /examples/linked-list2/Makefile: -------------------------------------------------------------------------------- 1 | BIN = linked-list2 2 | 3 | $(BIN): main.go ../../graphviz/*.go 4 | go build 5 | 6 | run: $(BIN) 7 | ./$(BIN) 8 | 9 | clean: 10 | go clean 11 | rm -f list-*.png 12 | -------------------------------------------------------------------------------- /examples/linked-list2/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | 6 | "github.com/mzohreva/GoGraphviz/graphviz" 7 | ) 8 | 9 | func main() { 10 | list := linkedList{} 11 | visualizeLinkedList(list, "list-1.png", "Empty linked list") 12 | 13 | list.append("hello") 14 | list.append("world") 15 | visualizeLinkedList(list, "list-2.png", "After appending 'hello' and 'world'") 16 | 17 | list.prepend("first!") 18 | visualizeLinkedList(list, "list-3.png", "After prepending 'first!'") 19 | } 20 | 21 | func visualizeLinkedList(list linkedList, filename, title string) { 22 | G := &graphviz.Graph{} 23 | nodes := make([]int, 1) 24 | nodes[0] = G.AddNode("head") 25 | head := list.head 26 | for head != nil { 27 | nodes = append(nodes, G.AddNode(head.value)) 28 | head = head.next 29 | } 30 | nilNode := G.AddNode("nil") 31 | nodes = append(nodes, nilNode) 32 | 33 | G.AddEdge(nodes[0], nodes[1], "") 34 | for i := 1; i < len(nodes)-1; i++ { 35 | G.AddEdge(nodes[i], nodes[i+1], "next") 36 | } 37 | G.NodeAttribute(nodes[0], graphviz.Shape, graphviz.ShapeNone) 38 | G.NodeAttribute(nilNode, graphviz.Shape, graphviz.ShapeNone) 39 | G.MakeSameRank(nodes[0], nodes[1], nodes[2:]...) 40 | 41 | G.DefaultNodeAttribute(graphviz.Shape, graphviz.ShapeBox) 42 | G.DefaultNodeAttribute(graphviz.FontName, "Courier") 43 | G.DefaultEdgeAttribute(graphviz.FontName, "Courier") 44 | G.GraphAttribute(graphviz.NodeSep, "0.5") 45 | G.SetTitle("\n\n" + title) 46 | G.MakeDirected() 47 | 48 | err := G.GenerateImage("dot", filename, "png") 49 | if err != nil { 50 | log.Fatal(err) 51 | } 52 | } 53 | 54 | type linkedList struct { 55 | head, tail *listNode 56 | } 57 | 58 | func (list *linkedList) append(value string) { 59 | node := &listNode{value, nil} 60 | if list.head == nil { 61 | list.head, list.tail = node, node 62 | return 63 | } 64 | list.tail.next = node 65 | list.tail = node 66 | } 67 | 68 | func (list *linkedList) prepend(value string) { 69 | node := &listNode{value, nil} 70 | if list.head == nil { 71 | list.head, list.tail = node, node 72 | return 73 | } 74 | node.next = list.head 75 | list.head = node 76 | } 77 | 78 | type listNode struct { 79 | value string 80 | next *listNode 81 | } 82 | -------------------------------------------------------------------------------- /examples/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | cwd=$(pwd) 4 | 5 | cd $cwd/linked-list && make && make run 6 | cd $cwd/binary-tree && make && make run 7 | cd $cwd/linked-list2 && make && make run 8 | 9 | open $cwd/*/*.png 10 | -------------------------------------------------------------------------------- /examples/web-links/README.md: -------------------------------------------------------------------------------- 1 | # Example: web-links 2 | 3 | This example is a simple web crawler that finds all anchor tags in a web page and 4 | follows the links to get to other pages. After a predefined number of pages are 5 | fetched, it creates a graph of the pages using graphviz. 6 | 7 | The crawler only fetches URLs that are of the form *github.com/user/repo*. Here is 8 | a graph generated by this program starting from 9 | [https://github.com/topics/go](https://github.com/topics/go) limited to 200 URLs: 10 | 11 | ![Example graph image](https://raw.githubusercontent.com/mzohreva/GoGraphviz/master/examples/web-links/example.png) 12 | -------------------------------------------------------------------------------- /examples/web-links/example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mzohreva/GoGraphviz/533f4a37d9c654551032e8367780ea97c27abaa3/examples/web-links/example.png -------------------------------------------------------------------------------- /examples/web-links/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "net/http" 7 | "net/url" 8 | "os" 9 | "regexp" 10 | "sort" 11 | "strings" 12 | 13 | "github.com/mzohreva/GoGraphviz/graphviz" 14 | "golang.org/x/net/html" 15 | ) 16 | 17 | func main() { 18 | startPage := "https://github.com/topics/go" 19 | fetchLimit := 200 20 | pattern := regexp.MustCompile("^https://github\\.com/[a-zA-Z0-9]+/[a-zA-Z0-9]+$") 21 | shouldFollow := func(link string) bool { 22 | return !strings.HasPrefix(link, "https://github.com/topics/") && 23 | !strings.HasPrefix(link, "https://github.com/trending/") && 24 | link != "https://github.com/trending" && 25 | link != "https://github.com/site/privacy" && 26 | pattern.MatchString(link) 27 | } 28 | w := crawl(startPage, fetchLimit, shouldFollow) 29 | title := fmt.Sprintf("start page: %#v, fetch limit: %v", startPage, fetchLimit) 30 | visualizeWebGraph(w, "repo-web", title) 31 | } 32 | 33 | func visualizeWebGraph(w *webGraph, filename, title string) error { 34 | G := graphviz.Graph{} 35 | G.MakeDirected() 36 | G.DefaultNodeAttribute(graphviz.Shape, graphviz.ShapePoint) 37 | G.DefaultEdgeAttribute("arrowhead", "vee") 38 | G.DefaultEdgeAttribute("arrowsize", "0.2") 39 | 40 | nodeID := make(map[int]int) 41 | for from, toList := range w.links { 42 | if _, ok := nodeID[from]; !ok { 43 | nodeID[from] = G.AddNode(w.page[from]) 44 | } 45 | for _, to := range toList { 46 | if !w.hasVisited(w.page[to]) { 47 | continue 48 | } 49 | if _, ok := nodeID[to]; !ok { 50 | nodeID[to] = G.AddNode(w.page[to]) 51 | } 52 | if from != to { 53 | G.AddEdge(nodeID[from], nodeID[to], "") 54 | } 55 | } 56 | } 57 | G.SetTitle("\n\n" + title) 58 | f, err := os.Create(filename + ".dot") 59 | if err != nil { 60 | return err 61 | } 62 | G.GenerateDOT(f) 63 | f.Close() 64 | log.Println("Saved graph description in", f.Name()) 65 | err = G.GenerateImage("neato", filename+".png", "png") 66 | log.Println("Saved graph image in", filename+".png") 67 | return err 68 | } 69 | 70 | func crawl(startPage string, fetchLimit int, shouldFollow func(string) bool) *webGraph { 71 | w := newWebGraph() 72 | Q := queue{} 73 | Q.push(startPage) 74 | for len(w.visited) < fetchLimit && !Q.empty() { 75 | page := Q.pop() 76 | if w.hasVisited(page) { 77 | continue 78 | } 79 | w.markVisited(page) 80 | linkCount, err := fetch(page) 81 | if err != nil { 82 | log.Println(err) 83 | continue 84 | } 85 | list := make([]string, 0, len(linkCount)) 86 | for k := range linkCount { 87 | if shouldFollow(k) { 88 | list = append(list, k) 89 | } 90 | } 91 | log.Printf("[%4d] Found %4d URLs on %v\n", len(w.visited), len(list), truncateString(page, 60)) 92 | sort.Slice(list, func(i, j int) bool { 93 | if linkCount[list[i]] != linkCount[list[j]] { 94 | return linkCount[list[i]] > linkCount[list[j]] 95 | } 96 | return list[i] < list[j] 97 | }) 98 | for i := range list { 99 | w.createLink(page, list[i]) 100 | if !w.hasVisited(list[i]) { 101 | Q.push(list[i]) 102 | } 103 | } 104 | } 105 | return w 106 | } 107 | 108 | func fetch(page string) (map[string]int, error) { 109 | baseURL, err := url.Parse(page) 110 | if err != nil { 111 | return nil, err 112 | } 113 | linkedPages := make(map[string]int) 114 | var dfs func(*html.Node) 115 | dfs = func(n *html.Node) { 116 | if n.Type == html.ElementNode && n.Data == "a" { 117 | for i := range n.Attr { 118 | if n.Attr[i].Key == "href" { 119 | u := n.Attr[i].Val 120 | parsed, err := baseURL.Parse(u) 121 | if err != nil { 122 | continue 123 | } 124 | parsed.Fragment = "" 125 | if parsed.Scheme == "http" || parsed.Scheme == "https" { 126 | linkedPages[parsed.String()]++ 127 | } 128 | } 129 | } 130 | } 131 | for c := n.FirstChild; c != nil; c = c.NextSibling { 132 | dfs(c) 133 | } 134 | } 135 | resp, err := http.Get(page) 136 | if err != nil { 137 | return nil, err 138 | } 139 | defer resp.Body.Close() 140 | doc, err := html.Parse(resp.Body) 141 | if err != nil { 142 | return nil, err 143 | } 144 | dfs(doc) 145 | return linkedPages, nil 146 | } 147 | 148 | type webGraph struct { 149 | number map[string]int // map a page URL to a unique id 150 | page map[int]string // map ids to page URLs 151 | visited map[int]struct{} // set of visited page ids 152 | links map[int][]int // links between pages 153 | } 154 | 155 | func newWebGraph() *webGraph { 156 | number := make(map[string]int) 157 | page := make(map[int]string) 158 | visited := make(map[int]struct{}) 159 | links := make(map[int][]int) 160 | return &webGraph{number, page, visited, links} 161 | } 162 | 163 | func (w *webGraph) id(page string) int { 164 | num, inTheMap := w.number[page] 165 | if !inTheMap { 166 | num = len(w.number) 167 | w.number[page] = num 168 | w.page[num] = page 169 | } 170 | return num 171 | } 172 | 173 | func (w *webGraph) hasVisited(page string) bool { 174 | num, inTheMap := w.number[page] 175 | if !inTheMap { 176 | return false 177 | } 178 | _, inTheMap = w.visited[num] 179 | return inTheMap 180 | } 181 | 182 | func (w *webGraph) markVisited(page string) { 183 | w.visited[w.id(page)] = struct{}{} 184 | } 185 | 186 | func (w *webGraph) createLink(fromPage, toPage string) { 187 | fromNum, toNum := w.id(fromPage), w.id(toPage) 188 | w.links[fromNum] = append(w.links[fromNum], toNum) 189 | } 190 | 191 | type queueNode struct { 192 | value string 193 | next *queueNode 194 | } 195 | 196 | type queue struct { 197 | head, tail *queueNode 198 | size int 199 | } 200 | 201 | func (Q *queue) push(value string) { 202 | node := &queueNode{value, nil} 203 | Q.size++ 204 | if Q.head == nil { 205 | Q.head, Q.tail = node, node 206 | return 207 | } 208 | Q.tail.next = node 209 | Q.tail = node 210 | } 211 | 212 | func (Q *queue) pop() string { 213 | value := Q.head.value 214 | Q.head = Q.head.next 215 | if Q.head == nil { 216 | Q.tail = nil 217 | } 218 | Q.size-- 219 | return value 220 | } 221 | 222 | func (Q *queue) empty() bool { 223 | return Q.head == nil 224 | } 225 | 226 | func truncateString(str string, maxLength int) string { 227 | if len(str) <= maxLength { 228 | return str 229 | } 230 | return str[0:maxLength-3] + "..." 231 | } 232 | -------------------------------------------------------------------------------- /graphviz/attributes.go: -------------------------------------------------------------------------------- 1 | package graphviz 2 | 3 | // Common attribute names 4 | const ( 5 | Shape = "shape" 6 | Style = "style" 7 | Color = "color" 8 | FillColor = "fillcolor" 9 | FontName = "fontname" 10 | FontSize = "fontsize" 11 | FontColor = "fontcolor" 12 | NodeSep = "nodesep" 13 | ) 14 | 15 | // Common values for shape attribute 16 | const ( 17 | ShapeBox = "box" 18 | ShapePolygon = "polygon" 19 | ShapeEllipse = "ellipse" 20 | ShapeOval = "oval" 21 | ShapeCircle = "circle" 22 | ShapePoint = "point" 23 | ShapeEgg = "egg" 24 | ShapeTriangle = "triangle" 25 | ShapeNone = "none" 26 | ShapeDiamond = "diamond" 27 | ShapeRectangle = "rectangle" 28 | ShapeSquare = "square" 29 | ) 30 | 31 | // Common values for style attribute 32 | const ( 33 | StyleSolid = "solid" 34 | StyleDashed = "dashed" 35 | StyleDotted = "dotted" 36 | StyleBold = "bold" 37 | StyleRounded = "rounded" 38 | StyleFilled = "filled" 39 | StyleStriped = "striped" 40 | ) 41 | -------------------------------------------------------------------------------- /graphviz/graphviz.go: -------------------------------------------------------------------------------- 1 | // Package graphviz provides an easy-to-use API for visualizing graphs 2 | // using Graphviz (http://graphviz.org). 3 | // It can generate graph descriptions in DOT language which can be made 4 | // into a picture using various Graphviz tools such as dot, neato, ... 5 | package graphviz 6 | 7 | import ( 8 | "fmt" 9 | "io" 10 | "os/exec" 11 | ) 12 | 13 | // Graph represents a set of nodes, edges and attributes that can be 14 | // translated to DOT language. 15 | type Graph struct { 16 | nodes map[int]*node 17 | edges map[int]*edge 18 | n, e int 19 | graphAttributes attributes 20 | nodeAttributes attributes 21 | edgeAttributes attributes 22 | drawMultiEdges bool 23 | directed bool 24 | sameRank [][]*node 25 | } 26 | 27 | // SetTitle sets a title for the graph. 28 | func (g *Graph) SetTitle(title string) { 29 | g.GraphAttribute("label", title) 30 | } 31 | 32 | // AddNode adds a new node with the given label and returns its id. 33 | func (g *Graph) AddNode(label string) int { 34 | nod := node{id: g.n, label: label} 35 | g.n++ 36 | if g.nodes == nil { 37 | g.nodes = make(map[int]*node) 38 | } 39 | g.nodes[nod.id] = &nod 40 | return nod.id 41 | } 42 | 43 | // MakeSameRank causes the specified nodes to be drawn on the same rank. 44 | // Only effective when using the dot tool. 45 | func (g *Graph) MakeSameRank(node1, node2 int, others ...int) { 46 | r := make([]*node, 2+len(others)) 47 | r[0] = g.nodes[node1] 48 | r[1] = g.nodes[node2] 49 | for i := range others { 50 | r[2+i] = g.nodes[others[i]] 51 | } 52 | g.sameRank = append(g.sameRank, r) 53 | } 54 | 55 | // AddEdge adds a new edge between the given nodes with the specified 56 | // label and returns an id for the new edge. 57 | func (g *Graph) AddEdge(from, to int, label string) int { 58 | fromNode := g.nodes[from] 59 | toNode := g.nodes[to] 60 | // TODO: Check for errors (non-existing nodes) 61 | edg := edge{from: fromNode, to: toNode, label: label} 62 | id := g.e 63 | g.e++ 64 | if g.edges == nil { 65 | g.edges = make(map[int]*edge) 66 | } 67 | g.edges[id] = &edg 68 | return id 69 | } 70 | 71 | // MakeDirected makes the graph a directed graph. By default, a new 72 | // graph is undirected 73 | func (g *Graph) MakeDirected() { 74 | g.directed = true 75 | } 76 | 77 | // DrawMultipleEdges causes multiple edges between same pair of nodes 78 | // to be drawn separately. By default, for a given pair of nodes, only 79 | // the edge that was last added to the graph is drawn. 80 | func (g *Graph) DrawMultipleEdges() { 81 | g.drawMultiEdges = true 82 | } 83 | 84 | // NodeAttribute sets an attribute for the specified node. 85 | func (g *Graph) NodeAttribute(id int, name, value string) { 86 | // TODO: check for errors (id out of range) 87 | g.nodes[id].attributes.set(name, value) 88 | } 89 | 90 | // EdgeAttribute sets an attribute for the specified edge. 91 | func (g *Graph) EdgeAttribute(id int, name, value string) { 92 | // TODO: check for errors (id out of range) 93 | g.edges[id].attributes.set(name, value) 94 | } 95 | 96 | // DefaultNodeAttribute sets an attribute for all nodes by default. 97 | func (g *Graph) DefaultNodeAttribute(name, value string) { 98 | g.nodeAttributes.set(name, value) 99 | } 100 | 101 | // DefaultEdgeAttribute sets an attribute for all edges by default. 102 | func (g *Graph) DefaultEdgeAttribute(name, value string) { 103 | g.edgeAttributes.set(name, value) 104 | } 105 | 106 | // GraphAttribute sets an attribute for the graph 107 | func (g *Graph) GraphAttribute(name, value string) { 108 | g.graphAttributes.set(name, value) 109 | } 110 | 111 | // GenerateDOT generates the graph description in DOT language 112 | func (g Graph) GenerateDOT(w io.Writer) { 113 | if !g.drawMultiEdges { 114 | fmt.Fprint(w, "strict ") 115 | } 116 | if g.directed { 117 | fmt.Fprint(w, "digraph ") 118 | } else { 119 | fmt.Fprint(w, "graph ") 120 | } 121 | fmt.Fprintln(w, "{") 122 | for graphAttribs := g.graphAttributes.iterate(); graphAttribs.hasMore(); { 123 | name, value := graphAttribs.next() 124 | fmt.Fprintf(w, " %v = %#v;\n", name, value) 125 | } 126 | for nodeAttribs := g.nodeAttributes.iterate(); nodeAttribs.hasMore(); { 127 | name, value := nodeAttribs.next() 128 | fmt.Fprintf(w, " node [ %v = %#v ]\n", name, value) 129 | } 130 | for edgeAttribs := g.edgeAttributes.iterate(); edgeAttribs.hasMore(); { 131 | name, value := edgeAttribs.next() 132 | fmt.Fprintf(w, " edge [ %v = %#v ]\n", name, value) 133 | } 134 | for i := 0; i < g.n; i++ { 135 | fmt.Fprint(w, " ") 136 | g.nodes[i].generateDOT(w) 137 | fmt.Fprintln(w) 138 | } 139 | for _, r := range g.sameRank { 140 | fmt.Fprint(w, " {rank=same; ") 141 | for _, x := range r { 142 | fmt.Fprintf(w, "%v; ", x.name()) 143 | } 144 | fmt.Fprintln(w, "}") 145 | } 146 | for i := 0; i < g.e; i++ { 147 | fmt.Fprint(w, " ") 148 | g.edges[i].generateDOT(w, g.directed) 149 | fmt.Fprintln(w) 150 | } 151 | fmt.Fprintln(w, "}") 152 | } 153 | 154 | // GenerateImage runs a Graphviz tool such as dot or neato to generate 155 | // an image of the graph. filetype can be any file type supported by 156 | // the tool, e.g. png or svg 157 | func (g Graph) GenerateImage(tool, filename, filetype string) error { 158 | var args []string 159 | if filetype != "" { 160 | args = append(args, "-T"+filetype) 161 | } 162 | if filename != "" { 163 | args = append(args, "-o", filename) 164 | } 165 | cmd := exec.Command(tool, args...) 166 | stdin, err := cmd.StdinPipe() 167 | if err != nil { 168 | return err 169 | } 170 | go func() { 171 | defer stdin.Close() 172 | g.GenerateDOT(stdin) 173 | }() 174 | return cmd.Run() 175 | } 176 | 177 | type attributes struct { 178 | attributeMap map[string]string 179 | namesOrdered []string 180 | } 181 | 182 | func (a *attributes) set(name, value string) { 183 | if a.attributeMap == nil { 184 | a.attributeMap = make(map[string]string) 185 | } 186 | if _, exists := a.attributeMap[name]; !exists { 187 | a.namesOrdered = append(a.namesOrdered, name) 188 | } 189 | a.attributeMap[name] = value 190 | } 191 | 192 | func (a *attributes) iterate() attributeIterator { 193 | return attributeIterator{a, 0} 194 | } 195 | 196 | type attributeIterator struct { 197 | attributes *attributes 198 | index int 199 | } 200 | 201 | func (ai *attributeIterator) hasMore() bool { 202 | return ai.index < len(ai.attributes.namesOrdered) 203 | } 204 | 205 | func (ai *attributeIterator) next() (name, value string) { 206 | name = ai.attributes.namesOrdered[ai.index] 207 | value = ai.attributes.attributeMap[name] 208 | ai.index++ 209 | return name, value 210 | } 211 | 212 | type node struct { 213 | id int 214 | label string 215 | attributes attributes 216 | } 217 | 218 | func (n node) name() string { 219 | return fmt.Sprintf("n%v", n.id) 220 | } 221 | 222 | func (n node) generateDOT(w io.Writer) { 223 | fmt.Fprintf(w, "%v [label=%#v", n.name(), n.label) 224 | for attribs := n.attributes.iterate(); attribs.hasMore(); { 225 | name, value := attribs.next() 226 | fmt.Fprintf(w, ", %v=%#v", name, value) 227 | } 228 | fmt.Fprint(w, "]") 229 | } 230 | 231 | type edge struct { 232 | from *node 233 | to *node 234 | label string 235 | attributes attributes 236 | } 237 | 238 | func (e edge) generateDOT(w io.Writer, directed bool) { 239 | edgeOp := "--" 240 | if directed { 241 | edgeOp = "->" 242 | } 243 | fmt.Fprintf(w, "%v %v %v [label=%#v", e.from.name(), edgeOp, e.to.name(), e.label) 244 | for attribs := e.attributes.iterate(); attribs.hasMore(); { 245 | name, value := attribs.next() 246 | fmt.Fprintf(w, ", %v=%#v", name, value) 247 | } 248 | fmt.Fprint(w, "]") 249 | } 250 | -------------------------------------------------------------------------------- /graphviz/graphviz_test.go: -------------------------------------------------------------------------------- 1 | package graphviz_test 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/mzohreva/GoGraphviz/graphviz" 7 | ) 8 | 9 | // This example shows how Graph can be used to display a simple linked list. 10 | // The output can be piped to the dot tool to generate an image. 11 | func Example_linkedList() { 12 | G := graphviz.Graph{} 13 | G.MakeDirected() 14 | n1 := G.AddNode("Hello") 15 | n2 := G.AddNode("World") 16 | n3 := G.AddNode("Hi") 17 | n4 := G.AddNode("NULL") 18 | G.AddEdge(n1, n2, "next") 19 | G.AddEdge(n2, n3, "next") 20 | G.AddEdge(n3, n4, "next") 21 | G.MakeSameRank(n1, n2, n3, n4) 22 | 23 | G.GraphAttribute(graphviz.NodeSep, "0.5") 24 | 25 | G.DefaultNodeAttribute(graphviz.Shape, graphviz.ShapeBox) 26 | G.DefaultNodeAttribute(graphviz.FontName, "Courier") 27 | G.DefaultNodeAttribute(graphviz.FontSize, "14") 28 | G.DefaultNodeAttribute(graphviz.Style, graphviz.StyleFilled+","+graphviz.StyleRounded) 29 | G.DefaultNodeAttribute(graphviz.FillColor, "yellow") 30 | 31 | G.NodeAttribute(n4, graphviz.Shape, graphviz.ShapeCircle) 32 | G.NodeAttribute(n4, graphviz.Style, graphviz.StyleDashed) 33 | 34 | G.DefaultEdgeAttribute(graphviz.FontName, "Courier") 35 | G.DefaultEdgeAttribute(graphviz.FontSize, "12") 36 | 37 | G.GenerateDOT(os.Stdout) 38 | // output: 39 | // strict digraph { 40 | // nodesep = "0.5"; 41 | // node [ shape = "box" ] 42 | // node [ fontname = "Courier" ] 43 | // node [ fontsize = "14" ] 44 | // node [ style = "filled,rounded" ] 45 | // node [ fillcolor = "yellow" ] 46 | // edge [ fontname = "Courier" ] 47 | // edge [ fontsize = "12" ] 48 | // n0 [label="Hello"] 49 | // n1 [label="World"] 50 | // n2 [label="Hi"] 51 | // n3 [label="NULL", shape="circle", style="dashed"] 52 | // {rank=same; n0; n1; n2; n3; } 53 | // n0 -> n1 [label="next"] 54 | // n1 -> n2 [label="next"] 55 | // n2 -> n3 [label="next"] 56 | // } 57 | } 58 | --------------------------------------------------------------------------------