Co-authored-by: Kevin Heis <heiskr@users.noreply.github.com> Co-authored-by: Felicity Chapman <felicitymay@github.com>
368 lines
12 KiB
Markdown
368 lines
12 KiB
Markdown
---
|
|
title: Rendering data as graphs
|
|
intro: Learn how to visualize the programming languages from your repository using the D3.js library and Ruby Octokit.
|
|
redirect_from:
|
|
- /guides/rendering-data-as-graphs
|
|
- /v3/guides/rendering-data-as-graphs
|
|
versions:
|
|
fpt: '*'
|
|
ghes: '*'
|
|
ghec: '*'
|
|
topics:
|
|
- API
|
|
---
|
|
|
|
|
|
|
|
In this guide, we're going to use the API to fetch information about repositories
|
|
that we own, and the programming languages that make them up. Then, we'll
|
|
visualize that information in a couple of different ways using the [D3.js](https://d3js.org/) library. To
|
|
interact with the {% data variables.product.github %} API, we'll be using the excellent Ruby library, [Octokit](https://github.com/octokit/octokit.rb).
|
|
|
|
If you haven't already, you should read the [Basics of Authentication](/apps/oauth-apps/building-oauth-apps/authenticating-to-the-rest-api-with-an-oauth-app)
|
|
guide before starting this example. You can find the complete source code for this project in the [platform-samples](https://github.com/github/platform-samples/tree/master/api/ruby/rendering-data-as-graphs) repository.
|
|
|
|
Let's jump right in!
|
|
|
|
## Setting up an {% data variables.product.prodname_oauth_app %}
|
|
|
|
First, [register a new application](https://github.com/settings/applications/new) on {% data variables.product.github %}. Set the main and callback
|
|
URLs to `http://localhost:4567/`. As [before](/apps/oauth-apps/building-oauth-apps/authenticating-to-the-rest-api-with-an-oauth-app), we're going to handle authentication for the API by
|
|
implementing a Rack middleware using [sinatra-auth-github](https://github.com/atmos/sinatra_auth_github):
|
|
|
|
``` ruby
|
|
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
|
|
```
|
|
|
|
Set up a similar _config.ru_ file as in the previous example:
|
|
|
|
``` ruby
|
|
ENV['RACK_ENV'] ||= 'development'
|
|
require "rubygems"
|
|
require "bundler/setup"
|
|
|
|
require File.expand_path(File.join(File.dirname(__FILE__), 'server'))
|
|
|
|
run Example::MyGraphApp
|
|
```
|
|
|
|
## Fetching repository information
|
|
|
|
This time, in order to talk to the {% data variables.product.github %} API, we're going to use the [Octokit
|
|
Ruby library](https://github.com/octokit/octokit.rb). This is much easier than directly making a bunch of
|
|
REST calls. Plus, Octokit was developed by a GitHubber, and is actively maintained,
|
|
so you know it'll work.
|
|
|
|
Authentication with the API via Octokit is easy. Just pass your login
|
|
and token to the `Octokit::Client` constructor:
|
|
|
|
``` ruby
|
|
if !authenticated?
|
|
authenticate!
|
|
else
|
|
octokit_client = Octokit::Client.new(:login => github_user.login, :oauth_token => github_user.token)
|
|
end
|
|
```
|
|
|
|
Let's do something interesting with the data about our repositories. We're going
|
|
to see the different programming languages they use, and count which ones are used
|
|
most often. To do that, we'll first need a list of our repositories from the API.
|
|
With Octokit, that looks like this:
|
|
|
|
``` ruby
|
|
repos = client.repositories
|
|
```
|
|
|
|
Next, we'll iterate over each repository, and count the language that {% data variables.product.github %}
|
|
associates with it:
|
|
|
|
``` ruby
|
|
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
|
|
```
|
|
|
|
When you restart your server, your web page should display something
|
|
that looks like this:
|
|
|
|
``` ruby
|
|
{"JavaScript"=>13, "PHP"=>1, "Perl"=>1, "CoffeeScript"=>2, "Python"=>1, "Java"=>3, "Ruby"=>3, "Go"=>1, "C++"=>1}
|
|
```
|
|
|
|
So far, so good, but not very human-friendly. A visualization
|
|
would be great in helping us understand how these language counts are distributed. Let's feed
|
|
our counts into D3 to get a neat bar graph representing the popularity of the languages we use.
|
|
|
|
## Visualizing language counts
|
|
|
|
D3.js, or just D3, is a comprehensive library for creating many kinds of charts, graphs, and interactive visualizations.
|
|
Using D3 in detail is beyond the scope of this guide, but for a good introductory article,
|
|
check out [D3 for Mortals](http://recursion.org/d3-for-mere-mortals/).
|
|
|
|
D3 is a JavaScript library, and likes working with data as arrays. So, let's convert our Ruby hash into
|
|
a JSON array for use by JavaScript in the browser.
|
|
|
|
``` ruby
|
|
languages = []
|
|
language_obj.each do |lang, count|
|
|
languages.push :language => lang, :count => count
|
|
end
|
|
|
|
erb :lang_freq, :locals => { :languages => languages.to_json}
|
|
```
|
|
|
|
We're simply iterating over each key-value pair in our object and pushing them into
|
|
a new array. The reason we didn't do this earlier is because we didn't want to iterate
|
|
over our `language_obj` object while we were creating it.
|
|
|
|
Now, _lang_freq.erb_ is going to need some JavaScript to support rendering a bar graph.
|
|
For now, you can just use the code provided here, and refer to the resources linked above
|
|
if you want to learn more about how D3 works:
|
|
|
|
``` html
|
|
<!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>
|
|
```
|
|
|
|
Phew! Again, don't worry about what most of this code is doing. The relevant part
|
|
here is a line way at the top--`var data = <%= languages %>;`--which indicates
|
|
that we're passing our previously created `languages` array into ERB for manipulation.
|
|
|
|
As the "D3 for Mortals" guide suggests, this isn't necessarily the best use of
|
|
D3. But it does serve to illustrate how you can use the library, along with Octokit,
|
|
to make some really amazing things.
|
|
|
|
## Combining different API calls
|
|
|
|
Now it's time for a confession: the `language` attribute within repositories
|
|
only identifies the "primary" language defined. That means that if you have
|
|
a repository that combines several languages, the one with the most bytes of code
|
|
is considered to be the primary language.
|
|
|
|
Let's combine a few API calls to get a _true_ representation of which language
|
|
has the greatest number of bytes written across all our code. A [treemap](https://www.d3-graph-gallery.com/treemap.html)
|
|
should be a great way to visualize the sizes of our coding languages used, rather
|
|
than simply the count. We'll need to construct an array of objects that looks
|
|
something like this:
|
|
|
|
``` json
|
|
[ { "name": "language1", "size": 100},
|
|
{ "name": "language2", "size": 23}
|
|
...
|
|
]
|
|
```
|
|
|
|
Since we already have a list of repositories above, let's inspect each one, and
|
|
call the [GET /repos/{owner}/{repo}/languages endpoint](/rest/repos/repos#list-repository-languages):
|
|
|
|
``` ruby
|
|
repos.each do |repo|
|
|
repo_name = repo.name
|
|
repo_langs = octokit_client.languages("#{github_user.login}/#{repo_name}")
|
|
end
|
|
```
|
|
|
|
From there, we'll cumulatively add each language found to a list of languages:
|
|
|
|
``` ruby
|
|
repo_langs.each do |lang, count|
|
|
if !language_obj[lang]
|
|
language_obj[lang] = count
|
|
else
|
|
language_obj[lang] += count
|
|
end
|
|
end
|
|
```
|
|
|
|
After that, we'll format the contents into a structure that D3 understands:
|
|
|
|
``` ruby
|
|
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]
|
|
```
|
|
|
|
(For more information on D3 tree map magic, check out [this simple tutorial](/rest/repos/repos#list-repository-languages).)
|
|
|
|
To wrap up, we pass this JSON information over to the same ERB template:
|
|
|
|
``` ruby
|
|
erb :lang_freq, :locals => { :languages => languages.to_json, :language_byte_count => language_bytes.to_json}
|
|
```
|
|
|
|
Like before, here's a bunch of JavaScript that you can drop
|
|
directly into your template:
|
|
|
|
``` html
|
|
<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>
|
|
```
|
|
|
|
Et voila! Beautiful rectangles containing your repo languages, with relative
|
|
proportions that are easy to see at a glance. You might need to
|
|
tweak the height and width of your treemap, passed as the first two
|
|
arguments to `drawTreemap` above, to get all the information to show up properly.
|