{"meta":{"title":"将数据渲染为图形","intro":"了解如何使用 D3.js 库和 Ruby Octokit 可视化仓库中的编程语言。","product":"REST API","breadcrumbs":[{"href":"/zh/rest","title":"REST API"},{"href":"/zh/rest/guides","title":"指南"},{"href":"/zh/rest/guides/rendering-data-as-graphs","title":"将数据渲染为图形"}],"documentType":"article"},"body":"# 将数据渲染为图形\n\n了解如何使用 D3.js 库和 Ruby Octokit 可视化仓库中的编程语言。\n\n在本指南中，我们将使用 API 来获取有关我们拥有的存储库以及构成这些存储库的编程语言的信息。 然后，我们将使用 [D3.js](https://d3js.org/) 库来以几种不同的方式可视化这些信息。 若要与 GitHub API 交互，我们将使用出色的 Ruby 库 [Octokit](https://github.com/octokit/octokit.rb)。\n\n在开始本示例之前，应阅读[身份验证基础知识](/zh/apps/oauth-apps/building-oauth-apps/authenticating-to-the-rest-api-with-an-oauth-app)指南（如果尚未阅读）。 可以在 [platform-samples](https://github.com/github/platform-samples/tree/master/api/ruby/rendering-data-as-graphs) 存储库中找到此项目的完整源代码。\n\n我们马上开始！\n\n## 设置 OAuth app\n\n首先，在 GitHub 上[注册新应用程序](https://github.com/settings/applications/new)。 将主 URL 和回叫 URL 设置为 `http://localhost:4567/`。 与 [以前一](/zh/apps/oauth-apps/building-oauth-apps/authenticating-to-the-rest-api-with-an-oauth-app)样，我们将使用 [sinatra-auth-github](https://rubygems.org/gems/sinatra_auth_github) 实现机架中间件来处理 API 的身份验证：\n\n```ruby\nrequire 'sinatra/auth/github'\n\nmodule Example\n  class MyGraphApp < Sinatra::Base\n    # !!! DO NOT EVER USE HARD-CODED VALUES IN A REAL APP !!!\n    # Instead, set and test environment variables, like below\n    # if ENV['GITHUB_CLIENT_ID'] && ENV['GITHUB_CLIENT_SECRET']\n    #  CLIENT_ID        = ENV['GITHUB_CLIENT_ID']\n    #  CLIENT_SECRET    = ENV['GITHUB_CLIENT_SECRET']\n    # end\n\n    CLIENT_ID = ENV['GH_GRAPH_CLIENT_ID']\n    CLIENT_SECRET = ENV['GH_GRAPH_SECRET_ID']\n\n    enable :sessions\n\n    set :github_options, {\n      :scopes    => \"repo\",\n      :secret    => CLIENT_SECRET,\n      :client_id => CLIENT_ID,\n      :callback_url => \"/\"\n    }\n\n    register Sinatra::Auth::Github\n\n    get '/' do\n      if !authenticated?\n        authenticate!\n      else\n        access_token = github_user[\"token\"]\n      end\n    end\n  end\nend\n```\n\n设置与上一个示例类似的 *config.ru* 文件：\n\n```ruby\nENV['RACK_ENV'] ||= 'development'\nrequire \"rubygems\"\nrequire \"bundler/setup\"\n\nrequire File.expand_path(File.join(File.dirname(__FILE__), 'server'))\n\nrun Example::MyGraphApp\n```\n\n## 获取仓库信息\n\n这次，为了与 GitHub API 通信，我们将使用 [Octokit Ruby 库](https://github.com/octokit/octokit.rb)。 这比直接进行一大堆 REST 调用要容易得多。 另外，Octokit 是由 GitHubber 开发的，并且一直在积极维护，因此可以确保有效性。\n\n通过 Octokit 进行 API 身份验证非常简单。 只需将登录凭证和令牌传递到 `Octokit::Client` 构造函数中：\n\n```ruby\nif !authenticated?\n  authenticate!\nelse\n  octokit_client = Octokit::Client.new(:login => github_user.login, :oauth_token => github_user.token)\nend\n```\n\n我们来用仓库的数据做一些有趣的事情。 我们将看到它们使用的不同编程语言，并计算出哪些是最常用的。 为此，我们首先需要从 API 获取仓库列表。\n使用 Octokit 时，其代码如下所述：\n\n```ruby\nrepos = client.repositories\n```\n\n接下来，我们将遍历每个存储库，并计算与 GitHub 关联的语言：\n\n```ruby\nlanguage_obj = {}\nrepos.each do |repo|\n  # sometimes language can be nil\n  if repo.language\n    if !language_obj[repo.language]\n      language_obj[repo.language] = 1\n    else\n      language_obj[repo.language] += 1\n    end\n  end\nend\n\nlanguages.to_s\n```\n\n重新启动服务器时，Web 页面应显示如下代码：\n\n```ruby\n{\"JavaScript\"=>13, \"PHP\"=>1, \"Perl\"=>1, \"CoffeeScript\"=>2, \"Python\"=>1, \"Java\"=>3, \"Ruby\"=>3, \"Go\"=>1, \"C++\"=>1}\n```\n\n到目前为止，进展不错，但不太便于人使用。 可视化将非常有助于我们理解这些语言计数是如何分布的。 现在将这些计数输入到 D3 中，得到一个表示语言使用频率的简洁条形图。\n\n## 可视化语言计数\n\nD3.js（或仅 D3）是用于创建多种图表、图形和交互式可视化内容的综合库。\nD3 的详细用法不属于本指南的范围，推荐阅读一篇不错的介绍性文章：“[面向大众的 D3](http://recursion.org/d3-for-mere-mortals/)”。\n\nD3 是一个JavaScript 库，喜欢以数组的形式处理数据。 因此，让我们将 Ruby 哈希转换为 JSON 数组，以供浏览器中的 JavaScript 使用。\n\n```ruby\nlanguages = []\nlanguage_obj.each do |lang, count|\n  languages.push :language => lang, :count => count\nend\n\nerb :lang_freq, :locals => { :languages => languages.to_json}\n```\n\n我们只需遍历对象中的每个键值对，然后将它们推送到新的数组内。 我们之前没有这样做是因为我们不想在创建 `language_obj` 对象的过程中遍历它。\n\n现在，lang\\_freq.erb 将需要一些 JavaScript 来支持渲染条形图。\n以后，只需使用此处提供的代码，如果想详细了解 D3 的工作原理，可以参考上面链接的资源：\n\n```html\n<!DOCTYPE html>\n<meta charset=\"utf-8\">\n<html>\n  <head>\n    <script src=\"//cdnjs.cloudflare.com/ajax/libs/d3/3.0.1/d3.v3.min.js\"></script>\n    <style>\n    svg {\n      padding: 20px;\n    }\n    rect {\n      fill: #2d578b\n    }\n    text {\n      fill: white;\n    }\n    text.yAxis {\n      font-size: 12px;\n      font-family: Helvetica, sans-serif;\n      fill: black;\n    }\n    </style>\n  </head>\n  <body>\n    <p>Check this sweet data out:</p>\n    <div id=\"lang_freq\"></div>\n\n  </body>\n  <script>\n    var data = <%= languages %>;\n\n    var barWidth = 40;\n    var width = (barWidth + 10) * data.length;\n    var height = 300;\n\n    var x = d3.scale.linear().domain([0, data.length]).range([0, width]);\n    var y = d3.scale.linear().domain([0, d3.max(data, function(datum) { return datum.count; })]).\n      rangeRound([0, height]);\n\n    // add the canvas to the DOM\n    var languageBars = d3.select(\"#lang_freq\").\n      append(\"svg:svg\").\n      attr(\"width\", width).\n      attr(\"height\", height);\n\n    languageBars.selectAll(\"rect\").\n      data(data).\n      enter().\n      append(\"svg:rect\").\n      attr(\"x\", function(datum, index) { return x(index); }).\n      attr(\"y\", function(datum) { return height - y(datum.count); }).\n      attr(\"height\", function(datum) { return y(datum.count); }).\n      attr(\"width\", barWidth);\n\n    languageBars.selectAll(\"text\").\n      data(data).\n      enter().\n      append(\"svg:text\").\n      attr(\"x\", function(datum, index) { return x(index) + barWidth; }).\n      attr(\"y\", function(datum) { return height - y(datum.count); }).\n      attr(\"dx\", -barWidth/2).\n      attr(\"dy\", \"1.2em\").\n      attr(\"text-anchor\", \"middle\").\n      text(function(datum) { return datum.count;});\n\n    languageBars.selectAll(\"text.yAxis\").\n      data(data).\n      enter().append(\"svg:text\").\n      attr(\"x\", function(datum, index) { return x(index) + barWidth; }).\n      attr(\"y\", height).\n      attr(\"dx\", -barWidth/2).\n      attr(\"text-anchor\", \"middle\").\n      text(function(datum) { return datum.language;}).\n      attr(\"transform\", \"translate(0, 18)\").\n      attr(\"class\", \"yAxis\");\n  </script>\n</html>\n```\n\n呼！ 同样，不必担心这段代码的主要功能。 这里值得注意的是顶行--`var data = <%= languages %>;`--它表示我们正在将先前创建的 `languages` 数组传递到 ERB 中进行处理。\n\n如“面向大众的 D3”所述，这未必是 D3 的最佳用法。 它的目的在于说明如何将该库与 Octokit 搭配使用，以实现一些非常棒的项目。\n\n## 结合不同的 API 调用\n\n现在是时候坦白了：存储库中的 `language` 属性仅标识定义的“主要”语言。 这意味着，如果存储库结合了多种语言，则代码字节数最多的语言将被视为主要语言。\n\n让我们结合使用几个 API 调用来\\_真正\\_呈现哪种语言在我们所有代码中写入的字节数最多。 相比简单的计数，[树状图](https://www.d3-graph-gallery.com/treemap.html)应是直观展示编码语言使用量的好方法。 我们需要构造一个如下所示的对象数组：\n\n```json\n[ { \"name\": \"language1\", \"size\": 100},\n  { \"name\": \"language2\", \"size\": 23}\n  ...\n]\n```\n\n我们在前面已经获取了存储库列表，现在来检查每个存储库，然后调用 [GET /repos/{owner}/{repo}/languages endpoint](/zh/rest/repos/repos#list-repository-languages)：\n\n```ruby\nrepos.each do |repo|\n  repo_name = repo.name\n  repo_langs = octokit_client.languages(\"#{github_user.login}/#{repo_name}\")\nend\n```\n\n我们将累积添加找到的每种语言到一个语言列表中。\n\n```ruby\nrepo_langs.each do |lang, count|\n  if !language_obj[lang]\n    language_obj[lang] = count\n  else\n    language_obj[lang] += count\n  end\nend\n```\n\n之后，我们将内容格式化为 D3 可以理解的结构：\n\n```ruby\nlanguage_obj.each do |lang, count|\n  language_byte_count.push :name => \"#{lang} (#{count})\", :count => count\nend\n\n# some mandatory formatting for D3\nlanguage_bytes = [ :name => \"language_bytes\", :elements => language_byte_count]\n```\n\n（想了解更多 D3 树状图的奇妙之处，请查看[这个入门教程](/zh/rest/repos/repos#list-repository-languages)。）\n\n最后，我们将这些 JSON 信息传递到同一个 ERB 模板：\n\n```ruby\nerb :lang_freq, :locals => { :languages => languages.to_json, :language_byte_count => language_bytes.to_json}\n```\n\n与以前一样，这里有一堆 JavaScript，可以直接将它们放到模板中：\n\n```html\n<div id=\"byte_freq\"></div>\n<script>\n  var language_bytes = <%= language_byte_count %>\n  var childrenFunction = function(d){return d.elements};\n  var sizeFunction = function(d){return d.count;};\n  var colorFunction = function(d){return Math.floor(Math.random()*20)};\n  var nameFunction = function(d){return d.name;};\n\n  var color = d3.scale.linear()\n              .domain([0,10,15,20])\n              .range([\"grey\",\"green\",\"yellow\",\"red\"]);\n\n  drawTreemap(5000, 2000, '#byte_freq', language_bytes, childrenFunction, nameFunction, sizeFunction, colorFunction, color);\n\n  function drawTreemap(height,width,elementSelector,language_bytes,childrenFunction,nameFunction,sizeFunction,colorFunction,colorScale){\n\n      var treemap = d3.layout.treemap()\n          .children(childrenFunction)\n          .size([width,height])\n          .value(sizeFunction);\n\n      var div = d3.select(elementSelector)\n          .append(\"div\")\n          .style(\"position\",\"relative\")\n          .style(\"width\",width + \"px\")\n          .style(\"height\",height + \"px\");\n\n      div.data(language_bytes).selectAll(\"div\")\n          .data(function(d){return treemap.nodes(d);})\n          .enter()\n          .append(\"div\")\n          .attr(\"class\",\"cell\")\n          .style(\"background\",function(d){ return colorScale(colorFunction(d));})\n          .call(cell)\n          .text(nameFunction);\n  }\n\n  function cell(){\n      this\n          .style(\"left\",function(d){return d.x + \"px\";})\n          .style(\"top\",function(d){return d.y + \"px\";})\n          .style(\"width\",function(d){return d.dx - 1 + \"px\";})\n          .style(\"height\",function(d){return d.dy - 1 + \"px\";});\n  }\n</script>\n```\n\n就这样！ 包含存储库语言信息的精美矩形，按相对比例显示，让你一目了然。 可能需要调整树状图的高度和宽度（作为前两个参数传递到上述 `drawTreemap`），使所有信息适当显示。"}