在本指南中,我们将使用 API 获取有关我们拥有的仓库以及构成它们的编程语言的信息。然后,我们将使用 D3.js 库以几种不同的方式将这些信息可视化。为了与 GitHub API 交互,我们将使用出色的 Ruby 库 Octokit。
如果您尚未阅读“身份验证基础”指南,请在开始此示例之前阅读。您可以在 platform-samples 仓库中找到此项目的完整源代码。
让我们立即开始吧!
设置 OAuth 应用
首先,在 GitHub 上 注册一个新的应用程序。将主 URL 和回调 URL 设置为 https://127.0.0.1:4567/
。如 之前 所述,我们将通过使用 sinatra-auth-github 实现 Rack 中间件来处理 API 的身份验证。
require 'sinatra/auth/github'
module Example
class MyGraphApp < Sinatra::Base
# !!! DO NOT EVER USE HARD-CODED VALUES IN A REAL APP !!!
# Instead, set and test environment variables, like below
# 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
设置与上一个示例中类似的 config.ru 文件。
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 库。这比直接进行大量 REST 调用要容易得多。此外,Octokit 由 GitHub 开发人员开发,并得到积极维护,因此您可以确信它会正常工作。
通过 Octokit 对 API 进行身份验证非常简单。只需将您的登录名和令牌传递给 Octokit::Client
构造函数即可。
if !authenticated?
authenticate!
else
octokit_client = Octokit::Client.new(:login => github_user.login, :oauth_token => github_user.token)
end
让我们对有关我们仓库的数据做一些有趣的事情。我们将查看它们使用的不同编程语言,并计算使用次数最多的语言。为此,我们首先需要从 API 获取仓库列表。使用 Octokit,如下所示:
repos = client.repositories
接下来,我们将遍历每个仓库,并计算 GitHub 与其关联的语言。
language_obj = {}
repos.each do |repo|
# sometimes language can be nil
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
重新启动服务器后,您的网页应该会显示如下内容:
{"JavaScript"=>13, "PHP"=>1, "Perl"=>1, "CoffeeScript"=>2, "Python"=>1, "Java"=>3, "Ruby"=>3, "Go"=>1, "C++"=>1}
到目前为止,一切顺利,但不太人性化。可视化将非常有助于我们了解这些语言计数是如何分布的。让我们将我们的计数输入到 D3 中,以获得一个简洁的条形图,表示我们使用的语言的流行度。
可视化语言计数
D3.js 或简称 D3,是一个用于创建各种图表、图形和交互式可视化的综合库。详细使用 D3 超出了本指南的范围,但对于一篇好的入门文章,请查看“D3 for Mortals”。
D3 是一个 JavaScript 库,喜欢将数据作为数组处理。因此,让我们将我们的 Ruby 哈希转换为 JSON 数组,以便在浏览器中供 JavaScript 使用。
languages = []
language_obj.each do |lang, count|
languages.push :language => lang, :count => count
end
erb :lang_freq, :locals => { :languages => languages.to_json}
我们只是遍历对象中的每个键值对,并将它们推送到一个新数组中。我们之前没有这样做是因为我们不想在创建 language_obj
对象时遍历它。
现在,lang_freq.erb 将需要一些 JavaScript 来支持渲染条形图。目前,您可以使用此处提供的代码,如果您想了解更多关于 D3 如何工作的信息,请参考上面链接的资源。
<!DOCTYPE html>
<meta charset="utf-8">
<html>
<head>
<script src="//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]);
// add the canvas to the DOM
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>
呼!再次强调,不要担心大部分代码的作用。此处相关部分是顶部的代码行 - var data = <%= languages %>;
- 指示我们将之前创建的 languages
数组传递给 ERB 进行操作。
正如“D3 for Mortals”指南所建议的那样,这并非 D3 的最佳用法。但它确实说明了如何将该库与 Octokit 结合使用,以创建一些真正令人惊叹的东西。
组合不同的 API 调用
现在是时候坦白了:仓库中的 language
属性仅标识定义的“主要”语言。这意味着,如果您有一个结合了多种语言的仓库,则代码字节数最多的语言被视为主要语言。
让我们组合几个 API 调用,以获得我们所有代码中哪种语言的编写字节数最多的真实表示。 树状图 应该是一种很好的方法来可视化我们使用的编码语言的大小,而不仅仅是计数。我们需要构造一个对象数组,看起来像这样:
[ { "name": "language1", "size": 100},
{ "name": "language2", "size": 23}
...
]
由于我们上面已经有一个仓库列表,让我们检查每个仓库,并调用 GET /repos/{owner}/{repo}/languages 端点
repos.each do |repo|
repo_name = repo.name
repo_langs = octokit_client.languages("#{github_user.login}/#{repo_name}")
end
从那里,我们将累积地将找到的每种语言添加到语言列表中。
repo_langs.each do |lang, count|
if !language_obj[lang]
language_obj[lang] = count
else
language_obj[lang] += count
end
end
之后,我们将内容格式化为 D3 能够理解的结构。
language_obj.each do |lang, count|
language_byte_count.push :name => "#{lang} (#{count})", :count => count
end
# some mandatory formatting for D3
language_bytes = [ :name => "language_bytes", :elements => language_byte_count]
(有关 D3 树状图魔术的更多信息,请查看 此简单教程。)
最后,我们将此 JSON 信息传递给相同的 ERB 模板。
erb :lang_freq, :locals => { :languages => languages.to_json, :language_byte_count => language_bytes.to_json}
与之前一样,这里有一堆 JavaScript 代码,您可以直接将其放入您的模板中。
<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>
瞧!包含您仓库语言的精美矩形,比例易于一目了然。您可能需要调整树状图的高度和宽度(作为上面 drawTreemap
的前两个参数传递),以使所有信息都能够正确显示。