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

。
发表于 @ 2006年09月20日 18:12:00|评论(loading...)|编辑