原文: 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是个不错的设计——或许这只能说明这些年来我的头脑扭曲成啥模样了 。