7
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Ruby | Gvizでインデント構造のテキストをグラフに変換する

Last updated at Posted at 2014-09-12

Ruby | Gvizでインデント構造のテキストをグラフに変換する

概要

Gvizでインデント構造のテキストをグラフに変換します。

利用ライブラリ

  • Gviz gem
  • TbpgrUtils gem (自作 gem)
    • 私の遊び場的ユーティリティ。実用性はない。
    • 今回は familyable を利用する。親子関係のデータの取得用。
  • Gottani gem (自作 gem)
    • タブ、スペース、 Markdown の見出し、はてな記法 などツリー構造のテキストの相互変換用ツール
    • 今回は変換時の中間処理用に用意されている、 Array[Hash{:level, :value}] を返却するメソッドを利用
    • level は深さ , value は文字列

サンプルコード

require 'familyable'
require 'gottani_core'
require "gviz"

text =<<-EOS
root
  child1
    child1_1
      child1_1_1
      child1_1_2
        child1_1_2_1
        child1_1_2_2
    child1_2
  child2
    child2_1
  child3
EOS

sp2 = Gottani::Space2.new
indent_text = sp2.space2_to_common(text)

indexed_indent_text = indent_text.reverse.map.with_index { |e, i|e.merge({id: i}) }
# indexed_indent_text はこんなデータが入っています
# [{:level=>1, :value=>"child3", :id=>0},
#  {:level=>2, :value=>"child2_1", :id=>1},
#  {:level=>1, :value=>"child2", :id=>2},
#  {:level=>2, :value=>"child1_2", :id=>3},
#  {:level=>4, :value=>"child1_1_2_2", :id=>4},
#  {:level=>4, :value=>"child1_1_2_1", :id=>5},
#  {:level=>3, :value=>"child1_1_2", :id=>6},
#  {:level=>3, :value=>"child1_1_1", :id=>7},
#  {:level=>2, :value=>"child1_1", :id=>8},
#  {:level=>1, :value=>"child1", :id=>9},
#  {:level=>0, :value=>"root", :id=>10}]

module Familyable
  class Person
    attr_accessor :value, :level
  end
end

i = 0
persons = indexed_indent_text.reduce([]) do |persons, person|
  parent = indexed_indent_text[(i + 1)..-1]
    .find { |e|e[:level] == person[:level] - 1 }
  parent_ids = parent.nil? ? [] : [parent[:id]]
  person = Familyable::Person.new(id: i, parent_ids: parent_ids).tap do |e|
    e.value = person[:value]
    e.level = person[:level]
  end
  persons << person
  i += 1
  persons
end

sorted_persons = persons.reverse
family = Familyable::Family.new(family: persons)
# family にはこんなデータが入っています。
#<Familyable::Family:0x00000600291778
# @family=
#  [#<Familyable::Person:0x00000600293d70
#    @id=0,
#    @level=1,
#    @parent_ids=[10],
#    @value="child3">,
#   #<Familyable::Person:0x00000600293aa0
#    @id=1,
#    @level=2,
#    @parent_ids=[2],
#    @value="child2_1">,
#   #<Familyable::Person:0x000006002937f8
#    @id=2,
#    @level=1,
#    @parent_ids=[10],
#    @value="child2">,
#   #<Familyable::Person:0x00000600293618
#    @id=3,
#    @level=2,
#    @parent_ids=[9],
#    @value="child1_2">,
#   #<Familyable::Person:0x00000600293208
#    @id=4,
#    @level=4,
#    @parent_ids=[6],
#    @value="child1_1_2_2">,
#   #<Familyable::Person:0x00000600292d08
#    @id=5,
#    @level=4,
#    @parent_ids=[6],
#    @value="child1_1_2_1">,
#   #<Familyable::Person:0x00000600292948
#    @id=6,
#    @level=3,
#    @parent_ids=[8],
#    @value="child1_1_2">,
#   #<Familyable::Person:0x000006002925b0
#    @id=7,
#    @level=3,
#    @parent_ids=[8],
#    @value="child1_1_1">,
#   #<Familyable::Person:0x00000600292240
#    @id=8,
#    @level=2,
#    @parent_ids=[9],
#    @value="child1_1">,
#   #<Familyable::Person:0x00000600291d90
#    @id=9,
#    @level=1,
#    @parent_ids=[10],
#    @value="child1">,
#   #<Familyable::Person:0x00000600291a48
#    @id=10,
#    @level=0,
#    @parent_ids=[],
#    @value="root">]>

Graph do
  global layout:'dot'
  # ノードの定義
  sorted_persons.each { |person|node :"person#{person.id}",  { label: person.value } }

  # エッジの定義
  sorted_persons.each do |person|
    parents =  family.get_parents(person)
    next if parents.empty?
    parent = parents.first
    edge :"person#{parent.id}_person#{person.id}"
  end

  # *** 根のランク, スタイル定義 ***
  min_id = persons.find { |e|e.level == 0 }.id
  min_person_key = :"person#{min_id}"
  rank :min, [min_person_key]
  node min_person_key, { width: 2, shape: 'doublecircle' }

  # *** 根以外のノードのランク定義 ***
  ranked_persons = sorted_persons.group_by { |e|e.level }.select { |key, value|key.nonzero? }
  ranked_persons.each do |rank, each_rank_persons|
    persons = each_rank_persons.map { |e|"person#{e.id}".to_sym }
    rank(:same, persons)
  end

  save :"givz_tree", :png
end

出力

givz_tree.png

補足1

gottani は space2 インデント以外に

  • space4
  • hard tab
  • markdown 見出し
  • hatena 見出し

などに対応しているため、サンプルコードの text の内容を
上記のフォーマットに変更し、 Gottani::Space2.new の代わりに
Gottani::Tagb.new などを利用すれば別のフォーマットにも対応可能です。

補足2

もう1段階抽象化すれば、

  • ツリー構造のテキスト
  • フォーマット(タブ?、インデント?)
  • 表示スタイル(色、形など)

あたりを設定値として与えるだけで、ツリー構造を画像化する
ツールが作成できそうな・・・。

設定の自由度を考えると、画像の出力まですべて行ってしまうよりも、
Gvizのソースを生成して、そのあと表示スタイル部分を好きに変更してもらう
「消極的な自動生成」※書籍 達人プログラマ参照
にしたほうが汎用性がありそう。

内部DSLの設定ファイル
=> ツリー to Gviz を実行
=> 生成された Gviz の色・形など設定部を変更
=> Gviz 実行
=> 画像が生成される。

オプションとして、デフォルトのレイアウトでよければ
画像の出力まで一気に行うサブコマンドがあってもよさそう。

内部DSLには

  • ツリーフォーマットのテキスト
  • 画像ファイル名
  • Gviz のコードを出力する rb ファイル名
  • 表示スタイル(大まかにスタイル指定。細かくやりたい人は自動生成後に手動で)

を設定できるようにすればいい。

参照

7
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?