在這個(gè)指南中,我們將要用 API 來獲取我們擁有的儲(chǔ)存庫(repository)的信息以及編寫它們所用的編程語言。然后,用 D3.js 庫把那些信息用幾種不同的方式進(jìn)行可視化。在此我們用一個(gè)極好的 Ruby 庫 Octokit 來和 GitHub API 交流。
如果您還沒準(zhǔn)備好,應(yīng)該先去閱讀“認(rèn)證基礎(chǔ)”指南再嘗試這個(gè)示例。 您能在 platform-samples 存儲(chǔ)庫中找到這個(gè)示例項(xiàng)目的完整源代碼。
讓我們開始吧!
首先, 在 GitHub 上注冊(cè)一個(gè)新應(yīng)用程序。 設(shè)置主 URL 和回調(diào) URL 為 http://localhost:4567/
。 和之前一樣,我們通過 sinatra-auth-github 來應(yīng)用 Rack 中間件來處理 API 的認(rèn)證:
require 'sinatra/auth/github'
module Example
class MyGraphApp < Sinatra::Base
# !!! 在真正的應(yīng)用內(nèi)永遠(yuǎn)不要用硬編碼把值寫死 !!!
# 而是設(shè)置環(huán)境變量并測(cè)試,和下例所示
# if ENV['GITHUB_CLIENT_ID'] && ENV['GITHUB_CLIENT_SECRET']
# CLIENT_ID = ENV['GITHUB_CLIENT_ID']
# CLIENT_SECRET = ENV['GITHUB_CLIENT_SECRET']
# end
CLIENT_ID = ENV['GH_GRAPH_CLIENT_ID']
CLIENT_SECRET = ENV['GH_GRAPH_SECRET_ID']
enable :sessions
set :github_options, {
:scopes => "repo",
:secret => CLIENT_SECRET,
:client_id => CLIENT_ID,
:callback_url => "/"
}
register Sinatra::Auth::Github
get '/' do
if !authenticated?
authenticate!
else
access_token = github_user["token"]
end
end
end
end
和之前的例子一樣,設(shè)置一個(gè)類似的 config.ru 文件:
language-ruby
ENV['RACK_ENV'] ||= 'development'
require "rubygems"
require "bundler/setup"
require File.expand_path(File.join(File.dirname(__FILE__), 'server'))
run Example::MyGraphApp
這次,為了能和 GitHub API 交流,我們將用 Octokit Ruby 庫。這要比直接進(jìn)行一大堆 REST 調(diào)用簡單許多,更別說 Octokit 就是由 GitHub 用戶開發(fā)和活躍維護(hù)的,所以這庫將是有效的。
通過 API 和 Octokit 庫來進(jìn)行認(rèn)證是很簡單的,只需要將您的登陸信息和令牌(token)作為參數(shù)穿給 Octokit::Client
構(gòu)造器即可:
if !authenticated?
authenticate!
else
octokit_client = Octokit::Client.new(:login => github_user.login, :oauth_token => github_user.token)
end
我們來對(duì)我們的存儲(chǔ)庫信息做點(diǎn)有趣的事情吧,例如查看他們使用的各種編程語言,并且數(shù)出哪一種是最常用的。要達(dá)成這個(gè)目的,首先需要通過 API 獲取一個(gè)存儲(chǔ)庫列表,用 Octokit 的話語句會(huì)是下面這樣子:
repos = client.repositories
接下來,我們會(huì)迭代每一個(gè)存儲(chǔ)庫,然后對(duì) GitHub 所關(guān)聯(lián)的編程語言進(jìn)行計(jì)數(shù):
language_obj = {}
repos.each do |repo|
# 某些情況下,language 可能為0
if repo.language
if !language_obj[repo.language]
language_obj[repo.language] = 1
else
language_obj[repo.language] += 1
end
end
end
languages.to_s
當(dāng)您重啟您的服務(wù)器時(shí),網(wǎng)頁應(yīng)該會(huì)顯示類似下面這樣的內(nèi)容:
{"JavaScript"=>13, "PHP"=>1, "Perl"=>1, "CoffeeScript"=>2, "Python"=>1, "Java"=>3, "Ruby"=>3, "Go"=>1, "C++"=>1}
到目前為止都十分順利,不過這樣的表現(xiàn)方法對(duì)人類不是十分友好。一個(gè)可視化圖可以很好地幫助我們了解這些語言計(jì)數(shù)的分布情況。我們把計(jì)數(shù)值傳給 D3 來獲得一個(gè)整潔的條形圖,顯示所使用的編程語言的流行程度。
D3.js,或者直接叫 D3,是一個(gè)功能全面的庫,專門用于創(chuàng)建許多不同種類的圖表和互動(dòng)可視化圖。D3 的詳細(xì)使用方法已經(jīng)超出本文范疇,如果您想要一篇不錯(cuò)的入門文章,不妨去看看“凡人用 D3”。
D3 是一個(gè) JavaScript 庫,并且喜歡把數(shù)據(jù)以數(shù)組形式處理。所以,我們先把 Ruby Hash 轉(zhuǎn)換成一個(gè) JSON 數(shù)組,以便在瀏覽器內(nèi)被 JavaScript 使用。
languages = []
language_obj.each do |lang, count|
languages.push :language => lang, :count => count
end
erb :lang_freq, :locals => { :languages => languages.to_json}
我們簡單地迭代對(duì)象中的每個(gè)鍵-值對(duì)并將他們寫入一個(gè)新的數(shù)組。之所以在早些時(shí)候沒做這步,是為了避免在建立 language_obj
對(duì)象的過程中對(duì)其進(jìn)行迭代。
這時(shí),lang_freq.erb 會(huì)需要一些 JavaScript 語句來渲染條形圖,在本例中您可以直接使用下方提供的代碼,如果您想了解 D3 如何工作,還能同時(shí)參照上方的資源鏈接:
<!DOCTYPE html>
<meta charset="utf-8">
<html>
<head>
<script src="http://cdnjs.cloudflare.com/ajax/libs/d3/3.0.1/d3.v3.min.js"></script>
<style>
svg {
padding: 20px;
}
rect {
fill: #2d578b
}
text {
fill: white;
}
text.yAxis {
font-size: 12px;
font-family: Helvetica, sans-serif;
fill: black;
}
</style>
</head>
<body>
<p>Check this sweet data out:</p>
<div id="lang_freq"></div>
</body>
<script>
var data = <%= languages %>;
var barWidth = 40;
var width = (barWidth + 10) * data.length;
var height = 300;
var x = d3.scale.linear().domain([0, data.length]).range([0, width]);
var y = d3.scale.linear().domain([0, d3.max(data, function(datum) { return datum.count; })]).
rangeRound([0, height]);
// 為 DOM 添加 canvas
var languageBars = d3.select("#lang_freq").
append("svg:svg").
attr("width", width).
attr("height", height);
languageBars.selectAll("rect").
data(data).
enter().
append("svg:rect").
attr("x", function(datum, index) { return x(index); }).
attr("y", function(datum) { return height - y(datum.count); }).
attr("height", function(datum) { return y(datum.count); }).
attr("width", barWidth);
languageBars.selectAll("text").
data(data).
enter().
append("svg:text").
attr("x", function(datum, index) { return x(index) + barWidth; }).
attr("y", function(datum) { return height - y(datum.count); }).
attr("dx", -barWidth/2).
attr("dy", "1.2em").
attr("text-anchor", "middle").
text(function(datum) { return datum.count;});
languageBars.selectAll("text.yAxis").
data(data).
enter().append("svg:text").
attr("x", function(datum, index) { return x(index) + barWidth; }).
attr("y", height).
attr("dx", -barWidth/2).
attr("text-anchor", "middle").
text(function(datum) { return datum.language;}).
attr("transform", "translate(0, 18)").
attr("class", "yAxis");
</script>
</html>
呼!再次地,您不用太擔(dān)心這堆代碼在干什么。重點(diǎn)是位于相對(duì)上方的 var data = <%= languages %>;
語句,這語句將我們先前創(chuàng)建的 languages
數(shù)組傳入 ERB 讓其進(jìn)行處理。
正如 《凡人用 D3》 指南所說的,這或許不是 D3 的最佳用例,但至少展示了如何結(jié)合 Octokit 來使用這個(gè)庫, 如何來做一些真正炫目的東西。
坦白的時(shí)候到了:存儲(chǔ)庫內(nèi)的 language
屬性只能識(shí)別“主要”的編程語言,這意味著如果您有一個(gè)存儲(chǔ)庫使用了幾種不同的編程語言,那么只有占用字節(jié)最多的代碼所使用的編程語言算數(shù)。
我們來結(jié)合幾種 API 調(diào)用來獲得一個(gè)能顯示哪種編程語言擁有最多字節(jié)的代碼的真實(shí)表示。treemap 是可以很好地可視化編程語言占用比例的方法,而不是像上面那樣簡單的計(jì)數(shù)。為此我們需要構(gòu)造類似這樣子的一個(gè)對(duì)象數(shù)組:
[ { "name": "language1", "size": 100},
{ "name": "language2", "size": 23}
...
]
因?yàn)槲覀冎耙呀?jīng)有了一個(gè)存儲(chǔ)庫列表,所以直接檢視每一個(gè)存儲(chǔ)庫,并調(diào)用列出編程語言的 API 方法:
repos.each do |repo|
repo_name = repo.name
repo_langs = octokit_client.languages("#{github_user.login}/#{repo_name}")
end
接著, 在“主表”中累加每個(gè)找到的編程語言:
repo_langs.each do |lang, count|
if !language_obj[lang]
language_obj[lang] = count
else
language_obj[lang] += count
end
end
然后, 我們將內(nèi)容的格式轉(zhuǎn)換成 D3 能理解的結(jié)構(gòu):
language_obj.each do |lang, count|
language_byte_count.push :name => "#{lang} (#{count})", :count => count
end
# 一些必須的格式化操作
language_bytes = [ :name => "language_bytes", :elements => language_byte_count]
(若想獲取更多關(guān)于 D3 tree map 原理的信息,參見這個(gè)簡單的教程)
最后, 將這個(gè) JSON 信息傳給一樣的 ERB 模板:
erb :lang_freq, :locals => { :languages => languages.to_json, :language_byte_count => language_bytes.to_json}
和之前一樣, 這是一段 JavaScript 代碼,您可以將其直接復(fù)制到您的模板中:
<div id="byte_freq"></div>
<script>
var language_bytes = <%= language_byte_count %>
var childrenFunction = function(d){return d.elements};
var sizeFunction = function(d){return d.count;};
var colorFunction = function(d){return Math.floor(Math.random()*20)};
var nameFunction = function(d){return d.name;};
var color = d3.scale.linear()
.domain([0,10,15,20])
.range(["grey","green","yellow","red"]);
drawTreemap(5000, 2000, '#byte_freq', language_bytes, childrenFunction, nameFunction, sizeFunction, colorFunction, color);
function drawTreemap(height,width,elementSelector,language_bytes,childrenFunction,nameFunction,sizeFunction,colorFunction,colorScale){
var treemap = d3.layout.treemap()
.children(childrenFunction)
.size([width,height])
.value(sizeFunction);
var div = d3.select(elementSelector)
.append("div")
.style("position","relative")
.style("width",width + "px")
.style("height",height + "px");
div.data(language_bytes).selectAll("div")
.data(function(d){return treemap.nodes(d);})
.enter()
.append("div")
.attr("class","cell")
.style("background",function(d){ return colorScale(colorFunction(d));})
.call(cell)
.text(nameFunction);
}
function cell(){
this
.style("left",function(d){return d.x + "px";})
.style("top",function(d){return d.y + "px";})
.style("width",function(d){return d.dx - 1 + "px";})
.style("height",function(d){return d.dy - 1 + "px";});
}
</script>
當(dāng)當(dāng)當(dāng)當(dāng)!一個(gè)美麗的矩形包含著您的儲(chǔ)存庫內(nèi)各種編程語言,面積均以相對(duì)比例顯示,一看就懂。為了能適當(dāng)?shù)仫@示所有的信息,您可能還需要對(duì)您的 treemap 的高度和寬度進(jìn)行一些調(diào)整,這兩個(gè)參數(shù)就是上面的 drawTreemap
的參數(shù)中的前兩個(gè)。