Skip to content

Instantly share code, notes, and snippets.

@mrinalvenky
Last active December 23, 2015 22:19
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save mrinalvenky/6702946 to your computer and use it in GitHub Desktop.
Save mrinalvenky/6702946 to your computer and use it in GitHub Desktop.
Dashing Bubble graph

Bubble Graph

Bubble Graph widget

Description

This widget depicts 4 dimensional data in form of a bubble graph. In this particular example contributor stats from a github repo are represented. The bubbles in the graph represent the contributors. The x-axis represents the number of commits, y-axis the commitsize(number of lines deleted or added) and the radius of the bubbles represent the age of the contributor (based on the first commit made).

The widget uses reusable d3 coffescript classes. It makes one call per 3mins to github api. Here is a live dashboard with other github widgets taken from foobugs dashboard

Setup

  1. Add widget files

The files bubblegraph.coffee, bubblegraph.html and bubblegraph.scss should be added to /widget/bubblegraph folder.

  1. Add Library files

The bubblegraph widget uses d3 version 3 instead of the existing version 2. So add the d3.v3.min.js to your assets/javascripts. It also uses loadsh.min.js so add it to assets/javascripts

  1. Add the bubblegraph div as a new list element to your dashboard file:
   <li data-row="1" data-col="1" data-sizex="2" data-sizey="2">
    <div data-id="bubblegithub" data-view="Bubblegraph" data-title="Dashing contributor stats" style="background-color:#96bf48"></div>
  </li>
#!/usr/bin/env ruby
require 'net/http'
require 'json'
# This job will plot the metrics of all collaborators of a repo
# as a bubble graph
# Config
# ------
github_reponame = ENV['GITHUB_REPONAME'] || 'shopify/dashing'
points_x = []
points_y = []
points_z = []
points_group = []
max_users = 11
SCHEDULER.every '3m', :first_in => 0 do |job|
http = Net::HTTP.new("api.github.com", Net::HTTP.https_default_port())
http.use_ssl = true
total_users = 0
oldest_week = 9999999999
total_commitsize = 0
total_commit = 0
total_age = 0
max_commitsize = 0
max_commit = 0
max_age = 0
# Get the list of contributors from the repo
response = http.request(Net::HTTP::Get.new("/repos/#{github_reponame}/stats/contributors"))
data = JSON.parse(response.body)
if response.code == "200"
data.each do |datum|
# get the values for each contributor
if datum['author']
aname = datum['author']['login']
else
aname = "Anonymous"
end
commits = datum['total']
total_commit += datum['total']
first_commit = false
commitsize = 0
age = 1
datum['weeks'].each do |wk|
if wk['w'] < oldest_week
oldest_week = wk['w']
end
if first_commit == false && (wk['a'] != 0 || wk['d'] != 0)
age = wk['w'] - oldest_week + 1
total_age += age
first_commit = true
end
commitsize += wk['a'] + wk['d']
total_commitsize += wk['a'] + wk['d']
end
# Insert into the points table
points_x.insert(total_users, commits)
if max_commit < commits
max_commit = commits
end
points_y.insert(total_users, commitsize)
if max_commitsize < commitsize
max_commitsize = commitsize
end
points_z.insert(total_users, age)
if max_age < age
max_age = age
end
points_group.insert(total_users, aname)
total_users += 1
end
else
puts "github api error (status-code: #{response.code})\n#{response.body}"
break
end
normal_points = []
#Github shows bigger values at the end so reverse
points_x.reverse!
points_y.reverse!
points_z.reverse!
points_group.reverse!
# Calculate normalized values
(0..(total_users - 1)).each do |i|
nx = (points_x[i] * 100) / max_commit
ny = (points_y[i] * 100) / max_commitsize
nz = (points_z[i] * 5) / max_age
normal_points << { x: nx, y: ny, z: nz, group: points_group[i]}
if i > max_users
break
end
end
send_event('bubblegithub', points: normal_points)
end
class AxesChart
# AxesChart is a chart that has the padding and a couple of axes on it.
constructor: ->
@_width = 800
@_height = 600
@_margin = {
top: 50
left: 50
bottom: 50
right: 50
}
@_colours = [
"#E41A1C"
"#377EB8"
"#4DAF4A"
"#984EA3"
"#FF7F00"
"#FFFF33"
"#A65628"
"#F781BF"
"#999999"
"#E41A1C"
"#377EB8"
"#4DAF4A"
"#984EA3"
"#FF7F00"
"#FFFF33"
"#A65628"
"#F781BF"
"#999999"
]
draw: =>
# setup the canvas
@setup()
# do any stuff that needs to be recalculated before drawing
@predraw()
# this bit does the drawing on each selection
@selection.each( (d, i) => @_draw(d, i))
# do any stuff that needs to be done after drawing
@postdraw()
setup: () ->
# setup the plot, make a plotarea with margins.
@selection = d3.select(@_el)
@selection.datum(@_data)
@_canvas = @selection.append("svg")
.attr("width", @_width + @_margin.left + @_margin.right)
.attr("height", @_height + @_margin.top + @_margin.bottom)
@_plotarea = @_canvas.append("g")
.attr("transform", "translate(#{@_margin.left}, #{@_margin.top})")
_draw: (d, i) ->
throw "Not implemented"
predraw: () ->
# calculate any stuff that needs to be done before drawing. re-implement if this is needed, if not then just leave
postdraw: () ->
# calculate any stuff that needs to be done after drawing. re-implement if this is needed, if not then just leave
# getters and setters
el: (value) ->
# the plot element
if not arguments.length
return @_el
@_el = value
@
data: (value) ->
# the plot data
if not arguments.length
return @_data
@_data = value
@
width: (value) ->
# plot width
if not arguments.length
return @_width
@_width = value
@
height: (value) ->
if not arguments.length
return @_height
@_height = value
@
margin: (value) ->
if not arguments.length
return @_margin
@_margin = value
@
xscale: (value) ->
if not arguments.length
return @_xscale
@_xscale = value
@
yscale: (value) ->
if not arguments.length
return @_yscale
@_yscale = value
@
# accessor functions that get access to the scales
x: (i)->
if not @_xscale?
return i
@_xscale(i)
y: (i)->
if not @_yscale?
return i
@_yscale(i)
colour: (i) ->
# colour scale function
# if the levels haven't been defined, calculate them
if not @levels?
@levels = _.uniq( _.pluck @data(), "group" )
console.log(@levels)
# return the colour lookup
@_colours[_.indexOf @levels, i]
xaxis: () ->
# create the x-axis svg element and position it correctly
if @_xscale?
if not @_xaxis?
@_xaxis = d3.svg.axis()
.scale(@_xscale)
.orient("bottom")
# calculate how much to move the x axis by
@_plotarea.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0, #{@_height})")
.call(@_xaxis)
yaxis: () ->
# create the y-axis svg element and position it correctly.
if @_yscale?
if not @_yaxis?
@_yaxis = d3.svg.axis()
.scale(@_yscale)
.orient('left')
@_plotarea.append("g")
.attr("class", "y axis")
.call(@_yaxis)
class BubblePlot extends AxesChart
constructor: (maxpointsize = 20) ->
super
@_maxpointsize = maxpointsize
predraw: () ->
extent = d3.extent(_.pluck @data(), "z")
@radiusscale = d3.scale.linear()
.domain(extent)
.range([5, @_maxpointsize])
postdraw: () ->
@xaxis()
@yaxis()
_draw: (d, i) ->
@_plotarea.selectAll("circle")
.data(d)
.enter()
.append("circle")
.attr("r", (d) => Math.pow(@radiusscale(d.z), 1/2)*Math.PI)
.attr("cx", (d) => @x(d.x))
.attr("cy", (d) => @y(d.y))
.attr("fill", (d) => @colour(d.group))
cleanup: () ->
# Cleanup the svg for new update
d3.select("svg")
.remove()
data1 = [
{x: 45, y:10, z:1, group: "one"}
{x: 20, y:99, z:1, group: "one"}
{x: 30, y:21, z:1, group: "one"}
{x: 21, y:67, z:2, group: "two"}
{x: 50, y:50, z:1, group: "three"}
{x: 34, y:34, z:3, group: "four"}
{x: 21, y:54, z:3, group: "three"}
{x: 65, y:23, z:3, group: "four"}
{x: 87, y:54, z:3, group: "three"}
{x: 45, y:98, z:3, group: "four"}
{x: 72, y:78, z:3, group: "three"}
{x: 23, y:12, z:3, group: "four"}
]
class Dashing.Bubblegraph extends Dashing.Widget
@accessor 'current', ->
return @get('displayedValue') if @get('displayedValue')
points = @get('points')
if points
points[points.length - 1].y
ready: ->
container = $(@node).parent()
@bubblewidth = (((Dashing.widget_base_dimensions[0] * container.data("sizex")) + Dashing.widget_margins[0] * 2 * (container.data("sizex") - 1)) * 3) / 4
@bubbleheight = ((Dashing.widget_base_dimensions[1] * container.data("sizey")) * 3) / 4
@bubble = new BubblePlot(15)
@bubble.width(@bubblewidth)
.height(@bubbleheight)
.el(@node)
.data(data1)
@bubble.xscale(d3.scale.linear()
.domain([0, 100])
.range([0, @bubble.width()]))
@bubble.yscale(d3.scale.linear()
.domain([0, 100])
.range([@bubble.height(), 0]))
@bubble.draw()
onData: (data) ->
if @bubble
@bubble.cleanup()
@bubble.width(@bubblewidth)
.height(@bubbleheight)
.el(@node)
.data(data.points)
@bubble.draw()
<h1 class="title" data-bind="title"></h1>
<p class="more-info" data-bind="moreinfo"></p>
// ----------------------------------------------------------------------------
// Sass declarations
// ----------------------------------------------------------------------------
$backgroundbubble-color: #96bf48;
$titlebubble-color: rgba(255, 255, 255, 0.7);
$moreinfobubble-color: rgba(255, 255, 255, 0.3);
$tickbubble-color: rgba(0, 0, 0, 0.6);
// ----------------------------------------------------------------------------
// Widget-bubblegraph styles
// ----------------------------------------------------------------------------
.widget-bubblegraph {
background-color: $backgroundbubble-color;
position: relative;
svg {
position: absolute;
opacity: 0.6;
fill-opacity: 0.6;
left: 0px;
top: 0px;
}
.title, .value {
position: relative;
z-index: 99;
}
.title {
color: $titlebubble-color;
}
.more-info {
color: $moreinfobubble-color;
font-weight: 600;
font-size: 20px;
margin-top: 0;
}
.x_tick {
position: absolute;
bottom: 0;
.title {
font-size: 20px;
color: $tickbubble-color;
opacity: 0.6;
padding-bottom: 3px;
}
}
.y_ticks {
font-size: 20px;
fill: $tickbubble-color;
fill-opacity: 1;
}
.domain {
display: none;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment