RubyPloticus

来源:互联网 发布:淘宝买肉暗语 编辑:程序博客网 时间:2024/05/16 01:00

原文:RubyPloticus    ruby        2006年6月19日            Bliki 索引

译注:代码和生成的图片示例可从这里下载。

在最近的帖子“评估Ruby”中,我提到一位同事曾在一个Web应用中加入了一些漂亮的数据图表,有人email问我是怎样实现的,我在原来那篇帖子上添了句简短的回答:用Ploticus。这就带来另一个问题——他是怎样把Ruby和Ploticus连起来的呢?

最近我自己也遇到个类似的问题,要用Ploticus把一个个人项目的一些数据图表化。我的解决办法虽然远不如那位同事的那么精致,但实际上很相似。于是我觉得应该和大家分享一下。

首先我声明一条警告——这只不过是我花了一个晚上弄出来的东西,并没想做得很鲁棒,也没怎么考虑性能,更别说“企业级超复杂”了——只是我自己、我一个人用来处理一些数据的。

要想整合并驱动一个Ploticus之类的C库,一种复杂而完善的办法是直接绑定C API,虽然别人告诉我用Ruby做这件事也很简单,但它的工作量对我来说还是太多了(尤其是我想在鸡尾酒时间之前搞定它:-)),因此我的做法是构建一份Ploticus脚本,通过管道(pipe)输给Ploticus。来自标准输入的脚本可以控制Ploticus做事,于是我要做的只是在Ruby中运行Ploticus,把脚本命令通过管道传给它。大致如下:

  def generate script, outfile
    IO.popen("ploticus -png -o #{outfile} -stdin", 'w'){|p| p << script}
  end

为了构建脚本,我想让Object们按我规定的条款工作,生成所需的Ploticus懂的东西。如果你在什么地方用到了Ploticus的预制件(prefabs),搭建东西就轻而易举了。我要画一张簇状条线图,就像这种,这需要一份Ploticus脚本。

我把要做的东西分三层构建,最底层是PloticusScripter,用这个class生成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对象有一个实例变量@procs,是个存proc命令的链表(所谓proc命令,就是能响应 script_output方法调用的东西——没有其他要求)。我可以实例化一个PloticusScripter,反复调用它的proc方法来定义我需要的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

有了PloticusClusterBar,我就能调用它的add_row方法添加数据行构建图表了,为图表增加数据变得非常简单。

为了画一张特定的图,还要在前两层之上再写一个class:

#生成的图与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

上面这个例子非常简单,但它很好地展示了一个模式——我称之为Gateway模式。PloticusClusterBar class是一个gateway,它的接口正好适合我要做的事,通过它便利的接口转换出实际输出需要的东西。PloticusScripter class是另一层gateway。即便做这么一件简单的事,我仍然觉得这样编排一组object是个不错的设计——或许这只能说明这些年来我的头脑扭曲成啥模样了