├── .gitignore ├── LICENSE ├── README.md ├── cmd ├── dot2graphml │ └── main.go └── glay │ ├── index.html │ ├── main.go │ └── world.gv ├── color.go ├── color_x11.go ├── doc.go ├── edge.go ├── examples ├── basic.go ├── basic.svg ├── complex.go ├── complex.svg ├── minimal.go ├── minimal.png └── minimal.svg ├── format ├── dot │ ├── parse.go │ └── write.go ├── graphml │ ├── graphml.dtd │ ├── write.go │ └── xml.go ├── svg │ └── write.go └── tgf │ └── write.go ├── geom.go ├── go.mod ├── go.sum ├── graph.go ├── internal ├── cmd │ ├── hier │ │ └── main.go │ └── propagate │ │ └── main.go └── hier │ ├── RESEARCH.md │ ├── decycle.go │ ├── decycle_test.go │ ├── example_test.go │ ├── graph.go │ ├── graph_cyclic.go │ ├── graph_cyclic_test.go │ ├── graph_debug.go │ ├── graph_edges.go │ ├── graph_generate.go │ ├── graph_generate_test.go │ ├── graph_nodes.go │ ├── graph_nodes_sort.go │ ├── graph_test.go │ ├── math.go │ ├── order.go │ ├── order_crossings.go │ ├── position.go │ ├── rank.go │ └── virtual.go ├── layout.go └── node.go /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | *~* 7 | *.huge.* 8 | 9 | # Folders 10 | _obj 11 | _test 12 | 13 | # Architecture specific extensions/prefixes 14 | *.[568vq] 15 | [568vq].out 16 | 17 | *.cgo1.go 18 | *.cgo2.c 19 | _cgo_defun.c 20 | _cgo_gotypes.go 21 | _cgo_export.* 22 | 23 | _testmain.go 24 | 25 | *.exe 26 | *.test 27 | *.prof 28 | 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018 Egon Elbre 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 13 | all 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 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # layout [![GoDoc](https://godoc.org/github.com/loov/layout?status.svg)](https://godoc.org/github.com/loov/layout) [![Go Report Card](https://goreportcard.com/badge/github.com/loov/layout)](https://goreportcard.com/report/github.com/loov/layout) 2 | 3 | ## Experimental 4 | 5 | Current version and API is in experimental stage. Property names may change. 6 | 7 | ## Installation 8 | 9 | The graph layouting can be used as a command-line tool and as a library. 10 | 11 | To install the command-line tool: 12 | ``` 13 | go get -u github.com/loov/layout/cmd/glay 14 | ``` 15 | 16 | To install the package: 17 | ``` 18 | go get -u github.com/loov/layout 19 | ``` 20 | 21 | ## Usage 22 | 23 | Minimal usage: 24 | 25 | ``` 26 | package main 27 | 28 | import ( 29 | "os" 30 | 31 | "github.com/loov/layout" 32 | "github.com/loov/layout/format/svg" 33 | ) 34 | 35 | func main() { 36 | graph := layout.NewDigraph() 37 | graph.Edge("A", "B") 38 | graph.Edge("A", "C") 39 | graph.Edge("B", "D") 40 | graph.Edge("C", "D") 41 | 42 | layout.Hierarchical(graph) 43 | 44 | svg.Write(os.Stdout, graph) 45 | } 46 | ``` 47 | 48 | ![Output](./examples/minimal.png) 49 | 50 | See other examples in `examples` folder. 51 | 52 | ## Quality 53 | 54 | Currently the `layout.Hierarchy` algorithm output is significantly worse than graphviz. It is recommended to use `graphviz dot`, if possible. -------------------------------------------------------------------------------- /cmd/dot2graphml/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "io" 7 | "os" 8 | 9 | "github.com/loov/layout/format/dot" 10 | "github.com/loov/layout/format/graphml" 11 | ) 12 | 13 | var ( 14 | eraseLabels = flag.Bool("erase-labels", false, "erase custom labels") 15 | setShape = flag.String("set-shape", "", "override default shape") 16 | ) 17 | 18 | func main() { 19 | flag.Parse() 20 | args := flag.Args() 21 | 22 | var in io.Reader = os.Stdin 23 | var out io.Writer = os.Stdout 24 | 25 | if len(args) >= 1 { 26 | filename := args[0] 27 | file, err := os.Open(filename) 28 | if err != nil { 29 | fmt.Fprintf(os.Stderr, "failed to open %v", filename) 30 | os.Exit(1) 31 | return 32 | } 33 | in = file 34 | defer file.Close() 35 | } 36 | 37 | if len(args) >= 2 { 38 | filename := args[1] 39 | file, err := os.Create(filename) 40 | if err != nil { 41 | fmt.Fprintf(os.Stderr, "failed to create %v", filename) 42 | os.Exit(1) 43 | return 44 | } 45 | out = file 46 | defer file.Close() 47 | } 48 | 49 | graphs, err := dot.Parse(in) 50 | if err != nil { 51 | fmt.Fprintln(os.Stderr, err.Error()) 52 | fmt.Fprintln(os.Stderr, "failed to parse input") 53 | os.Exit(1) 54 | return 55 | } 56 | 57 | if *eraseLabels { 58 | for _, graph := range graphs { 59 | for _, node := range graph.Nodes { 60 | node.Label = "" 61 | } 62 | } 63 | } 64 | 65 | graphml.Write(out, graphs...) 66 | } 67 | -------------------------------------------------------------------------------- /cmd/glay/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 |
6 | 7 | 15 | 16 | -------------------------------------------------------------------------------- /cmd/glay/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "flag" 6 | "fmt" 7 | "io" 8 | "os" 9 | "path/filepath" 10 | "runtime" 11 | "runtime/pprof" 12 | "strings" 13 | 14 | "github.com/loov/layout" 15 | "github.com/loov/layout/format/dot" 16 | "github.com/loov/layout/format/svg" 17 | ) 18 | 19 | var ( 20 | cpuprofile = flag.String("cpuprofile", "", "profile cpu usage") 21 | memprofile = flag.String("memprofile", "", "profile memory usage") 22 | 23 | informat = flag.String("s", "", "input format") 24 | outformat = flag.String("t", "svg", "output format") 25 | 26 | verbose = flag.Bool("v", false, "verbose output") 27 | ) 28 | 29 | func infof(format string, args ...interface{}) { 30 | if *verbose { 31 | fmt.Fprintf(os.Stderr, format, args...) 32 | if !strings.HasSuffix("\n", format) { 33 | fmt.Fprint(os.Stderr, "\n") 34 | } 35 | } 36 | } 37 | 38 | func errorf(format string, args ...interface{}) { 39 | fmt.Fprintf(os.Stderr, format, args...) 40 | if !strings.HasSuffix("\n", format) { 41 | fmt.Fprint(os.Stderr, "\n") 42 | } 43 | } 44 | 45 | func main() { 46 | flag.Parse() 47 | 48 | input := flag.Arg(0) 49 | output := flag.Arg(1) 50 | 51 | if input == "" { 52 | errorf("input is missing") 53 | flag.Usage() 54 | return 55 | } 56 | 57 | if *informat == "" { 58 | // try to detect input format 59 | switch strings.ToLower(filepath.Ext(input)) { 60 | case ".dot": 61 | *informat = "dot" 62 | case ".gv": 63 | *informat = "dot" 64 | } 65 | } 66 | 67 | if output != "" { 68 | *outformat = "" 69 | } 70 | if *outformat == "" { 71 | // try to detect output format 72 | switch strings.ToLower(filepath.Ext(output)) { 73 | case ".svg": 74 | *outformat = "svg" 75 | default: 76 | *outformat = "svg" 77 | } 78 | } 79 | 80 | if *informat == "" || *outformat == "" { 81 | errorf("unable to detect input or output format") 82 | flag.Usage() 83 | os.Exit(1) 84 | return 85 | } 86 | 87 | if *cpuprofile != "" { 88 | f, err := os.Create(*cpuprofile) 89 | if err != nil { 90 | errorf("unable to create cpu-profile %q: %v", *cpuprofile, err) 91 | os.Exit(1) 92 | } 93 | defer f.Close() 94 | if err := pprof.StartCPUProfile(f); err != nil { 95 | errorf("unable to start cpu-profile: %v", err) 96 | os.Exit(1) 97 | } 98 | defer pprof.StopCPUProfile() 99 | } 100 | 101 | if *memprofile != "" { 102 | defer func() { 103 | f, err := os.Create(*memprofile) 104 | if err != nil { 105 | errorf("unable to create mem-profile %q: %v", *memprofile, err) 106 | os.Exit(1) 107 | } 108 | defer f.Close() 109 | 110 | runtime.GC() 111 | if err := pprof.WriteHeapProfile(f); err != nil { 112 | errorf("unable to start mem-profile: %v", err) 113 | os.Exit(1) 114 | } 115 | }() 116 | } 117 | 118 | var graphs []*layout.Graph 119 | var err error 120 | 121 | infof("parsing %q", input) 122 | 123 | switch *informat { 124 | case "dot": 125 | graphs, err = dot.ParseFile(input) 126 | default: 127 | errorf("unknown input format %q", *informat) 128 | flag.Usage() 129 | os.Exit(1) 130 | return 131 | } 132 | 133 | if err != nil || len(graphs) == 0 { 134 | if len(graphs) == 0 && err == nil { 135 | err = errors.New("file doesn't contain graphs") 136 | } 137 | errorf("failed to parse %q: %v", input, err) 138 | os.Exit(1) 139 | return 140 | } 141 | 142 | if len(graphs) != 1 { 143 | infof("parsed %v graphs", len(graphs)) 144 | } else { 145 | infof("parsed 1 graph") 146 | } 147 | 148 | graph := graphs[0] 149 | if len(graphs) > 1 { 150 | errorf("file %q contains multiple graphs, processing only first\n", input) 151 | } 152 | 153 | // layout 154 | layout.Hierarchical(graph) 155 | 156 | // output 157 | var out io.Writer 158 | if output == "" { 159 | out = os.Stdout 160 | } else { 161 | file, err := os.Create(output) 162 | if err != nil { 163 | errorf("unable to create file %q: %v", output, err) 164 | os.Exit(1) 165 | return 166 | } 167 | defer file.Close() 168 | out = file 169 | } 170 | 171 | switch *outformat { 172 | case "svg": 173 | err = svg.Write(out, graph) 174 | default: 175 | errorf("unknown output format %q", *outformat) 176 | os.Exit(1) 177 | return 178 | } 179 | 180 | if err != nil { 181 | errorf("writing %q failed: %v", output, err) 182 | os.Exit(1) 183 | return 184 | } 185 | } 186 | -------------------------------------------------------------------------------- /cmd/glay/world.gv: -------------------------------------------------------------------------------- 1 | 2 | /* 3 | World Dynamics 4 | http://www.graphviz.org/Gallery/directed/world.html 5 | */ 6 | digraph world { 7 | size="7,7"; 8 | 9 | {rank=same; S8 S24 S1 S35 S30;} 10 | {rank=same; T8 T24 T1 T35 T30;} 11 | {rank=same; 43 37 36 10 2;} 12 | {rank=same; 25 9 38 40 13 17 12 18;} 13 | {rank=same; 26 42 11 3 33 19 39 14 16;} 14 | {rank=same; 4 31 34 21 41 28 20;} 15 | {rank=same; 27 5 22 32 29 15;} 16 | {rank=same; 6 23;} 17 | {rank=same; 7;} 18 | 19 | S8 -> 9; 20 | S24 -> 25; 21 | S24 -> 27; 22 | S1 -> 2; 23 | S1 -> 10; 24 | S35 -> 43; 25 | S35 -> 36; 26 | S30 -> 31; 27 | S30 -> 33; 28 | 9 -> 42; 29 | 9 -> T1; 30 | 25 -> T1; 31 | 25 -> 26; 32 | 27 -> T24; 33 | 2 -> {3 ; 16 ; 17 ; T1 ; 18} 34 | 10 -> { 11 ; 14 ; T1 ; 13; 12;} 35 | 31 -> T1; 36 | 31 -> 32; 37 | 33 -> T30; 38 | 33 -> 34; 39 | 42 -> 4; 40 | 26 -> 4; 41 | 3 -> 4; 42 | 16 -> 15; 43 | 17 -> 19; 44 | 18 -> 29; 45 | 11 -> 4; 46 | 14 -> 15; 47 | 37 -> {39 ; 41 ; 38 ; 40;} 48 | 13 -> 19; 49 | 12 -> 29; 50 | 43 -> 38; 51 | 43 -> 40; 52 | 36 -> 19; 53 | 32 -> 23; 54 | 34 -> 29; 55 | 39 -> 15; 56 | 41 -> 29; 57 | 38 -> 4; 58 | 40 -> 19; 59 | 4 -> 5; 60 | 19 -> {21 ; 20 ; 28;} 61 | 5 -> {6 ; T35 ; 23;} 62 | 21 -> 22; 63 | 20 -> 15; 64 | 28 -> 29; 65 | 6 -> 7; 66 | 15 -> T1; 67 | 22 -> T35; 68 | 22 -> 23; 69 | 29 -> T30; 70 | 7 -> T8; 71 | 23 -> T24; 72 | 23 -> T1; 73 | } -------------------------------------------------------------------------------- /color.go: -------------------------------------------------------------------------------- 1 | package layout 2 | 3 | import "math" 4 | 5 | type Color interface { 6 | // RGBA returns the non-alpha-premultiplied red, green, blue and alpha values 7 | // for the color. Each value ranges within [0, 0xff]. 8 | RGBA8() (r, g, b, a uint8) 9 | } 10 | 11 | // RGB represents an 24bit color 12 | type RGB struct{ R, G, B uint8 } 13 | 14 | func (rgb RGB) RGBA8() (r, g, b, a uint8) { return rgb.R, rgb.G, rgb.B, 0xFF } 15 | 16 | // RGBA represents an 24bit color 17 | type RGBA struct{ R, G, B, A uint8 } 18 | 19 | func (rgb RGBA) RGBA8() (r, g, b, a uint8) { return rgb.R, rgb.G, rgb.B, rgb.A } 20 | 21 | // HSL represents an color in hue, saturation and lightness space 22 | type HSL struct{ H, S, L float32 } 23 | 24 | func (hsl HSL) RGBA8() (r, g, b, a uint8) { 25 | return HSLA{hsl.H, hsl.S, hsl.L, 1.0}.RGBA8() 26 | } 27 | 28 | // HSLA represents an color in hue, saturation and lightness space 29 | type HSLA struct{ H, S, L, A float32 } 30 | 31 | func (hsl HSLA) RGBA8() (r, g, b, a uint8) { 32 | rf, gf, bf, af := hsla(hsl.H, hsl.S, hsl.L, hsl.A) 33 | return sat8(rf), sat8(gf), sat8(bf), sat8(af) 34 | } 35 | 36 | func hue(v1, v2, h float32) float32 { 37 | if h < 0 { 38 | h += 1 39 | } 40 | if h > 1 { 41 | h -= 1 42 | } 43 | if 6*h < 1 { 44 | return v1 + (v2-v1)*6*h 45 | } else if 2*h < 1 { 46 | return v2 47 | } else if 3*h < 2 { 48 | return v1 + (v2-v1)*(2.0/3.0-h)*6 49 | } 50 | 51 | return v1 52 | } 53 | 54 | func hsla(h, s, l, a float32) (r, g, b, ra float32) { 55 | if s == 0 { 56 | return l, l, l, a 57 | } 58 | 59 | h = float32(math.Mod(float64(h), 1)) 60 | 61 | var v2 float32 62 | if l < 0.5 { 63 | v2 = l * (1 + s) 64 | } else { 65 | v2 = (l + s) - s*l 66 | } 67 | 68 | v1 := 2*l - v2 69 | r = hue(v1, v2, h+1.0/3.0) 70 | g = hue(v1, v2, h) 71 | b = hue(v1, v2, h-1.0/3.0) 72 | ra = a 73 | 74 | return 75 | } 76 | 77 | // sat8 converts 0..1 float to 0..0xFF uint16 78 | func sat8(v float32) uint8 { 79 | v = v * 0xFF 80 | if v >= 0xFF { 81 | return 0xFF 82 | } else if v <= 0 { 83 | return 0 84 | } 85 | return uint8(v) 86 | } 87 | -------------------------------------------------------------------------------- /color_x11.go: -------------------------------------------------------------------------------- 1 | package layout 2 | 3 | import "strings" 4 | 5 | // ColorByName returns RGB color based on X11 scheme 6 | func ColorByName(name string) (Color, bool) { 7 | name = strings.ToLower(name) 8 | name = strings.Replace(name, " ", "", -1) 9 | rgb, ok := x11colors[name] 10 | if !ok { 11 | return nil, false 12 | } 13 | return rgb, true 14 | } 15 | 16 | // x11colors table is based on /usr/lib/X11/rgb.txt 17 | var x11colors = map[string]RGB{ 18 | "snow": {255, 250, 250}, 19 | "ghostwhite": {248, 248, 255}, 20 | "whitesmoke": {245, 245, 245}, 21 | "gainsboro": {220, 220, 220}, 22 | "floralwhite": {255, 250, 240}, 23 | "oldlace": {253, 245, 230}, 24 | "linen": {250, 240, 230}, 25 | "antiquewhite": {250, 235, 215}, 26 | "papayawhip": {255, 239, 213}, 27 | "blanchedalmond": {255, 235, 205}, 28 | "bisque": {255, 228, 196}, 29 | "peachpuff": {255, 218, 185}, 30 | "navajowhite": {255, 222, 173}, 31 | "moccasin": {255, 228, 181}, 32 | "cornsilk": {255, 248, 220}, 33 | "ivory": {255, 255, 240}, 34 | "lemonchiffon": {255, 250, 205}, 35 | "seashell": {255, 245, 238}, 36 | "honeydew": {240, 255, 240}, 37 | "mintcream": {245, 255, 250}, 38 | "azure": {240, 255, 255}, 39 | "aliceblue": {240, 248, 255}, 40 | "lavender": {230, 230, 250}, 41 | "lavenderblush": {255, 240, 245}, 42 | "mistyrose": {255, 228, 225}, 43 | "white": {255, 255, 255}, 44 | "black": {0, 0, 0}, 45 | "darkslategray": {47, 79, 79}, 46 | "darkslategrey": {47, 79, 79}, 47 | "dimgray": {105, 105, 105}, 48 | "dimgrey": {105, 105, 105}, 49 | "slategray": {112, 128, 144}, 50 | "slategrey": {112, 128, 144}, 51 | "lightslategray": {119, 136, 153}, 52 | "lightslategrey": {119, 136, 153}, 53 | "gray": {190, 190, 190}, 54 | "grey": {190, 190, 190}, 55 | "x11gray": {190, 190, 190}, 56 | "x11grey": {190, 190, 190}, 57 | "webgray": {128, 128, 128}, 58 | "webgrey": {128, 128, 128}, 59 | "lightgrey": {211, 211, 211}, 60 | "lightgray": {211, 211, 211}, 61 | "midnightblue": {25, 25, 112}, 62 | "navy": {0, 0, 128}, 63 | "navyblue": {0, 0, 128}, 64 | "cornflowerblue": {100, 149, 237}, 65 | "darkslateblue": {72, 61, 139}, 66 | "slateblue": {106, 90, 205}, 67 | "mediumslateblue": {123, 104, 238}, 68 | "lightslateblue": {132, 112, 255}, 69 | "mediumblue": {0, 0, 205}, 70 | "royalblue": {65, 105, 225}, 71 | "blue": {0, 0, 255}, 72 | "dodgerblue": {30, 144, 255}, 73 | "deepskyblue": {0, 191, 255}, 74 | "skyblue": {135, 206, 235}, 75 | "lightskyblue": {135, 206, 250}, 76 | "steelblue": {70, 130, 180}, 77 | "lightsteelblue": {176, 196, 222}, 78 | "lightblue": {173, 216, 230}, 79 | "powderblue": {176, 224, 230}, 80 | "paleturquoise": {175, 238, 238}, 81 | "darkturquoise": {0, 206, 209}, 82 | "mediumturquoise": {72, 209, 204}, 83 | "turquoise": {64, 224, 208}, 84 | "cyan": {0, 255, 255}, 85 | "aqua": {0, 255, 255}, 86 | "lightcyan": {224, 255, 255}, 87 | "cadetblue": {95, 158, 160}, 88 | "mediumaquamarine": {102, 205, 170}, 89 | "aquamarine": {127, 255, 212}, 90 | "darkgreen": {0, 100, 0}, 91 | "darkolivegreen": {85, 107, 47}, 92 | "darkseagreen": {143, 188, 143}, 93 | "seagreen": {46, 139, 87}, 94 | "mediumseagreen": {60, 179, 113}, 95 | "lightseagreen": {32, 178, 170}, 96 | "palegreen": {152, 251, 152}, 97 | "springgreen": {0, 255, 127}, 98 | "lawngreen": {124, 252, 0}, 99 | "green": {0, 255, 0}, 100 | "lime": {0, 255, 0}, 101 | "x11green": {0, 255, 0}, 102 | "webgreen": {0, 128, 0}, 103 | "chartreuse": {127, 255, 0}, 104 | "mediumspringgreen": {0, 250, 154}, 105 | "greenyellow": {173, 255, 47}, 106 | "limegreen": {50, 205, 50}, 107 | "yellowgreen": {154, 205, 50}, 108 | "forestgreen": {34, 139, 34}, 109 | "olivedrab": {107, 142, 35}, 110 | "darkkhaki": {189, 183, 107}, 111 | "khaki": {240, 230, 140}, 112 | "palegoldenrod": {238, 232, 170}, 113 | "lightgoldenrodyellow": {250, 250, 210}, 114 | "lightyellow": {255, 255, 224}, 115 | "yellow": {255, 255, 0}, 116 | "gold": {255, 215, 0}, 117 | "lightgoldenrod": {238, 221, 130}, 118 | "goldenrod": {218, 165, 32}, 119 | "darkgoldenrod": {184, 134, 11}, 120 | "rosybrown": {188, 143, 143}, 121 | "indianred": {205, 92, 92}, 122 | "saddlebrown": {139, 69, 19}, 123 | "sienna": {160, 82, 45}, 124 | "peru": {205, 133, 63}, 125 | "burlywood": {222, 184, 135}, 126 | "beige": {245, 245, 220}, 127 | "wheat": {245, 222, 179}, 128 | "sandybrown": {244, 164, 96}, 129 | "tan": {210, 180, 140}, 130 | "chocolate": {210, 105, 30}, 131 | "firebrick": {178, 34, 34}, 132 | "brown": {165, 42, 42}, 133 | "darksalmon": {233, 150, 122}, 134 | "salmon": {250, 128, 114}, 135 | "lightsalmon": {255, 160, 122}, 136 | "orange": {255, 165, 0}, 137 | "darkorange": {255, 140, 0}, 138 | "coral": {255, 127, 80}, 139 | "lightcoral": {240, 128, 128}, 140 | "tomato": {255, 99, 71}, 141 | "orangered": {255, 69, 0}, 142 | "red": {255, 0, 0}, 143 | "hotpink": {255, 105, 180}, 144 | "deeppink": {255, 20, 147}, 145 | "pink": {255, 192, 203}, 146 | "lightpink": {255, 182, 193}, 147 | "palevioletred": {219, 112, 147}, 148 | "maroon": {176, 48, 96}, 149 | "x11maroon": {176, 48, 96}, 150 | "webmaroon": {128, 0, 0}, 151 | "mediumvioletred": {199, 21, 133}, 152 | "violetred": {208, 32, 144}, 153 | "magenta": {255, 0, 255}, 154 | "fuchsia": {255, 0, 255}, 155 | "violet": {238, 130, 238}, 156 | "plum": {221, 160, 221}, 157 | "orchid": {218, 112, 214}, 158 | "mediumorchid": {186, 85, 211}, 159 | "darkorchid": {153, 50, 204}, 160 | "darkviolet": {148, 0, 211}, 161 | "blueviolet": {138, 43, 226}, 162 | "purple": {160, 32, 240}, 163 | "x11purple": {160, 32, 240}, 164 | "webpurple": {128, 0, 128}, 165 | "mediumpurple": {147, 112, 219}, 166 | "thistle": {216, 191, 216}, 167 | "snow1": {255, 250, 250}, 168 | "snow2": {238, 233, 233}, 169 | "snow3": {205, 201, 201}, 170 | "snow4": {139, 137, 137}, 171 | "seashell1": {255, 245, 238}, 172 | "seashell2": {238, 229, 222}, 173 | "seashell3": {205, 197, 191}, 174 | "seashell4": {139, 134, 130}, 175 | "antiquewhite1": {255, 239, 219}, 176 | "antiquewhite2": {238, 223, 204}, 177 | "antiquewhite3": {205, 192, 176}, 178 | "antiquewhite4": {139, 131, 120}, 179 | "bisque1": {255, 228, 196}, 180 | "bisque2": {238, 213, 183}, 181 | "bisque3": {205, 183, 158}, 182 | "bisque4": {139, 125, 107}, 183 | "peachpuff1": {255, 218, 185}, 184 | "peachpuff2": {238, 203, 173}, 185 | "peachpuff3": {205, 175, 149}, 186 | "peachpuff4": {139, 119, 101}, 187 | "navajowhite1": {255, 222, 173}, 188 | "navajowhite2": {238, 207, 161}, 189 | "navajowhite3": {205, 179, 139}, 190 | "navajowhite4": {139, 121, 94}, 191 | "lemonchiffon1": {255, 250, 205}, 192 | "lemonchiffon2": {238, 233, 191}, 193 | "lemonchiffon3": {205, 201, 165}, 194 | "lemonchiffon4": {139, 137, 112}, 195 | "cornsilk1": {255, 248, 220}, 196 | "cornsilk2": {238, 232, 205}, 197 | "cornsilk3": {205, 200, 177}, 198 | "cornsilk4": {139, 136, 120}, 199 | "ivory1": {255, 255, 240}, 200 | "ivory2": {238, 238, 224}, 201 | "ivory3": {205, 205, 193}, 202 | "ivory4": {139, 139, 131}, 203 | "honeydew1": {240, 255, 240}, 204 | "honeydew2": {224, 238, 224}, 205 | "honeydew3": {193, 205, 193}, 206 | "honeydew4": {131, 139, 131}, 207 | "lavenderblush1": {255, 240, 245}, 208 | "lavenderblush2": {238, 224, 229}, 209 | "lavenderblush3": {205, 193, 197}, 210 | "lavenderblush4": {139, 131, 134}, 211 | "mistyrose1": {255, 228, 225}, 212 | "mistyrose2": {238, 213, 210}, 213 | "mistyrose3": {205, 183, 181}, 214 | "mistyrose4": {139, 125, 123}, 215 | "azure1": {240, 255, 255}, 216 | "azure2": {224, 238, 238}, 217 | "azure3": {193, 205, 205}, 218 | "azure4": {131, 139, 139}, 219 | "slateblue1": {131, 111, 255}, 220 | "slateblue2": {122, 103, 238}, 221 | "slateblue3": {105, 89, 205}, 222 | "slateblue4": {71, 60, 139}, 223 | "royalblue1": {72, 118, 255}, 224 | "royalblue2": {67, 110, 238}, 225 | "royalblue3": {58, 95, 205}, 226 | "royalblue4": {39, 64, 139}, 227 | "blue1": {0, 0, 255}, 228 | "blue2": {0, 0, 238}, 229 | "blue3": {0, 0, 205}, 230 | "blue4": {0, 0, 139}, 231 | "dodgerblue1": {30, 144, 255}, 232 | "dodgerblue2": {28, 134, 238}, 233 | "dodgerblue3": {24, 116, 205}, 234 | "dodgerblue4": {16, 78, 139}, 235 | "steelblue1": {99, 184, 255}, 236 | "steelblue2": {92, 172, 238}, 237 | "steelblue3": {79, 148, 205}, 238 | "steelblue4": {54, 100, 139}, 239 | "deepskyblue1": {0, 191, 255}, 240 | "deepskyblue2": {0, 178, 238}, 241 | "deepskyblue3": {0, 154, 205}, 242 | "deepskyblue4": {0, 104, 139}, 243 | "skyblue1": {135, 206, 255}, 244 | "skyblue2": {126, 192, 238}, 245 | "skyblue3": {108, 166, 205}, 246 | "skyblue4": {74, 112, 139}, 247 | "lightskyblue1": {176, 226, 255}, 248 | "lightskyblue2": {164, 211, 238}, 249 | "lightskyblue3": {141, 182, 205}, 250 | "lightskyblue4": {96, 123, 139}, 251 | "slategray1": {198, 226, 255}, 252 | "slategray2": {185, 211, 238}, 253 | "slategray3": {159, 182, 205}, 254 | "slategray4": {108, 123, 139}, 255 | "lightsteelblue1": {202, 225, 255}, 256 | "lightsteelblue2": {188, 210, 238}, 257 | "lightsteelblue3": {162, 181, 205}, 258 | "lightsteelblue4": {110, 123, 139}, 259 | "lightblue1": {191, 239, 255}, 260 | "lightblue2": {178, 223, 238}, 261 | "lightblue3": {154, 192, 205}, 262 | "lightblue4": {104, 131, 139}, 263 | "lightcyan1": {224, 255, 255}, 264 | "lightcyan2": {209, 238, 238}, 265 | "lightcyan3": {180, 205, 205}, 266 | "lightcyan4": {122, 139, 139}, 267 | "paleturquoise1": {187, 255, 255}, 268 | "paleturquoise2": {174, 238, 238}, 269 | "paleturquoise3": {150, 205, 205}, 270 | "paleturquoise4": {102, 139, 139}, 271 | "cadetblue1": {152, 245, 255}, 272 | "cadetblue2": {142, 229, 238}, 273 | "cadetblue3": {122, 197, 205}, 274 | "cadetblue4": {83, 134, 139}, 275 | "turquoise1": {0, 245, 255}, 276 | "turquoise2": {0, 229, 238}, 277 | "turquoise3": {0, 197, 205}, 278 | "turquoise4": {0, 134, 139}, 279 | "cyan1": {0, 255, 255}, 280 | "cyan2": {0, 238, 238}, 281 | "cyan3": {0, 205, 205}, 282 | "cyan4": {0, 139, 139}, 283 | "darkslategray1": {151, 255, 255}, 284 | "darkslategray2": {141, 238, 238}, 285 | "darkslategray3": {121, 205, 205}, 286 | "darkslategray4": {82, 139, 139}, 287 | "aquamarine1": {127, 255, 212}, 288 | "aquamarine2": {118, 238, 198}, 289 | "aquamarine3": {102, 205, 170}, 290 | "aquamarine4": {69, 139, 116}, 291 | "darkseagreen1": {193, 255, 193}, 292 | "darkseagreen2": {180, 238, 180}, 293 | "darkseagreen3": {155, 205, 155}, 294 | "darkseagreen4": {105, 139, 105}, 295 | "seagreen1": {84, 255, 159}, 296 | "seagreen2": {78, 238, 148}, 297 | "seagreen3": {67, 205, 128}, 298 | "seagreen4": {46, 139, 87}, 299 | "palegreen1": {154, 255, 154}, 300 | "palegreen2": {144, 238, 144}, 301 | "palegreen3": {124, 205, 124}, 302 | "palegreen4": {84, 139, 84}, 303 | "springgreen1": {0, 255, 127}, 304 | "springgreen2": {0, 238, 118}, 305 | "springgreen3": {0, 205, 102}, 306 | "springgreen4": {0, 139, 69}, 307 | "green1": {0, 255, 0}, 308 | "green2": {0, 238, 0}, 309 | "green3": {0, 205, 0}, 310 | "green4": {0, 139, 0}, 311 | "chartreuse1": {127, 255, 0}, 312 | "chartreuse2": {118, 238, 0}, 313 | "chartreuse3": {102, 205, 0}, 314 | "chartreuse4": {69, 139, 0}, 315 | "olivedrab1": {192, 255, 62}, 316 | "olivedrab2": {179, 238, 58}, 317 | "olivedrab3": {154, 205, 50}, 318 | "olivedrab4": {105, 139, 34}, 319 | "darkolivegreen1": {202, 255, 112}, 320 | "darkolivegreen2": {188, 238, 104}, 321 | "darkolivegreen3": {162, 205, 90}, 322 | "darkolivegreen4": {110, 139, 61}, 323 | "khaki1": {255, 246, 143}, 324 | "khaki2": {238, 230, 133}, 325 | "khaki3": {205, 198, 115}, 326 | "khaki4": {139, 134, 78}, 327 | "lightgoldenrod1": {255, 236, 139}, 328 | "lightgoldenrod2": {238, 220, 130}, 329 | "lightgoldenrod3": {205, 190, 112}, 330 | "lightgoldenrod4": {139, 129, 76}, 331 | "lightyellow1": {255, 255, 224}, 332 | "lightyellow2": {238, 238, 209}, 333 | "lightyellow3": {205, 205, 180}, 334 | "lightyellow4": {139, 139, 122}, 335 | "yellow1": {255, 255, 0}, 336 | "yellow2": {238, 238, 0}, 337 | "yellow3": {205, 205, 0}, 338 | "yellow4": {139, 139, 0}, 339 | "gold1": {255, 215, 0}, 340 | "gold2": {238, 201, 0}, 341 | "gold3": {205, 173, 0}, 342 | "gold4": {139, 117, 0}, 343 | "goldenrod1": {255, 193, 37}, 344 | "goldenrod2": {238, 180, 34}, 345 | "goldenrod3": {205, 155, 29}, 346 | "goldenrod4": {139, 105, 20}, 347 | "darkgoldenrod1": {255, 185, 15}, 348 | "darkgoldenrod2": {238, 173, 14}, 349 | "darkgoldenrod3": {205, 149, 12}, 350 | "darkgoldenrod4": {139, 101, 8}, 351 | "rosybrown1": {255, 193, 193}, 352 | "rosybrown2": {238, 180, 180}, 353 | "rosybrown3": {205, 155, 155}, 354 | "rosybrown4": {139, 105, 105}, 355 | "indianred1": {255, 106, 106}, 356 | "indianred2": {238, 99, 99}, 357 | "indianred3": {205, 85, 85}, 358 | "indianred4": {139, 58, 58}, 359 | "sienna1": {255, 130, 71}, 360 | "sienna2": {238, 121, 66}, 361 | "sienna3": {205, 104, 57}, 362 | "sienna4": {139, 71, 38}, 363 | "burlywood1": {255, 211, 155}, 364 | "burlywood2": {238, 197, 145}, 365 | "burlywood3": {205, 170, 125}, 366 | "burlywood4": {139, 115, 85}, 367 | "wheat1": {255, 231, 186}, 368 | "wheat2": {238, 216, 174}, 369 | "wheat3": {205, 186, 150}, 370 | "wheat4": {139, 126, 102}, 371 | "tan1": {255, 165, 79}, 372 | "tan2": {238, 154, 73}, 373 | "tan3": {205, 133, 63}, 374 | "tan4": {139, 90, 43}, 375 | "chocolate1": {255, 127, 36}, 376 | "chocolate2": {238, 118, 33}, 377 | "chocolate3": {205, 102, 29}, 378 | "chocolate4": {139, 69, 19}, 379 | "firebrick1": {255, 48, 48}, 380 | "firebrick2": {238, 44, 44}, 381 | "firebrick3": {205, 38, 38}, 382 | "firebrick4": {139, 26, 26}, 383 | "brown1": {255, 64, 64}, 384 | "brown2": {238, 59, 59}, 385 | "brown3": {205, 51, 51}, 386 | "brown4": {139, 35, 35}, 387 | "salmon1": {255, 140, 105}, 388 | "salmon2": {238, 130, 98}, 389 | "salmon3": {205, 112, 84}, 390 | "salmon4": {139, 76, 57}, 391 | "lightsalmon1": {255, 160, 122}, 392 | "lightsalmon2": {238, 149, 114}, 393 | "lightsalmon3": {205, 129, 98}, 394 | "lightsalmon4": {139, 87, 66}, 395 | "orange1": {255, 165, 0}, 396 | "orange2": {238, 154, 0}, 397 | "orange3": {205, 133, 0}, 398 | "orange4": {139, 90, 0}, 399 | "darkorange1": {255, 127, 0}, 400 | "darkorange2": {238, 118, 0}, 401 | "darkorange3": {205, 102, 0}, 402 | "darkorange4": {139, 69, 0}, 403 | "coral1": {255, 114, 86}, 404 | "coral2": {238, 106, 80}, 405 | "coral3": {205, 91, 69}, 406 | "coral4": {139, 62, 47}, 407 | "tomato1": {255, 99, 71}, 408 | "tomato2": {238, 92, 66}, 409 | "tomato3": {205, 79, 57}, 410 | "tomato4": {139, 54, 38}, 411 | "orangered1": {255, 69, 0}, 412 | "orangered2": {238, 64, 0}, 413 | "orangered3": {205, 55, 0}, 414 | "orangered4": {139, 37, 0}, 415 | "red1": {255, 0, 0}, 416 | "red2": {238, 0, 0}, 417 | "red3": {205, 0, 0}, 418 | "red4": {139, 0, 0}, 419 | "deeppink1": {255, 20, 147}, 420 | "deeppink2": {238, 18, 137}, 421 | "deeppink3": {205, 16, 118}, 422 | "deeppink4": {139, 10, 80}, 423 | "hotpink1": {255, 110, 180}, 424 | "hotpink2": {238, 106, 167}, 425 | "hotpink3": {205, 96, 144}, 426 | "hotpink4": {139, 58, 98}, 427 | "pink1": {255, 181, 197}, 428 | "pink2": {238, 169, 184}, 429 | "pink3": {205, 145, 158}, 430 | "pink4": {139, 99, 108}, 431 | "lightpink1": {255, 174, 185}, 432 | "lightpink2": {238, 162, 173}, 433 | "lightpink3": {205, 140, 149}, 434 | "lightpink4": {139, 95, 101}, 435 | "palevioletred1": {255, 130, 171}, 436 | "palevioletred2": {238, 121, 159}, 437 | "palevioletred3": {205, 104, 137}, 438 | "palevioletred4": {139, 71, 93}, 439 | "maroon1": {255, 52, 179}, 440 | "maroon2": {238, 48, 167}, 441 | "maroon3": {205, 41, 144}, 442 | "maroon4": {139, 28, 98}, 443 | "violetred1": {255, 62, 150}, 444 | "violetred2": {238, 58, 140}, 445 | "violetred3": {205, 50, 120}, 446 | "violetred4": {139, 34, 82}, 447 | "magenta1": {255, 0, 255}, 448 | "magenta2": {238, 0, 238}, 449 | "magenta3": {205, 0, 205}, 450 | "magenta4": {139, 0, 139}, 451 | "orchid1": {255, 131, 250}, 452 | "orchid2": {238, 122, 233}, 453 | "orchid3": {205, 105, 201}, 454 | "orchid4": {139, 71, 137}, 455 | "plum1": {255, 187, 255}, 456 | "plum2": {238, 174, 238}, 457 | "plum3": {205, 150, 205}, 458 | "plum4": {139, 102, 139}, 459 | "mediumorchid1": {224, 102, 255}, 460 | "mediumorchid2": {209, 95, 238}, 461 | "mediumorchid3": {180, 82, 205}, 462 | "mediumorchid4": {122, 55, 139}, 463 | "darkorchid1": {191, 62, 255}, 464 | "darkorchid2": {178, 58, 238}, 465 | "darkorchid3": {154, 50, 205}, 466 | "darkorchid4": {104, 34, 139}, 467 | "purple1": {155, 48, 255}, 468 | "purple2": {145, 44, 238}, 469 | "purple3": {125, 38, 205}, 470 | "purple4": {85, 26, 139}, 471 | "mediumpurple1": {171, 130, 255}, 472 | "mediumpurple2": {159, 121, 238}, 473 | "mediumpurple3": {137, 104, 205}, 474 | "mediumpurple4": {93, 71, 139}, 475 | "thistle1": {255, 225, 255}, 476 | "thistle2": {238, 210, 238}, 477 | "thistle3": {205, 181, 205}, 478 | "thistle4": {139, 123, 139}, 479 | "gray0": {0, 0, 0}, 480 | "grey0": {0, 0, 0}, 481 | "gray1": {3, 3, 3}, 482 | "grey1": {3, 3, 3}, 483 | "gray2": {5, 5, 5}, 484 | "grey2": {5, 5, 5}, 485 | "gray3": {8, 8, 8}, 486 | "grey3": {8, 8, 8}, 487 | "gray4": {10, 10, 10}, 488 | "grey4": {10, 10, 10}, 489 | "gray5": {13, 13, 13}, 490 | "grey5": {13, 13, 13}, 491 | "gray6": {15, 15, 15}, 492 | "grey6": {15, 15, 15}, 493 | "gray7": {18, 18, 18}, 494 | "grey7": {18, 18, 18}, 495 | "gray8": {20, 20, 20}, 496 | "grey8": {20, 20, 20}, 497 | "gray9": {23, 23, 23}, 498 | "grey9": {23, 23, 23}, 499 | "gray10": {26, 26, 26}, 500 | "grey10": {26, 26, 26}, 501 | "gray11": {28, 28, 28}, 502 | "grey11": {28, 28, 28}, 503 | "gray12": {31, 31, 31}, 504 | "grey12": {31, 31, 31}, 505 | "gray13": {33, 33, 33}, 506 | "grey13": {33, 33, 33}, 507 | "gray14": {36, 36, 36}, 508 | "grey14": {36, 36, 36}, 509 | "gray15": {38, 38, 38}, 510 | "grey15": {38, 38, 38}, 511 | "gray16": {41, 41, 41}, 512 | "grey16": {41, 41, 41}, 513 | "gray17": {43, 43, 43}, 514 | "grey17": {43, 43, 43}, 515 | "gray18": {46, 46, 46}, 516 | "grey18": {46, 46, 46}, 517 | "gray19": {48, 48, 48}, 518 | "grey19": {48, 48, 48}, 519 | "gray20": {51, 51, 51}, 520 | "grey20": {51, 51, 51}, 521 | "gray21": {54, 54, 54}, 522 | "grey21": {54, 54, 54}, 523 | "gray22": {56, 56, 56}, 524 | "grey22": {56, 56, 56}, 525 | "gray23": {59, 59, 59}, 526 | "grey23": {59, 59, 59}, 527 | "gray24": {61, 61, 61}, 528 | "grey24": {61, 61, 61}, 529 | "gray25": {64, 64, 64}, 530 | "grey25": {64, 64, 64}, 531 | "gray26": {66, 66, 66}, 532 | "grey26": {66, 66, 66}, 533 | "gray27": {69, 69, 69}, 534 | "grey27": {69, 69, 69}, 535 | "gray28": {71, 71, 71}, 536 | "grey28": {71, 71, 71}, 537 | "gray29": {74, 74, 74}, 538 | "grey29": {74, 74, 74}, 539 | "gray30": {77, 77, 77}, 540 | "grey30": {77, 77, 77}, 541 | "gray31": {79, 79, 79}, 542 | "grey31": {79, 79, 79}, 543 | "gray32": {82, 82, 82}, 544 | "grey32": {82, 82, 82}, 545 | "gray33": {84, 84, 84}, 546 | "grey33": {84, 84, 84}, 547 | "gray34": {87, 87, 87}, 548 | "grey34": {87, 87, 87}, 549 | "gray35": {89, 89, 89}, 550 | "grey35": {89, 89, 89}, 551 | "gray36": {92, 92, 92}, 552 | "grey36": {92, 92, 92}, 553 | "gray37": {94, 94, 94}, 554 | "grey37": {94, 94, 94}, 555 | "gray38": {97, 97, 97}, 556 | "grey38": {97, 97, 97}, 557 | "gray39": {99, 99, 99}, 558 | "grey39": {99, 99, 99}, 559 | "gray40": {102, 102, 102}, 560 | "grey40": {102, 102, 102}, 561 | "gray41": {105, 105, 105}, 562 | "grey41": {105, 105, 105}, 563 | "gray42": {107, 107, 107}, 564 | "grey42": {107, 107, 107}, 565 | "gray43": {110, 110, 110}, 566 | "grey43": {110, 110, 110}, 567 | "gray44": {112, 112, 112}, 568 | "grey44": {112, 112, 112}, 569 | "gray45": {115, 115, 115}, 570 | "grey45": {115, 115, 115}, 571 | "gray46": {117, 117, 117}, 572 | "grey46": {117, 117, 117}, 573 | "gray47": {120, 120, 120}, 574 | "grey47": {120, 120, 120}, 575 | "gray48": {122, 122, 122}, 576 | "grey48": {122, 122, 122}, 577 | "gray49": {125, 125, 125}, 578 | "grey49": {125, 125, 125}, 579 | "gray50": {127, 127, 127}, 580 | "grey50": {127, 127, 127}, 581 | "gray51": {130, 130, 130}, 582 | "grey51": {130, 130, 130}, 583 | "gray52": {133, 133, 133}, 584 | "grey52": {133, 133, 133}, 585 | "gray53": {135, 135, 135}, 586 | "grey53": {135, 135, 135}, 587 | "gray54": {138, 138, 138}, 588 | "grey54": {138, 138, 138}, 589 | "gray55": {140, 140, 140}, 590 | "grey55": {140, 140, 140}, 591 | "gray56": {143, 143, 143}, 592 | "grey56": {143, 143, 143}, 593 | "gray57": {145, 145, 145}, 594 | "grey57": {145, 145, 145}, 595 | "gray58": {148, 148, 148}, 596 | "grey58": {148, 148, 148}, 597 | "gray59": {150, 150, 150}, 598 | "grey59": {150, 150, 150}, 599 | "gray60": {153, 153, 153}, 600 | "grey60": {153, 153, 153}, 601 | "gray61": {156, 156, 156}, 602 | "grey61": {156, 156, 156}, 603 | "gray62": {158, 158, 158}, 604 | "grey62": {158, 158, 158}, 605 | "gray63": {161, 161, 161}, 606 | "grey63": {161, 161, 161}, 607 | "gray64": {163, 163, 163}, 608 | "grey64": {163, 163, 163}, 609 | "gray65": {166, 166, 166}, 610 | "grey65": {166, 166, 166}, 611 | "gray66": {168, 168, 168}, 612 | "grey66": {168, 168, 168}, 613 | "gray67": {171, 171, 171}, 614 | "grey67": {171, 171, 171}, 615 | "gray68": {173, 173, 173}, 616 | "grey68": {173, 173, 173}, 617 | "gray69": {176, 176, 176}, 618 | "grey69": {176, 176, 176}, 619 | "gray70": {179, 179, 179}, 620 | "grey70": {179, 179, 179}, 621 | "gray71": {181, 181, 181}, 622 | "grey71": {181, 181, 181}, 623 | "gray72": {184, 184, 184}, 624 | "grey72": {184, 184, 184}, 625 | "gray73": {186, 186, 186}, 626 | "grey73": {186, 186, 186}, 627 | "gray74": {189, 189, 189}, 628 | "grey74": {189, 189, 189}, 629 | "gray75": {191, 191, 191}, 630 | "grey75": {191, 191, 191}, 631 | "gray76": {194, 194, 194}, 632 | "grey76": {194, 194, 194}, 633 | "gray77": {196, 196, 196}, 634 | "grey77": {196, 196, 196}, 635 | "gray78": {199, 199, 199}, 636 | "grey78": {199, 199, 199}, 637 | "gray79": {201, 201, 201}, 638 | "grey79": {201, 201, 201}, 639 | "gray80": {204, 204, 204}, 640 | "grey80": {204, 204, 204}, 641 | "gray81": {207, 207, 207}, 642 | "grey81": {207, 207, 207}, 643 | "gray82": {209, 209, 209}, 644 | "grey82": {209, 209, 209}, 645 | "gray83": {212, 212, 212}, 646 | "grey83": {212, 212, 212}, 647 | "gray84": {214, 214, 214}, 648 | "grey84": {214, 214, 214}, 649 | "gray85": {217, 217, 217}, 650 | "grey85": {217, 217, 217}, 651 | "gray86": {219, 219, 219}, 652 | "grey86": {219, 219, 219}, 653 | "gray87": {222, 222, 222}, 654 | "grey87": {222, 222, 222}, 655 | "gray88": {224, 224, 224}, 656 | "grey88": {224, 224, 224}, 657 | "gray89": {227, 227, 227}, 658 | "grey89": {227, 227, 227}, 659 | "gray90": {229, 229, 229}, 660 | "grey90": {229, 229, 229}, 661 | "gray91": {232, 232, 232}, 662 | "grey91": {232, 232, 232}, 663 | "gray92": {235, 235, 235}, 664 | "grey92": {235, 235, 235}, 665 | "gray93": {237, 237, 237}, 666 | "grey93": {237, 237, 237}, 667 | "gray94": {240, 240, 240}, 668 | "grey94": {240, 240, 240}, 669 | "gray95": {242, 242, 242}, 670 | "grey95": {242, 242, 242}, 671 | "gray96": {245, 245, 245}, 672 | "grey96": {245, 245, 245}, 673 | "gray97": {247, 247, 247}, 674 | "grey97": {247, 247, 247}, 675 | "gray98": {250, 250, 250}, 676 | "grey98": {250, 250, 250}, 677 | "gray99": {252, 252, 252}, 678 | "grey99": {252, 252, 252}, 679 | "gray100": {255, 255, 255}, 680 | "grey100": {255, 255, 255}, 681 | "darkgrey": {169, 169, 169}, 682 | "darkgray": {169, 169, 169}, 683 | "darkblue": {0, 0, 139}, 684 | "darkcyan": {0, 139, 139}, 685 | "darkmagenta": {139, 0, 139}, 686 | "darkred": {139, 0, 0}, 687 | "lightgreen": {144, 238, 144}, 688 | "crimson": {220, 20, 60}, 689 | "indigo": {75, 0, 130}, 690 | "olive": {128, 128, 0}, 691 | "rebeccapurple": {102, 51, 153}, 692 | "silver": {192, 192, 192}, 693 | "teal": {0, 128, 128}, 694 | } 695 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package layout implements different graph layouting functions 3 | */ 4 | package layout 5 | -------------------------------------------------------------------------------- /edge.go: -------------------------------------------------------------------------------- 1 | package layout 2 | 3 | type Edge struct { 4 | Directed bool 5 | From, To *Node 6 | Weight float64 7 | 8 | Tooltip string 9 | 10 | Label string 11 | FontName string 12 | FontSize Length 13 | FontColor Color 14 | 15 | LineWidth Length 16 | LineColor Color 17 | 18 | // computed in layouting 19 | Path []Vector 20 | } 21 | 22 | func NewEdge(from, to *Node) *Edge { 23 | edge := &Edge{} 24 | edge.From = from 25 | edge.To = to 26 | edge.Weight = 1.0 27 | edge.LineWidth = Point 28 | return edge 29 | } 30 | 31 | func (edge *Edge) String() string { 32 | return edge.From.String() + "->" + edge.To.String() 33 | } 34 | -------------------------------------------------------------------------------- /examples/basic.go: -------------------------------------------------------------------------------- 1 | // +build ignore 2 | 3 | package main 4 | 5 | import ( 6 | "os" 7 | 8 | "github.com/loov/layout" 9 | "github.com/loov/layout/format/svg" 10 | ) 11 | 12 | func main() { 13 | graph := layout.NewDigraph() 14 | graph.Node("A") 15 | graph.Node("B") 16 | graph.Node("C") 17 | graph.Node("D") 18 | graph.Edge("A", "B") 19 | graph.Edge("A", "C") 20 | graph.Edge("B", "D") 21 | graph.Edge("C", "D") 22 | graph.Edge("D", "A") 23 | 24 | layout.Hierarchical(graph) 25 | 26 | svg.Write(os.Stdout, graph) 27 | } 28 | -------------------------------------------------------------------------------- /examples/basic.svg: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 9 | A 10 | B 11 | C 12 | D 13 | 14 | -------------------------------------------------------------------------------- /examples/complex.go: -------------------------------------------------------------------------------- 1 | // +build ignore 2 | 3 | package main 4 | 5 | import ( 6 | "os" 7 | 8 | "github.com/loov/layout" 9 | "github.com/loov/layout/format/svg" 10 | ) 11 | 12 | func main() { 13 | graph := layout.NewDigraph() 14 | graph.RowPadding = 30 * layout.Point 15 | 16 | a := graph.Node("A") 17 | a.Shape = layout.Box 18 | a.Label = "Lorem\nIpsum\nDolorem" 19 | a.FillColor = layout.RGB{0xFF, 0xA0, 0x20} 20 | 21 | b := graph.Node("B") 22 | b.Shape = layout.Ellipse 23 | b.Label = "Ignitus" 24 | b.FillColor = layout.HSL{0, 0.7, 0.7} 25 | 26 | c := graph.Node("C") 27 | c.Shape = layout.Square 28 | c.FontSize = 12 * layout.Point 29 | c.FontColor = layout.RGB{0x20, 0x20, 0x20} 30 | 31 | graph.Node("D") 32 | 33 | ab := graph.Edge("A", "B") 34 | ab.LineWidth = 4 * layout.Point 35 | 36 | ac := graph.Edge("A", "C") 37 | ac.LineWidth = 4 * layout.Point 38 | if col, ok := layout.ColorByName("blue"); ok { 39 | ac.LineColor = col 40 | } 41 | 42 | bd := graph.Edge("B", "D") 43 | bd.LineColor = layout.RGB{0xA0, 0xFF, 0xA0} 44 | 45 | graph.Edge("C", "D") 46 | graph.Edge("D", "A") 47 | 48 | layout.Hierarchical(graph) 49 | 50 | svg.Write(os.Stdout, graph) 51 | } 52 | -------------------------------------------------------------------------------- /examples/complex.svg: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 9 | Lorem 10 | Ipsum 11 | Dolorem 12 | Ignitus 13 | C 14 | D 15 | 16 | -------------------------------------------------------------------------------- /examples/minimal.go: -------------------------------------------------------------------------------- 1 | // +build ignore 2 | 3 | package main 4 | 5 | import ( 6 | "os" 7 | 8 | "github.com/loov/layout" 9 | "github.com/loov/layout/format/svg" 10 | ) 11 | 12 | func main() { 13 | graph := layout.NewDigraph() 14 | graph.Edge("A", "B") 15 | graph.Edge("A", "C") 16 | graph.Edge("B", "D") 17 | graph.Edge("C", "D") 18 | 19 | layout.Hierarchical(graph) 20 | 21 | svg.Write(os.Stdout, graph) 22 | } 23 | -------------------------------------------------------------------------------- /examples/minimal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/loov/layout/df00cee5a29e8dc562b7222c9e45f5a784a1c3e1/examples/minimal.png -------------------------------------------------------------------------------- /examples/minimal.svg: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 9 | A 10 | B 11 | C 12 | D 13 | 14 | -------------------------------------------------------------------------------- /format/dot/parse.go: -------------------------------------------------------------------------------- 1 | // package dot implements dot file format parsing 2 | package dot 3 | 4 | import ( 5 | "io" 6 | "strconv" 7 | "strings" 8 | 9 | "github.com/loov/layout" 10 | 11 | "gonum.org/v1/gonum/graph/formats/dot" 12 | "gonum.org/v1/gonum/graph/formats/dot/ast" 13 | ) 14 | 15 | func Parse(r io.Reader) ([]*layout.Graph, error) { return parse(dot.Parse(r)) } 16 | func ParseFile(path string) ([]*layout.Graph, error) { return parse(dot.ParseFile(path)) } 17 | func ParseString(s string) ([]*layout.Graph, error) { return parse(dot.ParseString(s)) } 18 | 19 | func parse(file *ast.File, err error) ([]*layout.Graph, error) { 20 | if err != nil { 21 | return nil, err 22 | } 23 | 24 | graphs := []*layout.Graph{} 25 | for _, graphStmt := range file.Graphs { 26 | parser := &parserContext{} 27 | parser.Graph = layout.NewGraph() 28 | parser.parse(graphStmt) 29 | graphs = append(graphs, parser.Graph) 30 | } 31 | 32 | return graphs, nil 33 | } 34 | 35 | type parserContext struct { 36 | Graph *layout.Graph 37 | Cluster string 38 | 39 | allAttrs []*ast.Attr 40 | nodeAttrs []*ast.Attr 41 | edgeAttrs []*ast.Attr 42 | } 43 | 44 | func (context *parserContext) parse(src *ast.Graph) { 45 | context.Graph.ID = src.ID 46 | context.Graph.Directed = src.Directed 47 | context.parseStmts(src.Stmts) 48 | } 49 | 50 | func (context *parserContext) parseStmts(stmts []ast.Stmt) { 51 | for _, stmt := range stmts { 52 | switch stmt := stmt.(type) { 53 | case *ast.NodeStmt: 54 | context.parseNode(stmt) 55 | case *ast.EdgeStmt: 56 | context.parseEdge(stmt) 57 | case *ast.AttrStmt: 58 | switch stmt.Kind { 59 | case ast.NodeKind: 60 | context.nodeAttrs = append(context.nodeAttrs, stmt.Attrs...) 61 | case ast.EdgeKind: 62 | context.edgeAttrs = append(context.edgeAttrs, stmt.Attrs...) 63 | case ast.GraphKind: 64 | context.allAttrs = append(context.allAttrs, stmt.Attrs...) 65 | default: 66 | panic("unknown attr target kind") 67 | } 68 | case *ast.Attr: 69 | context.allAttrs = append(context.allAttrs, stmt) 70 | case *ast.Subgraph: 71 | subcontext := &parserContext{} 72 | subcontext.Graph = context.Graph 73 | subcontext.allAttrs = append(subcontext.allAttrs, context.allAttrs...) 74 | subcontext.nodeAttrs = append(subcontext.nodeAttrs, context.nodeAttrs...) 75 | subcontext.edgeAttrs = append(subcontext.edgeAttrs, context.edgeAttrs...) 76 | subcontext.parseStmts(stmt.Stmts) 77 | } 78 | } 79 | } 80 | 81 | func (context *parserContext) ensureNode(id string) *layout.Node { 82 | if node, exists := context.Graph.NodeByID[id]; exists { 83 | return node 84 | } 85 | 86 | node := context.Graph.Node(fixstring(id)) 87 | applyNodeAttrs(node, context.nodeAttrs) 88 | return node 89 | } 90 | 91 | func (context *parserContext) parseNode(src *ast.NodeStmt) *layout.Node { 92 | node := context.ensureNode(src.Node.ID) 93 | applyNodeAttrs(node, src.Attrs) 94 | return node 95 | } 96 | 97 | func (context *parserContext) parseEdge(edgeStmt *ast.EdgeStmt) { 98 | sources := context.ensureVertex(edgeStmt.From) 99 | to := edgeStmt.To 100 | for to != nil { 101 | targets := context.ensureVertex(to.Vertex) 102 | for _, source := range sources { 103 | for _, target := range targets { 104 | edge := layout.NewEdge(source, target) 105 | 106 | edge.Directed = to.Directed 107 | edge.From = source 108 | edge.To = target 109 | 110 | applyEdgeAttrs(edge, context.edgeAttrs) 111 | applyEdgeAttrs(edge, edgeStmt.Attrs) 112 | 113 | context.Graph.Edges = append(context.Graph.Edges, edge) 114 | } 115 | } 116 | 117 | sources = targets 118 | to = to.To 119 | } 120 | } 121 | 122 | func (context *parserContext) ensureVertex(src ast.Vertex) []*layout.Node { 123 | switch src := src.(type) { 124 | case *ast.Node: 125 | return []*layout.Node{context.ensureNode(src.ID)} 126 | case *ast.Subgraph: 127 | nodes := []*layout.Node{} 128 | for _, stmt := range src.Stmts { 129 | switch stmt := stmt.(type) { 130 | case *ast.NodeStmt: 131 | nodes = append(nodes, context.parseNode(stmt)) 132 | default: 133 | panic("unsupported stmt inside subgraph") 134 | } 135 | } 136 | return nodes 137 | default: 138 | panic("vertex not supported") 139 | } 140 | } 141 | 142 | func applyNodeAttrs(node *layout.Node, attrs []*ast.Attr) { 143 | for _, attr := range attrs { 144 | switch attr.Key { 145 | case "weight": 146 | setFloat(&node.Weight, attr.Val) 147 | case "shape": 148 | setShape(&node.Shape, attr.Val) 149 | case "label": 150 | setString(&node.Label, attr.Val) 151 | case "color": 152 | setColor(&node.FontColor, attr.Val) 153 | case "fontname": 154 | setString(&node.FontName, attr.Val) 155 | case "fontsize": 156 | setLength(&node.FontSize, attr.Val, layout.Point) 157 | case "pencolor": 158 | setColor(&node.LineColor, attr.Val) 159 | case "penwidth": 160 | setLength(&node.LineWidth, attr.Val, layout.Point) 161 | case "fillcolor": 162 | setColor(&node.FillColor, attr.Val) 163 | case "width": 164 | setLength(&node.Radius.X, attr.Val, layout.Inch*0.5) 165 | case "height": 166 | setLength(&node.Radius.Y, attr.Val, layout.Inch*0.5) 167 | case "tooltip": 168 | setString(&node.Tooltip, attr.Val) 169 | } 170 | } 171 | } 172 | 173 | func applyEdgeAttrs(edge *layout.Edge, attrs []*ast.Attr) { 174 | for _, attr := range attrs { 175 | switch attr.Key { 176 | case "weight": 177 | setFloat(&edge.Weight, attr.Val) 178 | case "label": 179 | setString(&edge.Label, attr.Val) 180 | case "color": 181 | setColor(&edge.FontColor, attr.Val) 182 | case "fontname": 183 | setString(&edge.FontName, attr.Val) 184 | case "fontsize": 185 | setLength(&edge.FontSize, attr.Val, layout.Point) 186 | case "pencolor": 187 | setColor(&edge.LineColor, attr.Val) 188 | case "penwidth": 189 | setLength(&edge.LineWidth, attr.Val, layout.Point) 190 | case "tooltip": 191 | setString(&edge.Tooltip, attr.Val) 192 | } 193 | } 194 | } 195 | 196 | func setColor(t *layout.Color, value string) { 197 | if value == "" { 198 | return 199 | } 200 | 201 | if value[0] == '#' { // hex 202 | value = value[1:] 203 | if len(value) == 6 { // RRGGBB 204 | v, err := strconv.ParseInt(value, 16, 64) 205 | if err == nil { 206 | c := layout.RGB{} 207 | c.R = uint8(v >> 16) 208 | c.G = uint8(v >> 8) 209 | c.B = uint8(v >> 0) 210 | *t = c 211 | } 212 | } else if len(value) == 8 { // RRGGBBAA 213 | v, err := strconv.ParseInt(value, 16, 64) 214 | if err == nil { 215 | c := layout.RGBA{} 216 | c.R = uint8(v >> 24) 217 | c.G = uint8(v >> 16) 218 | c.B = uint8(v >> 8) 219 | c.A = uint8(v >> 0) 220 | *t = c 221 | } 222 | } 223 | return 224 | } 225 | 226 | color, ok := layout.ColorByName(value) 227 | if ok { 228 | *t = color 229 | } 230 | } 231 | 232 | func setFloat(t *float64, value string) { 233 | v, err := strconv.ParseFloat(value, 64) 234 | if err == nil { 235 | *t = v 236 | } 237 | } 238 | 239 | func setLength(t *layout.Length, value string, unit layout.Length) { 240 | v, err := strconv.ParseFloat(value, 64) 241 | if err == nil { 242 | *t = layout.Length(v) * unit 243 | } 244 | } 245 | 246 | func setShape(t *layout.Shape, value string) { 247 | switch value { 248 | case "box", "rect", "rectangle": 249 | *t = layout.Box 250 | case "square": 251 | *t = layout.Square 252 | case "circle": 253 | *t = layout.Circle 254 | case "ellipse", "oval": 255 | *t = layout.Ellipse 256 | case "none": 257 | *t = layout.None 258 | default: 259 | *t = layout.Auto 260 | } 261 | } 262 | 263 | func setString(t *string, value string) { 264 | *t = fixstring(value) 265 | } 266 | 267 | func fixstring(s string) string { 268 | if len(s) > 2 && s[0] == '"' && s[len(s)-1] == '"' { 269 | s = s[1 : len(s)-1] 270 | } 271 | return strings.Replace(s, "\\n", "\n", -1) 272 | } 273 | -------------------------------------------------------------------------------- /format/dot/write.go: -------------------------------------------------------------------------------- 1 | package dot 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | 7 | "github.com/loov/layout/internal/hier" 8 | ) 9 | 10 | func writeLayout(out io.Writer, graph *hier.Graph) error { 11 | var err error 12 | 13 | write := func(format string, args ...interface{}) bool { 14 | if err != nil { 15 | return false 16 | } 17 | _, err = fmt.Fprintf(out, format, args...) 18 | return err == nil 19 | } 20 | 21 | write("digraph G {\n") 22 | for _, src := range graph.Nodes { 23 | if !src.Virtual { 24 | write("\t%v[rank = %v];\n", src.ID, src.Rank) 25 | } else { 26 | write("\t%v[rank = %v; shape=circle];\n", src.ID, src.Rank) 27 | } 28 | for _, dst := range src.Out { 29 | write("\t%v -> %v;\n", src.ID, dst.ID) 30 | } 31 | } 32 | write("}") 33 | return err 34 | } 35 | -------------------------------------------------------------------------------- /format/graphml/graphml.dtd: -------------------------------------------------------------------------------- 1 | 2 | 3 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 27 | 28 | 29 | 30 | 31 | 32 | 36 | 37 | 38 | 41 | 42 | 43 | 46 | 47 | 48 | 49 | 57 | 58 | 59 | 60 | 63 | 64 | 65 | 71 | 72 | 73 | 74 | 78 | 79 | 80 | 84 | 85 | 88 | -------------------------------------------------------------------------------- /format/graphml/write.go: -------------------------------------------------------------------------------- 1 | package graphml 2 | 3 | import ( 4 | "bytes" 5 | "encoding/xml" 6 | "io" 7 | 8 | "github.com/loov/layout" 9 | ) 10 | 11 | func Write(out io.Writer, graphs ...*layout.Graph) error { 12 | file := NewFile() 13 | for _, graph := range graphs { 14 | file.Graphs = append(file.Graphs, Convert(graph)) 15 | } 16 | 17 | file.Key = []Key{ 18 | Key{For: "node", ID: "label", AttrName: "label", AttrType: "string"}, 19 | Key{For: "node", ID: "shape", AttrName: "shape", AttrType: "string"}, 20 | Key{For: "edge", ID: "label", AttrName: "label", AttrType: "string"}, 21 | 22 | Key{For: "node", ID: "ynodelabel", YFilesType: "nodegraphics"}, 23 | Key{For: "edge", ID: "yedgelabel", YFilesType: "edgegraphics"}, 24 | } 25 | 26 | enc := xml.NewEncoder(out) 27 | enc.Indent("", "\t") 28 | return enc.Encode(file) 29 | } 30 | 31 | func Convert(graph *layout.Graph) *Graph { 32 | out := &Graph{} 33 | out.ID = graph.ID 34 | if graph.Directed { 35 | out.EdgeDefault = Directed 36 | } else { 37 | out.EdgeDefault = Undirected 38 | } 39 | 40 | for _, node := range graph.Nodes { 41 | outnode := Node{} 42 | outnode.ID = node.ID 43 | addAttr(&outnode.Attrs, "label", node.DefaultLabel()) 44 | addAttr(&outnode.Attrs, "shape", string(node.Shape)) 45 | addAttr(&outnode.Attrs, "tooltip", node.Tooltip) 46 | addYedLabelAttr(&outnode.Attrs, "ynodelabel", node.DefaultLabel()) 47 | out.Node = append(out.Node, outnode) 48 | } 49 | 50 | for _, edge := range graph.Edges { 51 | outedge := Edge{} 52 | outedge.Source = edge.From.ID 53 | outedge.Target = edge.To.ID 54 | addAttr(&outedge.Attrs, "label", edge.Label) 55 | addAttr(&outedge.Attrs, "tooltip", edge.Tooltip) 56 | addYedLabelAttr(&outedge.Attrs, "yedgelabel", edge.Label) 57 | out.Edge = append(out.Edge, outedge) 58 | } 59 | 60 | return out 61 | } 62 | 63 | func addAttr(attrs *[]Attr, key, value string) { 64 | if value == "" { 65 | return 66 | } 67 | *attrs = append(*attrs, Attr{key, escapeText(value)}) 68 | } 69 | 70 | func addYedLabelAttr(attrs *[]Attr, key, value string) { 71 | if value == "" { 72 | return 73 | } 74 | var buf bytes.Buffer 75 | buf.WriteString(``) 76 | if err := xml.EscapeText(&buf, []byte(value)); err != nil { 77 | // this shouldn't ever happen 78 | panic(err) 79 | } 80 | buf.WriteString(``) 81 | *attrs = append(*attrs, Attr{key, buf.Bytes()}) 82 | } 83 | 84 | func escapeText(s string) []byte { 85 | if s == "" { 86 | return []byte{} 87 | } 88 | 89 | var buf bytes.Buffer 90 | if err := xml.EscapeText(&buf, []byte(s)); err != nil { 91 | // this shouldn't ever happen 92 | panic(err) 93 | } 94 | return buf.Bytes() 95 | } 96 | -------------------------------------------------------------------------------- /format/graphml/xml.go: -------------------------------------------------------------------------------- 1 | package graphml 2 | 3 | import "encoding/xml" 4 | 5 | type File struct { 6 | XMLName xml.Name `xml:"graphml"` 7 | XMLNS string `xml:"xmlns,attr"` 8 | XMLNSXSI string `xml:"xmlns:xsi,attr"` 9 | XMLNSY string `xml:"xmlns:y,attr"` 10 | XSISchemaLocation string `xml:"xsi:schemalocation,attr"` 11 | 12 | Key []Key `xml:"key"` 13 | Graphs []*Graph `xml:"graph"` 14 | } 15 | 16 | func NewFile() *File { 17 | file := &File{} 18 | file.XMLNS = "http://graphml.graphdrawing.org/xmlns" 19 | file.XMLNSXSI = "http://www.w3.org/2001/XMLSchema-instance" 20 | file.XMLNSY = "http://www.yworks.com/xml/graphml" 21 | file.XSISchemaLocation = "http://graphml.graphdrawing.org/xmlns http://graphml.graphdrawing.org/xmlns/1.0/graphml.xsd" 22 | return file 23 | } 24 | 25 | type Graph struct { 26 | // XMLName xml.Name `xml:"graph"` 27 | ID string `xml:"id,attr"` 28 | EdgeDefault EdgeDefault `xml:"edgedefault,attr"` 29 | 30 | Node []Node `xml:"node"` 31 | Edge []Edge `xml:"edge"` 32 | Hyperedge []Hyperedge `xml:"hyperedge"` 33 | // TODO: parse info 34 | } 35 | 36 | type Key struct { 37 | ID string `xml:"id,attr"` 38 | For string `xml:"for,attr"` 39 | 40 | AttrName string `xml:"attr.name,attr,omitempty"` 41 | AttrType string `xml:"attr.type,attr,omitempty"` 42 | 43 | YFilesType string `xml:"yfiles.type,attr,omitempty"` 44 | } 45 | 46 | type Node struct { 47 | // XMLName xml.Name `xml:"node"` 48 | ID string `xml:"id,attr"` 49 | Port []Port `xml:"port"` 50 | Graph []*Graph `xml:"graph"` 51 | Attrs []Attr `xml:"data"` 52 | 53 | // TODO: parse info 54 | } 55 | 56 | type Port struct { 57 | // XMLName xml.Name `xml:"port"` 58 | Name string `xml:"name,attr"` 59 | } 60 | 61 | type Edge struct { 62 | // XMLName xml.Name `xml:"edge"` 63 | ID string `xml:"id,attr,omitempty"` 64 | 65 | Source string `xml:"source,attr"` 66 | Target string `xml:"target,attr"` 67 | Directed *bool `xml:"directed,attr,omitempty"` 68 | 69 | SourcePort string `xml:"sourceport,attr,omitempty"` 70 | TargetPort string `xml:"targetport,attr,omitempty"` 71 | 72 | Attrs []Attr `xml:"data"` 73 | } 74 | 75 | type EdgeDefault string 76 | 77 | const ( 78 | Undirected = EdgeDefault("undirected") 79 | Directed = EdgeDefault("directed") 80 | ) 81 | 82 | type Attr struct { 83 | // XMLName xml.Name `xml:"data"` 84 | Key string `xml:"key,attr"` 85 | Value []byte `xml:",innerxml"` 86 | } 87 | 88 | type Hyperedge struct { 89 | // XMLName xml.Name `xml:"hyperedge"` 90 | 91 | ID string `xml:"id,attr,omitempty"` 92 | Endpoint []Endpoint `xml:"endpoint"` 93 | } 94 | 95 | type Endpoint struct { 96 | // XMLName xml.Name `xml:"endpoint"` 97 | Node string `xml:"node,attr"` 98 | Port string `xml:"port,attr,omitempty"` 99 | Type EndpointType `xml:"type,attr,omitempty"` 100 | } 101 | 102 | type EndpointType string 103 | 104 | const ( 105 | EndpointIn = EndpointType("in") 106 | EndpointOut = EndpointType("out") 107 | EndpointUndir = EndpointType("undir") 108 | ) 109 | -------------------------------------------------------------------------------- /format/svg/write.go: -------------------------------------------------------------------------------- 1 | package svg 2 | 3 | import ( 4 | "fmt" 5 | stdhtml "html" 6 | "io" 7 | "strconv" 8 | "strings" 9 | 10 | "github.com/loov/layout" 11 | "golang.org/x/net/html" 12 | ) 13 | 14 | type writer struct { 15 | w io.Writer 16 | err error 17 | } 18 | 19 | func (svg *writer) erred() bool { return svg.err != nil } 20 | func (svg *writer) Error() error { return svg.err } 21 | 22 | func (svg *writer) write(format string, args ...interface{}) { 23 | if svg.erred() { 24 | return 25 | } 26 | _, svg.err = fmt.Fprintf(svg.w, format, args...) 27 | } 28 | 29 | func (svg *writer) start(width, height layout.Length) { 30 | svg.write("", width, height) 31 | } 32 | func (svg *writer) finish() { 33 | svg.write("\n") 34 | } 35 | 36 | func (svg *writer) writeStyle() { 37 | svg.write(` 38 | `) 41 | } 42 | 43 | func (svg *writer) startG() { svg.write("") } 44 | func (svg *writer) finishG() { svg.write("") } 45 | 46 | func (svg *writer) writeDefs() { 47 | svg.write(` 48 | 49 | 50 | 51 | 52 | `) 53 | } 54 | 55 | func colortext(color layout.Color) string { 56 | const hex = "0123456789ABCDEF" 57 | r, g, b, a := color.RGBA8() 58 | if a == 0 { 59 | return "none" 60 | } 61 | return string([]byte{'#', 62 | hex[r>>4], hex[r&7], 63 | hex[g>>4], hex[g&7], 64 | hex[b>>4], hex[b&7], 65 | //hex[a>>4], hex[a&7], 66 | }) 67 | } 68 | 69 | func dkcolor(color layout.Color) string { 70 | if color == nil { 71 | return "#000000" 72 | } 73 | return colortext(color) 74 | } 75 | 76 | func ltcolor(color layout.Color) string { 77 | if color == nil { 78 | return "#FFFFFF" 79 | } 80 | return colortext(color) 81 | } 82 | 83 | func vec(x, y layout.Length) string { 84 | return strconv.FormatFloat(float64(x), 'f', -1, 32) + "," + 85 | strconv.FormatFloat(float64(y), 'f', -1, 32) + " " 86 | } 87 | 88 | func straightPath(graph *layout.Graph, path []layout.Vector) string { 89 | line := "" 90 | 91 | p0 := path[0] 92 | line += "M" + vec(p0.X, p0.Y) 93 | for _, p := range path[1:] { 94 | line += "L" + vec(p.X, p.Y) 95 | } 96 | 97 | return line 98 | } 99 | 100 | func bezierPath(graph *layout.Graph, path []layout.Vector) string { 101 | line := "" 102 | 103 | p0 := path[0] 104 | dir := layout.Length(1) 105 | if p0.Y > path[1].Y { 106 | dir *= -1 107 | } 108 | cpoff := dir * graph.RowPadding * 2 109 | line += "M" + vec(p0.X, p0.Y) 110 | for _, p1 := range path[1:] { 111 | line += "C" + 112 | vec(p0.X, p0.Y+cpoff) + 113 | vec(p1.X, p1.Y-cpoff) + 114 | vec(p1.X, p1.Y) 115 | p0 = p1 116 | } 117 | 118 | return line 119 | } 120 | 121 | func smartPath(graph *layout.Graph, path []layout.Vector) string { 122 | line := "" 123 | 124 | p0 := path[0] 125 | p1 := path[1] 126 | dir := layout.Length(1) 127 | if p0.Y > p1.Y { 128 | dir *= -1 129 | } 130 | 131 | if len(path) == 2 && p0.X == p1.X { 132 | return "M" + vec(p0.X, p0.Y) + "L " + vec(p1.X, p1.Y) 133 | } 134 | 135 | var sx, sy layout.Length 136 | line += "M" + vec(p0.X, p0.Y) 137 | for i, p2 := range path[2:] { 138 | sx = p0.X*0.2 + p1.X*0.8 139 | if (p0.X < p1.X) != (p1.X < p2.X) { 140 | sx = p1.X 141 | } 142 | sy = p1.Y - dir*graph.RowPadding 143 | if i == 0 { 144 | line += "C" + vec(p0.X, p0.Y+dir*graph.RowPadding) + vec(sx, sy) + vec(p1.X, p1.Y) 145 | } else { 146 | line += "S" + vec(sx, sy) + vec(p1.X, p1.Y) 147 | } 148 | 149 | p0, p1 = p1, p2 150 | } 151 | sx = p0.X*0.2 + p1.X*0.8 152 | sy = p1.Y - 2*dir*graph.RowPadding 153 | 154 | if len(path) == 2 { 155 | line += "C" + vec(p0.X, p0.Y+dir*graph.RowPadding) + vec(sx, sy) + vec(p1.X, p1.Y) 156 | } else { 157 | line += "S" + vec(sx, sy) + vec(p1.X, p1.Y) 158 | } 159 | 160 | return line 161 | } 162 | 163 | func Write(w io.Writer, graph *layout.Graph) error { 164 | svg := &writer{} 165 | svg.w = w 166 | 167 | _, bottomRight := graph.Bounds() 168 | svg.start(bottomRight.X+graph.NodePadding, bottomRight.Y+graph.RowPadding) 169 | svg.writeStyle() 170 | svg.writeDefs() 171 | 172 | svg.startG() 173 | for _, edge := range graph.Edges { 174 | if len(edge.Path) == 0 { 175 | // TODO: log invalid path 176 | continue 177 | } 178 | 179 | if edge.Directed { 180 | svg.write("", smartPath(graph, edge.Path)) 188 | 189 | if edge.Tooltip != "" { 190 | svg.write("%v", escapeString(edge.Tooltip)) 191 | } 192 | 193 | svg.write("") 194 | } 195 | 196 | for _, node := range graph.Nodes { 197 | // TODO: add other shapes 198 | svgtag := "circle" 199 | switch node.Shape { 200 | default: 201 | fallthrough 202 | case layout.Circle: 203 | svgtag = "circle" 204 | r := max(node.Radius.X, node.Radius.Y) 205 | svg.write("") 235 | if node.Tooltip != "" { 236 | svg.write("%v", escapeString(node.Tooltip)) 237 | } 238 | svg.write("", svgtag) 239 | 240 | if label := node.DefaultLabel(); label != "" { 241 | if label[0] == '<' && label[len(label)-1] == '>' { 242 | svg.write("%v`, lowercaseTags(label[1:len(label)-1])) 251 | svg.write("") 252 | } else { 253 | lines := strings.Split(label, "\n") 254 | top := node.Center.Y - graph.LineHeight*layout.Length(len(lines))*0.5 255 | top += graph.LineHeight * 0.5 256 | for _, line := range lines { 257 | svg.write("%v\n", escapeString(line)) 266 | top += graph.LineHeight 267 | } 268 | } 269 | } 270 | } 271 | svg.finishG() 272 | svg.finish() 273 | 274 | return svg.err 275 | } 276 | 277 | func lowercaseTags(s string) string { 278 | root := &html.Node{Type: html.ElementNode} 279 | nodes, err := html.ParseFragment(strings.NewReader(s), root) 280 | if err != nil { 281 | return s 282 | } 283 | 284 | var out strings.Builder 285 | for _, node := range nodes { 286 | err := html.Render(&out, node) 287 | if err != nil { 288 | return s 289 | } 290 | } 291 | return out.String() 292 | } 293 | 294 | func max(a, b layout.Length) layout.Length { 295 | if a > b { 296 | return a 297 | } 298 | return b 299 | } 300 | 301 | func escapeString(s string) string { 302 | return stdhtml.EscapeString(s) 303 | } 304 | -------------------------------------------------------------------------------- /format/tgf/write.go: -------------------------------------------------------------------------------- 1 | package tgf 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | 7 | "github.com/loov/layout/internal/hier" 8 | ) 9 | 10 | func writeLayout(out io.Writer, graph *hier.Graph) error { 11 | var err error 12 | write := func(format string, args ...interface{}) bool { 13 | if err != nil { 14 | return false 15 | } 16 | 17 | _, err = fmt.Fprintf(out, format, args...) 18 | return err == nil 19 | } 20 | 21 | for _, src := range graph.Nodes { 22 | if !src.Virtual { 23 | write("%v %v\n", src.ID, src.ID) 24 | } else { 25 | write("%v\n", src.ID) 26 | } 27 | } 28 | 29 | write("#\n") 30 | 31 | for _, src := range graph.Nodes { 32 | for _, dst := range src.Out { 33 | write("%v %v\n", src.ID, dst.ID) 34 | } 35 | } 36 | 37 | return err 38 | } 39 | -------------------------------------------------------------------------------- /geom.go: -------------------------------------------------------------------------------- 1 | package layout 2 | 3 | type Shape string 4 | 5 | const ( 6 | Auto Shape = "" 7 | None = "none" 8 | Box = "box" 9 | Square = "square" 10 | Circle = "circle" 11 | Ellipse = "ellipse" 12 | ) 13 | 14 | type Vector struct{ X, Y Length } 15 | 16 | // Length is a value represented in points 17 | type Length float64 18 | 19 | const ( 20 | Point = 1 21 | Inch = 72 22 | Twip = Inch / 1440 23 | 24 | Meter = 39.3701 * Inch 25 | Centimeter = Meter * 0.01 26 | Millimeter = Meter * 0.001 27 | ) 28 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/loov/layout 2 | 3 | go 1.22 4 | 5 | toolchain go1.23.4 6 | 7 | require ( 8 | golang.org/x/net v0.34.0 9 | gonum.org/v1/gonum v0.15.1 10 | ) 11 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0= 2 | golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k= 3 | gonum.org/v1/gonum v0.15.1 h1:FNy7N6OUZVUaWG9pTiD+jlhdQ3lMP+/LcTpJ6+a8sQ0= 4 | gonum.org/v1/gonum v0.15.1/go.mod h1:eZTZuRFrzu5pcyjN5wJhcIhnUdNijYxX1T2IcrOGY0o= 5 | -------------------------------------------------------------------------------- /graph.go: -------------------------------------------------------------------------------- 1 | package layout 2 | 3 | type Graph struct { 4 | ID string 5 | Directed bool 6 | 7 | // Defaults 8 | LineHeight Length 9 | FontSize Length 10 | Shape Shape 11 | 12 | NodePadding Length 13 | RowPadding Length 14 | EdgePadding Length 15 | 16 | NodeByID map[string]*Node 17 | Nodes []*Node 18 | Edges []*Edge 19 | } 20 | 21 | func NewGraph() *Graph { 22 | graph := &Graph{} 23 | 24 | graph.LineHeight = 16 * Point 25 | graph.Shape = Auto 26 | 27 | graph.NodeByID = make(map[string]*Node) 28 | return graph 29 | } 30 | 31 | func NewDigraph() *Graph { 32 | graph := NewGraph() 33 | graph.Directed = true 34 | return graph 35 | } 36 | 37 | // Node finds or creates node with id 38 | func (graph *Graph) Node(id string) *Node { 39 | if id == "" { 40 | panic("invalid node id") 41 | } 42 | 43 | node, found := graph.NodeByID[id] 44 | if !found { 45 | node = NewNode(id) 46 | graph.AddNode(node) 47 | } 48 | return node 49 | } 50 | 51 | // Edge finds or creates new edge based on ids 52 | func (graph *Graph) Edge(from, to string) *Edge { 53 | source, target := graph.Node(from), graph.Node(to) 54 | for _, edge := range graph.Edges { 55 | if edge.From == source && edge.To == target { 56 | return edge 57 | } 58 | } 59 | 60 | edge := NewEdge(source, target) 61 | edge.Directed = graph.Directed 62 | graph.AddEdge(edge) 63 | return edge 64 | } 65 | 66 | // AddNode adds a new node. 67 | // 68 | // When a node with the specified id already it will return false 69 | // and the node is not added. 70 | func (graph *Graph) AddNode(node *Node) bool { 71 | if node.ID != "" { 72 | _, found := graph.NodeByID[node.ID] 73 | if found { 74 | return false 75 | } 76 | graph.NodeByID[node.ID] = node 77 | } 78 | graph.Nodes = append(graph.Nodes, node) 79 | return true 80 | } 81 | 82 | func (graph *Graph) AddEdge(edge *Edge) { 83 | graph.Edges = append(graph.Edges, edge) 84 | } 85 | 86 | func minvector(a *Vector, b Vector) { 87 | if b.X < a.X { 88 | a.X = b.X 89 | } 90 | if b.Y < a.Y { 91 | a.Y = b.Y 92 | } 93 | } 94 | 95 | func maxvector(a *Vector, b Vector) { 96 | if b.X > a.X { 97 | a.X = b.X 98 | } 99 | if b.Y > a.Y { 100 | a.Y = b.Y 101 | } 102 | } 103 | 104 | func (graph *Graph) Bounds() (min, max Vector) { 105 | for _, node := range graph.Nodes { 106 | minvector(&min, node.TopLeft()) 107 | maxvector(&max, node.BottomRight()) 108 | } 109 | 110 | for _, edge := range graph.Edges { 111 | for _, p := range edge.Path { 112 | minvector(&min, p) 113 | maxvector(&max, p) 114 | } 115 | } 116 | 117 | minvector(&min, max) 118 | maxvector(&max, min) 119 | 120 | return 121 | } 122 | -------------------------------------------------------------------------------- /internal/cmd/hier/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "flag" 6 | "fmt" 7 | "io" 8 | "math" 9 | "os" 10 | "path/filepath" 11 | "strings" 12 | "time" 13 | 14 | "github.com/loov/layout" 15 | "github.com/loov/layout/format/dot" 16 | "github.com/loov/layout/internal/hier" 17 | ) 18 | 19 | var ( 20 | informat = flag.String("S", "", "input format") 21 | outformat = flag.String("T", "svg", "output format") 22 | 23 | verbose = flag.Bool("v", false, "verbose output") 24 | veryVerbose = flag.Bool("vv", false, "very-verbose output") 25 | ) 26 | 27 | func info(format string, args ...interface{}) { 28 | if *verbose { 29 | fmt.Fprintf(os.Stderr, format, args...) 30 | if !strings.HasSuffix("\n", format) { 31 | fmt.Fprint(os.Stderr, "\n") 32 | } 33 | } 34 | } 35 | 36 | func main() { 37 | flag.Parse() 38 | 39 | input := flag.Arg(0) 40 | output := flag.Arg(1) 41 | 42 | if input == "" { 43 | info("input is missing") 44 | flag.Usage() 45 | return 46 | } 47 | 48 | *verbose = *verbose || *veryVerbose 49 | 50 | if *informat == "" { 51 | // try to detect input format 52 | switch strings.ToLower(filepath.Ext(input)) { 53 | case ".dot": 54 | *informat = "dot" 55 | case ".gv": 56 | *informat = "dot" 57 | } 58 | } 59 | 60 | if output != "" { 61 | *outformat = "" 62 | } 63 | if *outformat == "" { 64 | // try to detect output format 65 | switch strings.ToLower(filepath.Ext(output)) { 66 | case ".svg": 67 | *outformat = "svg" 68 | default: 69 | *outformat = "svg" 70 | } 71 | } 72 | 73 | if *informat == "" || *outformat == "" { 74 | info("unable to detect input or output format") 75 | flag.Usage() 76 | os.Exit(1) 77 | return 78 | } 79 | 80 | var graphs []*layout.Graph 81 | var err error 82 | 83 | info("parsing %q", input) 84 | 85 | switch *informat { 86 | case "dot": 87 | graphs, err = dot.ParseFile(input) 88 | default: 89 | info("unknown input format %q", *informat) 90 | flag.Usage() 91 | os.Exit(1) 92 | return 93 | } 94 | 95 | if err != nil || len(graphs) == 0 { 96 | if len(graphs) == 0 && err == nil { 97 | err = errors.New("file doesn't contain graphs") 98 | } 99 | info("failed to parse %q: %v", input, err) 100 | os.Exit(1) 101 | return 102 | } 103 | 104 | if len(graphs) != 1 { 105 | info("parsed %v graphs", len(graphs)) 106 | } else { 107 | info("parsed 1 graph") 108 | } 109 | 110 | graphdef := graphs[0] 111 | if len(graphs) > 1 { 112 | fmt.Fprintf(os.Stderr, "file %q contains multiple graphs, processing only first\n", input) 113 | } 114 | 115 | var start, stop time.Time 116 | 117 | info("\nCONVERTING") 118 | 119 | nodeID := map[*layout.Node]hier.ID{} 120 | 121 | graph := &hier.Graph{} 122 | for _, nodedef := range graphdef.Nodes { 123 | node := graph.AddNode() 124 | nodeID[nodedef] = node.ID 125 | node.Label = nodedef.ID 126 | } 127 | for _, edge := range graphdef.Edges { 128 | from, to := nodeID[edge.From], nodeID[edge.To] 129 | graph.AddEdge(graph.Nodes[from], graph.Nodes[to]) 130 | } 131 | 132 | if *verbose { 133 | info(" nodes: %-8v roots: %-8v", graph.NodeCount(), graph.CountRoots()) 134 | info(" edges: %-8v links: %-8v", graph.CountEdges(), graph.CountUndirectedLinks()) 135 | info(" cycle: %-8v", graph.IsCyclic()) 136 | } 137 | 138 | info("\nDECYCLING") 139 | start = time.Now() 140 | decycle := hier.NewDecycle(graph) 141 | decycle.Run() 142 | stop = time.Now() 143 | 144 | if *verbose { 145 | info(" time: %.3f ms", float64(stop.Sub(start).Nanoseconds())/1e6) 146 | info(" nodes: %-8v roots: %-8v", graph.NodeCount(), graph.CountRoots()) 147 | info(" edges: %-8v links: %-8v", graph.CountEdges(), graph.CountUndirectedLinks()) 148 | } 149 | 150 | info("\nRANKING") 151 | start = time.Now() 152 | hier.Rank(graph) 153 | stop = time.Now() 154 | if *verbose { 155 | info(" time: %.3f ms", float64(stop.Sub(start).Nanoseconds())/1e6) 156 | info(" ranks: %-8v avg: %-8.2f var: %-8.2f", len(graph.ByRank), rankWidthAverage(graph), rankWidthVariance(graph)) 157 | if *veryVerbose { 158 | for i, rank := range graph.ByRank { 159 | info(" %4d- count: %-2d %v", i, len(rank), rank) 160 | } 161 | } 162 | } 163 | 164 | info("\nADDING VIRTUALS") 165 | start = time.Now() 166 | hier.AddVirtualVertices(graph) 167 | stop = time.Now() 168 | if *verbose { 169 | info(" time: %.3f ms", float64(stop.Sub(start).Nanoseconds())/1e6) 170 | info(" nodes: %-8v roots: %-8v", graph.NodeCount(), graph.CountRoots()) 171 | info(" edges: %-8v links: %-8v", graph.CountEdges(), graph.CountUndirectedLinks()) 172 | // TODO: add info about crossings 173 | info(" ranks: %-8v avg: %-8.2f var: %-8.2f", len(graph.ByRank), rankWidthAverage(graph), rankWidthVariance(graph)) 174 | if *veryVerbose { 175 | for i, rank := range graph.ByRank { 176 | info(" %4d- count: %-2d %v", i, len(rank), rank) 177 | } 178 | } 179 | } 180 | 181 | info("\nORDERING") 182 | start = time.Now() 183 | hier.OrderRanks(graph) 184 | stop = time.Now() 185 | if *verbose { 186 | info(" time: %.3f ms", float64(stop.Sub(start).Nanoseconds())/1e6) 187 | // TODO: add info about crossings 188 | if *veryVerbose { 189 | for i, rank := range graph.ByRank { 190 | info(" %4d- count: %-2d %v", i, len(rank), rank) 191 | } 192 | } 193 | } 194 | 195 | // TODO: add step about initial positions 196 | // TODO: add average, max, total edge length 197 | 198 | info("\nPOSITIONING") 199 | start = time.Now() 200 | hier.Position(graph) 201 | stop = time.Now() 202 | if *verbose { 203 | info(" time: %.3f ms", float64(stop.Sub(start).Nanoseconds())/1e6) 204 | // TODO: add average, max, total edge length 205 | } 206 | 207 | info("\nOUTPUTTING") 208 | 209 | start = time.Now() 210 | 211 | var out io.Writer 212 | if output == "" { 213 | out = os.Stdout 214 | } else { 215 | file, err := os.Create(output) 216 | if err != nil { 217 | info("unable to create file %q: %v", output, err) 218 | os.Exit(1) 219 | return 220 | } 221 | defer file.Close() 222 | out = file 223 | } 224 | 225 | if err != nil { 226 | info("writing %q failed: %v", output, err) 227 | os.Exit(1) 228 | return 229 | } 230 | 231 | stop = time.Now() 232 | if *verbose { 233 | info(" time: %.3f ms", float64(stop.Sub(start).Nanoseconds())/1e6) 234 | } 235 | } 236 | 237 | func rankWidthAverage(graph *hier.Graph) float64 { 238 | return float64(len(graph.Nodes)) / float64(len(graph.ByRank)) 239 | } 240 | 241 | func rankWidthVariance(graph *hier.Graph) float64 { 242 | if graph.NodeCount() < 2 { 243 | return 0 244 | } 245 | 246 | averageWidth := rankWidthAverage(graph) 247 | total := float64(0) 248 | for _, rank := range graph.ByRank { 249 | deviation := float64(len(rank)) - averageWidth 250 | total += deviation * deviation 251 | } 252 | 253 | return math.Sqrt(total / float64(len(graph.ByRank)-1)) 254 | } 255 | -------------------------------------------------------------------------------- /internal/cmd/propagate/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | ) 7 | 8 | const ( 9 | EdgeCount = 6 10 | EdgeMask = (1 << EdgeCount) - 1 11 | ) 12 | 13 | type EdgeTable [EdgeCount][EdgeCount]byte 14 | 15 | func (Z *EdgeTable) Edge(x, y int) { 16 | (*Z)[x][y] = EdgeMask 17 | } 18 | 19 | func (Z *EdgeTable) DeleteOutbound() { 20 | for x := 0; x < EdgeCount; x++ { 21 | for y := 0; y < EdgeCount; y++ { 22 | (*Z)[x][y] &^= (1 << byte(x)) 23 | } 24 | } 25 | } 26 | 27 | func (Z *EdgeTable) DeleteInbound() { 28 | for x := 0; x < EdgeCount; x++ { 29 | for y := 0; y < EdgeCount; y++ { 30 | (*Z)[x][y] &^= (1 << byte(y)) 31 | } 32 | } 33 | } 34 | 35 | func (Z *EdgeTable) Or(A, B *EdgeTable) { 36 | for x := 0; x < EdgeCount; x++ { 37 | for y := 0; y < EdgeCount; y++ { 38 | (*Z)[x][y] = (*A)[x][y] | (*B)[x][y] 39 | } 40 | } 41 | } 42 | 43 | func (Z *EdgeTable) And(A, B *EdgeTable) { 44 | for x := 0; x < EdgeCount; x++ { 45 | for y := 0; y < EdgeCount; y++ { 46 | (*Z)[x][y] = (*A)[x][y] & (*B)[x][y] 47 | } 48 | } 49 | } 50 | 51 | func (Z *EdgeTable) Mul(A, B *EdgeTable) { 52 | for x := 0; x < EdgeCount; x++ { 53 | for y := 0; y < EdgeCount; y++ { 54 | t := byte(0) 55 | for k := 0; k < EdgeCount; k++ { 56 | t |= (*A)[x][k] & (*B)[k][y] 57 | } 58 | (*Z)[x][y] = t 59 | } 60 | } 61 | } 62 | 63 | func (Z *EdgeTable) Print() { 64 | for x := 0; x < EdgeCount; x++ { 65 | for y := 0; y < EdgeCount; y++ { 66 | v := (*Z)[x][y] 67 | if v == 0 { 68 | fmt.Printf(" · ") 69 | } else { 70 | fmt.Printf("%2d ", v) 71 | } 72 | } 73 | fmt.Printf("\n") 74 | } 75 | } 76 | 77 | func (Z *EdgeTable) PrintBit() { 78 | for x := 0; x < EdgeCount; x++ { 79 | for y := 0; y < EdgeCount; y++ { 80 | v := (*Z)[x][y] 81 | fmt.Printf("%2d ", v) 82 | s := fmt.Sprintf("%08b ", v) 83 | s = strings.Replace(s, "0", "░", -1) 84 | s = strings.Replace(s, "1", "█", -1) 85 | fmt.Print(s) 86 | } 87 | fmt.Printf("\n") 88 | } 89 | } 90 | 91 | func (Z *EdgeTable) PrintBool() { 92 | fmt.Printf(" ") 93 | for y := 0; y < EdgeCount; y++ { 94 | fmt.Printf("%d ", y) 95 | } 96 | fmt.Printf("\n") 97 | for x := 0; x < EdgeCount; x++ { 98 | fmt.Printf("%d ", x) 99 | for y := 0; y < EdgeCount; y++ { 100 | v := (*Z)[x][y] 101 | if v == 0 { 102 | fmt.Printf("░░") 103 | } else { 104 | fmt.Printf("██") 105 | } 106 | } 107 | fmt.Printf("\n") 108 | } 109 | } 110 | 111 | func (Z *EdgeTable) PrintLayer(n byte) { 112 | fmt.Printf(" ") 113 | for y := 0; y < EdgeCount; y++ { 114 | fmt.Printf("%d ", y) 115 | } 116 | fmt.Printf("\n") 117 | for x := 0; x < EdgeCount; x++ { 118 | fmt.Printf("%d ", x) 119 | for y := 0; y < EdgeCount; y++ { 120 | v := ((*Z)[x][y] >> n) & 1 121 | if v == 0 { 122 | fmt.Printf("░░") 123 | } else { 124 | fmt.Printf("██") 125 | } 126 | } 127 | fmt.Printf("\n") 128 | } 129 | } 130 | 131 | func PrintSideBySideLayer(A, B, C *EdgeTable, n byte) { 132 | fmt.Printf(" ") 133 | for y := 0; y < EdgeCount; y++ { 134 | fmt.Printf("%d ", y) 135 | } 136 | fmt.Printf(" ") 137 | for y := 0; y < EdgeCount; y++ { 138 | fmt.Printf("%d ", y) 139 | } 140 | fmt.Printf(" ") 141 | for y := 0; y < EdgeCount; y++ { 142 | fmt.Printf("%d ", y) 143 | } 144 | fmt.Printf("\n") 145 | 146 | for x := 0; x < EdgeCount; x++ { 147 | fmt.Printf("%d ", x) 148 | 149 | for y := 0; y < EdgeCount; y++ { 150 | v := ((*A)[x][y] >> n) & 1 151 | if v == 0 { 152 | fmt.Printf("░░") 153 | } else { 154 | fmt.Printf("██") 155 | } 156 | } 157 | 158 | fmt.Printf(" %d ", x) 159 | 160 | for y := 0; y < EdgeCount; y++ { 161 | v := ((*B)[x][y] >> n) & 1 162 | if v == 0 { 163 | fmt.Printf("░░") 164 | } else { 165 | fmt.Printf("██") 166 | } 167 | } 168 | 169 | fmt.Printf(" %d ", x) 170 | 171 | for y := 0; y < EdgeCount; y++ { 172 | v := ((*C)[x][y] >> n) & 1 173 | if v == 0 { 174 | fmt.Printf("░░") 175 | } else { 176 | fmt.Printf("██") 177 | } 178 | } 179 | 180 | fmt.Printf("\n") 181 | } 182 | } 183 | 184 | func (Z *EdgeTable) CountLayer(n byte) int { 185 | total := 0 186 | for x := 0; x < EdgeCount; x++ { 187 | for y := 0; y < EdgeCount; y++ { 188 | total += int(((*Z)[x][y] >> n) & 1) 189 | } 190 | } 191 | return total 192 | } 193 | 194 | const ( 195 | NodeA = iota 196 | NodeB 197 | NodeC 198 | NodeD 199 | NodeE 200 | NodeF 201 | ) 202 | 203 | func Process(input *EdgeTable) EdgeTable { 204 | var result, temp EdgeTable 205 | 206 | result = *input 207 | for i := 0; i < EdgeCount; i++ { 208 | temp.Mul(&result, input) 209 | result.Or(&result, &temp) 210 | } 211 | 212 | return result 213 | } 214 | 215 | func main() { 216 | var Input EdgeTable 217 | Input.Edge(NodeA, NodeB) 218 | Input.Edge(NodeA, NodeC) 219 | Input.Edge(NodeB, NodeD) 220 | Input.Edge(NodeC, NodeD) 221 | Input.Edge(NodeC, NodeE) 222 | Input.Edge(NodeD, NodeE) 223 | Input.Edge(NodeD, NodeF) 224 | Input.Edge(NodeE, NodeF) 225 | Input.Edge(NodeF, NodeA) 226 | 227 | Input.PrintBool() 228 | 229 | inbound := Input 230 | outbound := Input 231 | 232 | inbound.DeleteInbound() 233 | 234 | for layer := 0; layer < EdgeCount; layer++ { 235 | fmt.Println("------------------") 236 | inbound.PrintLayer(byte(layer)) 237 | } 238 | 239 | inbound = Process(&inbound) 240 | 241 | outbound.DeleteOutbound() 242 | outbound = Process(&outbound) 243 | 244 | anded := inbound 245 | anded.And(&inbound, &outbound) 246 | 247 | fmt.Println("~~~~~~~~~~~~~~~~~~~~~") 248 | inbound.PrintBit() 249 | fmt.Println("~~~~~~~~~~~~~~~~~~~~~") 250 | outbound.PrintBit() 251 | fmt.Println("~~~~~~~~~~~~~~~~~~~~~") 252 | anded.PrintBit() 253 | 254 | for layer := 0; layer < EdgeCount; layer++ { 255 | fmt.Println("------------------") 256 | PrintSideBySideLayer(&inbound, &outbound, &anded, byte(layer)) 257 | 258 | fmt.Println( 259 | "+ ", 260 | inbound.CountLayer(byte(layer)), 261 | outbound.CountLayer(byte(layer)), 262 | anded.CountLayer(byte(layer)), 263 | ) 264 | } 265 | 266 | } 267 | -------------------------------------------------------------------------------- /internal/hier/RESEARCH.md: -------------------------------------------------------------------------------- 1 | # Research 2 | 3 | ## graphviz - dot 4 | 5 | dot draws graphs in four main phases. Knowing this helps you to understand what kind of layouts dot makes and how you can control them. The layout procedure used by dot relies on the graph being acyclic. Thus, the first step is to break any cycles which occur in the input graph by reversing the internal direction of certain cyclic edges. The next step assigns nodes to discrete ranks or levels. In a top-to-bottom drawing, ranks determine Y coordinates. Edges that span more than one rank are broken into chains of “virtual” nodes and unit-length edges. The third step orders nodes within ranks to avoid crossings. The fourth step sets X coordinates of nodes to keep edges short, and the final step routes edge splines. This is the same general approach as most hierarchical graph drawing programs, based on the work of Warfield [War77], Carpano [Car80] and Sugiyama [STT81]. We refer the reader to [GKNV93] for a thorough explanation of dot’s algorithms. 6 | 7 | The dot algorithm produces a ranked layout of a graph respecting edge directions if possible. It is particularly appropriate for displaying hierarchies or directed acyclic graphs. The basic layout scheme is attributed to Sugiyama et al.[STT81] The specific algorithm used by dot follows the steps described by Gansner et al.[GKNV93] 8 | 9 | The steps in the dot layout are: 10 | 1) initialize 11 | 2) rank 12 | 3) mincross 13 | 4) position 14 | 5) sameports 15 | 6) splines 16 | 7) compoundEdges 17 | 18 | * http://www.graphviz.org/pdf/dotguide.pdf 19 | * http://www.graphviz.org/doc/libguide/libguide.pdf 20 | 21 | ## A Technique for Drawing Directed Graphs 22 | 23 | We describe a four-pass algorithm for drawing directed graphs. The first pass finds an optimal rank assignment using a network simplex algorithm. The second pass sets the vertex order within ranks by an iterative heuristic incorporating a novel weight function and local transpositions to reduce crossings. The third pass finds optimal coordinates for nodes by constructing and ranking an auxiliary graph. The fourth pass makes splines to draw edges. The algorithm makes good drawings and runs fast. 24 | 25 | * http://www.graphviz.org/Documentation/TSE93.pdf 26 | 27 | ## References 28 | 29 | * http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.38.3837&rep=rep1&type=pdf 30 | * http://docs.yworks.com/yfiles/doc/api/y/layout/hierarchic/IncrementalHierarchicLayouter.html 31 | * http://hci.stanford.edu/courses/cs448b/w09/lectures/20090204-GraphsAndTrees.pdf 32 | * http://marvl.infotech.monash.edu/~dwyer/ 33 | * http://mgarland.org/files/papers/layoutgpu.pdf 34 | * http://research.microsoft.com/en-us/um/people/holroyd/papers/bundle.pdf 35 | * http://stackoverflow.com/questions/19245350/graphviz-dot-algorithm 36 | * http://www.csse.monash.edu.au/~tdwyer/Dwyer2009FastConstraints.pdf 37 | * http://www.graphviz.org/Theory.php 38 | * http://www.graphviz.org/Documentation/TSE93.pdf 39 | * http://www.graphviz.org/pdf/dotguide.pdf 40 | * https://en.wikipedia.org/wiki/Graph_drawing 41 | * https://github.com/cpettitt/dagre/tree/master/lib/rank 42 | * https://github.com/cpettitt/dagre/wiki#recommended-reading 43 | * https://github.com/cpettitt/dig.js/tree/master/src/dig/dot 44 | * https://github.com/d3/d3/issues/349 45 | * https://github.com/ellson/graphviz/tree/master/lib/dotgen2 46 | * https://www.microsoft.com/en-us/research/publication/drawing-graphs-with-glee/ -------------------------------------------------------------------------------- /internal/hier/decycle.go: -------------------------------------------------------------------------------- 1 | package hier 2 | 3 | // DecycleDefault runs recommended decycling algorithm 4 | func DefaultDecycle(graph *Graph) *Graph { 5 | decycle := NewDecycle(graph) 6 | decycle.Recurse = true 7 | decycle.Reorder = true 8 | decycle.Run() 9 | return graph 10 | } 11 | 12 | // Decycle implements process for removing cycles from a Graph 13 | type Decycle struct { 14 | *Graph 15 | 16 | Recurse bool 17 | Reorder bool 18 | 19 | SkipUpdate bool 20 | 21 | info []DecycleNodeInfo 22 | edges [][2]ID 23 | } 24 | 25 | // NewDecycle creates new decycle process 26 | func NewDecycle(graph *Graph) *Decycle { 27 | dg := &Decycle{} 28 | dg.Graph = graph 29 | dg.Recurse = false 30 | dg.Reorder = true 31 | return dg 32 | } 33 | 34 | // DecycleNodeInfo contains running info necessary in decycling 35 | type DecycleNodeInfo struct { 36 | Processed bool 37 | In, Out int 38 | } 39 | 40 | // Run runs the default decycling process 41 | func (graph *Decycle) Run() { 42 | if !graph.IsCyclic() { 43 | return 44 | } 45 | 46 | graph.info = make([]DecycleNodeInfo, graph.NodeCount()) 47 | for _, node := range graph.Nodes { 48 | graph.info[node.ID].In = node.InDegree() 49 | graph.info[node.ID].Out = node.OutDegree() 50 | } 51 | 52 | graph.processNodes(*graph.Nodes.Clone()) 53 | 54 | if !graph.SkipUpdate { 55 | graph.updateEdges() 56 | } 57 | } 58 | 59 | // processNodes processes list of nodes 60 | func (graph *Decycle) processNodes(nodes Nodes) { 61 | if !graph.Reorder { 62 | for _, node := range nodes { 63 | graph.process(node) 64 | } 65 | } else { 66 | var node *Node 67 | for len(nodes) > 0 { 68 | graph.sortAscending(nodes) 69 | node, nodes = nodes[len(nodes)-1], nodes[:len(nodes)-1] 70 | graph.process(node) 71 | } 72 | } 73 | } 74 | 75 | // process flips unprocessed incoming edges in dst 76 | func (graph *Decycle) process(dst *Node) { 77 | if graph.info[dst.ID].Processed { 78 | return 79 | } 80 | graph.info[dst.ID].Processed = true 81 | 82 | var recurse Nodes 83 | for _, src := range dst.In { 84 | if src == dst { 85 | continue 86 | } 87 | 88 | if graph.info[src.ID].Processed { 89 | graph.addEdge(src, dst) 90 | } else { 91 | graph.addFlippedEdge(src, dst) 92 | if graph.Recurse { 93 | recurse.Append(src) 94 | } 95 | } 96 | } 97 | 98 | if graph.Recurse { 99 | graph.processNodes(recurse) 100 | } 101 | } 102 | 103 | // addEdge adds edge from src to dest 104 | func (graph *Decycle) addEdge(src, dst *Node) { 105 | graph.edges = append(graph.edges, [2]ID{src.ID, dst.ID}) 106 | } 107 | 108 | // addFlippedEdge adds edge and flips it 109 | func (graph *Decycle) addFlippedEdge(src, dst *Node) { 110 | graph.info[src.ID].Out-- 111 | graph.info[src.ID].In++ 112 | 113 | graph.info[dst.ID].In-- 114 | graph.info[dst.ID].Out++ 115 | 116 | graph.addEdge(dst, src) 117 | } 118 | 119 | // updateEdges, updates graph with new edge information 120 | func (graph *Decycle) updateEdges() { 121 | // recreate inbound links from outbound 122 | for _, node := range graph.Nodes { 123 | node.In.Clear() 124 | node.Out.Clear() 125 | } 126 | 127 | for _, edge := range graph.edges { 128 | graph.AddEdge(graph.Nodes[edge[0]], graph.Nodes[edge[1]]) 129 | } 130 | 131 | for _, node := range graph.Nodes { 132 | node.In.Normalize() 133 | node.Out.Normalize() 134 | } 135 | } 136 | 137 | // sortAscending sorts nodes such that the last node is most beneficial to process 138 | func (graph *Decycle) sortAscending(nodes Nodes) { 139 | nodes.SortBy(func(a, b *Node) bool { 140 | ai, bi := graph.info[a.ID], graph.info[b.ID] 141 | if ai.Out == bi.Out { 142 | return ai.In > bi.In 143 | } 144 | return ai.Out < bi.Out 145 | }) 146 | } 147 | -------------------------------------------------------------------------------- /internal/hier/decycle_test.go: -------------------------------------------------------------------------------- 1 | package hier 2 | 3 | import ( 4 | "testing" 5 | "testing/quick" 6 | ) 7 | 8 | var decyclerCases = []struct { 9 | name string 10 | recurse, reorder bool 11 | }{ 12 | {"Basic", false, false}, 13 | {"Recurse", true, false}, 14 | {"Reorder", false, true}, 15 | {"RecurseReorder", true, true}, 16 | } 17 | 18 | func TestDecycle(t *testing.T) { 19 | for _, decyclerCase := range decyclerCases { 20 | t.Run(decyclerCase.name, func(t *testing.T) { 21 | for _, testgraph := range TestGraphs { 22 | t.Run(testgraph.Name, func(t *testing.T) { 23 | graph := testgraph.Make() 24 | beforeCount := graph.CountUndirectedLinks() 25 | 26 | decycle := NewDecycle(graph) 27 | decycle.Recurse = decyclerCase.recurse 28 | decycle.Reorder = decyclerCase.reorder 29 | decycle.Run() 30 | 31 | printEdges := false 32 | if err := graph.CheckErrors(); err != nil { 33 | t.Errorf("got errors: %v", err) 34 | printEdges = true 35 | } 36 | if graph.IsCyclic() { 37 | t.Errorf("got cycles") 38 | printEdges = true 39 | } 40 | 41 | afterCount := graph.CountUndirectedLinks() 42 | if beforeCount != afterCount { 43 | t.Errorf("too many edges removed %v -> %v", beforeCount, afterCount) 44 | printEdges = true 45 | } 46 | 47 | if printEdges { 48 | t.Log("edge table: \n" + graph.EdgeMatrixString()) 49 | } 50 | }) 51 | } 52 | }) 53 | } 54 | } 55 | 56 | func TestDecycleRandom(t *testing.T) { 57 | for _, decyclerCase := range decyclerCases { 58 | t.Run(decyclerCase.name, func(t *testing.T) { 59 | err := quick.Check(func(graph *Graph) bool { 60 | decycle := NewDecycle(graph) 61 | decycle.Recurse = decyclerCase.recurse 62 | decycle.Reorder = decyclerCase.reorder 63 | decycle.Run() 64 | 65 | err := graph.CheckErrors() 66 | if err != nil { 67 | t.Errorf("invalid %v:\n%v", err, graph.EdgeMatrixString()) 68 | return false 69 | } 70 | if graph.IsCyclic() { 71 | t.Errorf("cyclic:\n%v", graph.EdgeMatrixString()) 72 | return false 73 | } 74 | return true 75 | }, nil) 76 | 77 | if err != nil { 78 | t.Error(err) 79 | } 80 | }) 81 | } 82 | } 83 | 84 | func BenchmarkDecycle(b *testing.B) { 85 | for _, decyclerCase := range decyclerCases { 86 | b.Run(decyclerCase.name, func(b *testing.B) { 87 | for _, size := range BenchmarkGraphSizes { 88 | b.Run(size.Name, func(b *testing.B) { 89 | graph := GenerateRegularGraph(size.Nodes, size.Connections) 90 | 91 | for i := 0; i < b.N; i++ { 92 | decycle := NewDecycle(graph) 93 | decycle.Recurse = decyclerCase.recurse 94 | decycle.Reorder = decyclerCase.reorder 95 | decycle.SkipUpdate = true 96 | decycle.Run() 97 | } 98 | }) 99 | } 100 | }) 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /internal/hier/example_test.go: -------------------------------------------------------------------------------- 1 | package hier 2 | 3 | import "fmt" 4 | 5 | func Example() { 6 | // create a new graph 7 | graph := NewGraph() 8 | a, b, c, d := graph.AddNode(), graph.AddNode(), graph.AddNode(), graph.AddNode() 9 | 10 | graph.AddEdge(a, b) 11 | graph.AddEdge(a, c) 12 | graph.AddEdge(b, d) 13 | graph.AddEdge(c, d) 14 | graph.AddEdge(d, a) 15 | 16 | // remove cycles from the graph 17 | decycledGraph := DefaultDecycle(graph) 18 | 19 | // assign nodes to ranks 20 | rankedGraph := DefaultRank(decycledGraph) 21 | 22 | // create virtual nodes 23 | filledGraph := DefaultAddVirtuals(rankedGraph) 24 | 25 | // order nodes in ranks 26 | orderedGraph := DefaultOrderRanks(filledGraph) 27 | 28 | for _, node := range orderedGraph.Nodes { 29 | node.Radius.X = 10 30 | node.Radius.Y = 10 31 | } 32 | 33 | // position nodes 34 | positionedGraph := DefaultPosition(orderedGraph) 35 | 36 | for _, node := range positionedGraph.Nodes { 37 | fmt.Println(node.ID, node.Center.X, node.Center.Y) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /internal/hier/graph.go: -------------------------------------------------------------------------------- 1 | package hier 2 | 3 | import "strconv" 4 | 5 | // Graph is the basic graph 6 | type Graph struct { 7 | Nodes Nodes 8 | // Ranking 9 | ByRank []Nodes 10 | } 11 | 12 | // ID is an unique identifier to a Node 13 | type ID int 14 | 15 | // Node is the basic information about a node 16 | type Node struct { 17 | ID ID 18 | 19 | Virtual bool 20 | 21 | In Nodes 22 | Out Nodes 23 | 24 | Label string 25 | 26 | // Rank info 27 | Rank int 28 | 29 | // Ordering info 30 | Coef float32 31 | GridX float32 32 | 33 | // Visuals 34 | Center Vector 35 | Radius Vector 36 | } 37 | 38 | // String returns node label 39 | func (node *Node) String() string { 40 | if node.Label == "" && node.Virtual { 41 | return "v" + strconv.Itoa(int(node.ID)) 42 | } 43 | if node.Label == "" { 44 | return "#" + strconv.Itoa(int(node.ID)) 45 | } 46 | return node.Label 47 | } 48 | 49 | // InDegree returns count of inbound edges 50 | func (node *Node) InDegree() int { return len(node.In) } 51 | 52 | // OutDegree returns count of outbound edges 53 | func (node *Node) OutDegree() int { return len(node.Out) } 54 | 55 | // Vector represents a 2D vector 56 | type Vector struct { 57 | X, Y float32 58 | } 59 | 60 | // NewGraph creates an empty graph 61 | func NewGraph() *Graph { return &Graph{} } 62 | 63 | // ensureNode adds nodes until we have reached id 64 | func (graph *Graph) ensureNode(id int) { 65 | for id >= len(graph.Nodes) { 66 | graph.AddNode() 67 | } 68 | } 69 | 70 | // NodeCount returns count of nodes 71 | func (graph *Graph) NodeCount() int { return len(graph.Nodes) } 72 | 73 | // AddNode adds a new node and returns it's ID 74 | func (graph *Graph) AddNode() *Node { 75 | node := &Node{ID: ID(len(graph.Nodes))} 76 | graph.Nodes = append(graph.Nodes, node) 77 | return node 78 | } 79 | 80 | // AddEdge adds a new edge to the node 81 | func (graph *Graph) AddEdge(src, dst *Node) { 82 | src.Out.Append(dst) 83 | dst.In.Append(src) 84 | } 85 | 86 | // Roots returns nodes without any incoming edges 87 | func (graph *Graph) Roots() Nodes { 88 | nodes := Nodes{} 89 | for _, node := range graph.Nodes { 90 | if node.InDegree() == 0 { 91 | nodes.Append(node) 92 | } 93 | } 94 | return nodes 95 | } 96 | 97 | // CountRoots returns count of roots 98 | func (graph *Graph) CountRoots() int { 99 | total := 0 100 | for _, node := range graph.Nodes { 101 | if node.InDegree() == 0 { 102 | total++ 103 | } 104 | } 105 | return total 106 | } 107 | 108 | // CountEdges counts all edges, including duplicates 109 | func (graph *Graph) CountEdges() int { 110 | total := 0 111 | for _, src := range graph.Nodes { 112 | total += len(src.Out) 113 | } 114 | return total 115 | } 116 | 117 | // CountUndirectedLinks counts unique edges in the graph excluding loops 118 | func (graph *Graph) CountUndirectedLinks() int { 119 | counted := map[[2]ID]struct{}{} 120 | 121 | for _, src := range graph.Nodes { 122 | for _, dst := range src.Out { 123 | if src == dst { 124 | continue 125 | } 126 | 127 | a, b := src.ID, dst.ID 128 | if a > b { 129 | a, b = b, a 130 | } 131 | 132 | counted[[2]ID{a, b}] = struct{}{} 133 | } 134 | } 135 | return len(counted) 136 | } 137 | -------------------------------------------------------------------------------- /internal/hier/graph_cyclic.go: -------------------------------------------------------------------------------- 1 | package hier 2 | 3 | // IsCyclic checks whether graph is cyclic 4 | func (graph *Graph) IsCyclic() bool { 5 | visited := NewNodeSet(graph.NodeCount()) 6 | recursing := NewNodeSet(graph.NodeCount()) 7 | 8 | var isCyclic func(node *Node) bool 9 | isCyclic = func(node *Node) bool { 10 | if !visited.Include(node) { 11 | return false 12 | } 13 | 14 | recursing.Add(node) 15 | for _, child := range node.Out { 16 | if isCyclic(child) { 17 | return true 18 | } else if recursing.Contains(child) { 19 | return true 20 | } 21 | } 22 | recursing.Remove(node) 23 | 24 | return false 25 | } 26 | 27 | for _, node := range graph.Nodes { 28 | if isCyclic(node) { 29 | return true 30 | } 31 | } 32 | 33 | return false 34 | } 35 | -------------------------------------------------------------------------------- /internal/hier/graph_cyclic_test.go: -------------------------------------------------------------------------------- 1 | package hier 2 | 3 | import "testing" 4 | 5 | func TestIsCyclic(t *testing.T) { 6 | for _, testgraph := range TestGraphs { 7 | graph := testgraph.Make() 8 | if graph.IsCyclic() != testgraph.Cyclic { 9 | t.Errorf("%v: expected IsCyclic = %v", testgraph.Name, testgraph.Cyclic) 10 | } 11 | if err := graph.CheckErrors(); err != nil { 12 | t.Errorf("%v: %v", testgraph.Name, err) 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /internal/hier/graph_debug.go: -------------------------------------------------------------------------------- 1 | package hier 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "strconv" 7 | "strings" 8 | ) 9 | 10 | // CheckErrors does sanity check of content of graph 11 | func (graph *Graph) CheckErrors() error { 12 | var errors []error 13 | for _, node := range graph.Nodes { 14 | countIn := NewNodeSet(graph.NodeCount()) 15 | countOut := NewNodeSet(graph.NodeCount()) 16 | 17 | for _, dst := range node.In { 18 | if int(dst.ID) >= graph.NodeCount() { 19 | errors = append(errors, fmt.Errorf("overflow in: %v -> %v", dst.ID, node.ID)) 20 | continue 21 | } 22 | 23 | if !countIn.Include(dst) { 24 | errors = append(errors, fmt.Errorf("dup in: %v -> %v", dst.ID, node.ID)) 25 | } 26 | } 27 | 28 | for _, dst := range node.Out { 29 | if int(dst.ID) >= graph.NodeCount() { 30 | errors = append(errors, fmt.Errorf("overflow out: %v -> %v", node.ID, dst.ID)) 31 | continue 32 | } 33 | 34 | if !countOut.Include(dst) { 35 | errors = append(errors, fmt.Errorf("dup out: %v -> %v", node.ID, dst.ID)) 36 | } 37 | } 38 | } 39 | 40 | // TODO: check for in/out cross-references 41 | if len(errors) == 0 { 42 | return nil 43 | } 44 | return fmt.Errorf("%v", errors) 45 | } 46 | 47 | // EdgeMatrixString creates a debug output showing both inbound and outbound edges 48 | func (graph *Graph) EdgeMatrixString() string { 49 | lines := []string{} 50 | 51 | n := graph.NodeCount() 52 | stride := 2*n + 4 53 | table := make([]byte, n*stride) 54 | for i := range table { 55 | table[i] = ' ' 56 | } 57 | 58 | createLine := func(nodes Nodes) string { 59 | line := make([]byte, n) 60 | for x := range line { 61 | line[x] = ' ' 62 | } 63 | for _, node := range nodes { 64 | line[node.ID] = 'X' 65 | } 66 | return string(line) 67 | } 68 | 69 | formatEdges := func(node *Node) string { 70 | var b bytes.Buffer 71 | b.WriteString(strconv.Itoa(int(node.ID))) 72 | b.WriteString(": []int{") 73 | for i, dst := range node.Out { 74 | b.WriteString(strconv.Itoa(int(dst.ID))) 75 | if i+1 < len(node.Out) { 76 | b.WriteString(", ") 77 | } 78 | } 79 | b.WriteString("}") 80 | return b.String() 81 | } 82 | 83 | for _, node := range graph.Nodes { 84 | lines = append(lines, "|"+createLine(node.In)+"|"+createLine(node.Out)+"| "+formatEdges(node)) 85 | } 86 | 87 | return strings.Join(lines, "\n") 88 | } 89 | -------------------------------------------------------------------------------- /internal/hier/graph_edges.go: -------------------------------------------------------------------------------- 1 | package hier 2 | 3 | // NewGraphFromEdgeList creates a graph from edge list 4 | // 5 | // Example: 6 | // graph := NewGraphFromEdgeList([][]int{ 7 | // 0: []int{1,2}, 8 | // 1: []int{2,0}, 9 | // }) 10 | // 11 | // Creates an graph with edges 0 -> 1, 0 -> 2, 1 -> 2, 1 -> 0. 12 | // 13 | func NewGraphFromEdgeList(edgeList [][]int) *Graph { 14 | graph := NewGraph() 15 | 16 | for from, out := range edgeList { 17 | for _, to := range out { 18 | graph.ensureNode(from) 19 | graph.ensureNode(to) 20 | graph.AddEdge(graph.Nodes[from], graph.Nodes[to]) 21 | } 22 | } 23 | 24 | return graph 25 | } 26 | 27 | // ConvertToEdgeList creates edge list 28 | // NewGraphFromEdgeList(edgeList).ConvertToEdgeList() == edgeList 29 | func (graph *Graph) ConvertToEdgeList() [][]int { 30 | edges := make([][]int, 0, graph.NodeCount()) 31 | for _, node := range graph.Nodes { 32 | list := make([]int, 0, len(node.Out)) 33 | for _, out := range node.Out { 34 | list = append(list, int(out.ID)) 35 | } 36 | edges = append(edges, list) 37 | } 38 | return edges 39 | } 40 | -------------------------------------------------------------------------------- /internal/hier/graph_generate.go: -------------------------------------------------------------------------------- 1 | package hier 2 | 3 | import ( 4 | "math/rand" 5 | "reflect" 6 | ) 7 | 8 | // GenerateRandomGraph creates a graph with n nodes and a P(src, dst) == p 9 | func GenerateRandomGraph(n int, p float64, rand *rand.Rand) *Graph { 10 | graph := NewGraph() 11 | for i := 0; i < n; i++ { 12 | graph.AddNode() 13 | } 14 | 15 | for _, src := range graph.Nodes { 16 | for _, dst := range graph.Nodes { 17 | if rand.Float64() < p { 18 | graph.AddEdge(src, dst) 19 | } 20 | } 21 | } 22 | 23 | return graph 24 | } 25 | 26 | // GenerateRegularGraph creates a circular graph 27 | func GenerateRegularGraph(n, connections int) *Graph { 28 | graph := NewGraph() 29 | for i := 0; i < n; i++ { 30 | graph.AddNode() 31 | } 32 | 33 | for i, node := range graph.Nodes { 34 | for k := i + 1; k <= i+connections; k++ { 35 | t := k 36 | for t >= n { 37 | t -= n 38 | } 39 | graph.AddEdge(node, graph.Nodes[t]) 40 | } 41 | } 42 | 43 | return graph 44 | } 45 | 46 | // Generate implements quick.Generator interface 47 | func (_ *Graph) Generate(rand *rand.Rand, size int) reflect.Value { 48 | switch rand.Intn(4) { 49 | case 0: 50 | return reflect.ValueOf(GenerateRandomGraph(size, 0.1, rand)) 51 | case 1: 52 | return reflect.ValueOf(GenerateRandomGraph(size, 0.3, rand)) 53 | case 2: 54 | return reflect.ValueOf(GenerateRandomGraph(size, 0.7, rand)) 55 | case 3: 56 | return reflect.ValueOf(GenerateRegularGraph(size, rand.Intn(size))) 57 | } 58 | 59 | return reflect.ValueOf(GenerateRandomGraph(size, 0.5, rand)) 60 | } 61 | -------------------------------------------------------------------------------- /internal/hier/graph_generate_test.go: -------------------------------------------------------------------------------- 1 | package hier 2 | 3 | import "testing" 4 | 5 | var BenchmarkGraphSizes = []struct { 6 | Name string 7 | Nodes, Connections int 8 | }{ 9 | {"1x1", 1, 1}, 10 | {"2x1", 2, 1}, 11 | {"4x2", 4, 2}, 12 | {"16x4", 16, 4}, 13 | {"256x4", 256, 4}, 14 | {"256x16", 256, 16}, 15 | } 16 | 17 | func TestGenerateRegularGraph(t *testing.T) { 18 | for _, size := range BenchmarkGraphSizes { 19 | t.Run(size.Name, func(t *testing.T) { 20 | graph := GenerateRegularGraph(size.Nodes, size.Connections) 21 | cyclic := size.Connections > 0 22 | if graph.IsCyclic() != cyclic { 23 | t.Errorf("expected %v", cyclic) 24 | } 25 | }) 26 | } 27 | } 28 | 29 | func BenchmarkGenerateRegularGraph(b *testing.B) { 30 | for _, size := range BenchmarkGraphSizes { 31 | b.Run(size.Name, func(b *testing.B) { 32 | for i := 0; i < b.N; i++ { 33 | _ = GenerateRegularGraph(size.Nodes, size.Connections) 34 | } 35 | }) 36 | } 37 | } 38 | 39 | func BenchmarkNewGraphFromEdgeList(b *testing.B) { 40 | for _, size := range BenchmarkGraphSizes { 41 | graph := GenerateRegularGraph(size.Nodes, size.Connections) 42 | edgeList := graph.ConvertToEdgeList() 43 | b.Run(size.Name, func(b *testing.B) { 44 | for i := 0; i < b.N; i++ { 45 | _ = NewGraphFromEdgeList(edgeList) 46 | } 47 | }) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /internal/hier/graph_nodes.go: -------------------------------------------------------------------------------- 1 | package hier 2 | 3 | // NodeSet is a dense node set 4 | type NodeSet []bool 5 | 6 | // NewNodeSet returns new set for n nodes 7 | func NewNodeSet(n int) NodeSet { return make(NodeSet, n) } 8 | 9 | // Add includes node in set 10 | func (nodes NodeSet) Add(n *Node) { nodes[n.ID] = true } 11 | 12 | // Remove removes node from set 13 | func (nodes NodeSet) Remove(n *Node) { nodes[n.ID] = false } 14 | 15 | // Contains whether node is in set 16 | func (nodes NodeSet) Contains(n *Node) bool { return nodes[n.ID] } 17 | 18 | // Include includes node in set, returns false when node already exists 19 | func (nodes NodeSet) Include(n *Node) bool { 20 | if nodes.Contains(n) { 21 | return false 22 | } 23 | nodes.Add(n) 24 | return true 25 | } 26 | 27 | // Nodes is a list of node identifiers 28 | type Nodes []*Node 29 | 30 | // Clear clears the list 31 | func (nodes *Nodes) Clear() { *nodes = nil } 32 | 33 | // Clone makes a clone of the list 34 | func (nodes *Nodes) Clone() *Nodes { 35 | result := make(Nodes, len(*nodes)) 36 | copy(result, *nodes) 37 | return &result 38 | } 39 | 40 | // Contains returns whether node is present in this list 41 | func (nodes *Nodes) Contains(node *Node) bool { return nodes.IndexOf(node) >= 0 } 42 | 43 | // Append adds node to the list without checking for duplicates 44 | func (nodes *Nodes) Append(node *Node) { *nodes = append(*nodes, node) } 45 | 46 | // Includes adds node to the list if it already doesn't contain it 47 | func (nodes *Nodes) Include(node *Node) { 48 | if !nodes.Contains(node) { 49 | nodes.Append(node) 50 | } 51 | } 52 | 53 | // Remove removes node, including any duplicates 54 | func (nodes *Nodes) Remove(node *Node) { 55 | i := nodes.IndexOf(node) 56 | for i >= 0 { 57 | nodes.Delete(i) 58 | i = nodes.indexOfAt(node, i) 59 | } 60 | } 61 | 62 | // Delete deletes 63 | func (nodes *Nodes) Delete(i int) { *nodes = append((*nodes)[:i], (*nodes)[i+1:]...) } 64 | 65 | // Normalize sorts and removes duplicates from the list 66 | func (nodes *Nodes) Normalize() { 67 | nodes.SortBy(func(a, b *Node) bool { 68 | return a.ID < b.ID 69 | }) 70 | // sort.Slice(*nodes, func(i, k int) bool { 71 | // return (*nodes)[i].ID < (*nodes)[k].ID 72 | // }) 73 | 74 | { // remove duplicates from sorted array 75 | var p *Node 76 | unique := (*nodes)[:0] 77 | for _, n := range *nodes { 78 | if p != n { 79 | unique = append(unique, n) 80 | p = n 81 | } 82 | } 83 | *nodes = unique 84 | } 85 | } 86 | 87 | // SortDescending sorts nodes in descending order of outdegree 88 | func (nodes Nodes) SortDescending() Nodes { 89 | nodes.SortBy(func(a, b *Node) bool { 90 | if a.OutDegree() == b.OutDegree() { 91 | return a.InDegree() < b.InDegree() 92 | } 93 | return a.OutDegree() > b.OutDegree() 94 | }) 95 | 96 | return nodes 97 | } 98 | 99 | // IndexOf finds the node index in this list 100 | func (nodes *Nodes) IndexOf(node *Node) int { 101 | for i, x := range *nodes { 102 | if x == node { 103 | return i 104 | } 105 | } 106 | return -1 107 | } 108 | 109 | // indexOfAt finds the node index starting from offset 110 | func (nodes *Nodes) indexOfAt(node *Node, offset int) int { 111 | for i, x := range (*nodes)[offset:] { 112 | if x == node { 113 | return offset + i 114 | } 115 | } 116 | return -1 117 | } 118 | -------------------------------------------------------------------------------- /internal/hier/graph_nodes_sort.go: -------------------------------------------------------------------------------- 1 | package hier 2 | 3 | // Sort implementation is a modification of http://golang.org/pkg/sort/#Sort 4 | // Copyright 2009 The Go Authors. All rights reserved. 5 | // Use of this source code is governed by a BSD-style 6 | // license that can be found at http://golang.org/LICENSE. 7 | 8 | // SortBy sorts nodes in place 9 | func (nodes *Nodes) SortBy(less func(*Node, *Node) bool) { 10 | // Switch to heapsort if depth of 2*ceil(lg(n+1)) is reached. 11 | n := len(*nodes) 12 | maxDepth := 0 13 | for i := n; i > 0; i >>= 1 { 14 | maxDepth++ 15 | } 16 | maxDepth *= 2 17 | nodes._quickSortNodeSlice(less, 0, n, maxDepth) 18 | } 19 | 20 | // Sort implementation based on http://golang.org/pkg/sort/#Sort 21 | 22 | func (rcv Nodes) _swapNodeSlice(a, b int) { 23 | rcv[a], rcv[b] = rcv[b], rcv[a] 24 | } 25 | 26 | // Insertion sort 27 | func (rcv Nodes) _insertionSortNodeSlice(less func(*Node, *Node) bool, a, b int) { 28 | for i := a + 1; i < b; i++ { 29 | for j := i; j > a && less(rcv[j], rcv[j-1]); j-- { 30 | rcv._swapNodeSlice(j, j-1) 31 | } 32 | } 33 | } 34 | 35 | // siftDown implements the heap property on rcv[lo, hi). 36 | // first is an offset into the array where the root of the heap lies. 37 | func (rcv Nodes) _siftDownNodeSlice(less func(*Node, *Node) bool, lo, hi, first int) { 38 | root := lo 39 | for { 40 | child := 2*root + 1 41 | if child >= hi { 42 | break 43 | } 44 | if child+1 < hi && less(rcv[first+child], rcv[first+child+1]) { 45 | child++ 46 | } 47 | if !less(rcv[first+root], rcv[first+child]) { 48 | return 49 | } 50 | rcv._swapNodeSlice(first+root, first+child) 51 | root = child 52 | } 53 | } 54 | 55 | func (rcv Nodes) _heapSortNodeSlice(less func(*Node, *Node) bool, a, b int) { 56 | first := a 57 | lo := 0 58 | hi := b - a 59 | 60 | // Build heap with greatest element at top. 61 | for i := (hi - 1) / 2; i >= 0; i-- { 62 | rcv._siftDownNodeSlice(less, i, hi, first) 63 | } 64 | 65 | // Pop elements, largest first, into end of rcv._ 66 | for i := hi - 1; i >= 0; i-- { 67 | rcv._swapNodeSlice(first, first+i) 68 | rcv._siftDownNodeSlice(less, lo, i, first) 69 | } 70 | } 71 | 72 | // Quicksort, following Bentley and McIlroy, 73 | // Engineering a Sort Function, SP&E November 1993. 74 | 75 | // medianOfThree moves the median of the three values rcv[a], rcv[b], rcv[c] into rcv[a]. 76 | func (rcv Nodes) _medianOfThreeNodeSlice(less func(*Node, *Node) bool, a, b, c int) { 77 | m0 := b 78 | m1 := a 79 | m2 := c 80 | // bubble sort on 3 elements 81 | if less(rcv[m1], rcv[m0]) { 82 | rcv._swapNodeSlice(m1, m0) 83 | } 84 | if less(rcv[m2], rcv[m1]) { 85 | rcv._swapNodeSlice(m2, m1) 86 | } 87 | if less(rcv[m1], rcv[m0]) { 88 | rcv._swapNodeSlice(m1, m0) 89 | } 90 | // now rcv[m0] <= rcv[m1] <= rcv[m2] 91 | } 92 | 93 | func (rcv Nodes) _swapRangeNodeSlice(a, b, n int) { 94 | for i := 0; i < n; i++ { 95 | rcv._swapNodeSlice(a+i, b+i) 96 | } 97 | } 98 | 99 | func (rcv Nodes) _doPivotNodeSlice(less func(*Node, *Node) bool, lo, hi int) (midlo, midhi int) { 100 | m := lo + (hi-lo)/2 // Written like this to avoid integer overflow. 101 | if hi-lo > 40 { 102 | // Tukey's Ninther, median of three medians of three. 103 | s := (hi - lo) / 8 104 | rcv._medianOfThreeNodeSlice(less, lo, lo+s, lo+2*s) 105 | rcv._medianOfThreeNodeSlice(less, m, m-s, m+s) 106 | rcv._medianOfThreeNodeSlice(less, hi-1, hi-1-s, hi-1-2*s) 107 | } 108 | rcv._medianOfThreeNodeSlice(less, lo, m, hi-1) 109 | 110 | // Invariants are: 111 | // rcv[lo] = pivot (set up by ChoosePivot) 112 | // rcv[lo <= i < a] = pivot 113 | // rcv[a <= i < b] < pivot 114 | // rcv[b <= i < c] is unexamined 115 | // rcv[c <= i < d] > pivot 116 | // rcv[d <= i < hi] = pivot 117 | // 118 | // Once b meets c, can swap the "= pivot" sections 119 | // into the middle of the slice. 120 | pivot := lo 121 | a, b, c, d := lo+1, lo+1, hi, hi 122 | for { 123 | for b < c { 124 | if less(rcv[b], rcv[pivot]) { // rcv[b] < pivot 125 | b++ 126 | } else if !less(rcv[pivot], rcv[b]) { // rcv[b] = pivot 127 | rcv._swapNodeSlice(a, b) 128 | a++ 129 | b++ 130 | } else { 131 | break 132 | } 133 | } 134 | for b < c { 135 | if less(rcv[pivot], rcv[c-1]) { // rcv[c-1] > pivot 136 | c-- 137 | } else if !less(rcv[c-1], rcv[pivot]) { // rcv[c-1] = pivot 138 | rcv._swapNodeSlice(c-1, d-1) 139 | c-- 140 | d-- 141 | } else { 142 | break 143 | } 144 | } 145 | if b >= c { 146 | break 147 | } 148 | // rcv[b] > pivot; rcv[c-1] < pivot 149 | rcv._swapNodeSlice(b, c-1) 150 | b++ 151 | c-- 152 | } 153 | 154 | min := func(a, b int) int { 155 | if a < b { 156 | return a 157 | } 158 | return b 159 | } 160 | 161 | n := min(b-a, a-lo) 162 | rcv._swapRangeNodeSlice(lo, b-n, n) 163 | 164 | n = min(hi-d, d-c) 165 | rcv._swapRangeNodeSlice(c, hi-n, n) 166 | 167 | return lo + b - a, hi - (d - c) 168 | } 169 | 170 | func (rcv Nodes) _quickSortNodeSlice(less func(*Node, *Node) bool, a, b, maxDepth int) { 171 | for b-a > 7 { 172 | if maxDepth == 0 { 173 | rcv._heapSortNodeSlice(less, a, b) 174 | return 175 | } 176 | maxDepth-- 177 | mlo, mhi := rcv._doPivotNodeSlice(less, a, b) 178 | // Avoiding recursion on the larger subproblem guarantees 179 | // a stack depth of at most lg(b-a). 180 | if mlo-a < b-mhi { 181 | rcv._quickSortNodeSlice(less, a, mlo, maxDepth) 182 | a = mhi // i.e., rcv._quickSortNodeSlice(mhi, b) 183 | } else { 184 | rcv._quickSortNodeSlice(less, mhi, b, maxDepth) 185 | b = mlo // i.e., rcv._quickSortNodeSlice(a, mlo) 186 | } 187 | } 188 | if b-a > 1 { 189 | rcv._insertionSortNodeSlice(less, a, b) 190 | } 191 | } 192 | -------------------------------------------------------------------------------- /internal/hier/graph_test.go: -------------------------------------------------------------------------------- 1 | package hier 2 | 3 | type TestGraph struct { 4 | Name string 5 | Cyclic bool 6 | Edges [][]int 7 | } 8 | 9 | func (testgraph *TestGraph) Make() *Graph { 10 | return NewGraphFromEdgeList(testgraph.Edges) 11 | } 12 | 13 | var TestGraphs = []TestGraph{ 14 | // acyclic graphs 15 | {"null", false, [][]int{}}, 16 | {"node", false, [][]int{ 17 | 0: {}, 18 | }}, 19 | {"link", false, [][]int{ 20 | 0: {1}, 21 | 1: {}, 22 | }}, 23 | {"link-reverse", false, [][]int{ 24 | 0: {}, 25 | 1: {0}, 26 | }}, 27 | {"chain", false, [][]int{ 28 | 0: {1}, 29 | 1: {2}, 30 | 2: {3}, 31 | 3: {}, 32 | }}, 33 | {"chain-reverse", false, [][]int{ 34 | 0: {}, 35 | 1: {0}, 36 | 2: {1}, 37 | 3: {2}, 38 | }}, 39 | {"split", false, [][]int{ 40 | 0: {1}, 41 | 1: {2, 3}, 42 | 2: {4}, 43 | 3: {5}, 44 | 4: {}, 45 | 5: {}, 46 | }}, 47 | {"merge", false, [][]int{ 48 | 0: {1}, 49 | 1: {2, 3}, 50 | 2: {4}, 51 | 3: {4}, 52 | 4: {}, 53 | }}, 54 | // acyclic graphs with 2 components 55 | {"2-node", false, [][]int{ 56 | 0: {}, 57 | 1: {}, 58 | }}, 59 | {"2-link", false, [][]int{ 60 | 0: {1}, 61 | 1: {}, 62 | 63 | 2: {}, 64 | 3: {2}, 65 | }}, 66 | {"2-split", false, [][]int{ 67 | 0: {1, 2}, 68 | 1: {}, 69 | 2: {}, 70 | 71 | 4: {}, 72 | 5: {}, 73 | 6: {5, 4}, 74 | }}, 75 | {"2-merge", false, [][]int{ 76 | 0: {1, 2}, 77 | 1: {3}, 78 | 2: {3}, 79 | 3: {}, 80 | 81 | 4: {}, 82 | 5: {4}, 83 | 6: {4}, 84 | 7: {6, 5}, 85 | }}, 86 | 87 | // cyclic graphs 88 | {"loop", true, [][]int{ 89 | 0: {0}, 90 | }}, 91 | {"2-circle", true, [][]int{ 92 | 0: {1}, 93 | 1: {0}, 94 | }}, 95 | {"4-circle", true, [][]int{ 96 | 0: {1}, 97 | 1: {2}, 98 | 2: {3}, 99 | 3: {0}, 100 | }}, 101 | {"5-split-cycle", true, [][]int{ 102 | 0: {1}, 103 | 1: {2, 3}, 104 | 2: {4}, 105 | 3: {4}, 106 | 4: {0}, 107 | }}, 108 | {"5-split-2-cycle", true, [][]int{ 109 | 0: {1}, 110 | 1: {2, 3, 0}, 111 | 2: {4}, 112 | 3: {4, 2}, 113 | 4: {2}, 114 | }}, 115 | {"5-complete", true, [][]int{ 116 | 0: {0, 1, 2, 3, 4}, 117 | 1: {0, 1, 2, 3, 4}, 118 | 2: {0, 1, 2, 3, 4}, 119 | 3: {0, 1, 2, 3, 4}, 120 | 4: {0, 1, 2, 3, 4}, 121 | }}, 122 | 123 | // regressions 124 | {"regression-0", true, [][]int{ 125 | 0: {0, 1, 2, 4, 5}, 126 | 1: {0, 2, 3}, 127 | 2: {0, 1, 4, 5, 6}, 128 | 3: {0, 3, 4}, 129 | 4: {0, 1, 2, 3, 4, 5}, 130 | 5: {0, 1, 2}, 131 | 6: {0, 6}, 132 | }}, 133 | {"regression-1", true, [][]int{ 134 | 0: {1, 2, 3, 4}, 135 | 1: {1, 5}, 136 | 2: {1}, 137 | 3: {0, 1, 2, 3}, 138 | 4: {0, 2}, 139 | 5: {0, 1, 2, 6}, 140 | 6: {1, 3, 4}, 141 | }}, 142 | {"regression-2", true, [][]int{ 143 | 0: {1, 2, 3, 4, 5, 6}, 144 | 1: {0, 1, 2, 3, 4, 5, 6}, 145 | 2: {1}, 146 | 3: {0, 3, 4, 5, 6}, 147 | 4: {0, 1, 2, 3, 4, 5, 6}, 148 | 5: {0, 1, 2, 5, 6}, 149 | 6: {0, 1, 2, 3, 4, 5, 6}, 150 | }}, 151 | } 152 | -------------------------------------------------------------------------------- /internal/hier/math.go: -------------------------------------------------------------------------------- 1 | package hier 2 | 3 | func abs(v int) int { 4 | if v < 0 { 5 | return -v 6 | } 7 | return v 8 | } 9 | 10 | func min(a, b int) int { 11 | if a < b { 12 | return a 13 | } 14 | return b 15 | } 16 | 17 | func max(a, b int) int { 18 | if a > b { 19 | return a 20 | } 21 | return b 22 | } 23 | 24 | func absf32(v float32) float32 { 25 | if v < 0 { 26 | return -v 27 | } 28 | return v 29 | } 30 | 31 | func minf32(a, b float32) float32 { 32 | if a < b { 33 | return a 34 | } 35 | return b 36 | } 37 | 38 | func maxf32(a, b float32) float32 { 39 | if a > b { 40 | return a 41 | } 42 | return b 43 | } 44 | 45 | func clampf32(v, min, max float32) float32 { 46 | if v < min { 47 | return min 48 | } else if v > max { 49 | return max 50 | } 51 | return v 52 | } 53 | -------------------------------------------------------------------------------- /internal/hier/order.go: -------------------------------------------------------------------------------- 1 | package hier 2 | 3 | import ( 4 | "sort" 5 | ) 6 | 7 | // DefaultOrderRanks does recommended rank ordering 8 | func DefaultOrderRanks(graph *Graph) *Graph { 9 | OrderRanks(graph) 10 | return graph 11 | } 12 | 13 | // OrderRanks tries to minimize crossign edges 14 | func OrderRanks(graph *Graph) { 15 | OrderRanksDepthFirst(graph) 16 | for i := 0; i < 100; i++ { 17 | OrderRanksByCoef(graph, i%2 == 0) 18 | if OrderRanksTranspose(graph) == 0 { 19 | break 20 | } 21 | } 22 | } 23 | 24 | // OrderRanksDepthFirst reorders based on depth first traverse 25 | func OrderRanksDepthFirst(graph *Graph) { 26 | if len(graph.ByRank) == 0 { 27 | return 28 | } 29 | 30 | seen := NewNodeSet(graph.NodeCount()) 31 | ranking := make([]Nodes, len(graph.ByRank)) 32 | 33 | var process func(node *Node) 34 | process = func(src *Node) { 35 | if !seen.Include(src) { 36 | return 37 | } 38 | 39 | ranking[src.Rank].Append(src) 40 | for _, dst := range src.Out { 41 | process(dst) 42 | } 43 | } 44 | 45 | roots := graph.Roots() 46 | roots.SortDescending() 47 | 48 | for _, id := range roots { 49 | process(id) 50 | } 51 | 52 | graph.ByRank = ranking 53 | } 54 | 55 | // OrderRanksByCoef reorders based on target grid and coef 56 | func OrderRanksByCoef(graph *Graph, down bool) { 57 | OrderRanksAssignMetrics(graph, down) 58 | 59 | for _, nodes := range graph.ByRank { 60 | sort.Slice(nodes, func(i, k int) bool { 61 | a, b := nodes[i], nodes[k] 62 | 63 | if a.Coef == -1.0 { 64 | if b.Coef == -1.0 { 65 | return a.GridX < b.GridX 66 | } else { 67 | return a.GridX < b.Coef 68 | } 69 | } else { 70 | if b.Coef == -1.0 { 71 | return a.Coef < b.GridX 72 | } else { 73 | return a.Coef < b.Coef 74 | } 75 | } 76 | }) 77 | } 78 | } 79 | 80 | // OrderRanksAssignMetrics recalculates metrics for ordering 81 | func OrderRanksAssignMetrics(graph *Graph, down bool) { 82 | for _, nodes := range graph.ByRank { 83 | for i, node := range nodes { 84 | node.GridX = float32(i) 85 | } 86 | } 87 | 88 | for _, node := range graph.Nodes { 89 | var adj Nodes 90 | if down { 91 | adj = node.Out 92 | } else { 93 | adj = node.In 94 | } 95 | 96 | if len(adj) == 0 { 97 | node.Coef = -1 98 | } else if len(adj)&1 == 1 { 99 | node.Coef = adj[len(adj)>>1].GridX 100 | } else if len(adj) == 2 { 101 | node.Coef = (adj[0].GridX + adj[1].GridX) / 2.0 102 | } else { 103 | leftx := adj[len(adj)>>1-1].GridX 104 | rightx := adj[len(adj)>>1].GridX 105 | 106 | left := leftx - adj[0].GridX 107 | right := adj[len(adj)-1].GridX - rightx 108 | node.Coef = (leftx*right + rightx*left) / (left + right) 109 | } 110 | } 111 | } 112 | 113 | // OrderRanksTranspose swaps nodes which are side by side and will use less crossings 114 | func OrderRanksTranspose(graph *Graph) (swaps int) { 115 | for limit := 0; limit < 20; limit++ { 116 | improved := false 117 | 118 | for _, nodes := range graph.ByRank[1:] { 119 | if len(nodes) == 0 { 120 | continue 121 | } 122 | left := nodes[0] 123 | for i, right := range nodes[1:] { 124 | if graph.CrossingsUp(left, right) > graph.CrossingsUp(right, left) { 125 | nodes[i], nodes[i+1] = right, left 126 | right, left = left, right 127 | swaps++ 128 | } 129 | left = right 130 | } 131 | } 132 | 133 | if !improved { 134 | return 135 | } 136 | } 137 | 138 | return 0 139 | } 140 | -------------------------------------------------------------------------------- /internal/hier/order_crossings.go: -------------------------------------------------------------------------------- 1 | package hier 2 | 3 | func (graph *Graph) CrossingsUp(u, v *Node) int { 4 | if u.Rank == 0 { 5 | return 0 6 | } 7 | 8 | count := 0 9 | prev := graph.ByRank[u.Rank-1] 10 | for _, w := range u.In { 11 | for _, z := range v.In { 12 | if prev.IndexOf(z) < prev.IndexOf(w) { 13 | count++ 14 | } 15 | } 16 | } 17 | return count 18 | } 19 | 20 | func (graph *Graph) CrossingsDown(u, v *Node) int { 21 | if u.Rank == len(graph.ByRank)-1 { 22 | return 0 23 | } 24 | 25 | count := 0 26 | next := graph.ByRank[u.Rank+1] 27 | for _, w := range u.In { 28 | for _, z := range v.In { 29 | if next.IndexOf(z) < next.IndexOf(w) { 30 | count++ 31 | } 32 | } 33 | } 34 | return count 35 | } 36 | 37 | func (graph *Graph) Crossings(u, v *Node) int { 38 | return graph.CrossingsDown(u, v) + graph.CrossingsUp(u, v) 39 | } 40 | -------------------------------------------------------------------------------- /internal/hier/position.go: -------------------------------------------------------------------------------- 1 | package hier 2 | 3 | import "fmt" 4 | 5 | // DefaultPosition does recommended positioning algorithm 6 | func DefaultPosition(graph *Graph) *Graph { 7 | Position(graph) 8 | return graph 9 | } 10 | 11 | // Position does basic node positioning 12 | func Position(graph *Graph) { 13 | PositionInitial(graph) 14 | 15 | // TODO: fold nudge into Node parameter 16 | nudge := float32(10.0) 17 | for i := 0; i < 100; i++ { 18 | PositionOutgoing(graph, false, nudge) 19 | PositionIncoming(graph, false, nudge) 20 | PositionOutgoing(graph, true, nudge) 21 | PositionIncoming(graph, true, nudge) 22 | nudge = nudge * 0.9 23 | flushLeft(graph) 24 | } 25 | 26 | for i := 0; i < 10; i++ { 27 | PositionIncoming(graph, true, 0) 28 | PositionOutgoing(graph, true, 0) 29 | 30 | flushLeft(graph) 31 | } 32 | } 33 | 34 | // PositionInitial assigns location based on size 35 | func PositionInitial(graph *Graph) { 36 | top := float32(0) 37 | for _, nodes := range graph.ByRank { 38 | left := float32(0) 39 | 40 | halfrow := float32(0) 41 | for _, node := range nodes { 42 | halfrow = maxf32(halfrow, node.Radius.Y) 43 | } 44 | 45 | top += halfrow 46 | for _, node := range nodes { 47 | node.Center.X = left + node.Radius.X 48 | node.Center.Y = top 49 | left += node.Center.X + node.Radius.X 50 | } 51 | top += halfrow 52 | } 53 | } 54 | 55 | // iterateLayers can traverse layers/nodes in different directions 56 | func iterateLayers(graph *Graph, leftToRight bool, dy int, fn func(layer Nodes, i int, node *Node)) { 57 | var starty int 58 | if dy < 0 { 59 | starty = len(graph.ByRank) - 1 60 | } 61 | 62 | if leftToRight { 63 | for y := starty; 0 <= y && y < len(graph.ByRank); y += dy { 64 | layer := graph.ByRank[y] 65 | for i, node := range layer { 66 | fn(layer, i, node) 67 | } 68 | } 69 | } else { 70 | for y := starty; 0 <= y && y < len(graph.ByRank); y += dy { 71 | layer := graph.ByRank[y] 72 | for i := len(layer) - 1; i >= 0; i-- { 73 | fn(layer, i, layer[i]) 74 | } 75 | } 76 | } 77 | } 78 | 79 | // NodeWalls calculates bounds where node can be moved 80 | func NodeWalls(graph *Graph, layer Nodes, i int, node *Node, leftToRight bool) (wallLeft, wallRight float32) { 81 | if i > 0 { 82 | wallLeft = layer[i-1].Center.X + layer[i-1].Radius.X 83 | } 84 | 85 | if i+1 < len(layer) { 86 | wallRight = layer[i+1].Center.X - layer[i+1].Radius.X 87 | } else { 88 | wallRight = float32(len(graph.Nodes)) * (2 * node.Radius.X) 89 | } 90 | 91 | // ensure we can fit at least one 92 | if leftToRight { 93 | if wallRight-node.Radius.X < wallLeft+node.Radius.X { 94 | wallRight = wallLeft + 2*node.Radius.X 95 | } 96 | } else { 97 | if wallRight-node.Radius.X < wallLeft+node.Radius.X { 98 | wallLeft = wallRight - 2*node.Radius.X 99 | } 100 | } 101 | 102 | if leftToRight { 103 | if node.Center.X < wallLeft+node.Radius.X { 104 | node.Center.X = wallLeft + node.Radius.X 105 | } 106 | } else { 107 | if node.Center.X > wallRight-node.Radius.X { 108 | node.Center.X = wallRight - node.Radius.X 109 | } 110 | } 111 | 112 | return wallLeft, wallRight 113 | } 114 | 115 | // PositionIncoming positions node based on incoming edges 116 | func PositionIncoming(graph *Graph, leftToRight bool, nudge float32) { 117 | iterateLayers(graph, leftToRight, 1, 118 | func(layer Nodes, i int, node *Node) { 119 | wallLeft, wallRight := NodeWalls(graph, layer, i, node, leftToRight) 120 | 121 | // calculate average location based on incoming 122 | if len(node.In) == 0 { 123 | return 124 | } 125 | center := float32(0.0) 126 | for _, node := range node.In { 127 | center += node.Center.X 128 | } 129 | center /= float32(len(node.In)) 130 | 131 | center = clampf32(center, wallLeft+node.Radius.X-nudge, wallRight-node.Radius.Y+nudge) 132 | 133 | // is between sides 134 | node.Center.X = center 135 | }) 136 | } 137 | 138 | // PositionOutgoing positions node based on outgoing edges 139 | func PositionOutgoing(graph *Graph, leftToRight bool, nudge float32) { 140 | iterateLayers(graph, leftToRight, -1, 141 | func(layer Nodes, i int, node *Node) { 142 | wallLeft, wallRight := NodeWalls(graph, layer, i, node, leftToRight) 143 | 144 | // calculate average location based on incoming 145 | if len(node.Out) == 0 { 146 | return 147 | } 148 | center := float32(0.0) 149 | for _, node := range node.Out { 150 | center += node.Center.X 151 | } 152 | center /= float32(len(node.Out)) 153 | 154 | center = clampf32(center, wallLeft+node.Radius.X-nudge, wallRight-node.Radius.X+nudge) 155 | 156 | // is between sides 157 | node.Center.X = center 158 | }) 159 | } 160 | 161 | // sanityCheckLayer checks whether any nodes are overlapping 162 | func sanityCheckLayer(graph *Graph, layer Nodes) { 163 | deltas := []float32{} 164 | positions := []float32{} 165 | fail := false 166 | wallLeft := float32(0) 167 | for _, node := range layer { 168 | delta := (node.Center.X - node.Radius.X) - wallLeft 169 | if delta < 0 { 170 | fail = true 171 | } 172 | deltas = append(deltas, delta) 173 | positions = append(positions, node.Center.X) 174 | wallLeft = node.Center.X + node.Radius.X 175 | } 176 | 177 | if fail { 178 | fmt.Println("=") 179 | fmt.Println(deltas) 180 | fmt.Println(positions) 181 | } 182 | } 183 | 184 | // flushLeft corrects for graph drift due to moving nodes around 185 | func flushLeft(graph *Graph) { 186 | node := graph.Nodes[0] 187 | minleft := node.Center.X - node.Radius.X 188 | for _, node := range graph.Nodes[1:] { 189 | if node.Center.X-node.Radius.X < minleft { 190 | minleft = node.Center.X - node.Radius.X 191 | } 192 | } 193 | 194 | for _, node := range graph.Nodes { 195 | node.Center.X -= minleft 196 | } 197 | } 198 | -------------------------------------------------------------------------------- /internal/hier/rank.go: -------------------------------------------------------------------------------- 1 | package hier 2 | 3 | // DefaultRank does recommended ranking algorithm 4 | func DefaultRank(graph *Graph) *Graph { 5 | Rank(graph) 6 | return graph 7 | } 8 | 9 | // Rank implements basic ranking algorithm 10 | func Rank(graph *Graph) { 11 | RankFrontload(graph) 12 | 13 | for i := 0; i < 7; i++ { 14 | RankMinimizeEdgeStep(graph, i%2 == 0) 15 | } 16 | 17 | graph.ByRank = nil 18 | for _, node := range graph.Nodes { 19 | if node.Rank >= len(graph.ByRank) { 20 | byRank := make([]Nodes, node.Rank+1) 21 | copy(byRank, graph.ByRank) 22 | graph.ByRank = byRank 23 | } 24 | graph.ByRank[node.Rank].Append(node) 25 | } 26 | } 27 | 28 | // RankFrontload assigns node.Rank := max(node.In[i].Rank) + 1 29 | func RankFrontload(graph *Graph) { 30 | roots := graph.Roots() 31 | 32 | incount := make([]int, len(graph.Nodes)) 33 | for _, node := range graph.Nodes { 34 | incount[node.ID] = len(node.In) 35 | } 36 | 37 | rank := 0 38 | for len(roots) > 0 { 39 | next := Nodes{} 40 | for _, src := range roots { 41 | src.Rank = rank 42 | for _, dst := range src.Out { 43 | incount[dst.ID]-- 44 | if incount[dst.ID] == 0 { 45 | next.Append(dst) 46 | } 47 | } 48 | } 49 | roots = next 50 | rank++ 51 | } 52 | } 53 | 54 | // RankBackload assigns node.Rank := min(node.Out[i].Rank) - 1 55 | func RankBackload(graph *Graph) { 56 | roots := Nodes{} 57 | outcount := make([]int, len(graph.Nodes)) 58 | for _, node := range graph.Nodes { 59 | outcount[node.ID] = len(node.Out) 60 | if len(node.Out) == 0 { 61 | roots.Append(node) 62 | } 63 | } 64 | 65 | rank := 0 66 | graph.ByRank = nil 67 | for len(roots) > 0 { 68 | graph.ByRank = append(graph.ByRank, roots) 69 | next := Nodes{} 70 | for _, root := range roots { 71 | root.Rank = rank 72 | for _, src := range root.In { 73 | outcount[src.ID]-- 74 | if outcount[src.ID] == 0 { 75 | next.Append(src) 76 | } 77 | } 78 | } 79 | roots = next 80 | rank++ 81 | } 82 | 83 | for i := range graph.ByRank[:len(graph.ByRank)/2] { 84 | k := len(graph.ByRank) - i - 1 85 | graph.ByRank[i], graph.ByRank[k] = graph.ByRank[k], graph.ByRank[i] 86 | } 87 | 88 | for rank, nodes := range graph.ByRank { 89 | for _, node := range nodes { 90 | node.Rank = rank 91 | } 92 | } 93 | } 94 | 95 | // RankMinimizeEdgeStep moves nodes up/down to more equally distribute 96 | func RankMinimizeEdgeStep(graph *Graph, down bool) (changed bool) { 97 | if down { 98 | // try to move nodes down 99 | for _, node := range graph.Nodes { 100 | if len(node.In) <= len(node.Out) { 101 | // there are more edges below, try to move node downwards 102 | minrank := len(graph.Nodes) 103 | for _, dst := range node.Out { 104 | minrank = min(dst.Rank, minrank) 105 | } 106 | if node.Rank <= minrank-1 { 107 | if len(node.In) == len(node.Out) { 108 | // node.Rank = node.Rank 109 | node.Rank = (node.Rank + (minrank - 1) + 1) / 2 110 | // node.Rank = randbetween(node.Rank, minrank-1) 111 | } else { 112 | node.Rank = minrank - 1 113 | } 114 | changed = true 115 | } 116 | } 117 | } 118 | } else { 119 | for _, node := range graph.Nodes { 120 | if len(node.In) >= len(node.Out) { 121 | // there are more edges above, try to move node upwards 122 | maxrank := 0 123 | for _, src := range node.In { 124 | maxrank = max(src.Rank, maxrank) 125 | } 126 | if node.Rank >= maxrank+1 { 127 | if len(node.In) == len(node.Out) { 128 | // node.Rank = node.Rank 129 | node.Rank = (node.Rank + (maxrank + 1)) / 2 130 | // node.Rank = randbetween(node.Rank, maxrank+1) 131 | } else { 132 | node.Rank = maxrank + 1 133 | } 134 | changed = true 135 | } 136 | } 137 | } 138 | } 139 | return 140 | } 141 | -------------------------------------------------------------------------------- /internal/hier/virtual.go: -------------------------------------------------------------------------------- 1 | package hier 2 | 3 | // DefaultAddVirtuals adds basic virtual nodes 4 | func DefaultAddVirtuals(graph *Graph) *Graph { 5 | AddVirtuals(graph) 6 | return graph 7 | } 8 | 9 | // AddVirtuals creates nodes for edges spanning multiple ranks 10 | // 11 | // Rank input output 12 | // 0 A A 13 | // /| / \ 14 | // 1 B | => B V 15 | // \| \ / 16 | // 2 C C 17 | func AddVirtuals(graph *Graph) { 18 | if len(graph.ByRank) == 0 { 19 | return 20 | } 21 | 22 | for _, src := range graph.Nodes { 23 | for di, dst := range src.Out { 24 | if dst.Rank-src.Rank <= 1 { 25 | continue 26 | } 27 | 28 | src.Out[di] = nil 29 | dst.In.Remove(src) 30 | 31 | for rank := dst.Rank - 1; rank > src.Rank; rank-- { 32 | node := graph.AddNode() 33 | node.Rank = rank 34 | node.Virtual = true 35 | graph.ByRank[node.Rank].Append(node) 36 | graph.AddEdge(node, dst) 37 | dst = node 38 | } 39 | 40 | src.Out[di] = dst 41 | dst.In.Append(src) 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /layout.go: -------------------------------------------------------------------------------- 1 | package layout 2 | 3 | import ( 4 | "github.com/loov/layout/internal/hier" 5 | ) 6 | 7 | const epsilon = 1e-6 8 | 9 | func (graph *Graph) AssignMissingValues() { 10 | if graph.FontSize <= 0 { 11 | graph.FontSize = graph.LineHeight * 14 / 16 12 | } 13 | if graph.NodePadding <= 0 { 14 | graph.NodePadding = graph.LineHeight 15 | } 16 | if graph.RowPadding <= 0 { 17 | graph.RowPadding = graph.LineHeight 18 | } 19 | if graph.EdgePadding <= 0 { 20 | graph.EdgePadding = 6 * Point 21 | } 22 | 23 | for _, node := range graph.Nodes { 24 | if node.Shape == "" { 25 | node.Shape = graph.Shape 26 | } 27 | 28 | if node.Weight < epsilon { 29 | node.Weight = epsilon 30 | } 31 | 32 | if node.FontSize <= 0 { 33 | node.FontSize = graph.FontSize 34 | } 35 | 36 | if node.Radius.X <= 0 || node.Radius.Y <= 0 { 37 | node.Radius.X = graph.LineHeight 38 | node.Radius.Y = graph.LineHeight 39 | 40 | labelRadius := node.approxLabelRadius(graph.LineHeight) 41 | labelRadius.X += node.FontSize * 0.5 42 | labelRadius.Y += node.FontSize * 0.25 43 | 44 | if node.Radius.X < labelRadius.X { 45 | node.Radius.X = labelRadius.X 46 | } 47 | if node.Radius.Y < labelRadius.Y { 48 | node.Radius.Y = labelRadius.Y 49 | } 50 | } 51 | } 52 | 53 | for _, edge := range graph.Edges { 54 | if edge.Weight < epsilon { 55 | edge.Weight = epsilon 56 | } 57 | } 58 | } 59 | 60 | func Hierarchical(graphdef *Graph) { 61 | graphdef.AssignMissingValues() 62 | 63 | nodes := map[*Node]hier.ID{} 64 | reverse := map[hier.ID]*Node{} 65 | 66 | // construct hierarchical graph 67 | graph := &hier.Graph{} 68 | for _, nodedef := range graphdef.Nodes { 69 | node := graph.AddNode() 70 | nodes[nodedef] = node.ID 71 | reverse[node.ID] = nodedef 72 | node.Label = nodedef.ID 73 | } 74 | for _, edge := range graphdef.Edges { 75 | from, to := nodes[edge.From], nodes[edge.To] 76 | graph.AddEdge(graph.Nodes[from], graph.Nodes[to]) 77 | } 78 | 79 | // remove cycles 80 | decycledGraph := hier.DefaultDecycle(graph) 81 | 82 | // assign nodes to ranks 83 | rankedGraph := hier.DefaultRank(decycledGraph) 84 | 85 | // create virtual nodes 86 | filledGraph := hier.DefaultAddVirtuals(rankedGraph) 87 | 88 | // order nodes in ranks 89 | orderedGraph := hier.DefaultOrderRanks(filledGraph) 90 | 91 | // assign node sizes 92 | for id, node := range orderedGraph.Nodes { 93 | if node.Virtual { 94 | node.Radius.X = float32(graphdef.EdgePadding) 95 | node.Radius.Y = float32(graphdef.EdgePadding) 96 | continue 97 | } 98 | 99 | nodedef, ok := reverse[hier.ID(id)] 100 | if !ok { 101 | // TODO: handle missing node 102 | continue 103 | } 104 | node.Radius.X = float32(nodedef.Radius.X + graphdef.NodePadding) 105 | node.Radius.Y = float32(nodedef.Radius.Y + graphdef.RowPadding) 106 | } 107 | 108 | // position nodes 109 | positionedGraph := hier.DefaultPosition(orderedGraph) 110 | 111 | // assign final positions 112 | for nodedef, id := range nodes { 113 | node := positionedGraph.Nodes[id] 114 | nodedef.Center.X = Length(node.Center.X) 115 | nodedef.Center.Y = Length(node.Center.Y) 116 | } 117 | 118 | // calculate edges 119 | edgePaths := map[[2]hier.ID][]Vector{} 120 | for _, source := range positionedGraph.Nodes { 121 | if source.Virtual { 122 | continue 123 | } 124 | 125 | sourcedef := reverse[source.ID] 126 | for _, out := range source.Out { 127 | path := []Vector{} 128 | path = append(path, sourcedef.BottomCenter()) 129 | 130 | target := out 131 | for target != nil && target.Virtual { 132 | if len(target.Out) < 1 { // should never happen 133 | target = nil 134 | break 135 | } 136 | 137 | path = append(path, Vector{ 138 | Length(target.Center.X), 139 | Length(target.Center.Y), 140 | }) 141 | 142 | target = target.Out[0] 143 | } 144 | if target == nil { 145 | continue 146 | } 147 | 148 | targetdef := reverse[target.ID] 149 | path = append(path, targetdef.TopCenter()) 150 | 151 | edgePaths[[2]hier.ID{source.ID, target.ID}] = path 152 | } 153 | } 154 | 155 | for _, edge := range graphdef.Edges { 156 | sourceid := nodes[edge.From] 157 | targetid := nodes[edge.To] 158 | 159 | if sourceid == targetid { 160 | // TODO: improve loops 161 | edge.Path = []Vector{ 162 | edge.From.BottomCenter(), 163 | edge.From.TopCenter(), 164 | } 165 | continue 166 | } 167 | 168 | path, ok := edgePaths[[2]hier.ID{sourceid, targetid}] 169 | if ok { 170 | edge.Path = path 171 | continue 172 | } 173 | 174 | // some paths may have been reversed 175 | revpath, ok := edgePaths[[2]hier.ID{targetid, sourceid}] 176 | if ok { 177 | edge.Path = reversePath(revpath) 178 | continue 179 | } 180 | } 181 | } 182 | 183 | func reversePath(path []Vector) []Vector { 184 | rs := make([]Vector, 0, len(path)) 185 | for i := len(path) - 1; i >= 0; i-- { 186 | rs = append(rs, path[i]) 187 | } 188 | return rs 189 | } 190 | -------------------------------------------------------------------------------- /node.go: -------------------------------------------------------------------------------- 1 | package layout 2 | 3 | import ( 4 | "strings" 5 | ) 6 | 7 | type Node struct { 8 | ID string 9 | 10 | Label string 11 | Weight float64 12 | 13 | Tooltip string 14 | FontName string 15 | FontSize Length 16 | FontColor Color 17 | 18 | LineWidth Length 19 | LineColor Color 20 | 21 | Shape Shape 22 | FillColor Color 23 | Radius Vector 24 | 25 | // computed in layouting 26 | Center Vector 27 | } 28 | 29 | func NewNode(id string) *Node { 30 | node := &Node{} 31 | node.ID = id 32 | node.Weight = 1.0 33 | node.LineWidth = Point 34 | return node 35 | } 36 | 37 | func (node *Node) String() string { 38 | if node == nil { 39 | return "?" 40 | } 41 | if node.ID != "" { 42 | return node.ID 43 | } 44 | return node.Label 45 | } 46 | 47 | func (node *Node) DefaultLabel() string { 48 | if node.Label != "" { 49 | return node.Label 50 | } 51 | return node.ID 52 | } 53 | 54 | func (node *Node) approxLabelRadius(lineHeight Length) Vector { 55 | const HeightWidthRatio = 0.5 56 | if lineHeight < node.FontSize { 57 | lineHeight = node.FontSize 58 | } 59 | 60 | size := Vector{} 61 | lines := strings.Split(node.DefaultLabel(), "\n") 62 | for _, line := range lines { 63 | width := Length(len(line)) * node.FontSize * HeightWidthRatio 64 | if width > size.X { 65 | size.X = width 66 | } 67 | size.Y += lineHeight 68 | } 69 | 70 | size.X *= 0.5 71 | size.Y = Length(len(lines)) * lineHeight * 0.5 72 | return size 73 | } 74 | 75 | func (node *Node) TopLeft() Vector { return Vector{node.Left(), node.Top()} } 76 | func (node *Node) BottomRight() Vector { return Vector{node.Right(), node.Bottom()} } 77 | 78 | func (node *Node) TopCenter() Vector { return Vector{node.Center.X, node.Top()} } 79 | func (node *Node) BottomCenter() Vector { return Vector{node.Center.X, node.Bottom()} } 80 | 81 | func (node *Node) Left() Length { return node.Center.X - node.Radius.X } 82 | func (node *Node) Top() Length { return node.Center.Y - node.Radius.Y } 83 | func (node *Node) Right() Length { return node.Center.X + node.Radius.X } 84 | func (node *Node) Bottom() Length { return node.Center.Y + node.Radius.Y } 85 | --------------------------------------------------------------------------------