summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
authorKarlo Soriano <dev+karlo@aelogica.com>2013-05-09 13:00:56 +0800
committerkarlo57 <karlo.karlo.karlo@gmail.com>2013-06-05 16:51:48 +0800
commit71d67e6557acb1ce3beeec2c2c6deb35015bd8bb (patch)
treec8e23f80ee62359d8db8574056ea69ac70eb4406 /app
parentb9d989dc056a2a2b9316ff9aa06b57c736426871 (diff)
downloadgitlab-ce-71d67e6557acb1ce3beeec2c2c6deb35015bd8bb.tar.gz
Contributors graphs feature for GitLab
Created tests and refactored some code along the way Added stat graph util spec, refactored code finsihed up tests and refactors finsihed up tests and refactors
Diffstat (limited to 'app')
-rw-r--r--app/assets/javascripts/application.js2
-rw-r--r--app/assets/javascripts/stat_graph.js.coffee6
-rw-r--r--app/assets/javascripts/stat_graph_contributors.js.coffee61
-rw-r--r--app/assets/javascripts/stat_graph_contributors_graph.js.coffee166
-rw-r--r--app/assets/javascripts/stat_graph_contributors_util.js.coffee91
-rw-r--r--app/assets/stylesheets/application.scss1
-rw-r--r--app/assets/stylesheets/sections/stat_graph.scss56
-rw-r--r--app/controllers/stat_graph_controller.rb14
-rw-r--r--app/views/layouts/nav/_project.html.haml2
-rw-r--r--app/views/stat_graph/show.html.haml29
10 files changed, 428 insertions, 0 deletions
diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js
index ab5fc1b860d..0767b82032d 100644
--- a/app/assets/javascripts/application.js
+++ b/app/assets/javascripts/application.js
@@ -27,3 +27,5 @@
//= require branch-graph
//= require ace-src-noconflict/ace
//= require_tree .
+//= require d3
+//= require underscore
diff --git a/app/assets/javascripts/stat_graph.js.coffee b/app/assets/javascripts/stat_graph.js.coffee
new file mode 100644
index 00000000000..b129619696f
--- /dev/null
+++ b/app/assets/javascripts/stat_graph.js.coffee
@@ -0,0 +1,6 @@
+class window.StatGraph
+ @log: {}
+ @get_log: ->
+ @log
+ @set_log: (data) ->
+ @log = data
diff --git a/app/assets/javascripts/stat_graph_contributors.js.coffee b/app/assets/javascripts/stat_graph_contributors.js.coffee
new file mode 100644
index 00000000000..12dfe4da841
--- /dev/null
+++ b/app/assets/javascripts/stat_graph_contributors.js.coffee
@@ -0,0 +1,61 @@
+class window.ContributorsStatGraph
+ init: (log) ->
+ @parsed_log = ContributorsStatGraphUtil.parse_log(log)
+ @set_current_field("commits")
+ total_commits = ContributorsStatGraphUtil.get_total_data(@parsed_log, @field)
+ author_commits = ContributorsStatGraphUtil.get_author_data(@parsed_log, @field)
+ @add_master_graph(total_commits)
+ @add_authors_graph(author_commits)
+ @change_date_header()
+ add_master_graph: (total_data) ->
+ @master_graph = new ContributorsMasterGraph(total_data)
+ @master_graph.draw()
+ add_authors_graph: (author_data) ->
+ @authors = []
+ _.each(author_data, (d) =>
+ author_header = @create_author_header(d)
+ $(".contributors-list").append(author_header)
+ @authors[d.author] = author_graph = new ContributorsAuthorGraph(d.dates)
+ author_graph.draw()
+ )
+ format_author_commit_info: (author) ->
+ author.commits + " commits " + author.additions + " ++ / " + author.deletions + " --"
+ create_author_header: (author) ->
+ list_item = $('<li/>', {
+ class: 'person'
+ style: 'display: block;'
+ })
+ author_name = $('<h4>' + author.author + '</h4>')
+ author_commit_info_span = $('<span/>', {
+ class: 'commits'
+ })
+ author_commit_info = @format_author_commit_info(author)
+ author_commit_info_span.text(author_commit_info)
+ list_item.append(author_name)
+ list_item.append(author_commit_info_span)
+ list_item
+ redraw_master: ->
+ total_data = ContributorsStatGraphUtil.get_total_data(@parsed_log, @field)
+ @master_graph.set_data(total_data)
+ @master_graph.redraw()
+ redraw_authors: ->
+ $("ol").html("")
+ x_domain = ContributorsGraph.prototype.x_domain
+ author_commits = ContributorsStatGraphUtil.get_author_data(@parsed_log, @field, x_domain)
+ _.each(author_commits, (d) =>
+ @redraw_author_commit_info(d)
+ $(@authors[d.author].list_item).appendTo("ol")
+ @authors[d.author].set_data(d.dates)
+ @authors[d.author].redraw()
+ )
+ set_current_field: (field) ->
+ @field = field
+ change_date_header: ->
+ x_domain = ContributorsGraph.prototype.x_domain
+ print_date_format = d3.time.format("%B %e %Y");
+ print = print_date_format(x_domain[0]) + " - " + print_date_format(x_domain[1]);
+ $("#date_header").text(print);
+ redraw_author_commit_info: (author) ->
+ author_list_item = $(@authors[author.author].list_item)
+ author_commit_info = @format_author_commit_info(author)
+ author_list_item.find("span").text(author_commit_info) \ No newline at end of file
diff --git a/app/assets/javascripts/stat_graph_contributors_graph.js.coffee b/app/assets/javascripts/stat_graph_contributors_graph.js.coffee
new file mode 100644
index 00000000000..e7a120fb572
--- /dev/null
+++ b/app/assets/javascripts/stat_graph_contributors_graph.js.coffee
@@ -0,0 +1,166 @@
+class window.ContributorsGraph
+ MARGIN:
+ top: 20
+ right: 20
+ bottom: 30
+ left: 50
+ x_domain: null
+ y_domain: null
+ dates: []
+ @set_x_domain: (data) =>
+ @prototype.x_domain = data
+ @set_y_domain: (data) =>
+ @prototype.y_domain = [0, d3.max(data, (d) ->
+ d.commits = d.commits ? d.additions ? d.deletions
+ )]
+ @init_x_domain: (data) =>
+ @prototype.x_domain = d3.extent(data, (d) ->
+ d.date
+ )
+ @init_y_domain: (data) =>
+ @prototype.y_domain = [0, d3.max(data, (d) ->
+ d.commits = d.commits ? d.additions ? d.deletions
+ )]
+ @init_domain: (data) =>
+ @init_x_domain(data)
+ @init_y_domain(data)
+ @set_dates: (data) =>
+ @prototype.dates = data
+ set_x_domain: ->
+ @x.domain(@x_domain)
+ set_y_domain: ->
+ @y.domain(@y_domain)
+ set_domain: ->
+ @set_x_domain()
+ @set_y_domain()
+ create_scale: (width, height) ->
+ @x = d3.time.scale().range([0, width]).clamp(true)
+ @y = d3.scale.linear().range([height, 0]).nice()
+ draw_x_axis: ->
+ @svg.append("g").attr("class", "x axis").attr("transform", "translate(0, #{@height})")
+ .call(@x_axis);
+ draw_y_axis: ->
+ @svg.append("g").attr("class", "y axis").call(@y_axis)
+ set_data: (data) ->
+ @data = data
+
+class window.ContributorsMasterGraph extends ContributorsGraph
+ constructor: (@data) ->
+ @width = 1100
+ @height = 125
+ @x = null
+ @y = null
+ @x_axis = null
+ @y_axis = null
+ @area = null
+ @svg = null
+ @brush = null
+ @x_max_domain = null
+ process_dates: (data) ->
+ dates = @get_dates(data)
+ @parse_dates(data)
+ ContributorsGraph.set_dates(dates)
+ get_dates: (data) ->
+ _.pluck(data, 'date')
+ parse_dates: (data) ->
+ parseDate = d3.time.format("%Y-%m-%d").parse
+ data.forEach((d) ->
+ d.date = parseDate(d.date)
+ )
+ create_scale: ->
+ super @width, @height
+ create_axes: ->
+ @x_axis = d3.svg.axis().scale(@x).orient("bottom")
+ @y_axis = d3.svg.axis().scale(@y).orient("left")
+ create_svg: ->
+ @svg = d3.select("#contributors-master").append("svg")
+ .attr("width", @width + @MARGIN.left + @MARGIN.right)
+ .attr("height", @height + @MARGIN.top + @MARGIN.bottom)
+ .attr("class", "tint-box")
+ .append("g")
+ .attr("transform", "translate(" + @MARGIN.left + "," + @MARGIN.top + ")")
+ create_area: (x, y) ->
+ @area = d3.svg.area().x((d) ->
+ x(d.date)
+ ).y0(@height).y1((d) ->
+ y(d.commits = d.commits ? d.additions ? d.deletions)
+ ).interpolate("basis")
+ create_brush: ->
+ @brush = d3.svg.brush().x(@x).on("brushend", @update_content);
+ draw_path: (data) ->
+ @svg.append("path").datum(data).attr("class", "area").attr("d", @area);
+ add_brush: ->
+ @svg.append("g").attr("class", "selection").call(@brush).selectAll("rect").attr("height", @height);
+ update_content: =>
+ ContributorsGraph.set_x_domain(if @brush.empty() then @x_max_domain else @brush.extent())
+ $("#brush_change").trigger('change')
+ draw: ->
+ @process_dates(@data)
+ @create_scale()
+ @create_axes()
+ ContributorsGraph.init_domain(@data)
+ @x_max_domain = @x_domain
+ @set_domain()
+ @create_area(@x, @y)
+ @create_svg()
+ @create_brush()
+ @draw_path(@data)
+ @draw_x_axis()
+ @draw_y_axis()
+ @add_brush()
+ redraw: ->
+ @process_dates(@data)
+ ContributorsGraph.set_y_domain(@data)
+ @set_y_domain()
+ @svg.select("path").datum(@data)
+ @svg.select("path").attr("d", @area)
+ @svg.select(".y.axis").call(@y_axis)
+
+class window.ContributorsAuthorGraph extends ContributorsGraph
+ constructor: (@data) ->
+ @width = 490
+ @height = 130
+ @x = null
+ @y = null
+ @x_axis = null
+ @y_axis = null
+ @area = null
+ @svg = null
+ @list_item = null
+ create_scale: ->
+ super @width, @height
+ create_axes: ->
+ @x_axis = d3.svg.axis().scale(@x).orient("bottom").tickFormat(d3.time.format("%m/%d"));
+ @y_axis = d3.svg.axis().scale(@y).orient("left")
+ create_area: (x, y) ->
+ @area = d3.svg.area().x((d) ->
+ parseDate = d3.time.format("%Y-%m-%d").parse
+ x(parseDate(d))
+ ).y0(@height).y1((d) =>
+ if @data[d]? then y(@data[d]) else y(0)
+ ).interpolate("basis")
+ create_svg: ->
+ @list_item = d3.selectAll(".person")[0].pop()
+ @svg = d3.select(@list_item).append("svg")
+ .attr("width", @width + @MARGIN.left + @MARGIN.right)
+ .attr("height", @height + @MARGIN.top + @MARGIN.bottom)
+ .attr("class", "spark")
+ .append("g")
+ .attr("transform", "translate(" + @MARGIN.left + "," + @MARGIN.top + ")")
+ draw_path: (data) ->
+ @svg.append("path").datum(data).attr("class", "area-contributor").attr("d", @area);
+ draw: ->
+ @create_scale()
+ @create_axes()
+ @set_domain()
+ @create_area(@x, @y)
+ @create_svg()
+ @draw_path(@dates)
+ @draw_x_axis()
+ @draw_y_axis()
+ redraw: ->
+ @set_domain()
+ @svg.select("path").datum(@dates)
+ @svg.select("path").attr("d", @area)
+ @svg.select(".x.axis").call(@x_axis)
+ @svg.select(".y.axis").call(@y_axis)
diff --git a/app/assets/javascripts/stat_graph_contributors_util.js.coffee b/app/assets/javascripts/stat_graph_contributors_util.js.coffee
new file mode 100644
index 00000000000..8f816313db3
--- /dev/null
+++ b/app/assets/javascripts/stat_graph_contributors_util.js.coffee
@@ -0,0 +1,91 @@
+window.ContributorsStatGraphUtil =
+ parse_log: (log) ->
+ total = {}
+ by_author = {}
+ for entry in log
+ @add_date(entry.date, total) unless total[entry.date]?
+ @add_author(entry.author, by_author) unless by_author[entry.author]?
+ @add_date(entry.date, by_author[entry.author]) unless by_author[entry.author][entry.date]
+ @store_data(entry, total[entry.date], by_author[entry.author][entry.date])
+ total = _.toArray(total)
+ by_author = _.toArray(by_author)
+ total: total, by_author: by_author
+
+ add_date: (date, collection) ->
+ collection[date] = {}
+ collection[date].date = date
+
+ add_author: (author, by_author) ->
+ by_author[author] = {}
+ by_author[author].author = author
+
+ store_data: (entry, total, by_author) ->
+ @store_commits(total, by_author)
+ @store_additions(entry, total, by_author)
+ @store_deletions(entry, total, by_author)
+
+ store_commits: (total, by_author) ->
+ @add(total, "commits", 1)
+ @add(by_author, "commits", 1)
+
+ add: (collection, field, value) ->
+ collection[field] ?= 0
+ collection[field] += value
+
+ store_additions: (entry, total, by_author) ->
+ entry.additions ?= 0
+ @add(total, "additions", entry.additions)
+ @add(by_author, "additions", entry.additions)
+
+ store_deletions: (entry, total, by_author) ->
+ entry.deletions ?= 0
+ @add(total, "deletions", entry.deletions)
+ @add(by_author, "deletions", entry.deletions)
+
+ get_total_data: (parsed_log, field) ->
+ log = parsed_log.total
+ total_data = @pick_field(log, field)
+ _.sortBy(total_data, (d) ->
+ d.date
+ )
+ pick_field: (log, field) ->
+ total_data = []
+ _.each(log, (d) ->
+ total_data.push(_.pick(d, [field, 'date']))
+ )
+ total_data
+
+ get_author_data: (parsed_log, field, date_range = null) ->
+ log = parsed_log.by_author
+ author_data = []
+
+ _.each(log, (log_entry) =>
+ parsed_log_entry = @parse_log_entry(log_entry, field, date_range)
+ if not _.isEmpty(parsed_log_entry.dates)
+ author_data.push(parsed_log_entry)
+ )
+
+ _.sortBy(author_data, (d) ->
+ d[field]
+ ).reverse()
+
+ parse_log_entry: (log_entry, field, date_range) ->
+ parsed_entry = {}
+ parsed_entry.author = log_entry.author
+ parsed_entry.dates = {}
+ parsed_entry.commits = parsed_entry.additions = parsed_entry.deletions = 0
+ _.each(_.omit(log_entry, 'author'), (value, key) =>
+ if @in_range(value.date, date_range)
+ parsed_entry.dates[value.date] = value[field]
+ parsed_entry.commits += value.commits
+ parsed_entry.additions += value.additions
+ parsed_entry.deletions += value.deletions
+ )
+ return parsed_entry
+
+ in_range: (date, date_range) ->
+ if date_range is null || date_range[0] <= new Date(date) <= date_range[1]
+ true
+ else
+ false
+ \ No newline at end of file
diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss
index 85e43ed0d35..b1a23427add 100644
--- a/app/assets/stylesheets/application.scss
+++ b/app/assets/stylesheets/application.scss
@@ -37,6 +37,7 @@
@import "sections/wiki.scss";
@import "sections/wall.scss";
@import "sections/dashboard.scss";
+@import "sections/stat_graph.scss";
@import "highlight/white.scss";
@import "highlight/dark.scss";
diff --git a/app/assets/stylesheets/sections/stat_graph.scss b/app/assets/stylesheets/sections/stat_graph.scss
new file mode 100644
index 00000000000..32b17d85e76
--- /dev/null
+++ b/app/assets/stylesheets/sections/stat_graph.scss
@@ -0,0 +1,56 @@
+.tint-box {
+ border-radius: 6px;
+ background: #f3f3f3;
+ position: relative;
+ margin-bottom: 10px;
+}
+
+.area {
+ fill: #1db34f;
+ fill-opacity: 0.5;
+}
+
+.axis {
+ fill: #aaa;
+ font-size: 10px;
+}
+
+#contributors .person {
+ -moz-box-sizing: border-box;
+ box-sizing: border-box;
+ float: left;
+ border-radius: 2px;
+ margin: 10px;
+ border: 1px solid #ddd;
+}
+
+.contributors-list {
+ margin: 0 0 10px 0;
+ list-style: none;
+ padding: 0;
+}
+
+#contributors .person .spark {
+ display: block;
+ background: #f7f7f7;
+}
+
+#contributors .person .area-contributor {
+ fill: #f17f49;
+}
+
+.selection rect {
+ fill: #333;
+ fill-opacity: 0.1;
+ stroke: #333;
+ stroke-width: 1px;
+ stroke-opacity: 0.4;
+ shape-rendering: crispedges;
+ stroke-dasharray: 3 3;
+}
+
+.right{
+ float: right;
+ display: inline-block;
+ margin-top: 5px;
+}
diff --git a/app/controllers/stat_graph_controller.rb b/app/controllers/stat_graph_controller.rb
new file mode 100644
index 00000000000..2a74409809f
--- /dev/null
+++ b/app/controllers/stat_graph_controller.rb
@@ -0,0 +1,14 @@
+class StatGraphController < ProjectResourceController
+
+ # Authorize
+ before_filter :authorize_read_project!
+ before_filter :authorize_code_access!
+ before_filter :require_non_empty_project
+
+ def show
+ @repo = @project.repository
+ @stats = Gitlab::GitStats.new(@repo.raw, @repo.root_ref)
+ @log = @stats.parsed_log.to_json
+ end
+
+end \ No newline at end of file
diff --git a/app/views/layouts/nav/_project.html.haml b/app/views/layouts/nav/_project.html.haml
index ec3da964037..399bcf5de2e 100644
--- a/app/views/layouts/nav/_project.html.haml
+++ b/app/views/layouts/nav/_project.html.haml
@@ -11,6 +11,8 @@
= link_to "Commits", project_commits_path(@project, @ref || @repository.root_ref)
= nav_link(controller: %w(graph)) do
= link_to "Network", project_graph_path(@project, @ref || @repository.root_ref)
+ = nav_link(controller: %w(stat_graph)) do
+ = link_to "Graphs", project_stat_graph_path(@project, @ref || @repository.root_ref)
- if @project.issues_enabled
= nav_link(controller: %w(issues milestones labels)) do
diff --git a/app/views/stat_graph/show.html.haml b/app/views/stat_graph/show.html.haml
new file mode 100644
index 00000000000..b7b27387c01
--- /dev/null
+++ b/app/views/stat_graph/show.html.haml
@@ -0,0 +1,29 @@
+.header.clearfix
+ .right
+ %select
+ %option{:value => "commits"} Commits
+ %option{:value => "additions"} Additions
+ %option{:value => "deletions"} Deletions
+ %h3#date_header
+ %input#brush_change{:type => "hidden"}
+
+.graphs
+ #contributors-master
+ #contributors.clearfix
+ %ol.contributors-list.clearfix
+
+:javascript
+ controller = new ContributorsStatGraph
+ controller.init(#{@log})
+
+ $("select").change( function () {
+ var field = $(this).val()
+ controller.set_current_field(field)
+ controller.redraw_master()
+ controller.redraw_authors()
+ })
+
+ $("#brush_change").change( function () {
+ controller.change_date_header()
+ controller.redraw_authors()
+ }) \ No newline at end of file