Ruby Ploticus

2006年6月19日

最近のEvaluatingRubyに関する投稿で、同僚がいくつかの凝った数値グラフを表示するWebアプリケーションを構築したと述べました。どのようにしてそれを行ったのかという質問がメールで寄せられました。私は短い回答として、ploticusについて元のblikiエントリに追加しましたが、そこから、彼がどのように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のものを生成できるオブジェクトを使用するのが好きです。プレハブを使用するものがあれば、何かを組み立てるのは簡単です。私は、ploticusスクリプトを必要とする、このようなクラスター化された棒グラフを作成したかったのです。

私は、必要なものを3つのレベルで構築しました。最も低いレベルは、ploticusスクリプトコマンドを構築するクラスであるPloticusScripterです。これがそれです。

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

ご覧のとおり、スクリプターは単なる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番目のクラスを作成します。

#produces similar to  ploticus example in ploticus/gallery/students.htm

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

これは非常に単純な例ですが、私がゲートウェイパターンと呼ぶものの良い例です。PloticusClusterBarクラスは、私がやりたいことに最適なインターフェースを備えたゲートウェイです。私は、その便利なインターフェースと実際の出力に必要なものとの間で変換を行います。PloticusScripterクラスは、別のレベルのゲートウェイです。このような単純なことでも、このように構成されたオブジェクトのデザインは良い方法だと私は感じています。これは、私の頭が長年どのようにねじれてきたのかを物語っているだけかもしれません。