Skip to content

akicho8/tree_support

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Tree structure visualization function library

https://travis-ci.org/akicho8/tree_support.png

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")

https://raw.github.com/akicho8/tree_support/master/images/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")

https://raw.github.com/akicho8/tree_support/master/images/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")

https://raw.github.com/akicho8/tree_support/master/images/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")

https://raw.github.com/akicho8/tree_support/master/images/drop.png

gv = TreeSupport.graphviz(root, take: 3)
gv.output("take.png")

https://raw.github.com/akicho8/tree_support/master/images/take.png

gv = TreeSupport.graphviz(root, take: 3, drop: 1)
gv.output("take_drop.png")

https://raw.github.com/akicho8/tree_support/master/images/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 it scope: -> { 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.