├── .github └── CODEOWNERS ├── .gitignore ├── LICENSE ├── README.md ├── go.mod ├── go.sum ├── layout ├── brandeskopf.go ├── cycle_remover.go ├── edge_layout.go ├── force_layout.go ├── gonum_layout.go ├── graph.go ├── layered_graph.go ├── layers_edgepath_assigner.go ├── layers_layers_assigner.go ├── layers_layout.go ├── layers_nodes_vertical_assigner.go ├── layers_ordering_assigner.go ├── layout.go ├── layout_test.go ├── scaler_layout.go └── testdata │ ├── brandeskopf.jsonl │ ├── brandeskopf_eades.svg │ ├── brandeskopf_forces.svg │ ├── brandeskopf_isomap.svg │ ├── brandeskopf_layers.svg │ ├── gin.jsonl │ ├── gin_eades.svg │ ├── gin_forces.svg │ ├── gin_isomap.svg │ ├── gin_layers.svg │ ├── small.jsonl │ ├── small_eades.svg │ ├── small_forces.svg │ ├── small_isomap.svg │ └── small_layers.svg └── svg ├── components.go ├── edge.go ├── graph.go └── node.go /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @gverger 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # Dependency directories (remove the comment below to include it) 15 | # vendor/ 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Nikolay Dubina 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 | ## Graph Layout Algorithms in Go 2 | 3 | This module provides algorithms for graph visualization in native Go. 4 | As of 2021-11-20, virtually all graph visualization algorithms are bindings to Graphviz dot code which is in C. 5 | This module attempts to provide implementation of latest and best graph visualization algorithms from scratch in Go. 6 | However, given this is very complex task this is work in progress. 7 | 8 | ## Features 9 | 10 | - [x] gonum Isomap 11 | - [x] gonum Eades 12 | - [x] Kozo Sugiyama layers strategy 13 | - [ ] Brandes-Köpf horizontal layers assignment [80% done] 14 | - [ ] Graphviz dot layers algorithm [80% done] 15 | - [x] Gravity force 16 | - [x] Spring force 17 | - [ ] Kozo Sugiyama Magnetic Force 18 | - [ ] Metro Style edges 19 | - [ ] Ports for edges 20 | - [ ] Spline edges 21 | - [ ] Collision avoidance (dot) edge path algorithm 22 | 23 | ## Contributions 24 | 25 | Yes please. These algorithms are hard. If you can, help to finish implementing any of above! 26 | 27 | If lots of contributions, I am ok to merge this into some org! 28 | 29 | ## References 30 | 31 | - [Wiki Layered Graph Drawing](https://en.wikipedia.org/wiki/Layered_graph_drawing) 32 | - ["Handbook of Graph Drawing and Visualization"](https://cs.brown.edu/people/rtamassi/gdhandbook/), Roberto Tamassia, Brown, Ch.13, 2013 33 | - ["A Technique for Drawing Directed Graphs"](https://ieeexplore.ieee.org/document/221135), Emden R. Gansner Eleftherios Koutsofios Stephen C. North Kiem-Phong Vo, AT&T Bell Laboratories, 1993 34 | - ["Fast and Simple Horizontal Coordinate Assignment"](https://link.springer.com/content/pdf/10.1007/3-540-45848-4_3.pdf), U. Brandes, Boris Köpf, 2002 35 | - "Methods for visual understanding of hierarchical system structures", Sugiyama, Kozo; Tagawa, Shôjirô; Toda, Mitsuhiko, 1981 36 | - "Graph Drawing by the Magnetic Spring Model", Kozo Sugiyama, 1995 37 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/gverger/go-graph-layout 2 | 3 | go 1.22 4 | 5 | toolchain go1.23.1 6 | 7 | require ( 8 | github.com/nikolaydubina/multiline-jsonl v1.0.3 // indirect 9 | golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa // indirect 10 | gonum.org/v1/gonum v0.15.1 11 | ) 12 | 13 | require github.com/nikolaydubina/jsonl-graph v1.1.0 14 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | git.sr.ht/~sbinet/gg v0.5.0 h1:6V43j30HM623V329xA9Ntq+WJrMjDxRjuAB1LFWF5m8= 2 | git.sr.ht/~sbinet/gg v0.5.0/go.mod h1:G2C0eRESqlKhS7ErsNey6HHrqU1PwsnCQlekFi9Q2Oo= 3 | github.com/ajstarks/svgo v0.0.0-20211024235047-1546f124cd8b h1:slYM766cy2nI3BwyRiyQj/Ud48djTMtMebDqepE95rw= 4 | github.com/ajstarks/svgo v0.0.0-20211024235047-1546f124cd8b/go.mod h1:1KcenG0jGWcpt8ov532z81sp/kMMUG485J2InIOyADM= 5 | github.com/campoy/embedmd v1.0.0 h1:V4kI2qTJJLf4J29RzI/MAt2c3Bl4dQSYPuflzwFH2hY= 6 | github.com/campoy/embedmd v1.0.0/go.mod h1:oxyr9RCiSXg0M3VJ3ks0UGfp98BpSSGr0kpiX3MzVl8= 7 | github.com/go-fonts/liberation v0.3.2 h1:XuwG0vGHFBPRRI8Qwbi5tIvR3cku9LUfZGq/Ar16wlQ= 8 | github.com/go-fonts/liberation v0.3.2/go.mod h1:N0QsDLVUQPy3UYg9XAc3Uh3UDMp2Z7M1o4+X98dXkmI= 9 | github.com/go-latex/latex v0.0.0-20231108140139-5c1ce85aa4ea h1:DfZQkvEbdmOe+JK2TMtBM+0I9GSdzE2y/L1/AmD8xKc= 10 | github.com/go-latex/latex v0.0.0-20231108140139-5c1ce85aa4ea/go.mod h1:Y7Vld91/HRbTBm7JwoI7HejdDB0u+e9AUBO9MB7yuZk= 11 | github.com/go-pdf/fpdf v0.9.0 h1:PPvSaUuo1iMi9KkaAn90NuKi+P4gwMedWPHhj8YlJQw= 12 | github.com/go-pdf/fpdf v0.9.0/go.mod h1:oO8N111TkmKb9D7VvWGLvLJlaZUQVPM+6V42pp3iV4Y= 13 | github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g= 14 | github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= 15 | github.com/nikolaydubina/jsonl-graph v1.1.0 h1:hxU2EFe0keB47tekjkDMGO2+DDbZvcfz21CvlV5Uv9A= 16 | github.com/nikolaydubina/jsonl-graph v1.1.0/go.mod h1:wQZL4BSABkCzcN3ual9qJ1AFE6wQSDJAbIZEmuYL1eY= 17 | github.com/nikolaydubina/multiline-jsonl v1.0.3 h1:rzfukoRroxvBorDgrtAjIcBasBpKQstqaOk9VfBPz70= 18 | github.com/nikolaydubina/multiline-jsonl v1.0.3/go.mod h1:/CONohg68Ltqm6QImYFWK2hC4025Sm/b53pqWQDVJ2A= 19 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 20 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 21 | golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa h1:FRnLl4eNAQl8hwxVVC17teOw8kdjVDVAiFMtgUdTSRQ= 22 | golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa/go.mod h1:zk2irFbV9DP96SEBUUAy67IdHUaZuSnrz1n472HUCLE= 23 | golang.org/x/image v0.14.0 h1:tNgSxAFe3jC4uYqvZdTr84SZoM1KfwdC9SKIFrLjFn4= 24 | golang.org/x/image v0.14.0/go.mod h1:HUYqC05R2ZcZ3ejNQsIHQDQiwWM4JBqmm6MKANTp4LE= 25 | golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= 26 | golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= 27 | gonum.org/v1/gonum v0.15.1 h1:FNy7N6OUZVUaWG9pTiD+jlhdQ3lMP+/LcTpJ6+a8sQ0= 28 | gonum.org/v1/gonum v0.15.1/go.mod h1:eZTZuRFrzu5pcyjN5wJhcIhnUdNijYxX1T2IcrOGY0o= 29 | gonum.org/v1/plot v0.14.0 h1:+LBDVFYwFe4LHhdP8coW6296MBEY4nQ+Y4vuUpJopcE= 30 | gonum.org/v1/plot v0.14.0/go.mod h1:MLdR9424SJed+5VqC6MsouEpig9pZX2VZ57H9ko2bXU= 31 | rsc.io/pdf v0.1.1 h1:k1MczvYDUvJBe93bYd7wrZLLUEcLZAuF824/I4e5Xr4= 32 | rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= 33 | -------------------------------------------------------------------------------- /layout/cycle_remover.go: -------------------------------------------------------------------------------- 1 | package layout 2 | 3 | import ( 4 | "math/rand" 5 | ) 6 | 7 | // SimpleCycleRemover will keep testing for cycles, if cycle found will randomly reverse one edge in cycle. 8 | // When restoring, will reverse previously reversed edges. 9 | type SimpleCycleRemover struct { 10 | Reversed map[[2]uint64]bool 11 | } 12 | 13 | func NewSimpleCycleRemover() SimpleCycleRemover { 14 | return SimpleCycleRemover{ 15 | Reversed: map[[2]uint64]bool{}, 16 | } 17 | } 18 | 19 | func getCycleDFS(neighbors map[uint64][]uint64, que []uint64) []uint64 { 20 | if len(que) == 0 { 21 | return que 22 | } 23 | 24 | p := que[len(que)-1] 25 | for _, d := range neighbors[p] { 26 | // check if cycle 27 | for i, t := range que { 28 | if d == t { 29 | return que[i:] 30 | } 31 | } 32 | 33 | // DFS deep call 34 | if t := getCycleDFS(neighbors, append(que, d)); len(t) > 0 { 35 | return t 36 | } 37 | } 38 | 39 | return nil 40 | } 41 | 42 | func getCycle(roots []uint64, neighbors map[uint64][]uint64) []uint64 { 43 | for _, root := range roots { 44 | if t := getCycleDFS(neighbors, []uint64{root}); len(t) > 0 { 45 | return t 46 | } 47 | } 48 | return nil 49 | } 50 | 51 | func reverseEdge(g Graph, e [2]uint64) { 52 | delete(g.Edges, e) 53 | g.Edges[[2]uint64{e[1], e[0]}] = Edge{} 54 | } 55 | 56 | func (s SimpleCycleRemover) RemoveCycles(g Graph) { 57 | neighbors := make(map[uint64][]uint64) 58 | for e := range g.Edges { 59 | neighbors[e[0]] = append(neighbors[e[0]], e[1]) 60 | } 61 | for cycle := getCycle(g.Roots(), neighbors); len(cycle) > 0; cycle = getCycle(g.Roots(), neighbors) { 62 | // pick edge randomly 63 | i := rand.Intn(len(cycle) - 1) 64 | e := [2]uint64{cycle[i], cycle[i+1]} 65 | 66 | reverseEdge(g, e) 67 | s.Reversed[e] = true 68 | 69 | neighbors[e[0]] = deleteValue(neighbors[e[0]], e[1]) 70 | neighbors[e[1]] = append(neighbors[e[1]], e[0]) 71 | } 72 | } 73 | 74 | func deleteValue(slice []uint64, value uint64) []uint64 { 75 | for i, n := range slice { 76 | if n == value { 77 | slice[i] = slice[len(slice)-1] 78 | return slice[:len(slice)-1] 79 | } 80 | } 81 | return slice 82 | } 83 | 84 | func (s SimpleCycleRemover) Restore(g Graph) { 85 | for e := range s.Reversed { 86 | reverseEdge(g, e) 87 | delete(s.Reversed, e) 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /layout/edge_layout.go: -------------------------------------------------------------------------------- 1 | package layout 2 | 3 | // DirectEdge is straight line from center of one node to another. 4 | func DirectEdge(from, to Node) Edge { 5 | return Edge{ 6 | Path: []Position{ 7 | { 8 | X: from.X + (from.W / 2), 9 | Y: from.Y + (from.H / 2), 10 | }, 11 | { 12 | X: to.X + (to.W / 2), 13 | Y: to.Y + (to.H / 2), 14 | }, 15 | }, 16 | } 17 | } 18 | 19 | // DirectEdgesLayout are straight single line edges. 20 | type DirectEdgesLayout struct{} 21 | 22 | func (l DirectEdgesLayout) UpdateGraphLayout(g Graph) { 23 | for e := range g.Edges { 24 | g.Edges[e] = DirectEdge(g.Nodes[e[0]], g.Nodes[e[1]]) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /layout/force_layout.go: -------------------------------------------------------------------------------- 1 | package layout 2 | 3 | import "math" 4 | 5 | // Force computes forces for Nodes. 6 | type Force interface { 7 | UpdateForce(g Graph, f map[uint64][2]float64) 8 | } 9 | 10 | // ForceGraphLayout will simulate node movement due to forces. 11 | type ForceGraphLayout struct { 12 | Delta float64 // how much move each step 13 | MaxSteps int // limit of iterations 14 | Epsilon float64 // minimal force 15 | Forces []Force 16 | } 17 | 18 | func (l ForceGraphLayout) UpdateGraphLayout(g Graph) { 19 | for step := 0; step < l.MaxSteps; step++ { 20 | f := make(map[uint64][2]float64, len(g.Nodes)) 21 | 22 | // accumulate all forces 23 | for i := range l.Forces { 24 | l.Forces[i].UpdateForce(g, f) 25 | } 26 | 27 | // delete tiny forces 28 | for i := range g.Nodes { 29 | if math.Hypot(f[i][0], f[i][1]) < l.Epsilon { 30 | delete(f, i) 31 | } 32 | } 33 | 34 | // early stop if no forces 35 | if len(f) == 0 { 36 | break 37 | } 38 | 39 | // move by delta 40 | for i := range g.Nodes { 41 | x := g.Nodes[i].X + int((f[i][0] * l.Delta)) 42 | y := g.Nodes[i].Y + int((f[i][1] * l.Delta)) 43 | g.Nodes[i] = Node{ 44 | Position: Position{X: x, Y: y}, 45 | W: g.Nodes[i].W, 46 | H: g.Nodes[i].H, 47 | } 48 | } 49 | } 50 | } 51 | 52 | // SpringForce is linear by distance. 53 | type SpringForce struct { 54 | K float64 // has to be positive 55 | L float64 // distance at rest 56 | EdgesOnly bool // true = only edges, false = all nodes 57 | } 58 | 59 | func (l SpringForce) UpdateForce(g Graph, f map[uint64][2]float64) { 60 | for i := range g.Nodes { 61 | var js []uint64 62 | 63 | if l.EdgesOnly { 64 | for e := range g.Edges { 65 | if e[0] == i { 66 | js = append(js, e[1]) 67 | } 68 | } 69 | } else { 70 | for j := range g.Nodes { 71 | if i != j { 72 | js = append(js, j) 73 | } 74 | } 75 | } 76 | 77 | xi := float64(g.Nodes[i].X) 78 | yi := float64(g.Nodes[i].Y) 79 | 80 | for _, j := range js { 81 | xj := float64(g.Nodes[j].X) 82 | yj := float64(g.Nodes[j].Y) 83 | 84 | d := math.Hypot(xi-xj, yi-yj) 85 | 86 | if d > 1 { 87 | // if stretch, then attraction 88 | // if shrink, then repulsion 89 | af := (d - l.L) * l.K 90 | f[i] = [2]float64{ 91 | f[i][0] + (af * (xj - xi) / d), 92 | f[i][1] + (af * (yj - yi) / d), 93 | } 94 | } 95 | } 96 | } 97 | } 98 | 99 | // GravityForce is gravity-like repulsive (or attractive) force. 100 | type GravityForce struct { 101 | K float64 // positive K for attraction 102 | EdgesOnly bool // true = only edges, false = all nodes 103 | } 104 | 105 | func (l GravityForce) UpdateForce(g Graph, f map[uint64][2]float64) { 106 | for i := range g.Nodes { 107 | var js []uint64 108 | if l.EdgesOnly { 109 | for e := range g.Edges { 110 | if e[0] == i { 111 | js = append(js, e[1]) 112 | } 113 | } 114 | } else { 115 | for j := range g.Nodes { 116 | if i != j { 117 | js = append(js, j) 118 | } 119 | } 120 | } 121 | 122 | xi := float64(g.Nodes[i].X) 123 | yi := float64(g.Nodes[i].Y) 124 | 125 | for _, j := range js { 126 | xj := float64(g.Nodes[j].X) 127 | yj := float64(g.Nodes[j].Y) 128 | 129 | d := math.Hypot(xi-xj, yi-yj) 130 | 131 | if d > 1 { 132 | af := l.K / d 133 | f[i] = [2]float64{ 134 | f[i][0] + (af * (xj - xi) / d), 135 | f[i][1] + (af * (yj - yi) / d), 136 | } 137 | } 138 | } 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /layout/gonum_layout.go: -------------------------------------------------------------------------------- 1 | package layout 2 | 3 | import ( 4 | "log" 5 | "math" 6 | 7 | gnlayout "gonum.org/v1/gonum/graph/layout" 8 | gnsimple "gonum.org/v1/gonum/graph/simple" 9 | gnr2 "gonum.org/v1/gonum/spatial/r2" 10 | ) 11 | 12 | func gonumNodeID(id uint64) int64 { 13 | return int64(float64(id)) 14 | } 15 | 16 | func toGonumGraph(g Graph) *gnsimple.UndirectedGraph { 17 | gn := gnsimple.NewUndirectedGraph() 18 | for e := range g.Edges { 19 | gn.SetEdge(gn.NewEdge(gnsimple.Node(gonumNodeID(e[0])), gnsimple.Node(gonumNodeID(e[1])))) 20 | } 21 | return gn 22 | } 23 | 24 | type gnLayoutGetter interface { 25 | Coord2(id int64) gnr2.Vec 26 | } 27 | 28 | // will make dimension such that all nodes data fits into square of the same area 29 | // this is for pretty layouts. 30 | func getSquareLayoutSize(g Graph) float64 { 31 | s := g.TotalNodesWidth() * g.TotalNodesHeight() 32 | return math.Sqrt(float64(s)) 33 | } 34 | 35 | func updateGraphByGonumLayout(g Graph, gnLayout gnLayoutGetter, scaleX float64, scaleY float64) { 36 | // get width and height of gonum layout 37 | gnw := 0.0 38 | gnh := 0.0 39 | for i := range g.Nodes { 40 | n := gnLayout.Coord2(gonumNodeID(i)) 41 | 42 | if n.X > gnw { 43 | gnw = n.X 44 | } 45 | if n.Y > gnh { 46 | gnh = n.Y 47 | } 48 | } 49 | 50 | // get width and height of our expected layout 51 | w := getSquareLayoutSize(g) * scaleX 52 | h := w * scaleY 53 | log.Printf("update gonum layout: gonum layout(%f x %f) our layout (%f x %f)", gnw, gnh, w, h) 54 | 55 | // update our coodinates and scale 56 | for nodeID := range g.Nodes { 57 | gnNode := gnLayout.Coord2(gonumNodeID(nodeID)) 58 | 59 | g.Nodes[nodeID] = Node{ 60 | Position: Position{ 61 | X: int(gnNode.X * w / gnw), 62 | Y: int(gnNode.Y * h / gnh), 63 | }, 64 | W: g.Nodes[nodeID].W, 65 | H: g.Nodes[nodeID].H, 66 | } 67 | } 68 | } 69 | 70 | // This works, but not as pretty. 71 | type EadesGonumLayout struct { 72 | Updates int 73 | Repulsion float64 74 | Rate float64 75 | Theta float64 76 | ScaleX float64 77 | ScaleY float64 78 | } 79 | 80 | func (l EadesGonumLayout) UpdateGraphLayout(g Graph) { 81 | gn := toGonumGraph(g) 82 | 83 | eades := gnlayout.EadesR2{ 84 | Updates: l.Updates, 85 | Repulsion: l.Repulsion, 86 | Rate: l.Rate, 87 | Theta: l.Theta, 88 | } 89 | optimizer := gnlayout.NewOptimizerR2(gn, eades.Update) 90 | for optimizer.Update() { 91 | } 92 | 93 | updateGraphByGonumLayout(g, optimizer, l.ScaleX, l.ScaleY) 94 | } 95 | 96 | type IsomapR2GonumLayout struct { 97 | Scale float64 98 | ScaleX float64 99 | ScaleY float64 100 | } 101 | 102 | func (l IsomapR2GonumLayout) UpdateGraphLayout(g Graph) { 103 | gn := toGonumGraph(g) 104 | optimizer := gnlayout.NewOptimizerR2(gn, gnlayout.IsomapR2{}.Update) 105 | for optimizer.Update() { 106 | } 107 | updateGraphByGonumLayout(g, optimizer, l.ScaleX, l.ScaleY) 108 | } 109 | -------------------------------------------------------------------------------- /layout/graph.go: -------------------------------------------------------------------------------- 1 | package layout 2 | 3 | type NodeID = uint64 4 | 5 | type Position struct { 6 | X int 7 | Y int 8 | } 9 | 10 | // Graph tells how to position nodes and paths for edges 11 | type Graph struct { 12 | Edges map[[2]NodeID]Edge 13 | Nodes map[NodeID]Node 14 | } 15 | 16 | // Node is how to position node and its dimensions 17 | type Node struct { 18 | Position 19 | W int 20 | H int 21 | } 22 | 23 | func (n Node) CenterXY() Position { 24 | x := n.X + n.W/2 25 | y := n.Y + n.H/2 26 | return Position{x, y} 27 | } 28 | 29 | // Edge is path of points that edge goes through 30 | type Edge struct { 31 | Path []Position // [start: {x,y}, ... finish: {x,y}] 32 | } 33 | 34 | func (g Graph) Copy() Graph { 35 | ng := Graph{ 36 | Nodes: make(map[NodeID]Node, len(g.Nodes)), 37 | Edges: make(map[[2]NodeID]Edge, len(g.Edges)), 38 | } 39 | for id, n := range g.Nodes { 40 | ng.Nodes[id] = n 41 | } 42 | for id, e := range g.Edges { 43 | ng.Edges[id] = Edge{Path: make([]Position, len(e.Path))} 44 | copy(ng.Edges[id].Path, e.Path) 45 | } 46 | return ng 47 | } 48 | 49 | func (g Graph) Roots() []NodeID { 50 | hasParent := make(map[NodeID]bool, len(g.Nodes)) 51 | for e := range g.Edges { 52 | hasParent[e[1]] = true 53 | } 54 | 55 | var roots []NodeID 56 | for n := range g.Nodes { 57 | if !hasParent[n] { 58 | roots = append(roots, n) 59 | } 60 | } 61 | return roots 62 | } 63 | 64 | func (g Graph) TotalNodesWidth() int { 65 | w := 0 66 | for _, node := range g.Nodes { 67 | w += node.W 68 | } 69 | return w 70 | } 71 | 72 | func (g Graph) TotalNodesHeight() int { 73 | h := 0 74 | for _, node := range g.Nodes { 75 | h += node.H 76 | } 77 | return h 78 | } 79 | 80 | // BoundingBox coordinates that should fit whole graph. 81 | // Does not consider edges. 82 | func (g Graph) BoundingBox() (minx, miny, maxx, maxy int) { 83 | for _, node := range g.Nodes { 84 | nx := node.X 85 | ny := node.Y 86 | 87 | if nx < minx { 88 | minx = nx 89 | } 90 | if x := nx + node.W; x > maxx { 91 | maxx = x 92 | } 93 | if ny < miny { 94 | miny = ny 95 | } 96 | if y := ny + node.H; y > maxy { 97 | maxy = y 98 | } 99 | } 100 | return minx, miny, maxx, maxy 101 | } 102 | -------------------------------------------------------------------------------- /layout/layered_graph.go: -------------------------------------------------------------------------------- 1 | package layout 2 | 3 | import ( 4 | "fmt" 5 | "sort" 6 | "strings" 7 | ) 8 | 9 | type LayerPosition struct { 10 | Layer int // Layer index: is the root 11 | Order int // Order in the layer 12 | } 13 | 14 | func (p LayerPosition) IsLeftOf(other LayerPosition) bool { 15 | if p.Layer != other.Layer { 16 | panic(fmt.Sprintf("positions not on same layer: %+v < %+v", p, other)) 17 | } 18 | 19 | return p.Order < other.Order 20 | } 21 | 22 | // LayeredGraph is graph with dummy nodes such that there is no long edges. 23 | // Short edge is between nodes in Layers next to each other. 24 | // Long edge is between nodes in 1+ Layers between each other. 25 | // Segment is either a short edge or a long edge. 26 | // Top layer has lowest layer number. 27 | type LayeredGraph struct { 28 | Segments map[[2]uint64]bool // segment is an edge in layered graph, can be real edge or piece of fake edge 29 | Dummy map[uint64]bool // fake nodes 30 | NodePosition map[uint64]LayerPosition // node -> {layer, ordering in layer} 31 | Edges map[[2]uint64][]uint64 // real long/short edge -> {real, fake, fake, fake, real} nodes 32 | } 33 | 34 | func (g LayeredGraph) Layers() [][]uint64 { 35 | maxLayer := 0 36 | for _, position := range g.NodePosition { 37 | if position.Layer > maxLayer { 38 | maxLayer = position.Layer 39 | } 40 | } 41 | 42 | layers := make([][]uint64, maxLayer+1) 43 | for node, position := range g.NodePosition { 44 | layers[position.Layer] = append(layers[position.Layer], node) 45 | } 46 | 47 | for layerIdx := 0; layerIdx < len(layers); layerIdx++ { 48 | sort.Slice(layers[layerIdx], func(i, j int) bool { 49 | return g.NodePosition[layers[layerIdx][i]].IsLeftOf(g.NodePosition[layers[layerIdx][j]]) 50 | }) 51 | } 52 | 53 | return layers 54 | } 55 | 56 | func (g LayeredGraph) Validate() error { 57 | for e := range g.Segments { 58 | from := g.NodePosition[e[0]].Layer 59 | to := g.NodePosition[e[1]].Layer 60 | if from >= to { 61 | return fmt.Errorf("edge(%v) is wrong direction, got from level(%d) to level(%d)", e, from, to) 62 | } 63 | } 64 | return nil 65 | } 66 | 67 | func (g LayeredGraph) String() string { 68 | out := "" 69 | 70 | out += fmt.Sprintf("fake nodes: %+v\n", g.Dummy) 71 | 72 | segments := []string{} 73 | for e := range g.Segments { 74 | segments = append(segments, fmt.Sprintf("%d->%d", e[0], e[1])) 75 | } 76 | sort.Strings(segments) 77 | out += fmt.Sprintf("segments: %s\n", strings.Join(segments, " ")) 78 | 79 | layers := g.Layers() 80 | for l, nodes := range layers { 81 | vs := "" 82 | for _, node := range nodes { 83 | vs += fmt.Sprintf(" %d", node) 84 | } 85 | out += fmt.Sprintf("%d: %s\n", l, vs) 86 | } 87 | return out 88 | } 89 | 90 | // IsInnerSegment tells when edge is between two Dummy nodes. 91 | func (g LayeredGraph) IsInnerSegment(segment [2]uint64) bool { 92 | return g.Dummy[segment[0]] && g.Dummy[segment[1]] 93 | } 94 | 95 | // UpperNeighbors are nodes in upper layer that are connected to given node. 96 | func (g LayeredGraph) UpperNeighbors(node uint64) []uint64 { 97 | var nodes []uint64 98 | for e := range g.Segments { 99 | if e[1] == node { 100 | if g.NodePosition[e[0]].Layer == (g.NodePosition[e[1]].Layer - 1) { 101 | nodes = append(nodes, e[0]) 102 | } 103 | } 104 | } 105 | return nodes 106 | } 107 | 108 | // LowerNeighbors are nodes in lower layer that are connected to given node. 109 | func (g LayeredGraph) LowerNeighbors(node uint64) []uint64 { 110 | var nodes []uint64 111 | for e := range g.Segments { 112 | if e[0] == node { 113 | if g.NodePosition[e[0]].Layer == (g.NodePosition[e[1]].Layer - 1) { 114 | nodes = append(nodes, e[0]) 115 | } 116 | } 117 | } 118 | return nodes 119 | } 120 | 121 | // newLayersFrom makes new layers with content identical to source. 122 | func newLayersFrom(src [][]uint64) (dst [][]uint64) { 123 | dst = make([][]uint64, len(src)) 124 | for i, l := range src { 125 | dst[i] = make([]uint64, len(l)) 126 | copy(dst[i], l) 127 | } 128 | return dst 129 | } 130 | 131 | // copyLayers copies from src to destination 132 | func copyLayers(dst, src [][]uint64) { 133 | for i := range src { 134 | copy(dst[i], src[i]) 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /layout/layers_edgepath_assigner.go: -------------------------------------------------------------------------------- 1 | package layout 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | // StraightEdgePathAssigner will check node locations for each fake/real node in path and set edge path to go through middle of it. 8 | type StraightEdgePathAssigner struct{} 9 | 10 | func (l StraightEdgePathAssigner) UpdateGraphLayout(g Graph, lg LayeredGraph, allNodesXY map[uint64]Position) { 11 | numAssignedEdges := 0 12 | for e, nodes := range lg.Edges { 13 | if _, ok := g.Edges[e]; !ok { 14 | panic(fmt.Errorf("layered graph edge(%v) is not found in the original graph", e)) 15 | } 16 | 17 | path := make([]Position, len(nodes)) 18 | for i, n := range nodes { 19 | path[i] = allNodesXY[n] 20 | } 21 | 22 | g.Edges[e] = Edge{Path: path} 23 | numAssignedEdges++ 24 | } 25 | 26 | if numAssignedEdges != len(g.Edges) { 27 | panic(fmt.Errorf("layered graph has wrong number of edges(%d) vs graph num edges (%d)", numAssignedEdges, len(g.Edges))) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /layout/layers_layers_assigner.go: -------------------------------------------------------------------------------- 1 | package layout 2 | 3 | import "fmt" 4 | 5 | // Expects that graph g does not have cycles. 6 | // This step creates fake nodes and splits long edges into segments. 7 | func NewLayeredGraph(g Graph) LayeredGraph { 8 | positions := assignLevels(g) 9 | edges := makeEdges(g, positions) 10 | return LayeredGraph{ 11 | NodePosition: positions, 12 | Segments: makeSegments(edges), 13 | Dummy: makeDummy(edges), 14 | Edges: edges, 15 | } 16 | } 17 | 18 | func maxNodeID(g Graph) uint64 { 19 | var maxNodeID uint64 20 | for e := range g.Edges { 21 | if e[0] > maxNodeID { 22 | maxNodeID = e[0] 23 | } 24 | if e[1] > maxNodeID { 25 | maxNodeID = e[1] 26 | } 27 | } 28 | return maxNodeID 29 | } 30 | 31 | func assignLevels(g Graph) map[uint64]LayerPosition { 32 | nodeYX := make(map[uint64]LayerPosition, len(g.Nodes)) 33 | neighbors := make(map[uint64][]uint64) 34 | for e := range g.Edges { 35 | neighbors[e[0]] = append(neighbors[e[0]], e[1]) 36 | } 37 | for _, root := range g.Roots() { 38 | nodeYX[root] = LayerPosition{} 39 | for que := []uint64{root}; len(que) > 0; { 40 | // pop 41 | p := que[0] 42 | if len(que) > 1 { 43 | que = que[1:] 44 | } else { 45 | que = nil 46 | } 47 | 48 | // set max depth for each child 49 | for _, child := range neighbors[p] { 50 | if l := nodeYX[p].Layer + 1; l > nodeYX[child].Layer { 51 | nodeYX[child] = LayerPosition{Layer: l, Order: 0} 52 | } 53 | que = append(que, child) 54 | } 55 | } 56 | } 57 | return nodeYX 58 | } 59 | 60 | // for each long edge breaks it down to multiple segments, for short edge just adds it 61 | func makeSegments(edges map[[2]uint64][]uint64) map[[2]uint64]bool { 62 | segments := map[[2]uint64]bool{} 63 | for e, nodes := range edges { 64 | switch { 65 | case len(nodes) == 2: 66 | segments[e] = true 67 | case len(nodes) > 2: 68 | for i := range nodes { 69 | if i == 0 { 70 | continue 71 | } 72 | segments[[2]uint64{nodes[i-1], nodes[i]}] = true 73 | } 74 | default: 75 | panic(fmt.Errorf("edge(%v) has only one node(%v) but at least 2 expected", e, nodes)) 76 | } 77 | } 78 | return segments 79 | } 80 | 81 | // extracts all fake nodes for edges that are long into separate map 82 | func makeDummy(edges map[[2]uint64][]uint64) map[uint64]bool { 83 | dummy := map[uint64]bool{} 84 | for _, nodes := range edges { 85 | if len(nodes) > 2 { 86 | for i, n := range nodes { 87 | if i == 0 || i == (len(nodes)-1) { 88 | continue 89 | } 90 | dummy[n] = true 91 | } 92 | } 93 | } 94 | return dummy 95 | } 96 | 97 | // makeEdges split long edges into segments and add fake nodes 98 | // adds new fake nodes to nodeYX 99 | func makeEdges(g Graph, nodeYX map[uint64]LayerPosition) map[[2]uint64][]uint64 { 100 | edges := make(map[[2]uint64][]uint64, len(g.Edges)) 101 | 102 | nextFakeNodeID := maxNodeID(g) + 1 103 | for e := range g.Edges { 104 | fromLayer := nodeYX[e[0]].Layer 105 | toLayer := nodeYX[e[1]].Layer 106 | 107 | newEdge := []uint64{} 108 | newEdge = append(newEdge, e[0]) 109 | 110 | if (toLayer - fromLayer) > 1 { 111 | for layer := fromLayer + 1; layer < toLayer; layer++ { 112 | nodeYX[nextFakeNodeID] = LayerPosition{Layer: layer, Order: 0} 113 | newEdge = append(newEdge, nextFakeNodeID) 114 | nextFakeNodeID++ 115 | } 116 | } 117 | 118 | newEdge = append(newEdge, e[1]) 119 | 120 | edges[e] = newEdge 121 | } 122 | 123 | return edges 124 | } 125 | -------------------------------------------------------------------------------- /layout/layers_layout.go: -------------------------------------------------------------------------------- 1 | package layout 2 | 3 | type CycleRemover interface { 4 | RemoveCycles(g Graph) 5 | Restore(g Graph) 6 | } 7 | 8 | type NodesHorizontalCoordinatesAssigner interface { 9 | NodesHorizontalCoordinates(g Graph, lg LayeredGraph) map[uint64]int 10 | } 11 | 12 | type NodesVerticalCoordinatesAssigner interface { 13 | NodesVerticalCoordinates(g Graph, lg LayeredGraph) map[uint64]int 14 | } 15 | 16 | // Kozo Sugiyama algorithm breaks down layered graph construction in phases. 17 | type SugiyamaLayersStrategyGraphLayout struct { 18 | CycleRemover CycleRemover 19 | LevelsAssigner func(g Graph) LayeredGraph 20 | OrderingAssigner func(g Graph, lg LayeredGraph) 21 | NodesHorizontalCoordinatesAssigner NodesHorizontalCoordinatesAssigner 22 | NodesVerticalCoordinatesAssigner NodesVerticalCoordinatesAssigner 23 | EdgePathAssigner func(g Graph, lg LayeredGraph, allNodesXY map[uint64]Position) 24 | } 25 | 26 | // UpdateGraphLayout breaks down layered graph construction in phases. 27 | func (l SugiyamaLayersStrategyGraphLayout) UpdateGraphLayout(g Graph) { 28 | l.CycleRemover.RemoveCycles(g) 29 | 30 | lg := l.LevelsAssigner(g) 31 | if err := lg.Validate(); err != nil { 32 | panic(err) 33 | } 34 | 35 | l.OrderingAssigner(g, lg) 36 | 37 | nodeX := l.NodesHorizontalCoordinatesAssigner.NodesHorizontalCoordinates(g, lg) 38 | nodeY := l.NodesVerticalCoordinatesAssigner.NodesVerticalCoordinates(g, lg) 39 | 40 | // real and fake node coordinates 41 | allNodesXY := make(map[uint64]Position, len(g.Nodes)) 42 | for n := range lg.NodePosition { 43 | allNodesXY[n] = Position{X: nodeX[n], Y: nodeY[n]} 44 | } 45 | 46 | // export coordinates for edges 47 | l.EdgePathAssigner(g, lg, allNodesXY) 48 | 49 | // export coordinates to real nodes 50 | for n, node := range g.Nodes { 51 | g.Nodes[n] = Node{ 52 | Position: Position{ 53 | X: nodeX[n] - node.W/2, 54 | Y: nodeY[n] - node.H/2, 55 | }, 56 | W: node.W, 57 | H: node.H, 58 | } 59 | } 60 | 61 | l.CycleRemover.Restore(g) 62 | } 63 | -------------------------------------------------------------------------------- /layout/layers_nodes_vertical_assigner.go: -------------------------------------------------------------------------------- 1 | package layout 2 | 3 | // BasicNodesVerticalCoordinatesAssigner will check maximum height in each layer. 4 | // It will keep each node vertically in the middle within each layer. 5 | type BasicNodesVerticalCoordinatesAssigner struct { 6 | MarginLayers int // distance between layers 7 | FakeNodeHeight int 8 | } 9 | 10 | func layersMaxHeights(g Graph, layers [][]uint64) []int { 11 | hmax := make([]int, len(layers)) 12 | for i, nodes := range layers { 13 | for _, node := range nodes { 14 | if hmax[i] < g.Nodes[node].H { 15 | hmax[i] = g.Nodes[node].H 16 | } 17 | } 18 | } 19 | return hmax 20 | } 21 | 22 | func (s BasicNodesVerticalCoordinatesAssigner) NodesVerticalCoordinates(g Graph, lg LayeredGraph) map[uint64]int { 23 | nodeY := make(map[uint64]int, len(lg.NodePosition)) 24 | 25 | layers := lg.Layers() 26 | layersHMax := layersMaxHeights(g, layers) 27 | 28 | yOffset := 0 29 | for i, nodes := range layers { 30 | for _, node := range nodes { 31 | // put in the middle vertically 32 | nodeY[node] = yOffset + (layersHMax[i]) / 2 33 | } 34 | 35 | // move to next layer 36 | yOffset += layersHMax[i] + s.MarginLayers 37 | } 38 | 39 | return nodeY 40 | } 41 | -------------------------------------------------------------------------------- /layout/layers_ordering_assigner.go: -------------------------------------------------------------------------------- 1 | package layout 2 | 3 | import ( 4 | "log" 5 | "math/rand" 6 | "sort" 7 | ) 8 | 9 | type LayerOrderingInitializer interface { 10 | Init(segments map[[2]uint64]bool, layers [][]uint64) 11 | } 12 | 13 | type LayerOrderingOptimizer interface { 14 | Optimize(segments map[[2]uint64]bool, layers [][]uint64, idx int, downUp bool) 15 | } 16 | 17 | type CompositeLayerOrderingOptimizer struct { 18 | Optimizers []LayerOrderingOptimizer 19 | } 20 | 21 | func (o CompositeLayerOrderingOptimizer) Optimize(segments map[[2]uint64]bool, layers [][]uint64, idx int, downUp bool) { 22 | for _, q := range o.Optimizers { 23 | q.Optimize(segments, layers, idx, downUp) 24 | } 25 | } 26 | 27 | // WarfieldOrderingOptimizer is heuristic based strategy for ordering optimization. 28 | // Goes up and down number of iterations across all layers. 29 | // Considers upper and lower fixed and permutes ordering in layer. 30 | // Used in Graphviz/dot. 31 | type WarfieldOrderingOptimizer struct { 32 | Epochs int 33 | LayerOrderingInitializer LayerOrderingInitializer 34 | LayerOrderingOptimizer LayerOrderingOptimizer 35 | } 36 | 37 | func (o WarfieldOrderingOptimizer) Optimize(g Graph, lg LayeredGraph) { 38 | // layers is temporary layers 39 | layers := lg.Layers() 40 | o.LayerOrderingInitializer.Init(lg.Segments, layers) 41 | 42 | bestN := -1 43 | bestLayers := newLayersFrom(layers) 44 | 45 | for t := 0; t < o.Epochs; t++ { 46 | downUp := (t % 2) == 0 47 | for i := range layers { 48 | j := i 49 | if downUp { 50 | j = len(layers) - 1 - i 51 | } 52 | o.LayerOrderingOptimizer.Optimize(lg.Segments, layers, j, downUp) 53 | } 54 | 55 | N := numCrossings(lg.Segments, layers) 56 | if bestN < 0 || N < bestN { 57 | bestN = N 58 | copyLayers(bestLayers, layers) 59 | } 60 | log.Printf("warfield ordering optimizer:\t epoch(%d)\t best(%d)\t current(%d)\n", t, bestN, N) 61 | if N == 0 { 62 | break 63 | } 64 | } 65 | 66 | // store to graph 67 | for y, layer := range bestLayers { 68 | for x, node := range layer { 69 | lg.NodePosition[node] = LayerPosition{Layer: y, Order: x} 70 | } 71 | } 72 | } 73 | 74 | // BFSOrderingInitializer will set order in each layer by traversing BFS from roots. 75 | type BFSOrderingInitializer struct{} 76 | 77 | func (o BFSOrderingInitializer) Init(segments map[[2]uint64]bool, layers [][]uint64) { 78 | // accumulate where node can lead to 79 | fromNodeToNodes := map[uint64][]uint64{} 80 | for e := range segments { 81 | if _, ok := fromNodeToNodes[e[0]]; !ok { 82 | fromNodeToNodes[e[0]] = []uint64{} 83 | } 84 | fromNodeToNodes[e[0]] = append(fromNodeToNodes[e[0]], e[1]) 85 | } 86 | 87 | // get roots 88 | hasParent := map[uint64]bool{} 89 | for e := range segments { 90 | hasParent[e[1]] = true 91 | } 92 | var roots []uint64 93 | for e := range segments { 94 | if _, ok := hasParent[e[1]]; !ok { 95 | roots = append(roots, e[1]) 96 | } 97 | } 98 | 99 | // BFS starting with roots, keeping order when node is visited 100 | cnt := 0 101 | que := roots 102 | ord := map[uint64]int{} 103 | for len(que) > 0 { 104 | p := que[0] 105 | if len(que) > 1 { 106 | que = que[1:] 107 | } else { 108 | que = nil 109 | } 110 | 111 | if _, ok := ord[p]; ok { 112 | continue 113 | } 114 | 115 | ord[p] = cnt 116 | cnt++ 117 | 118 | que = append(que, fromNodeToNodes[p]...) 119 | } 120 | 121 | for l := range layers { 122 | sort.Slice(layers[l], func(i, j int) bool { return ord[layers[l][i]] < ord[layers[l][j]] }) 123 | } 124 | } 125 | 126 | // RandomLayerOrderingInitializer assigns random ordering in each layer 127 | type RandomLayerOrderingInitializer struct{} 128 | 129 | func (o RandomLayerOrderingInitializer) Init(_ map[[2]uint64]bool, layers [][]uint64) { 130 | for i := range layers { 131 | l := layers[i] 132 | rand.Shuffle(len(l), func(a, b int) { l[a], l[b] = l[b], l[a] }) 133 | } 134 | } 135 | 136 | // WMedianOrderingOptimizer takes median of upper (or lower) level neighbors for each node in layer. 137 | // Median has property of stable vertical edges which is especially useful for "long" edges (fake nodes). 138 | // Eades and Wormald, 1994 139 | // This is used in dot/Graphviz, Figure 3-2 in Graphviz dot paper TSE93. 140 | type WMedianOrderingOptimizer struct{} 141 | 142 | func (o WMedianOrderingOptimizer) Optimize(segments map[[2]uint64]bool, layers [][]uint64, y int, downUp bool) { 143 | w := map[uint64]float64{} 144 | 145 | for i, node := range layers[y] { 146 | var xs []int 147 | if downUp { 148 | xs = lowerNeighborsX(segments, layers, i, y) 149 | } else { 150 | xs = upperNeighborsX(segments, layers, i, y) 151 | } 152 | 153 | P := make([]float64, len(xs)) 154 | for i, v := range xs { 155 | P[i] = float64(v) 156 | } 157 | w[node] = median(P) 158 | } 159 | 160 | sort.Slice(layers[y], func(i, j int) bool { return w[layers[y][i]] < w[layers[y][j]] }) 161 | } 162 | 163 | // time: O(len(layer)) 164 | // space: O(len(layer)) 165 | func lowerNeighborsX(segments map[[2]uint64]bool, layers [][]uint64, x int, y int) []int { 166 | if y == (len(layers) - 1) { 167 | return nil 168 | } 169 | 170 | t := layers[y][x] 171 | 172 | var nx []int 173 | for i, n := range layers[y+1] { 174 | if segments[[2]uint64{t, n}] { 175 | nx = append(nx, i) 176 | } 177 | } 178 | 179 | return nx 180 | } 181 | 182 | // time: O(len(layer)) 183 | // space: O(len(layer)) 184 | func upperNeighborsX(segments map[[2]uint64]bool, layers [][]uint64, x int, y int) []int { 185 | if y == 0 { 186 | return nil 187 | } 188 | 189 | t := layers[y][x] 190 | 191 | var nx []int 192 | for i, n := range layers[y-1] { 193 | if segments[[2]uint64{n, t}] { 194 | nx = append(nx, i) 195 | } 196 | } 197 | 198 | return nx 199 | } 200 | 201 | func median(P []float64) float64 { 202 | m := len(P) / 2 203 | switch { 204 | case len(P) == 0: 205 | return -1 206 | case len(P)%2 == 1: 207 | return P[m] 208 | case len(P) == 2: 209 | return (P[0] + P[1]) / 2 210 | default: 211 | left := P[m-1] - P[0] 212 | right := P[len(P)-1] - P[m] 213 | return (P[m-1]*right + P[m]*left) / (left + right) 214 | } 215 | } 216 | 217 | // SwitchAdjacentOrderingOptimizer will try swapping two adjacent nodes in a layer will improve crossings. 218 | // This is used in dot/Graphviz, Figure 3-3 in Graphviz dot paper TSE93 and called "transpose". 219 | type SwitchAdjacentOrderingOptimizer struct{} 220 | 221 | func (o SwitchAdjacentOrderingOptimizer) Optimize(segments map[[2]uint64]bool, layers [][]uint64, y int, downUp bool) { 222 | if len(layers[y]) < 2 { 223 | return 224 | } 225 | 226 | // does not have bellow 227 | if downUp && y == (len(layers)-1) { 228 | return 229 | } 230 | 231 | // does not have above 232 | if !downUp && y == 0 { 233 | return 234 | } 235 | 236 | for i := 0; i < (len(layers[y]) - 1); i++ { 237 | j := i + 1 238 | 239 | current := []uint64{layers[y][i], layers[y][j]} 240 | swapped := []uint64{layers[y][j], layers[y][i]} 241 | var currCrossings, swappedCrossings int 242 | if downUp { 243 | currCrossings = numCrossingsBetweenLayers(segments, current, layers[y+1]) 244 | swappedCrossings = numCrossingsBetweenLayers(segments, swapped, layers[y+1]) 245 | } else { 246 | currCrossings = numCrossingsBetweenLayers(segments, layers[y-1], current) 247 | swappedCrossings = numCrossingsBetweenLayers(segments, layers[y-1], swapped) 248 | } 249 | 250 | if swappedCrossings < currCrossings { 251 | layers[y][i], layers[y][j] = layers[y][j], layers[y][i] 252 | } 253 | } 254 | } 255 | 256 | // RandomLayerOrderingOptimizer picks best out of epochs random orderings. 257 | type RandomLayerOrderingOptimizer struct { 258 | Epochs int 259 | } 260 | 261 | func (o RandomLayerOrderingOptimizer) Optimize(segments map[[2]uint64]bool, layers [][]uint64, idx int, downUp bool) { 262 | bestN := -1 263 | layer := make([]uint64, len(layers[idx])) 264 | copy(layer, layers[idx]) 265 | 266 | for i := 0; i < o.Epochs; i++ { 267 | rand.Shuffle(len(layer), func(a, b int) { layer[a], layer[b] = layer[b], layer[a] }) 268 | 269 | N := 0 270 | if idx > 0 { 271 | N += numCrossingsBetweenLayers(segments, layers[idx-1], layers[idx]) 272 | } 273 | if (idx + 1) < len(layers) { 274 | N += numCrossingsBetweenLayers(segments, layers[idx], layers[idx+1]) 275 | } 276 | 277 | if bestN < 0 || N < bestN { 278 | bestN = N 279 | copy(layers[idx], layer) 280 | } 281 | } 282 | } 283 | 284 | // time: O(ntop * nbot * log(ntop)) 285 | // memory: O(ntop) 286 | func numCrossingsBetweenLayers(segments map[[2]uint64]bool, ltop, lbottom []uint64) int { 287 | sum := 0 288 | bit := NewFenwickTree(len(ltop)) 289 | for i := len(lbottom) - 1; i >= 0; i-- { 290 | node := lbottom[i] 291 | for j := len(ltop) - 1; j >= 0; j-- { 292 | neighbor := ltop[j] 293 | if segments[[2]uint64{neighbor, node}] { 294 | bit.Update(j+1, 1) 295 | sum += bit.Query(j) 296 | } 297 | } 298 | } 299 | return sum 300 | } 301 | 302 | type FenwickTree []int 303 | 304 | func NewFenwickTree(nbElements int) FenwickTree { 305 | return make(FenwickTree, nbElements+1) 306 | } 307 | 308 | func (bit FenwickTree) Update(idx int, value int) { 309 | for ; idx < len(bit); idx += idx & (-idx) { 310 | bit[idx] += value 311 | } 312 | } 313 | 314 | func (bit FenwickTree) Query(idx int) int { 315 | sum := 0 316 | for ; idx > 0; idx -= idx & (-idx) { 317 | sum += bit[idx] 318 | } 319 | return sum 320 | } 321 | 322 | // time: O(?) 323 | // memory: O(1) 324 | func numCrossings(segments map[[2]uint64]bool, layers [][]uint64) int { 325 | count := 0 326 | for i := range layers { 327 | if i == 0 { 328 | continue 329 | } 330 | count += numCrossingsBetweenLayers(segments, layers[i-1], layers[i]) 331 | } 332 | return count 333 | } 334 | -------------------------------------------------------------------------------- /layout/layout.go: -------------------------------------------------------------------------------- 1 | package layout 2 | 3 | // Layout is something that can update graph layout 4 | type Layout interface { 5 | UpdateGraphLayout(g Graph) 6 | } 7 | 8 | // SequenceLayout applies sequence of layouts 9 | type SequenceLayout struct { 10 | Layouts []Layout 11 | } 12 | 13 | func (s SequenceLayout) UpdateGraphLayout(g Graph) { 14 | for _, l := range s.Layouts { 15 | l.UpdateGraphLayout(g) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /layout/layout_test.go: -------------------------------------------------------------------------------- 1 | package layout_test 2 | 3 | import ( 4 | _ "embed" 5 | "fmt" 6 | "os" 7 | "strings" 8 | "testing" 9 | 10 | "github.com/nikolaydubina/jsonl-graph/graph" 11 | 12 | "github.com/gverger/go-graph-layout/layout" 13 | "github.com/gverger/go-graph-layout/svg" 14 | ) 15 | 16 | func parseJSONLGraph(in string) (*graph.Graph, *layout.Graph, error) { 17 | gd, err := graph.NewGraphFromJSONL(strings.NewReader(in)) 18 | if err != nil { 19 | return nil, nil, err 20 | } 21 | 22 | gl := layout.Graph{ 23 | Nodes: make(map[uint64]layout.Node), 24 | Edges: make(map[[2]uint64]layout.Edge), 25 | } 26 | 27 | for id, node := range gd.Nodes { 28 | // compute w and h for nodes, since width and height of node depends on content 29 | rnodeData := node 30 | rnode := svg.Node{ 31 | Title: node.ID(), 32 | NodeData: rnodeData, 33 | } 34 | gl.Nodes[id] = layout.Node{W: rnode.Width(), H: rnode.Height()} 35 | } 36 | 37 | for e := range gd.Edges { 38 | gl.Edges[e] = layout.Edge{} 39 | } 40 | 41 | return &gd, &gl, nil 42 | } 43 | 44 | func writeSVG(gd graph.Graph, gl layout.Graph) string { 45 | graph := svg.Graph{ 46 | ID: "graph-root", 47 | Nodes: map[uint64]svg.Node{}, 48 | Edges: map[[2]uint64]svg.Edge{}, 49 | } 50 | 51 | for id, node := range gd.Nodes { 52 | graph.Nodes[id] = svg.Node{ 53 | ID: fmt.Sprintf("%d", id), 54 | X: gl.Nodes[id].X, 55 | Y: gl.Nodes[id].Y, 56 | Title: node.ID(), 57 | NodeData: node, 58 | } 59 | } 60 | 61 | for e, edata := range gl.Edges { 62 | path := make([][2]int, 0, len(edata.Path)) 63 | for _, p := range edata.Path { 64 | path = append(path, [2]int{p.X, p.Y}) 65 | } 66 | graph.Edges[e] = svg.Edge{ 67 | Path: path, 68 | } 69 | } 70 | 71 | svgContainer := svg.SVG{ 72 | ID: "svg-root", 73 | Definitions: []svg.Renderable{}, 74 | Body: graph, 75 | } 76 | return svgContainer.Render() 77 | } 78 | 79 | //go:embed testdata/gin.jsonl 80 | var ginJSONL string 81 | 82 | //go:embed testdata/small.jsonl 83 | var smallJSONL string 84 | 85 | //go:embed testdata/brandeskopf.jsonl 86 | var brandeskopfJSONL string 87 | 88 | func TestE2E(t *testing.T) { 89 | inputJSONLGraphs := []struct { 90 | name string 91 | inputJSONLGraph string 92 | }{ 93 | { 94 | name: "gin", 95 | inputJSONLGraph: ginJSONL, 96 | }, 97 | { 98 | name: "small", 99 | inputJSONLGraph: smallJSONL, 100 | }, 101 | { 102 | name: "brandeskopf", 103 | inputJSONLGraph: brandeskopfJSONL, 104 | }, 105 | } 106 | 107 | layouts := []struct { 108 | name string 109 | l layout.Layout 110 | }{ 111 | { 112 | name: "forces", 113 | l: layout.SequenceLayout{ 114 | Layouts: []layout.Layout{ 115 | layout.ForceGraphLayout{ 116 | Delta: 1, 117 | MaxSteps: 5000, 118 | Epsilon: 1.5, 119 | Forces: []layout.Force{ 120 | layout.GravityForce{ 121 | K: -50, 122 | EdgesOnly: false, 123 | }, 124 | layout.SpringForce{ 125 | K: 0.2, 126 | L: 200, 127 | EdgesOnly: true, 128 | }, 129 | }, 130 | }, 131 | layout.DirectEdgesLayout{}, 132 | }, 133 | }, 134 | }, 135 | { 136 | name: "eades", 137 | l: layout.SequenceLayout{ 138 | Layouts: []layout.Layout{ 139 | layout.EadesGonumLayout{ 140 | Repulsion: 1, 141 | Rate: 0.05, 142 | Updates: 30, 143 | Theta: 0.2, 144 | ScaleX: 0.5, 145 | ScaleY: 0.5, 146 | }, 147 | layout.DirectEdgesLayout{}, 148 | }, 149 | }, 150 | }, 151 | { 152 | name: "isomap", 153 | l: layout.SequenceLayout{ 154 | Layouts: []layout.Layout{ 155 | layout.IsomapR2GonumLayout{ 156 | ScaleX: 0.5, 157 | ScaleY: 0.5, 158 | }, 159 | layout.DirectEdgesLayout{}, 160 | }, 161 | }, 162 | }, 163 | { 164 | name: "layers", 165 | l: layout.SugiyamaLayersStrategyGraphLayout{ 166 | CycleRemover: layout.NewSimpleCycleRemover(), 167 | LevelsAssigner: layout.NewLayeredGraph, 168 | OrderingAssigner: layout.WarfieldOrderingOptimizer{ 169 | Epochs: 100, 170 | LayerOrderingInitializer: layout.BFSOrderingInitializer{}, 171 | LayerOrderingOptimizer: layout.CompositeLayerOrderingOptimizer{ 172 | Optimizers: []layout.LayerOrderingOptimizer{ 173 | layout.WMedianOrderingOptimizer{}, 174 | layout.SwitchAdjacentOrderingOptimizer{}, 175 | }, 176 | }, 177 | }.Optimize, 178 | NodesHorizontalCoordinatesAssigner: layout.BrandesKopfLayersNodesHorizontalAssigner{ 179 | Delta: 25, 180 | }, 181 | NodesVerticalCoordinatesAssigner: layout.BasicNodesVerticalCoordinatesAssigner{ 182 | MarginLayers: 25, 183 | FakeNodeHeight: 25, 184 | }, 185 | EdgePathAssigner: layout.StraightEdgePathAssigner{}.UpdateGraphLayout, 186 | }, 187 | }, 188 | } 189 | for _, inputJSONLGraph := range inputJSONLGraphs { 190 | for _, l := range layouts { 191 | name := fmt.Sprintf("testdata/%s_%s.svg", inputJSONLGraph.name, l.name) 192 | t.Run(name, func(t *testing.T) { 193 | gd, gl, err := parseJSONLGraph(inputJSONLGraph.inputJSONLGraph) 194 | if err != nil { 195 | t.Error(err) 196 | } 197 | 198 | l.l.UpdateGraphLayout(*gl) 199 | svgResult := writeSVG(*gd, *gl) 200 | 201 | outputfile, err := os.Create(name) 202 | if err != nil { 203 | t.Error(err) 204 | } 205 | 206 | if _, err := outputfile.WriteString(svgResult); err != nil { 207 | t.Error(err) 208 | } 209 | if err := outputfile.Close(); err != nil { 210 | t.Error(err) 211 | } 212 | }) 213 | } 214 | } 215 | } 216 | -------------------------------------------------------------------------------- /layout/scaler_layout.go: -------------------------------------------------------------------------------- 1 | package layout 2 | 3 | // ScalerLayout will scale existing layout by constant factor. 4 | type ScalerLayout struct { 5 | Scale float64 6 | } 7 | 8 | func (l *ScalerLayout) UpdateGraphLayout(g Graph) { 9 | for i := range g.Nodes { 10 | x := float64(g.Nodes[i].X) 11 | y := float64(g.Nodes[i].Y) 12 | 13 | g.Nodes[i] = Node{ 14 | Position: Position{ 15 | X: int(x * l.Scale), 16 | Y: int(y * l.Scale), 17 | }, 18 | W: g.Nodes[i].W, 19 | H: g.Nodes[i].H, 20 | } 21 | } 22 | 23 | // can not recompute edge layout as some paths are complex and not direct 24 | for e := range g.Edges { 25 | for p, pos := range g.Edges[e].Path { 26 | g.Edges[e].Path[p] = Position{ 27 | X: int(float64(pos.X) * l.Scale), 28 | Y: int(float64(pos.Y) * l.Scale), 29 | } 30 | } 31 | 32 | // if edge was not previously set adding at least two nodes for start and end 33 | if len(g.Edges[e].Path) == 0 { 34 | g.Edges[e] = Edge{Path: make([]Position, 2)} 35 | } 36 | 37 | // end and start should use center coordinates of nodes 38 | // note, this overrites ports for edges 39 | g.Edges[e].Path[0] = g.Nodes[e[0]].CenterXY() 40 | g.Edges[e].Path[len(g.Edges[e].Path)-1] = g.Nodes[e[1]].CenterXY() 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /layout/testdata/brandeskopf.jsonl: -------------------------------------------------------------------------------- 1 | {"from": "1", "to": "13"} 2 | {"from": "1", "to": "21"} 3 | {"from": "1", "to": "4"} 4 | {"from": "1", "to": "3"} 5 | {"from": "2", "to": "3"} 6 | {"from": "2", "to": "20"} 7 | {"from": "3", "to": "4"} 8 | {"from": "3", "to": "5"} 9 | {"from": "3", "to": "23"} 10 | {"from": "4", "to": "6"} 11 | {"from": "5", "to": "7"} 12 | {"from": "6", "to": "8"} 13 | {"from": "6", "to": "16"} 14 | {"from": "6", "to": "23"} 15 | {"from": "7", "to": "9"} 16 | {"from": "8", "to": "10"} 17 | {"from": "8", "to": "11"} 18 | {"from": "9", "to": "12"} 19 | {"from": "10", "to": "13"} 20 | {"from": "10", "to": "14"} 21 | {"from": "10", "to": "15"} 22 | {"from": "11", "to": "15"} 23 | {"from": "11", "to": "16"} 24 | {"from": "12", "to": "20"} 25 | {"from": "13", "to": "17"} 26 | {"from": "14", "to": "17"} 27 | {"from": "14", "to": "18"} 28 | {"from": "16", "to": "18"} 29 | {"from": "16", "to": "19"} 30 | {"from": "16", "to": "20"} 31 | {"from": "18", "to": "21"} 32 | {"from": "19", "to": "22"} 33 | {"from": "21", "to": "23"} 34 | {"from": "22", "to": "23"} 35 | -------------------------------------------------------------------------------- /layout/testdata/brandeskopf_eades.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 |
44 | 45 |
46 | 1 47 |
48 |
49 | 50 | 51 |
52 |
53 | 54 |
55 |
56 |
57 | 58 | 59 | 60 | 61 |
62 | 63 |
64 | 13 65 |
66 |
67 | 68 | 69 |
70 |
71 | 72 |
73 |
74 |
75 | 76 | 77 | 78 | 79 |
80 | 81 |
82 | 16 83 |
84 |
85 | 86 | 87 |
88 |
89 | 90 |
91 |
92 |
93 | 94 | 95 | 96 | 97 |
98 | 99 |
100 | 9 101 |
102 |
103 | 104 | 105 |
106 |
107 | 108 |
109 |
110 |
111 | 112 | 113 | 114 | 115 |
116 | 117 |
118 | 19 119 |
120 |
121 | 122 | 123 |
124 |
125 | 126 |
127 |
128 |
129 | 130 | 131 | 132 | 133 |
134 | 135 |
136 | 11 137 |
138 |
139 | 140 | 141 |
142 |
143 | 144 |
145 |
146 |
147 | 148 | 149 | 150 | 151 |
152 | 153 |
154 | 2 155 |
156 |
157 | 158 | 159 |
160 |
161 | 162 |
163 |
164 |
165 | 166 | 167 | 168 | 169 |
170 | 171 |
172 | 7 173 |
174 |
175 | 176 | 177 |
178 |
179 | 180 |
181 |
182 |
183 | 184 | 185 | 186 | 187 |
188 | 189 |
190 | 12 191 |
192 |
193 | 194 | 195 |
196 |
197 | 198 |
199 |
200 |
201 | 202 | 203 | 204 | 205 |
206 | 207 |
208 | 5 209 |
210 |
211 | 212 | 213 |
214 |
215 | 216 |
217 |
218 |
219 | 220 | 221 | 222 | 223 |
224 | 225 |
226 | 21 227 |
228 |
229 | 230 | 231 |
232 |
233 | 234 |
235 |
236 |
237 | 238 | 239 | 240 | 241 |
242 | 243 |
244 | 15 245 |
246 |
247 | 248 | 249 |
250 |
251 | 252 |
253 |
254 |
255 | 256 | 257 | 258 | 259 |
260 | 261 |
262 | 8 263 |
264 |
265 | 266 | 267 |
268 |
269 | 270 |
271 |
272 |
273 | 274 | 275 | 276 | 277 |
278 | 279 |
280 | 18 281 |
282 |
283 | 284 | 285 |
286 |
287 | 288 |
289 |
290 |
291 | 292 | 293 | 294 | 295 |
296 | 297 |
298 | 20 299 |
300 |
301 | 302 | 303 |
304 |
305 | 306 |
307 |
308 |
309 | 310 | 311 | 312 | 313 |
314 | 315 |
316 | 23 317 |
318 |
319 | 320 | 321 |
322 |
323 | 324 |
325 |
326 |
327 | 328 | 329 | 330 | 331 |
332 | 333 |
334 | 6 335 |
336 |
337 | 338 | 339 |
340 |
341 | 342 |
343 |
344 |
345 | 346 | 347 | 348 | 349 |
350 | 351 |
352 | 22 353 |
354 |
355 | 356 | 357 |
358 |
359 | 360 |
361 |
362 |
363 | 364 | 365 | 366 | 367 |
368 | 369 |
370 | 17 371 |
372 |
373 | 374 | 375 |
376 |
377 | 378 |
379 |
380 |
381 | 382 | 383 | 384 | 385 |
386 | 387 |
388 | 4 389 |
390 |
391 | 392 | 393 |
394 |
395 | 396 |
397 |
398 |
399 | 400 | 401 | 402 | 403 |
404 | 405 |
406 | 14 407 |
408 |
409 | 410 | 411 |
412 |
413 | 414 |
415 |
416 |
417 | 418 | 419 | 420 | 421 |
422 | 423 |
424 | 3 425 |
426 |
427 | 428 | 429 |
430 |
431 | 432 |
433 |
434 |
435 | 436 | 437 | 438 | 439 |
440 | 441 |
442 | 10 443 |
444 |
445 | 446 | 447 |
448 |
449 | 450 |
451 |
452 |
453 | 454 |
455 |
-------------------------------------------------------------------------------- /layout/testdata/brandeskopf_forces.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 |
44 | 45 |
46 | 12 47 |
48 |
49 | 50 | 51 |
52 |
53 | 54 |
55 |
56 |
57 | 58 | 59 | 60 | 61 |
62 | 63 |
64 | 20 65 |
66 |
67 | 68 | 69 |
70 |
71 | 72 |
73 |
74 |
75 | 76 | 77 | 78 | 79 |
80 | 81 |
82 | 2 83 |
84 |
85 | 86 | 87 |
88 |
89 | 90 |
91 |
92 |
93 | 94 | 95 | 96 | 97 |
98 | 99 |
100 | 7 101 |
102 |
103 | 104 | 105 |
106 |
107 | 108 |
109 |
110 |
111 | 112 | 113 | 114 | 115 |
116 | 117 |
118 | 14 119 |
120 |
121 | 122 | 123 |
124 |
125 | 126 |
127 |
128 |
129 | 130 | 131 | 132 | 133 |
134 | 135 |
136 | 21 137 |
138 |
139 | 140 | 141 |
142 |
143 | 144 |
145 |
146 |
147 | 148 | 149 | 150 | 151 |
152 | 153 |
154 | 17 155 |
156 |
157 | 158 | 159 |
160 |
161 | 162 |
163 |
164 |
165 | 166 | 167 | 168 | 169 |
170 | 171 |
172 | 13 173 |
174 |
175 | 176 | 177 |
178 |
179 | 180 |
181 |
182 |
183 | 184 | 185 | 186 | 187 |
188 | 189 |
190 | 5 191 |
192 |
193 | 194 | 195 |
196 |
197 | 198 |
199 |
200 |
201 | 202 | 203 | 204 | 205 |
206 | 207 |
208 | 6 209 |
210 |
211 | 212 | 213 |
214 |
215 | 216 |
217 |
218 |
219 | 220 | 221 | 222 | 223 |
224 | 225 |
226 | 19 227 |
228 |
229 | 230 | 231 |
232 |
233 | 234 |
235 |
236 |
237 | 238 | 239 | 240 | 241 |
242 | 243 |
244 | 18 245 |
246 |
247 | 248 | 249 |
250 |
251 | 252 |
253 |
254 |
255 | 256 | 257 | 258 | 259 |
260 | 261 |
262 | 9 263 |
264 |
265 | 266 | 267 |
268 |
269 | 270 |
271 |
272 |
273 | 274 | 275 | 276 | 277 |
278 | 279 |
280 | 10 281 |
282 |
283 | 284 | 285 |
286 |
287 | 288 |
289 |
290 |
291 | 292 | 293 | 294 | 295 |
296 | 297 |
298 | 1 299 |
300 |
301 | 302 | 303 |
304 |
305 | 306 |
307 |
308 |
309 | 310 | 311 | 312 | 313 |
314 | 315 |
316 | 22 317 |
318 |
319 | 320 | 321 |
322 |
323 | 324 |
325 |
326 |
327 | 328 | 329 | 330 | 331 |
332 | 333 |
334 | 11 335 |
336 |
337 | 338 | 339 |
340 |
341 | 342 |
343 |
344 |
345 | 346 | 347 | 348 | 349 |
350 | 351 |
352 | 23 353 |
354 |
355 | 356 | 357 |
358 |
359 | 360 |
361 |
362 |
363 | 364 | 365 | 366 | 367 |
368 | 369 |
370 | 4 371 |
372 |
373 | 374 | 375 |
376 |
377 | 378 |
379 |
380 |
381 | 382 | 383 | 384 | 385 |
386 | 387 |
388 | 3 389 |
390 |
391 | 392 | 393 |
394 |
395 | 396 |
397 |
398 |
399 | 400 | 401 | 402 | 403 |
404 | 405 |
406 | 15 407 |
408 |
409 | 410 | 411 |
412 |
413 | 414 |
415 |
416 |
417 | 418 | 419 | 420 | 421 |
422 | 423 |
424 | 8 425 |
426 |
427 | 428 | 429 |
430 |
431 | 432 |
433 |
434 |
435 | 436 | 437 | 438 | 439 |
440 | 441 |
442 | 16 443 |
444 |
445 | 446 | 447 |
448 |
449 | 450 |
451 |
452 |
453 | 454 |
455 |
-------------------------------------------------------------------------------- /layout/testdata/brandeskopf_isomap.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 |
44 | 45 |
46 | 4 47 |
48 |
49 | 50 | 51 |
52 |
53 | 54 |
55 |
56 |
57 | 58 | 59 | 60 | 61 |
62 | 63 |
64 | 6 65 |
66 |
67 | 68 | 69 |
70 |
71 | 72 |
73 |
74 |
75 | 76 | 77 | 78 | 79 |
80 | 81 |
82 | 9 83 |
84 |
85 | 86 | 87 |
88 |
89 | 90 |
91 |
92 |
93 | 94 | 95 | 96 | 97 |
98 | 99 |
100 | 19 101 |
102 |
103 | 104 | 105 |
106 |
107 | 108 |
109 |
110 |
111 | 112 | 113 | 114 | 115 |
116 | 117 |
118 | 1 119 |
120 |
121 | 122 | 123 |
124 |
125 | 126 |
127 |
128 |
129 | 130 | 131 | 132 | 133 |
134 | 135 |
136 | 18 137 |
138 |
139 | 140 | 141 |
142 |
143 | 144 |
145 |
146 |
147 | 148 | 149 | 150 | 151 |
152 | 153 |
154 | 5 155 |
156 |
157 | 158 | 159 |
160 |
161 | 162 |
163 |
164 |
165 | 166 | 167 | 168 | 169 |
170 | 171 |
172 | 8 173 |
174 |
175 | 176 | 177 |
178 |
179 | 180 |
181 |
182 |
183 | 184 | 185 | 186 | 187 |
188 | 189 |
190 | 20 191 |
192 |
193 | 194 | 195 |
196 |
197 | 198 |
199 |
200 |
201 | 202 | 203 | 204 | 205 |
206 | 207 |
208 | 10 209 |
210 |
211 | 212 | 213 |
214 |
215 | 216 |
217 |
218 |
219 | 220 | 221 | 222 | 223 |
224 | 225 |
226 | 23 227 |
228 |
229 | 230 | 231 |
232 |
233 | 234 |
235 |
236 |
237 | 238 | 239 | 240 | 241 |
242 | 243 |
244 | 14 245 |
246 |
247 | 248 | 249 |
250 |
251 | 252 |
253 |
254 |
255 | 256 | 257 | 258 | 259 |
260 | 261 |
262 | 13 263 |
264 |
265 | 266 | 267 |
268 |
269 | 270 |
271 |
272 |
273 | 274 | 275 | 276 | 277 |
278 | 279 |
280 | 21 281 |
282 |
283 | 284 | 285 |
286 |
287 | 288 |
289 |
290 |
291 | 292 | 293 | 294 | 295 |
296 | 297 |
298 | 16 299 |
300 |
301 | 302 | 303 |
304 |
305 | 306 |
307 |
308 |
309 | 310 | 311 | 312 | 313 |
314 | 315 |
316 | 11 317 |
318 |
319 | 320 | 321 |
322 |
323 | 324 |
325 |
326 |
327 | 328 | 329 | 330 | 331 |
332 | 333 |
334 | 22 335 |
336 |
337 | 338 | 339 |
340 |
341 | 342 |
343 |
344 |
345 | 346 | 347 | 348 | 349 |
350 | 351 |
352 | 2 353 |
354 |
355 | 356 | 357 |
358 |
359 | 360 |
361 |
362 |
363 | 364 | 365 | 366 | 367 |
368 | 369 |
370 | 12 371 |
372 |
373 | 374 | 375 |
376 |
377 | 378 |
379 |
380 |
381 | 382 | 383 | 384 | 385 |
386 | 387 |
388 | 17 389 |
390 |
391 | 392 | 393 |
394 |
395 | 396 |
397 |
398 |
399 | 400 | 401 | 402 | 403 |
404 | 405 |
406 | 15 407 |
408 |
409 | 410 | 411 |
412 |
413 | 414 |
415 |
416 |
417 | 418 | 419 | 420 | 421 |
422 | 423 |
424 | 7 425 |
426 |
427 | 428 | 429 |
430 |
431 | 432 |
433 |
434 |
435 | 436 | 437 | 438 | 439 |
440 | 441 |
442 | 3 443 |
444 |
445 | 446 | 447 |
448 |
449 | 450 |
451 |
452 |
453 | 454 |
455 |
-------------------------------------------------------------------------------- /layout/testdata/brandeskopf_layers.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 |
44 | 45 |
46 | 5 47 |
48 |
49 | 50 | 51 |
52 |
53 | 54 |
55 |
56 |
57 | 58 | 59 | 60 | 61 |
62 | 63 |
64 | 15 65 |
66 |
67 | 68 | 69 |
70 |
71 | 72 |
73 |
74 |
75 | 76 | 77 | 78 | 79 |
80 | 81 |
82 | 1 83 |
84 |
85 | 86 | 87 |
88 |
89 | 90 |
91 |
92 |
93 | 94 | 95 | 96 | 97 |
98 | 99 |
100 | 21 101 |
102 |
103 | 104 | 105 |
106 |
107 | 108 |
109 |
110 |
111 | 112 | 113 | 114 | 115 |
116 | 117 |
118 | 13 119 |
120 |
121 | 122 | 123 |
124 |
125 | 126 |
127 |
128 |
129 | 130 | 131 | 132 | 133 |
134 | 135 |
136 | 12 137 |
138 |
139 | 140 | 141 |
142 |
143 | 144 |
145 |
146 |
147 | 148 | 149 | 150 | 151 |
152 | 153 |
154 | 16 155 |
156 |
157 | 158 | 159 |
160 |
161 | 162 |
163 |
164 |
165 | 166 | 167 | 168 | 169 |
170 | 171 |
172 | 19 173 |
174 |
175 | 176 | 177 |
178 |
179 | 180 |
181 |
182 |
183 | 184 | 185 | 186 | 187 |
188 | 189 |
190 | 20 191 |
192 |
193 | 194 | 195 |
196 |
197 | 198 |
199 |
200 |
201 | 202 | 203 | 204 | 205 |
206 | 207 |
208 | 18 209 |
210 |
211 | 212 | 213 |
214 |
215 | 216 |
217 |
218 |
219 | 220 | 221 | 222 | 223 |
224 | 225 |
226 | 7 227 |
228 |
229 | 230 | 231 |
232 |
233 | 234 |
235 |
236 |
237 | 238 | 239 | 240 | 241 |
242 | 243 |
244 | 8 245 |
246 |
247 | 248 | 249 |
250 |
251 | 252 |
253 |
254 |
255 | 256 | 257 | 258 | 259 |
260 | 261 |
262 | 10 263 |
264 |
265 | 266 | 267 |
268 |
269 | 270 |
271 |
272 |
273 | 274 | 275 | 276 | 277 |
278 | 279 |
280 | 4 281 |
282 |
283 | 284 | 285 |
286 |
287 | 288 |
289 |
290 |
291 | 292 | 293 | 294 | 295 |
296 | 297 |
298 | 11 299 |
300 |
301 | 302 | 303 |
304 |
305 | 306 |
307 |
308 |
309 | 310 | 311 | 312 | 313 |
314 | 315 |
316 | 3 317 |
318 |
319 | 320 | 321 |
322 |
323 | 324 |
325 |
326 |
327 | 328 | 329 | 330 | 331 |
332 | 333 |
334 | 6 335 |
336 |
337 | 338 | 339 |
340 |
341 | 342 |
343 |
344 |
345 | 346 | 347 | 348 | 349 |
350 | 351 |
352 | 14 353 |
354 |
355 | 356 | 357 |
358 |
359 | 360 |
361 |
362 |
363 | 364 | 365 | 366 | 367 |
368 | 369 |
370 | 2 371 |
372 |
373 | 374 | 375 |
376 |
377 | 378 |
379 |
380 |
381 | 382 | 383 | 384 | 385 |
386 | 387 |
388 | 9 389 |
390 |
391 | 392 | 393 |
394 |
395 | 396 |
397 |
398 |
399 | 400 | 401 | 402 | 403 |
404 | 405 |
406 | 22 407 |
408 |
409 | 410 | 411 |
412 |
413 | 414 |
415 |
416 |
417 | 418 | 419 | 420 | 421 |
422 | 423 |
424 | 17 425 |
426 |
427 | 428 | 429 |
430 |
431 | 432 |
433 |
434 |
435 | 436 | 437 | 438 | 439 |
440 | 441 |
442 | 23 443 |
444 |
445 | 446 | 447 |
448 |
449 | 450 |
451 |
452 |
453 | 454 |
455 |
-------------------------------------------------------------------------------- /layout/testdata/gin.jsonl: -------------------------------------------------------------------------------- 1 | {"id":"github.com/gin-gonic/gin","can_get_git":true,"can_run_tests":true,"can_get_github":true,"github_url":"https://github.com/gin-gonic/gin","git_url":"https://github.com/gin-gonic/gin","git_last_commit":"2021-04-21","git_last_commit_days_since":4,"git_num_contributors":321,"codecov_url":"https://app.codecov.io/gh/gin-gonic/gin","codecov_files":41,"codecov_lines":2036,"codecov_coverage":98.67,"gotest_has_tests":true,"gotest_all_tests_passed":true,"gotest_num_packages":6,"gotest_num_packages_with_tests":4,"gotest_num_packages_tests_passed":4,"gotest_package_coverage_avg":98.9,"goreportcard_average":0.99,"goreportcard_grade":"A+","goreportcard_files":83,"goreportcard_issues":6,"files_has_benchmarks":true,"files_has_tests":true,"awesomelists_is_mentioned":true,"github_repo_stars":47520} 2 | {"id":"github.com/gin-contrib/sse","can_get_git":true,"can_run_tests":true,"can_get_github":true,"github_url":"https://github.com/gin-contrib/sse","git_url":"https://github.com/gin-contrib/sse","git_last_commit":"2020-08-15","git_last_commit_days_since":252,"git_num_contributors":3,"codecov_url":"https://app.codecov.io/gh/gin-contrib/sse","codecov_files":3,"codecov_lines":107,"codecov_coverage":90.65,"gotest_has_tests":true,"gotest_all_tests_passed":true,"gotest_num_packages":1,"gotest_num_packages_with_tests":1,"gotest_num_packages_tests_passed":1,"gotest_package_coverage_avg":94.1,"goreportcard_average":0.93,"goreportcard_grade":"A+","goreportcard_files":5,"goreportcard_issues":3,"files_has_benchmarks":true,"files_has_tests":true,"github_repo_stars":56} 3 | {"id":"github.com/go-playground/validator/v10","can_get_git":true,"can_run_tests":true,"can_get_github":true,"github_url":"https://github.com/go-playground/validator","git_url":"https://github.com/go-playground/validator","git_last_commit":"2021-04-07","git_last_commit_days_since":18,"git_num_contributors":116,"gotest_has_tests":true,"gotest_all_tests_passed":true,"gotest_num_packages":14,"gotest_num_packages_with_tests":14,"gotest_num_packages_tests_passed":14,"gotest_package_coverage_avg":73.70714285714287,"goreportcard_average":0.87,"goreportcard_grade":"A","goreportcard_files":48,"goreportcard_issues":28,"files_has_benchmarks":true,"files_has_tests":true,"awesomelists_is_mentioned":true,"github_repo_stars":7569} 4 | {"id":"github.com/golang/protobuf","can_get_git":true,"can_run_tests":true,"can_get_github":true,"github_url":"https://github.com/golang/protobuf","git_url":"https://github.com/golang/protobuf","git_last_commit":"2021-03-30","git_last_commit_days_since":26,"git_num_contributors":96,"gotest_has_tests":true,"gotest_all_tests_passed":true,"gotest_num_packages":21,"gotest_num_packages_with_tests":5,"gotest_num_packages_tests_passed":5,"gotest_package_coverage_avg":79.8,"goreportcard_average":0.92,"goreportcard_grade":"A+","goreportcard_files":39,"goreportcard_issues":16,"files_has_benchmarks":false,"files_has_tests":true,"readme_deprecated":true,"awesomelists_is_mentioned":true,"github_repo_stars":7608} 5 | {"id":"github.com/json-iterator/go","can_get_git":true,"can_run_tests":false,"can_get_github":true,"github_url":"https://github.com/json-iterator/go","git_url":"https://github.com/json-iterator/go","git_last_commit":"2020-11-18","git_last_commit_days_since":158,"git_num_contributors":55,"codecov_url":"https://app.codecov.io/gh/json-iterator/go","codecov_files":41,"codecov_lines":5115,"codecov_coverage":86.37,"goreportcard_average":0.95,"goreportcard_grade":"A+","goreportcard_files":127,"goreportcard_issues":47,"files_has_benchmarks":true,"files_has_tests":true,"awesomelists_is_mentioned":true,"github_repo_stars":9204} 6 | {"id":"github.com/mattn/go-isatty","can_get_git":true,"can_run_tests":true,"can_get_github":true,"github_url":"https://github.com/mattn/go-isatty","git_url":"https://github.com/mattn/go-isatty","git_last_commit":"2021-01-18","git_last_commit_days_since":96,"git_num_contributors":23,"codecov_url":"https://app.codecov.io/gh/mattn/go-isatty","codecov_files":1,"codecov_lines":3,"codecov_coverage":100,"gotest_has_tests":true,"gotest_all_tests_passed":true,"gotest_num_packages":1,"gotest_num_packages_with_tests":1,"gotest_num_packages_tests_passed":1,"gotest_package_coverage_avg":100,"goreportcard_average":1.00,"goreportcard_grade":"A+","goreportcard_files":10,"goreportcard_issues":1,"files_has_benchmarks":false,"files_has_tests":true,"awesomelists_is_mentioned":true,"github_repo_stars":511} 7 | {"id":"github.com/stretchr/testify","can_get_git":true,"can_run_tests":true,"can_get_github":true,"github_url":"https://github.com/stretchr/testify","git_url":"https://github.com/stretchr/testify","git_last_commit":"2020-11-09","git_last_commit_days_since":167,"git_num_contributors":217,"gotest_has_tests":true,"gotest_all_tests_passed":true,"gotest_num_packages":6,"gotest_num_packages_with_tests":5,"gotest_num_packages_tests_passed":5,"gotest_package_coverage_avg":54.260000000000005,"goreportcard_average":0.98,"goreportcard_grade":"A+","goreportcard_files":36,"goreportcard_issues":6,"files_has_benchmarks":true,"files_has_tests":true,"readme_deprecated":true,"awesomelists_is_mentioned":true,"github_repo_stars":13106} 8 | {"id":"github.com/ugorji/go/codec","can_get_git":true,"can_run_tests":false,"can_get_github":true,"github_url":"https://github.com/ugorji/go","git_url":"https://github.com/ugorji/go","git_last_commit":"2021-04-19","git_last_commit_days_since":5,"git_num_contributors":13,"codecov_url":"https://app.codecov.io/gh/ugorji/go","codecov_files":29,"codecov_lines":11645,"codecov_coverage":90.06,"goreportcard_average":0.93,"goreportcard_grade":"A+","goreportcard_files":69,"goreportcard_issues":24,"files_has_benchmarks":true,"files_has_tests":true,"awesomelists_is_mentioned":true,"github_repo_stars":1532} 9 | {"id":"gopkg.in/yaml.v2","can_get_git":true,"can_run_tests":false,"can_get_github":false,"git_url":"https://gopkg.in/yaml.v2","git_last_commit":"2020-11-17","git_last_commit_days_since":158,"git_num_contributors":47,"goreportcard_average":0.75,"goreportcard_grade":"B","goreportcard_files":18,"goreportcard_issues":15,"files_has_benchmarks":true,"files_has_tests":true,"awesomelists_is_mentioned":true} 10 | {"id":"github.com/go-playground/assert/v2","can_get_git":true,"can_run_tests":true,"can_get_github":true,"github_url":"https://github.com/go-playground/assert","git_url":"https://github.com/go-playground/assert","git_last_commit":"2019-10-18","git_last_commit_days_since":555,"git_num_contributors":4,"gotest_has_tests":true,"gotest_all_tests_passed":true,"gotest_num_packages":1,"gotest_num_packages_with_tests":1,"gotest_num_packages_tests_passed":1,"gotest_package_coverage_avg":70.4,"goreportcard_average":1.00,"goreportcard_grade":"A+","goreportcard_files":3,"goreportcard_issues":0,"files_has_benchmarks":false,"files_has_tests":true,"awesomelists_is_mentioned":true,"github_repo_stars":32} 11 | {"id":"github.com/go-playground/locales","can_get_git":true,"can_run_tests":true,"can_get_github":true,"github_url":"https://github.com/go-playground/locales","git_url":"https://github.com/go-playground/locales","git_last_commit":"2020-09-28","git_last_commit_days_since":209,"git_num_contributors":10,"gotest_has_tests":true,"gotest_all_tests_passed":true,"gotest_num_packages":739,"gotest_num_packages_with_tests":736,"gotest_num_packages_tests_passed":736,"gotest_package_coverage_avg":8.753668478260886,"goreportcard_average":0.46,"goreportcard_grade":"E","goreportcard_files":1477,"goreportcard_issues":924,"files_has_benchmarks":true,"files_has_tests":true,"github_repo_stars":174} 12 | {"id":"github.com/go-playground/universal-translator","can_get_git":true,"can_run_tests":true,"can_get_github":true,"github_url":"https://github.com/go-playground/universal-translator","git_url":"https://github.com/go-playground/universal-translator","git_last_commit":"2019-11-12","git_last_commit_days_since":530,"git_num_contributors":7,"gotest_has_tests":true,"gotest_all_tests_passed":true,"gotest_num_packages":1,"gotest_num_packages_with_tests":1,"gotest_num_packages_tests_passed":1,"gotest_package_coverage_avg":99,"goreportcard_average":0.95,"goreportcard_grade":"A+","goreportcard_files":10,"goreportcard_issues":4,"files_has_benchmarks":true,"files_has_tests":true,"github_repo_stars":205} 13 | {"id":"github.com/leodido/go-urn","can_get_git":true,"can_run_tests":true,"can_get_github":true,"github_url":"https://github.com/leodido/go-urn","git_url":"https://github.com/leodido/go-urn","git_last_commit":"2020-12-14","git_last_commit_days_since":132,"git_num_contributors":3,"codecov_url":"https://app.codecov.io/gh/leodido/go-urn","codecov_files":1,"codecov_lines":34,"codecov_coverage":100,"gotest_has_tests":true,"gotest_all_tests_passed":true,"gotest_num_packages":1,"gotest_num_packages_with_tests":1,"gotest_num_packages_tests_passed":1,"gotest_package_coverage_avg":45.5,"goreportcard_average":0.84,"goreportcard_grade":"A","goreportcard_files":6,"goreportcard_issues":3,"files_has_benchmarks":true,"files_has_tests":true,"github_repo_stars":32} 14 | {"id":"golang.org/x/crypto","can_get_git":true,"can_run_tests":true,"can_get_github":false,"git_url":"https://go.googlesource.com/crypto","git_last_commit":"2020-11-25","git_last_commit_days_since":151,"git_num_contributors":255,"gotest_has_tests":true,"gotest_all_tests_passed":true,"gotest_num_packages":55,"gotest_num_packages_with_tests":48,"gotest_num_packages_tests_passed":48,"gotest_package_coverage_avg":82.68125,"goreportcard_average":0.96,"goreportcard_grade":"A+","goreportcard_files":324,"goreportcard_issues":84,"files_has_benchmarks":true,"files_has_tests":true,"awesomelists_is_mentioned":true} 15 | {"id":"github.com/davecgh/go-spew","can_get_git":true,"can_run_tests":false,"can_get_github":true,"github_url":"https://github.com/davecgh/go-spew","git_url":"https://github.com/davecgh/go-spew","git_last_commit":"2018-08-31","git_last_commit_days_since":968,"git_num_contributors":20,"goreportcard_average":0.98,"goreportcard_grade":"A+","goreportcard_files":17,"goreportcard_issues":4,"files_has_benchmarks":false,"files_has_tests":true,"awesomelists_is_mentioned":true,"github_repo_stars":4389} 16 | {"id":"github.com/google/gofuzz","can_get_git":true,"can_run_tests":true,"can_get_github":true,"github_url":"https://github.com/google/gofuzz","git_url":"https://github.com/google/gofuzz","git_last_commit":"2021-01-06","git_last_commit_days_since":109,"git_num_contributors":19,"gotest_has_tests":true,"gotest_all_tests_passed":true,"gotest_num_packages":2,"gotest_num_packages_with_tests":2,"gotest_num_packages_tests_passed":2,"gotest_package_coverage_avg":86.7,"goreportcard_average":0.94,"goreportcard_grade":"A+","goreportcard_files":6,"goreportcard_issues":2,"files_has_benchmarks":true,"files_has_tests":true,"awesomelists_is_mentioned":true,"github_repo_stars":1036} 17 | {"id":"github.com/modern-go/concurrent","can_get_git":true,"can_run_tests":false,"can_get_github":true,"github_url":"https://github.com/modern-go/concurrent","git_url":"https://github.com/modern-go/concurrent","git_last_commit":"2018-03-06","git_last_commit_days_since":1146,"git_num_contributors":2,"codecov_url":"https://app.codecov.io/gh/modern-go/concurrent","codecov_files":3,"codecov_lines":69,"codecov_coverage":79.71,"goreportcard_average":0.85,"goreportcard_grade":"A","goreportcard_files":7,"goreportcard_issues":3,"files_has_benchmarks":false,"files_has_tests":true,"github_repo_stars":187} 18 | {"id":"github.com/modern-go/reflect2","can_get_git":true,"can_run_tests":false,"can_get_github":true,"github_url":"https://github.com/modern-go/reflect2","git_url":"https://github.com/modern-go/reflect2","git_last_commit":"2021-01-09","git_last_commit_days_since":106,"git_num_contributors":7,"codecov_url":"https://app.codecov.io/gh/modern-go/reflect2","codecov_files":19,"codecov_lines":533,"codecov_coverage":65.29,"goreportcard_average":0.93,"goreportcard_grade":"A+","goreportcard_files":22,"goreportcard_issues":10,"files_has_benchmarks":false,"files_has_tests":false,"github_repo_stars":413} 19 | {"id":"golang.org/x/sys","can_get_git":true,"can_run_tests":true,"can_get_github":false,"git_url":"https://go.googlesource.com/sys","git_last_commit":"2021-04-20","git_last_commit_days_since":5,"git_num_contributors":212,"gotest_has_tests":true,"gotest_all_tests_passed":true,"gotest_num_packages":5,"gotest_num_packages_with_tests":4,"gotest_num_packages_tests_passed":4,"gotest_package_coverage_avg":40.325,"goreportcard_average":0.99,"goreportcard_grade":"A+","goreportcard_files":381,"goreportcard_issues":23,"files_has_benchmarks":false,"files_has_tests":true,"awesomelists_is_mentioned":true} 20 | {"id":"github.com/pmezard/go-difflib","can_get_git":true,"can_run_tests":false,"can_get_github":true,"github_url":"https://github.com/pmezard/go-difflib","git_url":"https://github.com/pmezard/go-difflib","git_last_commit":"2018-12-26","git_last_commit_days_since":850,"git_num_contributors":6,"goreportcard_average":0.53,"goreportcard_grade":"D","goreportcard_files":2,"goreportcard_issues":2,"files_has_benchmarks":true,"files_has_tests":true,"readme_deprecated":true,"github_repo_stars":263} 21 | {"id":"github.com/stretchr/objx","can_get_git":true,"can_run_tests":true,"can_get_github":true,"github_url":"https://github.com/stretchr/objx","git_url":"https://github.com/stretchr/objx","git_last_commit":"2021-02-08","git_last_commit_days_since":75,"git_num_contributors":12,"gotest_has_tests":true,"gotest_all_tests_passed":true,"gotest_num_packages":1,"gotest_num_packages_with_tests":1,"gotest_num_packages_tests_passed":1,"gotest_package_coverage_avg":99.1,"goreportcard_average":0.97,"goreportcard_grade":"A+","goreportcard_files":21,"goreportcard_issues":4,"files_has_benchmarks":false,"files_has_tests":true,"github_repo_stars":364} 22 | {"id":"github.com/ugorji/go","can_get_git":true,"can_run_tests":false,"can_get_github":true,"github_url":"https://github.com/ugorji/go","git_url":"https://github.com/ugorji/go","git_last_commit":"2021-04-19","git_last_commit_days_since":5,"git_num_contributors":13,"codecov_url":"https://app.codecov.io/gh/ugorji/go","codecov_files":29,"codecov_lines":11645,"codecov_coverage":90.06,"goreportcard_average":0.93,"goreportcard_grade":"A+","goreportcard_files":69,"goreportcard_issues":24,"files_has_benchmarks":true,"files_has_tests":true,"awesomelists_is_mentioned":true,"github_repo_stars":1532} 23 | {"id":"gopkg.in/check.v1","can_get_git":false,"can_run_tests":true,"can_get_github":false,"git_url":"https://gopkg.in/check.v1","gotest_has_tests":true,"gotest_all_tests_passed":true,"gotest_num_packages":1,"gotest_num_packages_with_tests":1,"gotest_num_packages_tests_passed":1,"gotest_package_coverage_avg":92.6,"goreportcard_average":0.89,"goreportcard_grade":"A","goreportcard_files":19,"goreportcard_issues":8,"files_has_benchmarks":false,"files_has_tests":true,"awesomelists_is_mentioned":true} 24 | {"id":"golang.org/x/text","can_get_git":true,"can_run_tests":true,"can_get_github":false,"git_url":"https://go.googlesource.com/text","git_last_commit":"2021-04-11","git_last_commit_days_since":14,"git_num_contributors":56,"gotest_has_tests":true,"gotest_all_tests_passed":true,"gotest_num_packages":63,"gotest_num_packages_with_tests":48,"gotest_num_packages_tests_passed":48,"gotest_package_coverage_avg":81.4833333333333,"goreportcard_average":0.94,"goreportcard_grade":"A+","goreportcard_files":375,"goreportcard_issues":125,"files_has_benchmarks":true,"files_has_tests":true,"readme_deprecated":true,"awesomelists_is_mentioned":true} 25 | {"id":"golang.org/x/net","can_get_git":true,"can_run_tests":true,"can_get_github":false,"git_url":"https://go.googlesource.com/net","git_last_commit":"2021-04-20","git_last_commit_days_since":4,"git_num_contributors":196,"gotest_has_tests":true,"gotest_all_tests_passed":true,"gotest_num_packages":33,"gotest_num_packages_with_tests":30,"gotest_num_packages_tests_passed":30,"gotest_package_coverage_avg":71.35666666666665,"goreportcard_average":0.98,"goreportcard_grade":"A+","goreportcard_files":432,"goreportcard_issues":73,"files_has_benchmarks":true,"files_has_tests":true,"awesomelists_is_mentioned":true} 26 | {"id":"golang.org/x/tools","can_get_git":true,"can_run_tests":false,"can_get_github":false,"git_url":"https://go.googlesource.com/tools","git_last_commit":"2021-04-20","git_last_commit_days_since":4,"git_num_contributors":389,"goreportcard_average":0.94,"goreportcard_grade":"A+","goreportcard_files":832,"goreportcard_issues":346,"files_has_benchmarks":true,"files_has_tests":true,"awesomelists_is_mentioned":true} 27 | {"from":"github.com/gin-gonic/gin","to":"github.com/gin-contrib/sse"} 28 | {"from":"github.com/gin-gonic/gin","to":"github.com/go-playground/validator/v10"} 29 | {"from":"github.com/gin-gonic/gin","to":"github.com/golang/protobuf"} 30 | {"from":"github.com/gin-gonic/gin","to":"github.com/json-iterator/go"} 31 | {"from":"github.com/gin-gonic/gin","to":"github.com/mattn/go-isatty"} 32 | {"from":"github.com/gin-gonic/gin","to":"github.com/stretchr/testify"} 33 | {"from":"github.com/gin-gonic/gin","to":"github.com/ugorji/go/codec"} 34 | {"from":"github.com/gin-gonic/gin","to":"gopkg.in/yaml.v2"} 35 | {"from":"github.com/gin-contrib/sse","to":"github.com/stretchr/testify"} 36 | {"from":"github.com/go-playground/validator/v10","to":"github.com/go-playground/assert/v2"} 37 | {"from":"github.com/go-playground/validator/v10","to":"github.com/go-playground/locales"} 38 | {"from":"github.com/go-playground/validator/v10","to":"github.com/go-playground/universal-translator"} 39 | {"from":"github.com/go-playground/validator/v10","to":"github.com/leodido/go-urn"} 40 | {"from":"github.com/go-playground/validator/v10","to":"golang.org/x/crypto"} 41 | {"from":"github.com/json-iterator/go","to":"github.com/davecgh/go-spew"} 42 | {"from":"github.com/json-iterator/go","to":"github.com/google/gofuzz"} 43 | {"from":"github.com/json-iterator/go","to":"github.com/modern-go/concurrent"} 44 | {"from":"github.com/json-iterator/go","to":"github.com/modern-go/reflect2"} 45 | {"from":"github.com/json-iterator/go","to":"github.com/stretchr/testify"} 46 | {"from":"github.com/mattn/go-isatty","to":"golang.org/x/sys"} 47 | {"from":"github.com/stretchr/testify","to":"github.com/davecgh/go-spew"} 48 | {"from":"github.com/stretchr/testify","to":"github.com/pmezard/go-difflib"} 49 | {"from":"github.com/stretchr/testify","to":"github.com/stretchr/objx"} 50 | {"from":"github.com/stretchr/testify","to":"gopkg.in/yaml.v2"} 51 | {"from":"github.com/ugorji/go/codec","to":"github.com/ugorji/go"} 52 | {"from":"gopkg.in/yaml.v2","to":"gopkg.in/check.v1"} 53 | {"from":"github.com/go-playground/locales","to":"golang.org/x/text"} 54 | {"from":"github.com/go-playground/universal-translator","to":"github.com/go-playground/locales"} 55 | {"from":"github.com/leodido/go-urn","to":"github.com/stretchr/testify"} 56 | {"from":"golang.org/x/crypto","to":"golang.org/x/net"} 57 | {"from":"golang.org/x/crypto","to":"golang.org/x/sys"} 58 | {"from":"github.com/ugorji/go","to":"github.com/ugorji/go/codec"} 59 | {"from":"golang.org/x/text","to":"golang.org/x/tools"} 60 | {"from":"golang.org/x/net","to":"golang.org/x/crypto"} 61 | {"from":"golang.org/x/net","to":"golang.org/x/text"} 62 | -------------------------------------------------------------------------------- /layout/testdata/small.jsonl: -------------------------------------------------------------------------------- 1 | {"from":"github.com/nikolaydubina/jsonl-graph/graph","to":"bufio"} 2 | {"from":"github.com/nikolaydubina/jsonl-graph/graph","to":"bytes"} 3 | {"from":"github.com/nikolaydubina/jsonl-graph/graph","to":"encoding/json"} 4 | {"from":"github.com/nikolaydubina/jsonl-graph/graph","to":"errors"} 5 | {"from":"github.com/nikolaydubina/jsonl-graph/graph","to":"fmt"} 6 | {"from":"github.com/nikolaydubina/jsonl-graph/graph","to":"io"} 7 | {"from":"github.com/nikolaydubina/jsonl-graph/dot","to":"embed"} 8 | {"from":"github.com/nikolaydubina/jsonl-graph/dot","to":"encoding/json"} 9 | {"from":"github.com/nikolaydubina/jsonl-graph/dot","to":"errors"} 10 | {"from":"github.com/nikolaydubina/jsonl-graph/dot","to":"fmt"} 11 | {"from":"github.com/nikolaydubina/jsonl-graph/dot","to":"github.com/nikolaydubina/jsonl-graph/graph"} 12 | {"from":"github.com/nikolaydubina/jsonl-graph/dot","to":"image/color"} 13 | {"from":"github.com/nikolaydubina/jsonl-graph/dot","to":"io"} 14 | {"from":"github.com/nikolaydubina/jsonl-graph/dot","to":"io/ioutil"} 15 | {"from":"github.com/nikolaydubina/jsonl-graph/dot","to":"net/http"} 16 | {"from":"github.com/nikolaydubina/jsonl-graph/dot","to":"sort"} 17 | {"from":"github.com/nikolaydubina/jsonl-graph/dot","to":"strconv"} 18 | {"from":"github.com/nikolaydubina/jsonl-graph/dot","to":"strings"} 19 | {"from":"github.com/nikolaydubina/jsonl-graph/dot","to":"text/template"} 20 | {"from":"github.com/nikolaydubina/jsonl-graph","to":"flag"} 21 | {"from":"github.com/nikolaydubina/jsonl-graph","to":"github.com/nikolaydubina/jsonl-graph/dot"} 22 | {"from":"github.com/nikolaydubina/jsonl-graph","to":"github.com/nikolaydubina/jsonl-graph/graph"} 23 | {"from":"github.com/nikolaydubina/jsonl-graph","to":"io"} 24 | {"from":"github.com/nikolaydubina/jsonl-graph","to":"log"} 25 | {"from":"github.com/nikolaydubina/jsonl-graph","to":"os"} 26 | -------------------------------------------------------------------------------- /layout/testdata/small_eades.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 |
35 | 36 |
37 | embed 38 |
39 |
40 | 41 | 42 |
43 |
44 | 45 |
46 |
47 |
48 | 49 | 50 | 51 | 52 |
53 | 54 |
55 | net/http 56 |
57 |
58 | 59 | 60 |
61 |
62 | 63 |
64 |
65 |
66 | 67 | 68 | 69 | 70 |
71 | 72 |
73 | image/color 74 |
75 |
76 | 77 | 78 |
79 |
80 | 81 |
82 |
83 |
84 | 85 | 86 | 87 | 88 |
89 | 90 |
91 | sort 92 |
93 |
94 | 95 | 96 |
97 |
98 | 99 |
100 |
101 |
102 | 103 | 104 | 105 | 106 |
107 | 108 |
109 | io 110 |
111 |
112 | 113 | 114 |
115 |
116 | 117 |
118 |
119 |
120 | 121 | 122 | 123 | 124 |
125 | 126 |
127 | log 128 |
129 |
130 | 131 | 132 |
133 |
134 | 135 |
136 |
137 |
138 | 139 | 140 | 141 | 142 |
143 | 144 |
145 | os 146 |
147 |
148 | 149 | 150 |
151 |
152 | 153 |
154 |
155 |
156 | 157 | 158 | 159 | 160 |
161 | 162 |
163 | strings 164 |
165 |
166 | 167 | 168 |
169 |
170 | 171 |
172 |
173 |
174 | 175 | 176 | 177 | 178 |
179 | 180 |
181 | io/ioutil 182 |
183 |
184 | 185 | 186 |
187 |
188 | 189 |
190 |
191 |
192 | 193 | 194 | 195 | 196 |
197 | 198 |
199 | fmt 200 |
201 |
202 | 203 | 204 |
205 |
206 | 207 |
208 |
209 |
210 | 211 | 212 | 213 | 214 |
215 | 216 |
217 | bufio 218 |
219 |
220 | 221 | 222 |
223 |
224 | 225 |
226 |
227 |
228 | 229 | 230 | 231 | 232 |
233 | 234 |
235 | errors 236 |
237 |
238 | 239 | 240 |
241 |
242 | 243 |
244 |
245 |
246 | 247 | 248 | 249 | 250 |
251 | 252 |
253 | bytes 254 |
255 |
256 | 257 | 258 |
259 |
260 | 261 |
262 |
263 |
264 | 265 | 266 | 267 | 268 |
269 | 270 |
271 | encoding/json 272 |
273 |
274 | 275 | 276 |
277 |
278 | 279 |
280 |
281 |
282 | 283 | 284 | 285 | 286 |
287 | 288 |
289 | flag 290 |
291 |
292 | 293 | 294 |
295 |
296 | 297 |
298 |
299 |
300 | 301 | 302 | 303 | 304 |
305 | 306 |
307 | github.com/nikolaydubina/jsonl-graph/graph 308 |
309 |
310 | 311 | 312 |
313 |
314 | 315 |
316 |
317 |
318 | 319 | 320 | 321 | 322 |
323 | 324 |
325 | text/template 326 |
327 |
328 | 329 | 330 |
331 |
332 | 333 |
334 |
335 |
336 | 337 | 338 | 339 | 340 |
341 | 342 |
343 | github.com/nikolaydubina/jsonl-graph/dot 344 |
345 |
346 | 347 | 348 |
349 |
350 | 351 |
352 |
353 |
354 | 355 | 356 | 357 | 358 |
359 | 360 |
361 | strconv 362 |
363 |
364 | 365 | 366 |
367 |
368 | 369 |
370 |
371 |
372 | 373 | 374 | 375 | 376 |
377 | 378 |
379 | github.com/nikolaydubina/jsonl-graph 380 |
381 |
382 | 383 | 384 |
385 |
386 | 387 |
388 |
389 |
390 | 391 |
392 |
-------------------------------------------------------------------------------- /layout/testdata/small_forces.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 |
35 | 36 |
37 | embed 38 |
39 |
40 | 41 | 42 |
43 |
44 | 45 |
46 |
47 |
48 | 49 | 50 | 51 | 52 |
53 | 54 |
55 | github.com/nikolaydubina/jsonl-graph/dot 56 |
57 |
58 | 59 | 60 |
61 |
62 | 63 |
64 |
65 |
66 | 67 | 68 | 69 | 70 |
71 | 72 |
73 | errors 74 |
75 |
76 | 77 | 78 |
79 |
80 | 81 |
82 |
83 |
84 | 85 | 86 | 87 | 88 |
89 | 90 |
91 | github.com/nikolaydubina/jsonl-graph/graph 92 |
93 |
94 | 95 | 96 |
97 |
98 | 99 |
100 |
101 |
102 | 103 | 104 | 105 | 106 |
107 | 108 |
109 | strings 110 |
111 |
112 | 113 | 114 |
115 |
116 | 117 |
118 |
119 |
120 | 121 | 122 | 123 | 124 |
125 | 126 |
127 | log 128 |
129 |
130 | 131 | 132 |
133 |
134 | 135 |
136 |
137 |
138 | 139 | 140 | 141 | 142 |
143 | 144 |
145 | bufio 146 |
147 |
148 | 149 | 150 |
151 |
152 | 153 |
154 |
155 |
156 | 157 | 158 | 159 | 160 |
161 | 162 |
163 | flag 164 |
165 |
166 | 167 | 168 |
169 |
170 | 171 |
172 |
173 |
174 | 175 | 176 | 177 | 178 |
179 | 180 |
181 | os 182 |
183 |
184 | 185 | 186 |
187 |
188 | 189 |
190 |
191 |
192 | 193 | 194 | 195 | 196 |
197 | 198 |
199 | io 200 |
201 |
202 | 203 | 204 |
205 |
206 | 207 |
208 |
209 |
210 | 211 | 212 | 213 | 214 |
215 | 216 |
217 | io/ioutil 218 |
219 |
220 | 221 | 222 |
223 |
224 | 225 |
226 |
227 |
228 | 229 | 230 | 231 | 232 |
233 | 234 |
235 | image/color 236 |
237 |
238 | 239 | 240 |
241 |
242 | 243 |
244 |
245 |
246 | 247 | 248 | 249 | 250 |
251 | 252 |
253 | sort 254 |
255 |
256 | 257 | 258 |
259 |
260 | 261 |
262 |
263 |
264 | 265 | 266 | 267 | 268 |
269 | 270 |
271 | text/template 272 |
273 |
274 | 275 | 276 |
277 |
278 | 279 |
280 |
281 |
282 | 283 | 284 | 285 | 286 |
287 | 288 |
289 | encoding/json 290 |
291 |
292 | 293 | 294 |
295 |
296 | 297 |
298 |
299 |
300 | 301 | 302 | 303 | 304 |
305 | 306 |
307 | fmt 308 |
309 |
310 | 311 | 312 |
313 |
314 | 315 |
316 |
317 |
318 | 319 | 320 | 321 | 322 |
323 | 324 |
325 | bytes 326 |
327 |
328 | 329 | 330 |
331 |
332 | 333 |
334 |
335 |
336 | 337 | 338 | 339 | 340 |
341 | 342 |
343 | net/http 344 |
345 |
346 | 347 | 348 |
349 |
350 | 351 |
352 |
353 |
354 | 355 | 356 | 357 | 358 |
359 | 360 |
361 | strconv 362 |
363 |
364 | 365 | 366 |
367 |
368 | 369 |
370 |
371 |
372 | 373 | 374 | 375 | 376 |
377 | 378 |
379 | github.com/nikolaydubina/jsonl-graph 380 |
381 |
382 | 383 | 384 |
385 |
386 | 387 |
388 |
389 |
390 | 391 |
392 |
-------------------------------------------------------------------------------- /layout/testdata/small_isomap.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 |
35 | 36 |
37 | bufio 38 |
39 |
40 | 41 | 42 |
43 |
44 | 45 |
46 |
47 |
48 | 49 | 50 | 51 | 52 |
53 | 54 |
55 | embed 56 |
57 |
58 | 59 | 60 |
61 |
62 | 63 |
64 |
65 |
66 | 67 | 68 | 69 | 70 |
71 | 72 |
73 | bytes 74 |
75 |
76 | 77 | 78 |
79 |
80 | 81 |
82 |
83 |
84 | 85 | 86 | 87 | 88 |
89 | 90 |
91 | fmt 92 |
93 |
94 | 95 | 96 |
97 |
98 | 99 |
100 |
101 |
102 | 103 | 104 | 105 | 106 |
107 | 108 |
109 | io 110 |
111 |
112 | 113 | 114 |
115 |
116 | 117 |
118 |
119 |
120 | 121 | 122 | 123 | 124 |
125 | 126 |
127 | log 128 |
129 |
130 | 131 | 132 |
133 |
134 | 135 |
136 |
137 |
138 | 139 | 140 | 141 | 142 |
143 | 144 |
145 | github.com/nikolaydubina/jsonl-graph/dot 146 |
147 |
148 | 149 | 150 |
151 |
152 | 153 |
154 |
155 |
156 | 157 | 158 | 159 | 160 |
161 | 162 |
163 | strconv 164 |
165 |
166 | 167 | 168 |
169 |
170 | 171 |
172 |
173 |
174 | 175 | 176 | 177 | 178 |
179 | 180 |
181 | image/color 182 |
183 |
184 | 185 | 186 |
187 |
188 | 189 |
190 |
191 |
192 | 193 | 194 | 195 | 196 |
197 | 198 |
199 | github.com/nikolaydubina/jsonl-graph/graph 200 |
201 |
202 | 203 | 204 |
205 |
206 | 207 |
208 |
209 |
210 | 211 | 212 | 213 | 214 |
215 | 216 |
217 | errors 218 |
219 |
220 | 221 | 222 |
223 |
224 | 225 |
226 |
227 |
228 | 229 | 230 | 231 | 232 |
233 | 234 |
235 | io/ioutil 236 |
237 |
238 | 239 | 240 |
241 |
242 | 243 |
244 |
245 |
246 | 247 | 248 | 249 | 250 |
251 | 252 |
253 | strings 254 |
255 |
256 | 257 | 258 |
259 |
260 | 261 |
262 |
263 |
264 | 265 | 266 | 267 | 268 |
269 | 270 |
271 | sort 272 |
273 |
274 | 275 | 276 |
277 |
278 | 279 |
280 |
281 |
282 | 283 | 284 | 285 | 286 |
287 | 288 |
289 | text/template 290 |
291 |
292 | 293 | 294 |
295 |
296 | 297 |
298 |
299 |
300 | 301 | 302 | 303 | 304 |
305 | 306 |
307 | github.com/nikolaydubina/jsonl-graph 308 |
309 |
310 | 311 | 312 |
313 |
314 | 315 |
316 |
317 |
318 | 319 | 320 | 321 | 322 |
323 | 324 |
325 | flag 326 |
327 |
328 | 329 | 330 |
331 |
332 | 333 |
334 |
335 |
336 | 337 | 338 | 339 | 340 |
341 | 342 |
343 | encoding/json 344 |
345 |
346 | 347 | 348 |
349 |
350 | 351 |
352 |
353 |
354 | 355 | 356 | 357 | 358 |
359 | 360 |
361 | net/http 362 |
363 |
364 | 365 | 366 |
367 |
368 | 369 |
370 |
371 |
372 | 373 | 374 | 375 | 376 |
377 | 378 |
379 | os 380 |
381 |
382 | 383 | 384 |
385 |
386 | 387 |
388 |
389 |
390 | 391 |
392 |
-------------------------------------------------------------------------------- /layout/testdata/small_layers.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 |
35 | 36 |
37 | bytes 38 |
39 |
40 | 41 | 42 |
43 |
44 | 45 |
46 |
47 |
48 | 49 | 50 | 51 | 52 |
53 | 54 |
55 | strconv 56 |
57 |
58 | 59 | 60 |
61 |
62 | 63 |
64 |
65 |
66 | 67 | 68 | 69 | 70 |
71 | 72 |
73 | github.com/nikolaydubina/jsonl-graph 74 |
75 |
76 | 77 | 78 |
79 |
80 | 81 |
82 |
83 |
84 | 85 | 86 | 87 | 88 |
89 | 90 |
91 | log 92 |
93 |
94 | 95 | 96 |
97 |
98 | 99 |
100 |
101 |
102 | 103 | 104 | 105 | 106 |
107 | 108 |
109 | github.com/nikolaydubina/jsonl-graph/graph 110 |
111 |
112 | 113 | 114 |
115 |
116 | 117 |
118 |
119 |
120 | 121 | 122 | 123 | 124 |
125 | 126 |
127 | errors 128 |
129 |
130 | 131 | 132 |
133 |
134 | 135 |
136 |
137 |
138 | 139 | 140 | 141 | 142 |
143 | 144 |
145 | text/template 146 |
147 |
148 | 149 | 150 |
151 |
152 | 153 |
154 |
155 |
156 | 157 | 158 | 159 | 160 |
161 | 162 |
163 | net/http 164 |
165 |
166 | 167 | 168 |
169 |
170 | 171 |
172 |
173 |
174 | 175 | 176 | 177 | 178 |
179 | 180 |
181 | flag 182 |
183 |
184 | 185 | 186 |
187 |
188 | 189 |
190 |
191 |
192 | 193 | 194 | 195 | 196 |
197 | 198 |
199 | os 200 |
201 |
202 | 203 | 204 |
205 |
206 | 207 |
208 |
209 |
210 | 211 | 212 | 213 | 214 |
215 | 216 |
217 | encoding/json 218 |
219 |
220 | 221 | 222 |
223 |
224 | 225 |
226 |
227 |
228 | 229 | 230 | 231 | 232 |
233 | 234 |
235 | github.com/nikolaydubina/jsonl-graph/dot 236 |
237 |
238 | 239 | 240 |
241 |
242 | 243 |
244 |
245 |
246 | 247 | 248 | 249 | 250 |
251 | 252 |
253 | sort 254 |
255 |
256 | 257 | 258 |
259 |
260 | 261 |
262 |
263 |
264 | 265 | 266 | 267 | 268 |
269 | 270 |
271 | strings 272 |
273 |
274 | 275 | 276 |
277 |
278 | 279 |
280 |
281 |
282 | 283 | 284 | 285 | 286 |
287 | 288 |
289 | image/color 290 |
291 |
292 | 293 | 294 |
295 |
296 | 297 |
298 |
299 |
300 | 301 | 302 | 303 | 304 |
305 | 306 |
307 | fmt 308 |
309 |
310 | 311 | 312 |
313 |
314 | 315 |
316 |
317 |
318 | 319 | 320 | 321 | 322 |
323 | 324 |
325 | bufio 326 |
327 |
328 | 329 | 330 |
331 |
332 | 333 |
334 |
335 |
336 | 337 | 338 | 339 | 340 |
341 | 342 |
343 | io/ioutil 344 |
345 |
346 | 347 | 348 |
349 |
350 | 351 |
352 |
353 |
354 | 355 | 356 | 357 | 358 |
359 | 360 |
361 | io 362 |
363 |
364 | 365 | 366 |
367 |
368 | 369 |
370 |
371 |
372 | 373 | 374 | 375 | 376 |
377 | 378 |
379 | embed 380 |
381 |
382 | 383 | 384 |
385 |
386 | 387 |
388 |
389 |
390 | 391 |
392 |
-------------------------------------------------------------------------------- /svg/components.go: -------------------------------------------------------------------------------- 1 | package svg 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | ) 7 | 8 | type Renderable interface { 9 | Render() string 10 | } 11 | 12 | type SVG struct { 13 | ID string 14 | Definitions []Renderable 15 | Body Renderable 16 | } 17 | 18 | func (s SVG) Render() string { 19 | defs := make([]string, 0, len(s.Definitions)) 20 | for _, d := range s.Definitions { 21 | defs = append(defs, d.Render()) 22 | } 23 | return strings.Join( 24 | []string{ 25 | fmt.Sprintf(``, s.ID), 26 | ``, 27 | strings.Join(defs, "\n"), 28 | ``, 29 | s.Body.Render(), 30 | ``, 31 | }, 32 | "\n", 33 | ) 34 | } 35 | -------------------------------------------------------------------------------- /svg/edge.go: -------------------------------------------------------------------------------- 1 | package svg 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | ) 7 | 8 | // Edge is polylines of straight lines going through all points. 9 | type Edge struct { 10 | Path [][2]int 11 | } 12 | 13 | func (e Edge) Render() string { 14 | var points []string 15 | for _, point := range e.Path { 16 | points = append(points, fmt.Sprintf("%d,%d", point[0], point[1])) 17 | } 18 | return fmt.Sprintf(``, strings.Join(points, " ")) 19 | } 20 | -------------------------------------------------------------------------------- /svg/graph.go: -------------------------------------------------------------------------------- 1 | package svg 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | ) 7 | 8 | // Graph is rendered graph. 9 | type Graph struct { 10 | ID string 11 | Nodes map[uint64]Node 12 | Edges map[[2]uint64]Edge 13 | } 14 | 15 | // Render creates root svg element 16 | func (g Graph) Render() string { 17 | body := []string{ 18 | fmt.Sprintf(``, g.ID), 19 | } 20 | 21 | for _, edge := range g.Edges { 22 | body = append(body, edge.Render()) 23 | } 24 | 25 | // draw nodes always on top of edges 26 | for _, node := range g.Nodes { 27 | body = append(body, node.Render()) 28 | } 29 | 30 | body = append(body, "") 31 | 32 | return strings.Join(body, "\n") 33 | } 34 | -------------------------------------------------------------------------------- /svg/node.go: -------------------------------------------------------------------------------- 1 | package svg 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "sort" 7 | "strings" 8 | ) 9 | 10 | const ( 11 | nodeFontSize int = 9 12 | padding int = 10 13 | textHeightMultiplier int = 2 14 | textWidthMultiplier float64 = 0.8 15 | ) 16 | 17 | // Node is rendered point. 18 | // Can render contents as table. 19 | type Node struct { 20 | ID string // used to make DOM IDs 21 | X int 22 | Y int 23 | Title string 24 | NodeData map[string]interface{} 25 | } 26 | 27 | func (n Node) TitleID() string { 28 | return fmt.Sprintf("svg:graph:node:title:%s", n.ID) 29 | } 30 | 31 | func (n Node) Render() string { 32 | body := "" 33 | if len(n.NodeData) > 0 { 34 | body = NodeDataTable{NodeData: n.NodeData, FontSize: nodeFontSize}.Render() 35 | } 36 | 37 | // https://developer.mozilla.org/en-US/docs/Web/SVG/Element/foreignObject 38 | return fmt.Sprintf(` 39 | 40 | 41 |
42 | %s 43 | %s 44 |
45 |
46 |
47 | `, 48 | n.X, 49 | n.Y, 50 | n.Width()+padding, 51 | n.Height()+padding, 52 | NodeTitle{ID: fmt.Sprintf("svg:graph:node:title:%s", n.ID), Title: n.Title, FontSize: nodeFontSize}.Render(), 53 | body, 54 | ) 55 | } 56 | 57 | func (n Node) Width() int { 58 | w := int(float64(nodeFontSize*len(n.Title)) * textWidthMultiplier) 59 | if len(n.NodeData) == 0 { 60 | return w 61 | } 62 | 63 | nd := NodeDataTable{NodeData: n.NodeData, FontSize: nodeFontSize} 64 | if nd.Width() > w { 65 | w = nd.Width() 66 | } 67 | return w 68 | } 69 | 70 | func (n Node) Height() int { 71 | titleHeight := nodeFontSize * textHeightMultiplier 72 | if len(n.NodeData) == 0 { 73 | return titleHeight 74 | } 75 | 76 | nd := NodeDataTable{NodeData: n.NodeData, FontSize: nodeFontSize} 77 | return titleHeight + nd.Height() 78 | } 79 | 80 | type NodeTitle struct { 81 | ID string 82 | Title string 83 | FontSize int 84 | } 85 | 86 | func (n NodeTitle) Render() string { 87 | return fmt.Sprintf(` 88 |
89 | %s 90 |
`, 91 | n.ID, 92 | n.FontSize, 93 | n.Title, 94 | ) 95 | } 96 | 97 | // NodeDataTable renders key-value data of node. 98 | // It will render table. 99 | type NodeDataTable struct { 100 | NodeData map[string]interface{} 101 | FontSize int 102 | } 103 | 104 | func (n NodeDataTable) Width() int { 105 | maxlen := 0 106 | for k, v := range n.NodeData { 107 | if k == "id" || strings.HasSuffix(k, "_url") { 108 | continue 109 | } 110 | currLen := len(k) + len(RenderValue(v)) 111 | if currLen > maxlen { 112 | maxlen = currLen 113 | } 114 | } 115 | return int(float64(nodeFontSize*maxlen) * textWidthMultiplier) 116 | } 117 | 118 | func (n NodeDataTable) Height() int { 119 | nrows := 0 120 | for k := range n.NodeData { 121 | if k == "id" || strings.HasSuffix(k, "_url") { 122 | continue 123 | } 124 | nrows++ 125 | } 126 | return nodeFontSize * nrows * textHeightMultiplier 127 | } 128 | 129 | func (n NodeDataTable) Render() string { 130 | rows := []string{} 131 | 132 | for k, v := range n.NodeData { 133 | if k == "id" || strings.HasSuffix(k, "_url") { 134 | continue 135 | } 136 | 137 | row := fmt.Sprintf(` 138 | 139 | %s 140 | %s 141 | `, 142 | k, 143 | RenderValue(v), 144 | ) 145 | 146 | rows = append(rows, row) 147 | } 148 | 149 | // sort by key, since key is first 150 | sort.Strings(rows) 151 | 152 | return fmt.Sprintf( 153 | `
154 | 155 | %s 156 |
157 |
158 | `, 159 | n.FontSize, 160 | strings.Join(rows, "\n"), 161 | ) 162 | } 163 | 164 | // RenderValue coerces to json.Number and tries to avoid adding decimal points to integers 165 | func RenderValue(v interface{}) string { 166 | if v, ok := v.(json.Number); ok { 167 | if vInt, err := v.Int64(); err == nil { 168 | return fmt.Sprintf("%d", vInt) 169 | } 170 | if v, err := v.Float64(); err == nil { 171 | return fmt.Sprintf("%.2f", v) 172 | } 173 | } 174 | return fmt.Sprintf("%v", v) 175 | } 176 | --------------------------------------------------------------------------------