treedrawer
treedrawer is a Go module for drawing trees on the terminal like the one below.
โญโโฎ
โ9โ
โฐโฌโฏ
โญโโโโโโโโโโโโโโโโโโดโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโฌโโโโฌโโโโฎ
โญโโโโโโโโโโโดโโโโโโโโโโฎ โญโดโฎ โญโดโฎ โญโดโฎ โญโดโฎ
โI can handle stringsโ โ1โ โ2โ โ3โ โ4โ
โฐโโโโโโโโโโโฌโโโโโโโโโโฏ โฐโฌโฏ โฐโโฏ โฐโโฏ โฐโโฏ
โ โญโโโโโโฌโโโดโโโโโโฎ
โญโโโโโโโโโโโโโโโโโดโโโโโโโโโโโโโโโโโฎ โญโโดโโฎ โญโโดโฎ โญโโโโโดโโโโฎ
โwith as many children as you wantโ โ124โ โ13โ โa stringโ
โฐโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโฏ โฐโโโโฏ โฐโโโฏ โฐโโโโโโโโโฏ
โ
โญโโโโโโโโโโโโโโโโดโโโโโโโโโโโโโโโโฎ
โwith as many layers as you wantโ
โฐโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโฏ
โ
โญโโโโโโโโโโโโโโโโโโดโโโโโโโโโโโโโโโโโโฎ
โactually I can handle everything...โ
โฐโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโฏ
โ
โญโโโโโโโโโโโโโโโโโโโดโโโโโโโโโโโโโโโโโโโฎ
โ...that satisfies NodeValue interfaceโ
โฐโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฏ
Import
import "github.com/m1gwings/treedrawer/tree"
Quick start
$ cat example.go
package main
import (
"fmt"
"github.com/m1gwings/treedrawer/tree"
)
func main() {
t := tree.NewTree(tree.NodeInt64(5))
t.AddChild(tree.NodeString("adding a string"))
t.AddChild(tree.NodeInt64(4))
t.AddChild(tree.NodeInt64(3))
fmt.Println(t)
}
$ go run example.go
โญโโฎ
โ5โ
โฐโฌโฏ
โญโโโโดโโโโโโโฌโโโโฎ
โญโโโโโโโโดโโโโโโโโฎ โญโดโฎ โญโดโฎ
โadding a stringโ โ4โ โ3โ
โฐโโโโโโโโโโโโโโโโฏ โฐโโฏ โฐโโฏ
Usage
Building the tree
Creating the tree with 1 as the value of the root node
t := tree.NewTree(tree.NodeInt64(1))
Adding the first child to t with value 2
t.AddChild(tree.NodeInt64(2))
Adding more children
t.AddChild(tree.NodeInt64(3))
t.AddChild(tree.NodeInt64(4))
t.AddChild(tree.NodeInt64(5))
We've just built the tree below
โญโโฎ
โ1โ
โฐโฌโฏ
โญโโโโฌโโดโโฌโโโโฎ
โญโดโฎ โญโดโฎ โญโดโฎ โญโดโฎ
โ2โ โ3โ โ4โ โ5โ
โฐโโฏ โฐโโฏ โฐโโฏ โฐโโฏ
Navigating the tree
Navigating to first child of t (we're still working with the tree above)
tFirstChild, err := t.Child(0)
Adding children to first child
tFirstChild.AddChild(tree.NodeInt64(6))
tFirstChild.AddChild(tree.NodeInt64(7))
tFirstChild.AddChild(tree.NodeInt64(8))
Going back to parent
tFirstChildParent, ok := tFirstChild.Parent()
_ := tFirstChildParent == t
Navigating to third child of t
tThirdChild, err := t.Child(2)
Adding a string child to third child
tThirdChild.AddChild(tree.NodeString("I'm a string"))
Getting a pointer to the root of the tree
tRoot := tThirdChild.Root()
_ := tRoot == t
Now the tree looks like this
โญโโฎ
โ1โ
โฐโฌโฏ
โญโโโโโโโโฌโโโโดโโโโโโฌโโโโโโโโโโฎ
โญโดโฎ โญโดโฎ โญโดโฎ โญโดโฎ
โ2โ โ3โ โ4โ โ5โ
โฐโฌโฏ โฐโโฏ โฐโฌโฏ โฐโโฏ
โญโโโโผโโโโฎ โ
โญโดโฎ โญโดโฎ โญโดโฎ โญโโโโโโโดโโโโโโฎ
โ6โ โ7โ โ8โ โI'm a stringโ
โฐโโฏ โฐโโฏ โฐโโฏ โฐโโโโโโโโโโโโโฏ
Getting and setting values from the tree
Getting the value of a node
v := t.Val()
Setting the value of a node
t.SetVal(tree.NodeInt64(3))
Drawing the tree
*tree.Tree implements the Stringer interface, just use package fmt to draw trees to terminal
fmt.Println(t)
Implementing NodeValue interface
The tree can handle every type that satisfies the NodeValue interface
type NodeValue interface {
Draw() *drawer.Drawer
}
The wrappers for built-in are defined inside the package treedrawer/tree like tree.NodeInt64 or tree.NodeString used above, so you don't need to worry about them.
Continue reading this section if you want to draw custom types instead.
- Importing treedrawer/drawer
First of all we need access to the drawer.Drawer type. Just import the following
import "github.com/m1gwings/treedrawer/drawer"
drawer.Drawer under the hood is just a 2D slice of runes on which you can draw a rune specifying its coordinates or another entire drawer.Drawer specifying the coordinates of its upper-left corner.
type NodeAsterisk struct {
Width, Height int
}
NodeAsterisk represents a rectangle of width NodeAsterisk.Width and height NodeAsterisk.Height.
- Implementing NodeAsterisk.Draw() in order to satisfy NodeValue interface
func (nA NodeAsterisk) Draw() *drawer.Drawer {
d, err := drawer.NewDrawer(nA.Width, nA.Height)
if err != nil {
log.Fatal(err)
}
for x := 0; x < nA.Width; x++ {
for y := 0; y < nA.Height; y++ {
err = d.DrawRune('*', x, y)
if err != nil {
log.Fatal(err)
}
}
}
return d
}
The method allocates a new drawer with width nA.Width and height nA.Height, then loops over each cell and fills it with an '*'.
You can implement this method to represent your data as you want.
- Adding instances of NodeAsterisk to a tree
t := tree.NewTree(NodeAsterisk{3, 4})
t.AddChild(NodeAsterisk{1, 2})
t.AddChild(NodeAsterisk{3, 3})
fmt.Println(t)
โญโโโโฎ
โ***โ
โ***โ
โ***โ
โ***โ
โฐโโฌโโฏ
โญโโโดโโฎ
โญโดโฎ โญโโดโโฎ
โ*โ โ***โ
โ*โ โ***โ
โฐโโฏ โ***โ
โฐโโโโฏ
Examples
You can find these examples inside the ./examples folder
HTML tree
$ cat htmltree.go
package main
import (
"fmt"
"log"
"strings"
"github.com/m1gwings/treedrawer/tree"
"golang.org/x/net/html"
)
func main() {
s := `<p>Links:</p><ul><li><a href="foo">Foo</a><li><a href="/bar/baz">BarBaz</a></ul>`
doc, err := html.Parse(strings.NewReader(s))
if err != nil {
log.Fatal(err)
}
t := tree.NewTree(tree.NodeString(""))
var f func(*html.Node, *tree.Tree)
f = func(n *html.Node, t *tree.Tree) {
t.SetVal(tree.NodeString(n.Data))
for c := n.FirstChild; c != nil; c = c.NextSibling {
tChild := t.AddChild(tree.NodeString(""))
f(c, tChild)
}
}
f(doc.FirstChild, t)
fmt.Println(t)
}
$ go run htmltree.go
โญโโโโโฎ
โhtmlโ
โฐโโโฌโโฏ
โญโโโโโโโโโโโโโดโโโโฎ
โญโโโดโโฎ โญโโโดโโฎ
โheadโ โbodyโ
โฐโโโโโฏ โฐโโโฌโโฏ
โญโโโโโโโโดโโโโโฎ
โญโดโฎ โญโโดโฎ
โpโ โulโ
โฐโฌโฏ โฐโโฌโฏ
โ โญโโโโโดโโโฎ
โญโโโโดโโโฎ โญโโดโฎ โญโโดโฎ
โLinks:โ โliโ โliโ
โฐโโโโโโโฏ โฐโโฌโฏ โฐโโฌโฏ
โ โ
โญโดโฎ โญโดโฎ
โaโ โaโ
โฐโฌโฏ โฐโฌโฏ
โ โ
โญโโดโโฎ โญโโโโดโโโฎ
โFooโ โBarBazโ
โฐโโโโฏ โฐโโโโโโโฏ
File system tree
$ cat filesystemtree.go
package main
import (
"fmt"
"log"
"strings"
"github.com/m1gwings/treedrawer/tree"
"golang.org/x/net/html"
)
func main() {
s := `<p>Links:</p><ul><li><a href="foo">Foo</a><li><a href="/bar/baz">BarBaz</a></ul>`
doc, err := html.Parse(strings.NewReader(s))
if err != nil {
log.Fatal(err)
}
t := tree.NewTree(tree.NodeString(""))
var f func(*html.Node, *tree.Tree)
f = func(n *html.Node, t *tree.Tree) {
t.SetVal(tree.NodeString(n.Data))
for c := n.FirstChild; c != nil; c = c.NextSibling {
tChild := t.AddChild(tree.NodeString(""))
f(c, tChild)
}
}
f(doc.FirstChild, t)
fmt.Println(t)
}
$ go run filesystemtree.go
โญโโโโโโโโโโโฎ
โtreedrawerโ
โฐโโโโโโฌโโโโโฏ
โญโโโโโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโดโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฎ
โญโโโโโดโโโโโฎ โญโโโโโดโโโโฎ โญโโโโดโโโโฎ โญโโโโดโโโฎ โญโโโโดโโโฎ โญโโโดโโฎ
โREADME.mdโ โexamplesโ โLICENSEโ โdrawerโ โgo.sumโ โtreeโ
โฐโโโโโโโโโโฏ โฐโโโโโฌโโโโฏ โฐโโโโโโโโฏ โฐโโโโฌโโโฏ โฐโโโโโโโฏ โฐโโโฌโโฏ
โญโโโโโโโโโโดโโโโโโโฎ โญโโโโโโดโโโโโโโโโฎ โญโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโดโโฌโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโฎ
โญโโโโโโดโโโโโโฎ โญโโโโโโโโโดโโโโโโโโโฎ โญโโโโโโโโดโโโโโโโฎ โญโโโโโดโโโโโฎ โญโโโโโโโโโดโโโโโโโโฎ โญโโโโโโโดโโโโโโฎ โญโโโโดโโโโฎ โญโโโโโโโดโโโโโโโฎ โญโโโโโโโดโโโโโโฎ
โhtmltree.goโ โfilesystemtree.goโ โdrawer_test.goโ โdrawer.goโ โexamples_test.goโ โstringify.goโ โtree.goโ โnodevalues.goโ โtree_test.goโ
โฐโโโโโโโโโโโโฏ โฐโโโโโโโโโโโโโโโโโโฏ โฐโโโโโโโโโโโโโโโฏ โฐโโโโโโโโโโฏ โฐโโโโโโโโโโโโโโโโโฏ โฐโโโโโโโโโโโโโฏ โฐโโโโโโโโฏ โฐโโโโโโโโโโโโโโฏ โฐโโโโโโโโโโโโโฏ
Benchmarks
You can find the code used for the benchmark inside ./tree/stringify_test.go
In order to profile the module we first create trees with l layers and c children for each node, except leaf nodes. Each node has a tree.NodeString("*") as value.
For example the tree below has 3 layers and 2 children for each node.
โญโโฎ
โ*โ
โฐโฌโฏ
โญโโโโดโโโโฎ
โญโดโฎ โญโดโฎ
โ*โ โ*โ
โฐโฌโฏ โฐโฌโฏ
โญโโดโโฎ โญโโดโโฎ
โญโดโฎ โญโดโฎ โญโดโฎ โญโดโฎ
โ*โ โ*โ โ*โ โ*โ
โฐโโฏ โฐโโฏ โฐโโฏ โฐโโฏ
In our benchmark function we print to /dev/null a tree with the specified l and c parameter.
BenchmarkDrawing3L3C-12 | 10000 | 100063 ns/op | 3.00 children | 3.00 layers | 13.0 nodes | 135576 B/op | 722 allocs/op |
BenchmarkDrawing100L1C-12 | 382 | 3096956 ns/op | 1.00 children | 100 layers | 100 nodes | 3727628 B/op | 23297 allocs/op |
BenchmarkDrawing6L3C-12 | 9 | 119789317 ns/op | 3.00 children | 6.00 layers | 364 nodes | 366549320 B/op | 33606 allocs/op |
BenchmarkDrawing1000L1C-12 | 7 | 161607620 ns/op | 1.00 children | 1000 layers | 1000 nodes | 373048737 B/op | 2033004 allocs/op |
BenchmarkDrawing10L2C-12 | 2 | 733952840 ns/op | 2.00 children | 10.0 layers | 1023 nodes | 4375790984 B/op | 114909 allocs/op |
BenchmarkDrawing11L2C-12 | 1 | 3661883138 ns/op | 2.00 children | 11.0 layers | 2047 nodes | 20557038432 B/op | 249910 allocs/op |
BenchmarkDrawing8L3C-12 | 1 | 8550947886 ns/op | 3.00 children | 8.00 layers | 3280 nodes | 50574173168 B/op | 387828 allocs/op |
BenchmarkDrawing12L2C-12 | 1 | 13559015034 ns/op | 2.00 children | 12.0 layers | 4095 nodes | 96166288000 B/op | 538283 allocs/op |
Generated using go version go1.15.1 linux/amd64
Known issues ๐
- Emojis are larger than normal characters
fmt.Println(tree.NewTree(tree.NodeString("emojis are buggy ๐คช")))
โญโโโโโโโโโโโโโโโโโโโฎ
โemojis are buggy ๐คชโ
โฐโโโโโโโโโโโโโโโโโโโฏ