跳至主要内容

将数据呈现为图表

了解如何使用 D3.js 库和 Ruby Octokit 可视化存储库中的编程语言。

在本指南中,我们将使用 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 是由 GitHubber 开发的,并且积极维护,因此您知道它会起作用。

通过 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”。

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` 的前两个参数传递),以正确显示所有信息。