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

正如您所見,腳本編寫器只是一個 proc 命令清單(它們可以是任何回應 script_output 的內容,但我目前還不需要其他任何內容)。我可以建立腳本編寫器實例,重複呼叫 proc 來定義我的 ploticus proc,然後在我完成時呼叫腳本以取得要傳輸到 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

這是一個非常簡單的範例,但它很好地說明了我所稱的 Gateway 模式。PloticusClusterBar 類別是具有完美介面的閘道,用於執行我想做的事情。我會讓它在便利介面與實際輸出需求之間進行轉換。PloticusScripter 類別是另一層級的閘道。即使對於像這樣的簡單事物,我也發現像這樣的組合物件設計是一個不錯的方法。這可能只說明了我的大腦在這些年來是如何扭曲的。