Tree structure visualization function library
Installation
Install as a standalone gem
$ gem install tree_support
Or install within application using Gemfile
$ bundle add tree_support
$ bundle install
How to use in one line
Just pass the object that has the parent, children method to TreeSupport.tree
require "tree_support"
puts TreeSupport.tree(TreeSupport.example)
# >> *root*
# >> ├─Battle
# >> │ ├─Attack
# >> │ │ ├─Shake the sword
# >> │ │ ├─Attack magic
# >> │ │ │ ├─Summoned Beast X
# >> │ │ │ └─Summoned Beast Y
# >> │ │ └─Repel sword in length
# >> │ └─Defense
# >> ├─Withdraw
# >> │ ├─To stop
# >> │ │ ├─Place a trap
# >> │ │ └─Shoot a bow and arrow
# >> │ └─To escape
# >> └─Break
# >> ├─Stop
# >> └─Recover
# >> ├─Recovery magic
# >> └─Drink recovery medicine
Detailed usage
Prepare a node class like this
class Node
attr_accessor :name, :parent, :children
def initialize(name = nil, &block)
@name = name
@children = []
if block_given?
instance_eval(&block)
end
end
def add(*args, &block)
tap do
children << self.class.new(*args, &block).tap { |v| v.parent = self }
end
end
end
Create a tree
root = Node.new("*root*") do
add "Battle" do
add "Attack" do
add "Shake the sword"
add "Attack magic" do
add "Summoned Beast X"
add "Summoned Beast Y"
end
add "Repel sword in length"
end
add "Defense"
end
add "Withdraw" do
add "To stop" do
add "Place a trap"
add "Shoot a bow and arrow"
end
add "To escape"
end
add "Break" do
add "Stop"
add "Recover" do
add "Recovery magic"
add "Drink recovery medicine"
end
end
end
Visualization
puts TreeSupport.tree(root)
# >> *root*
# >> ├─Battle
# >> │ ├─Attack
# >> │ │ ├─Shake the sword
# >> │ │ ├─Attack magic
# >> │ │ │ ├─Summoned Beast X
# >> │ │ │ └─Summoned Beast Y
# >> │ │ └─Repel sword in length
# >> │ └─Defense
# >> ├─Withdraw
# >> │ ├─To stop
# >> │ │ ├─Place a trap
# >> │ │ └─Shoot a bow and arrow
# >> │ └─To escape
# >> └─Break
# >> ├─Stop
# >> └─Recover
# >> ├─Recovery magic
# >> └─Drink recovery medicine
Troublesome writing TreeSupport.tree
include TreeSupport::Stringify
Node.include(TreeSupport::Stringify)
puts root.to_s_tree
# >> *root*
# >> ├─Battle
# >> │ ├─Attack
# >> │ │ ├─Shake the sword
# >> │ │ ├─Attack magic
# >> │ │ │ ├─Summoned Beast X
# >> │ │ │ └─Summoned Beast Y
# >> │ │ └─Repel sword in length
# >> │ └─Defense
# >> ├─Withdraw
# >> │ ├─To stop
# >> │ │ ├─Place a trap
# >> │ │ └─Shoot a bow and arrow
# >> │ └─To escape
# >> └─Break
# >> ├─Stop
# >> └─Recover
# >> ├─Recovery magic
# >> └─Drink recovery medicine
How do I change the label of a node?
We look for to_s_tree_name
, name
, subject
, title
, to_s
defined by TreeSupport.name_methods
in that order, so we define the method by considering the priority
How do I change labels without defining methods?
Add a block to tree
puts TreeSupport.tree(root) { |node| node.object_id }
# >> 70308514816100
# >> ├─70308514815920
# >> │ ├─70308514815780
# >> │ │ ├─70308514815680
# >> │ │ ├─70308514815580
# >> │ │ │ ├─70308514815480
# >> │ │ │ └─70308514815420
# >> │ │ └─70308514815360
# >> │ └─70308514815300
# >> ├─70308514815220
# >> │ ├─70308514815080
# >> │ │ ├─70308514814980
# >> │ │ └─70308514814920
# >> │ └─70308514814860
# >> └─70308514814780
# >> ├─70308514814680
# >> └─70308514814580
# >> ├─70308514814480
# >> └─70308514814420
How to use methods that are common in tree structure?
The following methods become available in include of TreeSupport::Treeable
- root
- root?
- leaf?
- each
- each_node
- descendants
- self_and_descendants
- ancestors
- self_and_ancestors
- siblings
- self_and_siblings
How to convert to Gviz object?
gv = TreeSupport.graphviz(root)
How to image it?
gv.output("tree.png")
How do I change the color of a particular node?
Return the graphviz attribute as a hash in TreeSupport.graphviz block
gv = TreeSupport.graphviz(root) do |node|
if node.name.include?("Attack")
{fillcolor: "lightblue", style: "filled"}
elsif node.name.include?("Recover")
{fillcolor: "lightpink", style: "filled"}
end
end
gv.output("tree_color.png")
How do I change the label of a particular node?
As with the above method, it returns a hash containing the label value
gv = TreeSupport.graphviz(root) do |node|
{label: node.name.chars.first}
end
gv.output("tree_label.png")
How can I check the dot format of Graphviz?
puts gv.to_dot
# >> digraph n70146110700700 {
# >> graph [charset = "UTF-8", rankdir = "LR"];
# >> n70146110700700 [label = "*root*"];
# >> n70146110700700 -> {n70146110698600; n70146110691220; n70146110689500;};
# >> n70146110698600 [label = "Battle"];
# >> n70146110698600 -> {n70146110698320; n70146110691720;};
# >> n70146110698320 [label = "Attack"];
# >> n70146110698320 -> {n70146110697900; n70146110697240; n70146110692060;};
# >> n70146110697900 [label = "Shake the sword"];
# >> n70146110697240 [label = "Attack magic"];
# >> n70146110697240 -> {n70146110695080; n70146110694480;};
# >> n70146110695080 [label = "Summoned Beast X"];
# >> n70146110694480 [label = "Summoned Beast Y"];
# >> n70146110692060 [label = "Repel sword in length"];
# >> n70146110691720 [label = "Defense"];
# >> n70146110691220 [label = "Withdraw"];
# >> n70146110691220 -> {n70146110690400; n70146110689620;};
# >> n70146110690400 [label = "To stop"];
# >> n70146110690400 -> {n70146110690220; n70146110689820;};
# >> n70146110690220 [label = "Place a trap"];
# >> n70146110689820 [label = "Shoot a bow and arrow"];
# >> n70146110689620 [label = "To escape"];
# >> n70146110689500 [label = "Break"];
# >> n70146110689500 -> {n70146110688500; n70146110687660;};
# >> n70146110688500 [label = "Stop"];
# >> n70146110687660 [label = "Recover"];
# >> n70146110687660 -> {n70146110686920; n70146110686220;};
# >> n70146110686920 [label = "Recovery magic"];
# >> n70146110686220 [label = "Drink recovery medicine"];
# >> }
How can I check the image conversion immediately when debugging?
TreeSupport.graph_open(root)
Equivalent to the next shortcut
TreeSupport.graphviz(root).output("_output.png")
`open _output.png`
Troublesome making node classes yourself
You can use TreeSupport::Node
as it is.
TreeSupport::Node.new("*root*") do
add "Battle" do
add "Attack" do
add "Shake the sword"
add "Attack magic" do
add "Summoned Beast X"
add "Summoned Beast Y"
end
end
end
end
Troublesome making trees
TreeSupport.example
There is a simple sample tree
How to trace leaves?
If you include TreeSupport::Treeable
you can use each_node
root = TreeSupport.example
root.each_node.with_index { |n, i| p [i, n.name] }
# >> [0, "*root*"]
# >> [1, "Battle"]
# >> [2, "Attack"]
# >> [3, "Shake the sword"]
# >> [4, "Attack magic"]
# >> [5, "Summoned Beast X"]
# >> [6, "Summoned Beast Y"]
# >> [7, "Repel sword in length"]
# >> [8, "Defense"]
# >> [9, "Withdraw"]
# >> [10, "To stop"]
# >> [11, "Place a trap"]
# >> [12, "Shoot a bow and arrow"]
# >> [13, "To escape"]
# >> [14, "Break"]
# >> [15, "Stop"]
# >> [16, "Recover"]
# >> [17, "Recovery magic"]
# >> [18, "Drink recovery medicine"]
I do not want to display the root
puts TreeSupport.tree(root, drop: 1)
# >> Battle
# >> ├─Attack
# >> │ ├─Shake the sword
# >> │ ├─Attack magic
# >> │ │ ├─Summoned Beast X
# >> │ │ └─Summoned Beast Y
# >> │ └─Repel sword in length
# >> └─Defense
# >> Withdraw
# >> ├─To stop
# >> │ ├─Place a trap
# >> │ └─Shoot a bow and arrow
# >> └─To escape
# >> Break
# >> ├─Stop
# >> └─Recover
# >> ├─Recovery magic
# >> └─Drink recovery medicine
Since the trees are too big, it is enough up to the depth 3
puts TreeSupport.tree(root, take: 3)
# >> *root*
# >> ├─Battle
# >> │ ├─Attack
# >> │ └─Defense
# >> ├─Withdraw
# >> │ ├─To stop
# >> │ └─To escape
# >> └─Break
# >> ├─Stop
# >> └─Recover
When you combine both
puts TreeSupport.tree(root, take: 3, drop: 1)
# >> Battle
# >> ├─Attack
# >> └─Defense
# >> Withdraw
# >> ├─To stop
# >> └─To escape
# >> Break
# >> ├─Stop
# >> └─Recover
Image version also has similar options
gv = TreeSupport.graphviz(root, drop: 1)
gv.output("drop.png")
gv = TreeSupport.graphviz(root, take: 3)
gv.output("take.png")
gv = TreeSupport.graphviz(root, take: 3, drop: 1)
gv.output("take_drop.png")
Methods for easily making trees from data like CSV
Conversion from Array to Tree
require "tree_support"
records = [
{key: :a, parent: nil},
{key: :b, parent: :a},
{key: :c, parent: :b},
]
# When the first node is regarded as a parent
puts TreeSupport.records_to_tree(records).first.to_s_tree
# >> a
# >> └─b
# >> └─c
# When you make a parent parenting the whole
puts TreeSupport.records_to_tree(records, root_key: :root).to_s_tree
# >> root
# >> └─a
# >> └─b
# >> └─c
Conversion from Tree to Array
tree = TreeSupport.records_to_tree(records)
pp TreeSupport.tree_to_records(tree.first)
# >> [
# >> {:key=>:a, :parent=>nil},
# >> {:key=>:b, :parent=>:a},
# >> {:key=>:c, :parent=>:b},
# >> ]
How to use acts_as_tree equivalent?
Migration
create_table :nodes do |t|
t.belongs_to :parent
end
Model
class Node < ActiveRecord::Base
ar_tree_model
end
Difference from https://github.com/amerine/acts_as_tree
- simple
- Safely delete all safe_destroy_all (accident with destroy_all in combination with acts_as_list)
- Node.roots is defined by scope
- Arguments are different.
:order => :id
if you want to do itscope: -> { order(:id) }
. By doing this you can also pass the where condition.
How do I correspond to memory_record gem?
Just as with ordinary classes, we need parent and children methods
class TreeModel
include MemoryRecord
memory_record [
{key: :a, parent: nil},
{key: :b, parent: :a},
{key: :c, parent: :b},
]
include TreeSupport::Treeable
include TreeSupport::Stringify
def parent
self.class[super]
end
def children
self.class.find_all { |e| e.parent == self }
end
end
puts TreeModel.find_all(&:root?).collect(&:to_s_tree)
# >> A
# >> └─B
# >> └─C
With concern
- Since Gviz extends the standard class, concerns about future interference when combined with Rails (Active Support) etc.