Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Merge pull request #407 from andrew/interactive_stats_graphs

Interactive stats graphs
  • Loading branch information...
commit 25d805cd36d46f3f0ee726dea4b57aab7abf13c4 2 parents ce6acad + 854679b
@sferik sferik authored
View
46 app/helpers/chart_helper.rb
@@ -16,24 +16,17 @@ def most_downloaded_chart(rubygems)
end
def downloads_over_time_chart(versions, days_ago = 90)
- download_counts = Download.counts_by_day_for_versions(versions, days_ago)
- range = [nil, 0]
chart = GoogleChart::LineChart.new('630x400', "Downloads over the last #{pluralize(days_ago, 'day')}") do |lc|
- versions.each_with_index do |version, idx|
- counts = []
- days_ago.times do |t|
- date = t.days.ago.to_date
- count = download_counts["#{version.id}-#{date}"] || 0
- range[0] = count if !range[0] || (count < range[0])
- range[1] = count if count > range[1]
- counts << count
- end
-
- lc.data version.slug, counts.reverse, color_from_cycle(idx, versions.size)
+ versions = downloads_over_time(versions, days_ago)
+ versions.each_with_index do |v, idx|
+ lc.data v[:slug], v[:counts], v[:color]
end
+ max = versions.map{|v| v[:counts].max}.max
+ range = [0, max]
+
lc.axis :y, :range => range
- lc.axis :x, :labels => [60, 40, 20, 0].map { |t| t.days.ago.to_date }
+ lc.axis :x, :labels => downloads_over_time_labels
lc.grid :x_step => 100.0 / 12.0,
:y_step => 100.0 / 15.0,
:length_segment => 1,
@@ -41,6 +34,31 @@ def downloads_over_time_chart(versions, days_ago = 90)
end
image_tag(chart.to_url(:chf => 'bg,s,FFFFFF00'), :alt => 'title')
end
+
+ def downloads_over_time_labels
+ [60, 40, 20, 0].map { |t| t.days.ago.to_date }
+ end
+
+ def downloads_over_time(versions, days_ago = 90)
+ download_counts = Download.counts_by_day_for_versions(versions, days_ago)
+ versions.map.with_index do |version, idx|
+ counts = []
+ days_ago.times do |t|
+ date = t.days.ago.to_date
+ count = download_counts["#{version.id}-#{date}"] || 0
+ counts << count
+ end
+ {
+ :slug => version.slug,
+ :counts => counts.reverse,
+ :color => color_from_cycle(idx, versions.size)
+ }
+ end
+ end
+
+ def downloads_over_time_chart_dates(days_ago = 90)
+ (0..days_ago).map { |n| n.days.ago.to_date }.reverse.map { |date| date.strftime("%m/%d") }
+ end
def color_from_cycle(idx, length)
hex = "0123456789ABCDEF"
View
45 app/views/stats/show.html.erb
@@ -28,8 +28,51 @@
</div>
<div class="border">
<div class="meta">
- <div class="stats-graph">
+ <div id='downloads_over_time' class="stats-graph">
<%= downloads_over_time_chart(@versions) %>
</div>
</div>
</div>
+
+<% content_for :javascript do %>
+ <%= javascript_include_tag "raphael", "g.raphael", "g.line" %>
+ <script type="text/javascript">
+ $(document).ready(function() {
+ $('#downloads_over_time img').hide();
+ $('#downloads_over_time').height(430)
+ var dates = <%= downloads_over_time_chart_dates.to_json.html_safe %>;
+ var r = Raphael("downloads_over_time");
+
+ var days = <%= (0..90).to_json.html_safe %>
+
+ var versions = <%= downloads_over_time(@versions).map{|v| v[:counts] }.to_json.html_safe
+ %>
+ var labels = <%= downloads_over_time_labels.to_json.html_safe %>
+
+ var fin = function () {
+ this.flag = r.g.popup(this.x, this.y, dates[this.axis] + ": " + this.value || "0").insertBefore(this);
+ };
+ var fout = function () {
+ this.flag.animate({opacity: 0}, 300, function () {this.remove();});
+ };
+
+ chart = r.g.linechart(30, 20, 500, 400, days, versions, {'axis':'0 0 0 1'}).hover(fin, fout);
+ yaxis = r.g.axis(40,410,480,0,90,3,0, labels);
+
+ var raph = r
+ var labels = <%= @versions.map(&:number).to_json.html_safe %>;
+ chart.labels = raph.set();
+ var x = 540; var h = (400 - (labels.length*10))/2;
+ for( var i = 0; i < labels.length; ++i ) {
+ var clr = chart.lines[i].attr("stroke");
+ chart.labels.push(raph.set());
+ chart.labels[i].push(raph.g["disc"](x + 5, h, 5)
+ .attr({fill: clr, stroke: "none"}));
+ chart.labels[i].push(txt = raph.text(x + 20, h, labels[i])
+ .attr(raph.g.txtattr)
+ .attr({fill: "#000", "text-anchor": "start"}));
+ h += chart.labels[i].getBBox().height * 1.2;
+ };
+ });
+ </script>
+<% end %>
View
217 public/javascripts/g.line.js
@@ -0,0 +1,217 @@
+/*
+ * g.Raphael 0.4 - Charting library, based on Raphaël
+ *
+ * Copyright (c) 2009 Dmitry Baranovskiy (http://g.raphaeljs.com)
+ * Licensed under the MIT (http://www.opensource.org/licenses/mit-license.php) license.
+ */
+Raphael.fn.g.linechart = function (x, y, width, height, valuesx, valuesy, opts) {
+ function shrink(values, dim) {
+ var k = values.length / dim,
+ j = 0,
+ l = k,
+ sum = 0,
+ res = [];
+ while (j < values.length) {
+ l--;
+ if (l < 0) {
+ sum += values[j] * (1 + l);
+ res.push(sum / k);
+ sum = values[j++] * -l;
+ l += k;
+ } else {
+ sum += values[j++];
+ }
+ }
+ return res;
+ }
+ opts = opts || {};
+ if (!this.raphael.is(valuesx[0], "array")) {
+ valuesx = [valuesx];
+ }
+ if (!this.raphael.is(valuesy[0], "array")) {
+ valuesy = [valuesy];
+ }
+ var allx = Array.prototype.concat.apply([], valuesx),
+ ally = Array.prototype.concat.apply([], valuesy),
+ xdim = this.g.snapEnds(Math.min.apply(Math, allx), Math.max.apply(Math, allx), valuesx[0].length - 1),
+ minx = xdim.from,
+ maxx = xdim.to,
+ gutter = opts.gutter || 10,
+ kx = (width - gutter * 2) / (maxx - minx),
+ ydim = this.g.snapEnds(Math.min.apply(Math, ally), Math.max.apply(Math, ally), valuesy[0].length - 1),
+ miny = ydim.from,
+ maxy = ydim.to,
+ ky = (height - gutter * 2) / (maxy - miny),
+ len = Math.max(valuesx[0].length, valuesy[0].length),
+ symbol = opts.symbol || "",
+ colors = opts.colors || Raphael.fn.g.colors,
+ that = this,
+ columns = null,
+ dots = null,
+ chart = this.set(),
+ path = [];
+
+ for (var i = 0, ii = valuesy.length; i < ii; i++) {
+ len = Math.max(len, valuesy[i].length);
+ }
+ var shades = this.set();
+ for (var i = 0, ii = valuesy.length; i < ii; i++) {
+ if (opts.shade) {
+ shades.push(this.path().attr({stroke: "none", fill: colors[i], opacity: opts.nostroke ? 1 : .3}));
+ }
+ if (valuesy[i].length > width - 2 * gutter) {
+ valuesy[i] = shrink(valuesy[i], width - 2 * gutter);
+ len = width - 2 * gutter;
+ }
+ if (valuesx[i] && valuesx[i].length > width - 2 * gutter) {
+ valuesx[i] = shrink(valuesx[i], width - 2 * gutter);
+ }
+ }
+ var axis = this.set();
+ if (opts.axis) {
+ var ax = (opts.axis + "").split(/[,\s]+/);
+ +ax[0] && axis.push(this.g.axis(x + gutter, y + gutter, width - 2 * gutter, minx, maxx, opts.axisxstep || Math.floor((width - 2 * gutter) / 20), 2));
+ +ax[1] && axis.push(this.g.axis(x + width - gutter, y + height - gutter, height - 2 * gutter, miny, maxy, opts.axisystep || Math.floor((height - 2 * gutter) / 20), 3));
+ +ax[2] && axis.push(this.g.axis(x + gutter, y + height - gutter, width - 2 * gutter, minx, maxx, opts.axisxstep || Math.floor((width - 2 * gutter) / 20), 0));
+ +ax[3] && axis.push(this.g.axis(x + gutter, y + height - gutter, height - 2 * gutter, miny, maxy, opts.axisystep || Math.floor((height - 2 * gutter) / 20), 1));
+ }
+ var lines = this.set(),
+ symbols = this.set(),
+ line;
+ for (var i = 0, ii = valuesy.length; i < ii; i++) {
+ if (!opts.nostroke) {
+ lines.push(line = this.path().attr({
+ stroke: colors[i],
+ "stroke-width": opts.width || 2,
+ "stroke-linejoin": "round",
+ "stroke-linecap": "round",
+ "stroke-dasharray": opts.dash || ""
+ }));
+ }
+ var sym = this.raphael.is(symbol, "array") ? symbol[i] : symbol,
+ symset = this.set();
+ path = [];
+ for (var j = 0, jj = valuesy[i].length; j < jj; j++) {
+ var X = x + gutter + ((valuesx[i] || valuesx[0])[j] - minx) * kx;
+ var Y = y + height - gutter - (valuesy[i][j] - miny) * ky;
+ (Raphael.is(sym, "array") ? sym[j] : sym) && symset.push(this.g[Raphael.fn.g.markers[this.raphael.is(sym, "array") ? sym[j] : sym]](X, Y, (opts.width || 2) * 3).attr({fill: colors[i], stroke: "none"}));
+ path = path.concat([j ? "L" : "M", X, Y]);
+ }
+ symbols.push(symset);
+ if (opts.shade) {
+ shades[i].attr({path: path.concat(["L", X, y + height - gutter, "L", x + gutter + ((valuesx[i] || valuesx[0])[0] - minx) * kx, y + height - gutter, "z"]).join(",")});
+ }
+ !opts.nostroke && line.attr({path: path.join(",")});
+ }
+ function createColumns(f) {
+ // unite Xs together
+ var Xs = [];
+ for (var i = 0, ii = valuesx.length; i < ii; i++) {
+ Xs = Xs.concat(valuesx[i]);
+ }
+ Xs.sort();
+ // remove duplicates
+ var Xs2 = [],
+ xs = [];
+ for (var i = 0, ii = Xs.length; i < ii; i++) {
+ Xs[i] != Xs[i - 1] && Xs2.push(Xs[i]) && xs.push(x + gutter + (Xs[i] - minx) * kx);
+ }
+ Xs = Xs2;
+ ii = Xs.length;
+ var cvrs = f || that.set();
+ for (var i = 0; i < ii; i++) {
+ var X = xs[i] - (xs[i] - (xs[i - 1] || x)) / 2,
+ w = ((xs[i + 1] || x + width) - xs[i]) / 2 + (xs[i] - (xs[i - 1] || x)) / 2,
+ C;
+ f ? (C = {}) : cvrs.push(C = that.rect(X - 1, y, Math.max(w + 1, 1), height).attr({stroke: "none", fill: "#000", opacity: 0}));
+ C.values = [];
+ C.symbols = that.set();
+ C.y = [];
+ C.x = xs[i];
+ C.axis = Xs[i];
+ for (var j = 0, jj = valuesy.length; j < jj; j++) {
+ Xs2 = valuesx[j] || valuesx[0];
+ for (var k = 0, kk = Xs2.length; k < kk; k++) {
+ if (Xs2[k] == Xs[i]) {
+ C.values.push(valuesy[j][k]);
+ C.y.push(y + height - gutter - (valuesy[j][k] - miny) * ky);
+ C.symbols.push(chart.symbols[j][k]);
+ }
+ }
+ }
+ f && f.call(C);
+ }
+ !f && (columns = cvrs);
+ }
+ function createDots(f) {
+ var cvrs = f || that.set(),
+ C;
+ for (var i = 0, ii = valuesy.length; i < ii; i++) {
+ for (var j = 0, jj = valuesy[i].length; j < jj; j++) {
+ var X = x + gutter + ((valuesx[i] || valuesx[0])[j] - minx) * kx,
+ nearX = x + gutter + ((valuesx[i] || valuesx[0])[j ? j - 1 : 1] - minx) * kx,
+ Y = y + height - gutter - (valuesy[i][j] - miny) * ky;
+ f ? (C = {}) : cvrs.push(C = that.circle(X, Y, Math.abs(nearX - X) / 2).attr({stroke: "none", fill: "#000", opacity: 0}));
+ C.x = X;
+ C.y = Y;
+ C.value = valuesy[i][j];
+ C.line = chart.lines[i];
+ C.shade = chart.shades[i];
+ C.symbol = chart.symbols[i][j];
+ C.symbols = chart.symbols[i];
+ C.axis = (valuesx[i] || valuesx[0])[j];
+ f && f.call(C);
+ }
+ }
+ !f && (dots = cvrs);
+ }
+ chart.push(lines, shades, symbols, axis, columns, dots);
+ chart.lines = lines;
+ chart.shades = shades;
+ chart.symbols = symbols;
+ chart.axis = axis;
+ chart.hoverColumn = function (fin, fout) {
+ !columns && createColumns();
+ columns.mouseover(fin).mouseout(fout);
+ return this;
+ };
+ chart.clickColumn = function (f) {
+ !columns && createColumns();
+ columns.click(f);
+ return this;
+ };
+ chart.hrefColumn = function (cols) {
+ var hrefs = that.raphael.is(arguments[0], "array") ? arguments[0] : arguments;
+ if (!(arguments.length - 1) && typeof cols == "object") {
+ for (var x in cols) {
+ for (var i = 0, ii = columns.length; i < ii; i++) if (columns[i].axis == x) {
+ columns[i].attr("href", cols[x]);
+ }
+ }
+ }
+ !columns && createColumns();
+ for (var i = 0, ii = hrefs.length; i < ii; i++) {
+ columns[i] && columns[i].attr("href", hrefs[i]);
+ }
+ return this;
+ };
+ chart.hover = function (fin, fout) {
+ !dots && createDots();
+ dots.mouseover(fin).mouseout(fout);
+ return this;
+ };
+ chart.click = function (f) {
+ !dots && createDots();
+ dots.click(f);
+ return this;
+ };
+ chart.each = function (f) {
+ createDots(f);
+ return this;
+ };
+ chart.eachColumn = function (f) {
+ createColumns(f);
+ return this;
+ };
+ return chart;
+};
Please sign in to comment.
Something went wrong with that request. Please try again.