RubyPloticus
http://martinfowler.com/bliki/RubyPloticus.html
先ごろEvaluatingRubyを投稿したが、その中で私は、 同僚がWebアプリケーションにファンシーな数値グラフを組み込んだと述べた。 それについて、どのようにやっているのかという質問のメールを頂いた。 簡単な答えとして「Ploticusを使った」と元のエントリーに追記しておいたのだが、 今度は、RubyとPloticusをどのようにつなげたのかという疑問がでてくる。
実は私も、最近、同様の問題に出くわしたのだ。 個人的なプロジェクトでploticusを使ってデータをグラフ化したいと思っていた。 解決策としては、先ほどの同僚が使ったものと似たようなものになった(私のほうが洗練されていないだろうけど)。 なので、ここでその情報を共有したいと思う。
最初に注意しておくこと(文字通り「注意」して欲しい。私が本当にハマったところだから)。 これは堅牢的、パフォーマンス的、エンタープライズ的なものは一切考慮していない。 単に、私の、私の、(何度も言うよ)私のデータに使用したものだからだ。
ploticusのようなCライブラリを使用するときにイケてるやり方は、C APIを使って直接バインドすることだ。 Rubyではこれを簡単に行うことができる……と私も聞いた。 だが、私には難しいよ(お酒の時間の前に終わらせなきゃいけないんだったら、なおさら無理)。 なので私がやったのは、ploticusのスクリプトを作って、ploticusに渡すやり方だ。ploticusは標準入力から命令の書かれたスクリプトを受け取って実行することができる。 私がやることは、rubyでploticusを起動して、コマンドを渡してやることだ。 だいたいこんな感じになる。
def generate script, outfile
IO.popen("ploticus -png -o #{outfile} -stdin", 'w'){](p| p << script}
end
このスクリプトを作ることで、 自分の言葉で動くオブジェクトを手にすることができた。 そして、必要なploticusの結果を手にすることができた。 プレハブ式なものを使っていれば、それを組み合わせるのは簡単だ。 [こういう感じの|http://ploticus.sourceforge.net/gallery/students.htm) 棒グラフが欲しかったのだが、おれにはploticusスクリプトが必要だった。
私は必要なものを3つのレベルで作った。 最下層はPloticusScripter。これはploticusスクリプトコマンドを作成するクラスだ。 こんな感じである。
class PloticusScripter
def initialize
@procs = []
end
def proc name
result = PloticusProc.new name
yield result
@procs << result
return result
end
def script
result = ""
@procs.each do |p|
result << p.script_output << "\n\n"
end
return result
end
end
class PloticusProc
def initialize name
@name = name
@lines = []
end
def script_output
return (["#proc " + @name] + @lines).join("\n")
end
def method_missing name, *args, &proc
line = name.to_s + ": "
line.tr!('_', '.')
args.each {|a| line << a.to_s << " "}
@lines << line
end
end
ご覧のように、PloticusScripterはprocコマンドのリストである (script_outputに反応するものでもよかったが、そのときは必要なかった)。 これを使用するには、 procメソッドを繰り返し呼び出して自分のploticus procを定義していき、 すべてが終わったところで、scriptメソッドを呼び出して、 できあがったスクリプトをゲットして、ploticusに投げる。
次のレベルは棒グラフを作るためのものだ。
class PloticusClusterBar
attr_accessor :rows, :column_names
def initialize
@rows = []
end
def add_row label, data
@rows << [label] + data
end
def getdata scripter
scripter.proc("getdata") do |p|
p.data generate_data
end
end
def colors
%w[red yellow blue green orange]
end
def clusters scripter
column_names.size.times do |i|
scripter.proc("bars") do |p|
p.lenfield i + 2
p.cluster i+1 , "/", column_names.size
p.color colors[i]
p.hidezerobars 'yes'
p.horizontalbars 'yes'
p.legendlabel column_names[i]
end
end
end
def generate_data
result = []
rows.each {|r| result << r.join(" ")}
result << "\n"
return result.join("\n")
end
end
add_rowメソッドを使ってデータ行を追加することで、 簡単にグラフを作ることができる。 グラフの元となるデータをより簡単に作ることができる。
特定のグラフを作るには、 これらの上に位置する3つ目のクラスを作ることになる。
#ploticusのサイトにある例と同じようなものを作る
class StudentGrapher
def initialize
@ps = PloticusScripter.new
@pcb = PloticusClusterBar.new
end
def run
load_data
@pcb.getdata @ps
areadef
@pcb.clusters @ps
end
def load_data
@pcb.column_names = ['Exam A', 'Exam B', 'Exam C', 'Exam D']
@pcb.add_row '01001', [44, 45, 71, 89]
@pcb.add_row '01002', [56, 44, 54, 36]
@pcb.add_row '01003', [46, 63, 28, 87]
@pcb.add_row '01004', [42, 28, 39, 49]
@pcb.add_row '01005', [52, 74, 84, 66]
end
def areadef
@ps.proc("areadef") do |p|
p.title "Example Student Data"
p.yrange 0, 6
p.xrange 0, 100
p.xaxis_stubs "inc 10"
p.yaxis_stubs "datafield=1"
p.rectangle 1, 1, 6, 6
end
end
def generate outfile
IO.popen("ploticus -png -o #{outfile} -stdin", 'w'){|p| p << script}
end
def script
return @ps.script
end
end
def run
output = 'fooStudents.png'
File.delete output if File.exists? output
s = StudentGrapher.new
s.run
s.generate output
end
非常にシンプルな例だが、私がGatewayパターンと呼んでいるものの良い例だ。 PloticusClusterBarクラスは、私がやりたいことへの完璧なインタフェースを持ったゲートウェイとなっている。 この便利なインタフェースから実際に必要とされる出力に変換することができる。 PloticusScripterクラスはまた別のレベルのゲートウェイである。 このようなシンプルなものでも、composed objectを用いた設計は有用であると分かった。 長い間かけて私の脳がゆがんだだけかもしれないけど。