diff options
author | Karlo Soriano <dev+karlo@aelogica.com> | 2013-05-09 13:00:56 +0800 |
---|---|---|
committer | karlo57 <karlo.karlo.karlo@gmail.com> | 2013-06-05 16:51:48 +0800 |
commit | 71d67e6557acb1ce3beeec2c2c6deb35015bd8bb (patch) | |
tree | c8e23f80ee62359d8db8574056ea69ac70eb4406 /app | |
parent | b9d989dc056a2a2b9316ff9aa06b57c736426871 (diff) | |
download | gitlab-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.js | 2 | ||||
-rw-r--r-- | app/assets/javascripts/stat_graph.js.coffee | 6 | ||||
-rw-r--r-- | app/assets/javascripts/stat_graph_contributors.js.coffee | 61 | ||||
-rw-r--r-- | app/assets/javascripts/stat_graph_contributors_graph.js.coffee | 166 | ||||
-rw-r--r-- | app/assets/javascripts/stat_graph_contributors_util.js.coffee | 91 | ||||
-rw-r--r-- | app/assets/stylesheets/application.scss | 1 | ||||
-rw-r--r-- | app/assets/stylesheets/sections/stat_graph.scss | 56 | ||||
-rw-r--r-- | app/controllers/stat_graph_controller.rb | 14 | ||||
-rw-r--r-- | app/views/layouts/nav/_project.html.haml | 2 | ||||
-rw-r--r-- | app/views/stat_graph/show.html.haml | 29 |
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 |