Ruby Ploticus

2006年6月19日

在我最近关于EvaluatingRuby的文章中,我提到一位同事用一些花哨的数值图表制作了一个网络应用程序。有人发邮件询问他是怎么做到的。我在原始的 bliki 条目中添加了我的简短答案,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 内容。如果你有任何使用预制组件的东西,那么构建一些东西就很容易了。我想制作集群条形图,像这样,这需要一个 ploticus 脚本。

我在三个级别上构建了我需要的东西。在最低级别是 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

正如你所看到的,scripter 只是一个 proc 命令列表(好吧,它们可以是任何对 script_output 有响应的东西 - 但我目前不需要其他东西)。我可以实例化 scripter,重复调用 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 来添加数据行来构建图表。这使得我构建图表数据变得容易得多。

为了制作特定的图表,我将在其之上编写第三个类

#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 类是另一个级别的网关。即使对于像这样简单的事情,我也发现像这样组合对象的設計是一个好方法。这可能只是说明了我的大脑多年来是如何扭曲的。