Rubyでグラフを書く

New Relic(http://www.newrelic.com/)のツールとか見て、そうかブラウザでパフォーマンスモニタリングすればいいんだと思い、とりあえずグラフをブラウザで出す実験をしてみました。最終的には、色々なプロファイル情報をターゲットプログラムの実行時に得られるようにしたいです。

ポイントは次の2点です。

  1. WEBrickを使ってWebサーバを内蔵する
  2. Canvasを使ってグラフを書いています。なので、IEでは見えないです。Firefox 2(まだVer Upしてないんです)でテストしています。

最後に示すプログラムを実行して、ブラウザでttp://localhost:8088/graphをアクセスするとこんな感じのグラフを表示されます(リンクになっちゃうのでhを省いてあります)。 怖いのでlocalhostでしか見えないようにしてあります。

ソースです。
ちなみに、すごく分かりにくいですが、

 @graph_gen.graph_script([[[10, 100], [20, 21], [30, 33], [40, 112], [50, 132], [60, 78], [70, 99]]])

となっている、ところの配列がグラフの元データです。
今後、もうちょっとまともなインタフェースにして独立したライブラリにまとめる予定です。

require 'webrick'
require 'erb'

include WEBrick

class GraphGen
  def initialize(xsize, ysize)
    @xsize = xsize
    @ysize = ysize
  end

  def set_data_range(minx, maxx, miny, maxy)
    @minx = minx.to_f
    @maxx = maxx.to_f
    @miny = miny.to_f
    @maxy = maxy.to_f
  end

  def position_in_graph(x, y)
    dx = @maxx - @minx
    posx = ((x - @minx) / dx) * @xsize

    dy = @maxy - @miny
    posy = ((y - @miny) / dy) * @ysize

    [posx, posy]
  end
end

class GraphGenCanvas<GraphGen
  BODRER_SCRIPT_TEMPLATE = <<BEOS
    function draw_border() {
      var canctx = document.getElementById('border').getContext('2d')
      canctx.rect(0, 0, <%= @xsize%>, <%= @ysize%>);
      canctx.stroke();

      canctx.strokeStyle = 'rgb(0, 128, 128)';
      canctx.lineWidth = 0.5;

      var cnt = 0;
      while (cnt < <%= @ysize%>){
        cnt += 40;
        canctx.beginPath();
        canctx.moveTo(0,  cnt);
        canctx.lineTo(<%= @xsize%>,  cnt);
        canctx.stroke();
      }

      cnt = 0;
      while (cnt < <%= @xsize%>){
        cnt += 40;
        canctx.beginPath();
        canctx.moveTo(cnt, 0);
        canctx.lineTo(cnt, <%= @ysize%>);
        canctx.stroke();
      }
    }
BEOS

  GRAPH_SCRIPT_BEGIN = <<GBEOS
    function draw_graph() {
      var canctx = document.getElementById('graph').getContext('2d')
      canctx.strokeStyle = 'rgb(0, 0, 0)';
      canctx.lineWidth = 0.5;
GBEOS

  GRAPH_SCRIPT_END = <<GEEOS
      canctx.stroke();
    }
GEEOS

  def initialize(xsize, ysize)
    super
    @border_erb = ERB.new(BODRER_SCRIPT_TEMPLATE)
  end

  def border_script
    @border_erb.result(binding)
  end

  def graph_script(data)
    res = ""
    data.each do |gd|
      sd = gd.sort_by {|a| a[0]}
      sdx = sd.map {|a| a[0]}
      sdy = sd.map {|a| a[1]}
      ymin = sdy.min
      if ymin > 0 then
        ymin = 0
      end
      set_data_range(sdx.min, sdx.max, ymin, sdy.max)
      agraph = nil
      sd.each do |a|
        x, y = position_in_graph(a[0], a[1])
        y = @ysize - y
        if agraph then
          agraph += "canctx.lineTo(#{x}, #{y});\n"
        else
          agraph = "canctx.moveTo(#{x}, #{y});\n"
        end
      end

      res += "canctx.beginPath();\n"
      res += agraph
    end

    GRAPH_SCRIPT_BEGIN + res + GRAPH_SCRIPT_END
  end
end

s = HTTPServer.new(:Port => 8088, :BindAddress => "localhost")
trap("INT"){s.shutdown}

class GraphServlet<HTTPServlet::AbstractServlet
  SCRIPT = <<EOS
<html>
  <style type="text/css">
    canvas {position: absolute}
    canvas#border {left: 10px; top: 10px}
    canvas#graph {left: 10px; top: 10px}
  </style>
    
  <script type="text/javascript">
   <%= @graph_gen.border_script %>
   <%= @graph_gen.graph_script([[[10, 100], [20, 21], [30, 33], [40, 112], [50, 132], [60, 78], [70, 99]]]) %>

    window.onload = function() {
      draw_border();
      draw_graph();
    };

  </script>
  <body>
    <canvas id="border" width="600px" height="400px"></canvas>
    <canvas id="graph" width="600px" height="400px"></canvas>
  </body>
</html>
EOS

  def do_GET(req, res)
    if !defined? @graph_gen then
      @graph_gen = GraphGenCanvas.new(600, 400)
    end

    res.body = ERB.new(SCRIPT).result(binding)
    res['Content-Type'] = "text/html"
  end
end

s.mount("/graph", GraphServlet)

s.start