RubyのようなCoffeeScriptを使ってJavaのようなProcessingを書いてJavascriptで実行してWebでビジュアライジング・データを実現しようよ!
「ビジュアライジング・データ ―Processingによる情報視覚化手法」(Ben Fry著) という、情報視覚化の実践的テクニックを解説する素晴らしい本があります。
ビジュアライジング・データ ―Processingによる情報視覚化手法 by Ben Fry
この本では情報の視覚化にProcessingというJavaをベースにしたグラフィック専用言語を使っています。Processingはマルチプラットフォームの統合開発環境にその実行環境を備えていますが、エクスポート機能でJAVAアプレットを生成することで成果物をWeb上に公開することもできます。
しかしJAVAアプレットによる情報の視覚化に不満を持っている人がいました。できればプラグインを介さずに直接ブラウザのCanvas上で情報視覚化を実現したい。jQuery作者のJohn Resig氏はProcessingをJavaScriptにポートして、ブラウザ上でProcessingのコードを直接実行できるようにしました。
Processing.jsの登場です。
一方で、Rubyしか知らない素人プログラマがいました。彼もWeb上での情報の視覚化をしてみたかったので、サイ本を入手してJavaScriptをマスターしようと考えました。Processing.jsの登場によりJavaScriptが書ければProcessingによる情報の視覚化ができますからね。でもその異質感1に簡単に跳ね返されて、ビジュアライジング・データの夢は儚く消えました。そして彼はつぶやきました。
「世界が全部Rubyで記述できたらいいのに..」
でも、そんな日はなかなか来そうにありません。
ところがそんな彼に一条の光明が差しました。まるでRubyと見間違うようなJavaScript用のコンパイラが登場したらしいのです!
その名もCoffeeScript。
「プログラミング言語にジャワまでなら許せるけど、コーヒーはねーよ」と内心思いつつも、そのサイトを覗いていみると..
「パパ 僕にも読めるよ!」
そんなわけで…
CoffeeScriptとProcessing.jsを使ってビジュアライジング・データをしてみました :)
つまりRubyのようなCoffeeScriptを使って、JavaのようなProcessingのコードを書いて、Javascriptで実行して、WebのCanvas上でビジュアライジング・データを実現します。僕の言ってることわかりますか?
以下では、先の書籍にあった時系列グラフのサンプルをCoffeeScriptに移植し、Webブラウザで実行できるようにしたデモを紹介します。WebフレームワークはSinatra、開発環境はMac OSX Snow Leopardです。CoffeeScriptの書き方がたぶんイマイチなのはご容赦ください^ ^;
デモをHerokuにアップしたので、まずは見てやってください。
Visualizing-Data with Processing Demo
タブを切り替えるとグラフがぐにゅっとなるのが気持ちいいですよね。グラフは書籍にあったサンプルの他、2009年の国内人口推計2を使っています。
ディレクトリ構成
最終的なファイル構成は以下のようになります。
.
├── Gemfile
├── Gemfile.lock
├── app.rb
├── config.ru
├── public
│ ├── census.ssv
│ ├── js
│ │ ├── graph.coffee
│ │ └── processing-1.2.3.min.js
│ └── milk-tea-coffee.tsv
└── views
├── index.haml
├── layout.haml
└── style.scss
設計方針
以下のような方針でビジュアライジング・データを実現します。
- データを格納したcensus.ssv3とmilk-tea-coffee.tsv4をrubyで読みだして解析する。
- 解析したデータをJSON APIとして特定のURLで提供できるようにする。
- graph.coffeeにProcessingによるグラフ描画コードを記述する。
- graph.coffee側で解析データを非同期で取得しグラフに描画する。
app.rb
Webフレームワークのコントローラとなるapp.rbの要点だけを書きます。
get "/milk" do
haml :index
end
get "/milk.json" do
redirect '/milk' unless request.xhr?
content_type :json
parse_data('public/milk-tea-coffee.tsv', '\t', [5, 2.5, 10]).to_json
end
helpers do
def parse_data(path, sep, intervals)
q = {}
File.open(path) do |file|
q['label'] = retrieve_label(file.lines.first, sep)
q['data'] = retrieve_data(file.lines, sep)
all_data = q['data'].map { |d| d[0..-2] }.flatten
q['dataMin'], q['dataMax'] = all_data.min.floor, all_data.max.ceil
q['intervals'] = intervals
end
q
end
end
ここではグラフを描画するURLを’/milk’としています。JavaScriptから’/milk.json’がgetされると、データファイルをparse_dataメソッドで解析して結果をJSONで返します。parse_dataでは、label data dataMin dataMax intervalsに値をセットします。labelとdataの取得はretrieve_label retrieve_dataに委ねますが、ここではその説明は省略します。
Processingオブジェクト(graph.coffee)
graph.coffeeは長いので分けて要点だけ説明します。
$ ->
$.getJSON "/milk.json", (json) ->
label = json.label
data = json.data
dataMax = Math.ceil(json.dataMax/10.0)*10
dataMin = if json.dataMin > 0 then 0 else json.dataMin
[yInterval, yIntervalMinor, xInterval] = json.intervals
canvas = $("canvas#processing")[0]
processing = new Processing(canvas, graph)
まずHTMLの読み込み完了を待って、jQueryの$.getJSONを使って非同期で先のURLからJSON化されたデータを取得し、それぞれの値をグローバル変数にセットします。
そしてnew Processing(canvas graph)で、グラフ描画の実体であるgraphオブジェクトをcanvas要素に結びつけたProcessingオブジェクトを生成します。これによってgraphオブジェクト内のdrawメソッドが指定のフレームレートで繰り返し実行され、Canvas上にグラフが描かれることになります。
graphオブジェクト(graph.coffee)
次にgraphオブジェクトを示します。
graph = (p) ->
p.setup = ->
[rowCount, columnCount] = [data.length-1, label.length-1]
[dateMin, dateMax] = [data[0][columnCount], data[rowCount][columnCount]]
[xMin, yMin] = [dateMin[0], dateMax[0]]
p.size(can_w, can_h)
p.frameRate(20)
p.smooth()
setTabPositions(p)
for row in [0..rowCount]
interpolators[row] = new Integrator(0)
interpolators[row].set_target(data[row][0])
p.draw = ->
drawMainFrame(p)
for row in [0..rowCount]
interpolators[row].update()
drawDataArea(p, areaColor)
drawXLabels(p)
drawYLabels(p)
drawDataHighlight(p, [255, 63, 0])
drawTabs(p)
graphオブジェクトはsetupメソッドとdrawメソッドを持っています。setupメソッドはgraphオブジェクトの生成時に実行されるので、ここでグラフのサイズやフレームレートなどを設定します。そしてdrawメソッドは指定フレームレートで、そこに書かれた処理を繰り返し実行します。グラフの枠組みをdrawMainFrame, drawXLabels, drawYLabels, drawTabsで描画し、グラフの描画はdrawDataArea, drawDataHighlightで行っています。
drawDataArea(graph.coffee)
次にdrawDataAreaについて説明します。
graph = (p) ->
drawDataArea = (p, color)->
[r,g,b] = color
p.fill(r,g,b)
p.noStroke()
p.beginShape()
p.vertex(borderLeft, borderBottom)
for row in [0..rowCount]
year = data[row][columnCount][0]
val = interpolators[row].value
x = p.map(year, xMin, yMin, borderLeft, borderRight)
y = p.map(val, dataMin, dataMax, borderBottom, borderTop)
p.curveVertex(x, y)
if row is 0 or row is rowCount
p.curveVertex(x, y)
p.vertex(borderRight, borderBottom)
p.vertex(borderRight, borderBottom)
p.endShape(p.CLOSE)
curveVertex関数を使ってデータエリアを描画します。色やストロークを決定した後、beginShape関数で描画を開始し、各点をcurveVertexでプロットしてendShape関数で終了します。他の描画関数も同じようなことをしているので説明は省略します。
Integrator(graph.coffee)
ここで各点をプロットするときにデータ値を直接渡さずにinterpolatorを介します。interpolatorはBenFryによるIntegratorオブジェクトで、これを介すると描画点をターゲット値に向けて徐々に増分して描画できるようになります。Integratorの実装を以下に示します。
class Integrator
# Ben Fry's Integrator
constructor: (@value, @damping=0.5, @attraction=0.2) ->
@mass = 1
@targeting = false
@vel = 0
@force = 0.1
update: ->
if @targeting
@force += @attraction * (@target - @value)
accel = @force / @mass
@vel = (@vel + accel) * @damping
@value += @vel
@force = 0
set_target: (t) ->
@targeting = true
@target = t
layout.haml
!!! 5
%html
%head
%meta{:charset => 'utf-8'}
%title= APP_TITLE
%link{:rel => 'stylesheet', :href => '/style.css'}
%script{:src => "http://ajax.googleapis.com/ajax/libs/jquery/1.6.1/jquery.min.js"}
%script{:src => '/js/processing-1.2.3.min.js'}
%script{:src => "http://jashkenas.github.com/coffee-script/extras/coffee-script.js"}
%script{:type => 'text/coffeescript', :src => '/js/graph.coffee'}
%body
= yield
layout.hamlではscriptとしてjquery.js, processing.js, coffee-script.js, graph.coffeeを読み込みます。coffee-script.jsはclientサイドでcoffeeファイルをjavascriptに変換します。
config.ru
require 'bundler'
Bundler.require
$LOAD_PATH << File.expand_path(File.dirname(__FILE__))
require 'app'
mime_type :coffee, "text/coffeescript"
run Sinatra::Application
coffeeファイルを’text/coffeescript’ mime_typeで扱えるようconfig.ruでその指定をします。
説明が大雑把で詳細が掴めないと思いますが、ソースコードを添付しますので、そちらを参照頂ければ助かります^ ^;
参考サイト:
CoffeeScript + Processing.js == Crazy Delicious
関連記事:
blog comments powered by Disqus