diff --git a/Gemfile b/Gemfile index 174be1d5..44388fc3 100644 --- a/Gemfile +++ b/Gemfile @@ -39,6 +39,7 @@ gem "oauth" # for twitter-posting bot gem "mail" # for parsing incoming mail gem "ruumba" # tests views gem "sitemap_generator" # for better search engine indexing +gem "svg-graph", require: 'SVG/Graph/TimeSeries' # for charting, note workaround in lib/time_series.rb gem 'transaction_retry' # mitigate https://github.com/lobsters/lobsters-ansible/issues/39 group :test, :development do diff --git a/Gemfile.lock b/Gemfile.lock index 7a892fea..6ff2c559 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -231,6 +231,7 @@ GEM actionpack (>= 3.0) activesupport (>= 3.0) sprockets (>= 2.8, < 4.0) + svg-graph (2.2.0) thor (1.0.1) thread_safe (0.3.6) transaction_isolation (1.0.5) @@ -296,6 +297,7 @@ DEPENDENCIES scenic-mysql_adapter sitemap_generator sprockets-rails (= 2.3.3) + svg-graph transaction_retry uglifier (>= 1.3.0) vcr diff --git a/app/controllers/stats_controller.rb b/app/controllers/stats_controller.rb new file mode 100644 index 00000000..a2247207 --- /dev/null +++ b/app/controllers/stats_controller.rb @@ -0,0 +1,77 @@ +class StatsController < ApplicationController + FIRST_MONTH = Time.new(2012, 7, 3).utc.freeze + TIMESCALE_DIVISIONS = "1 year".freeze + + def index + @title = "Stats" + + @users_graph = monthly_graph("users_graph", { + :graph_title => "Users joining by month", + :scale_y_divisions => 50, + }) { + User.group("date_format(created_at, '%Y-%m')") + } + + @stories_graph = monthly_graph("stories_graph", { + :graph_title => "Stories submitted by month", + :scale_y_divisions => 100, + }) { + Story.group("date_format(created_at, '%Y-%m')") + } + + @comments_graph = monthly_graph("comments_graph", { + :graph_title => "Comments posted by month", + :scale_y_divisions => 1_000, + }) { + Comment.group("date_format(created_at, '%Y-%m')") + } + + @votes_graph = monthly_graph("votes_graph", { + :graph_title => "Votes cast by month", + :scale_y_divisions => 10_000, + }) { + Vote.group("date_format(updated_at, '%Y-%m')") + } + end + +private + + def monthly_graph(cache_key, opts) + Rails.cache.fetch(cache_key, expires_in: 1.day) { + defaults = { + :width => 800, + :height => 300, + :graph_title => "Graph", + :show_graph_title => false, + :no_css => false, + :key => false, + :scale_x_integers => true, + :scale_y_integers => false, + :show_data_values => false, + :show_x_guidelines => false, + :show_x_title => false, + :x_title => "Time", + :show_y_title => false, + :y_title => "Users", + :y_title_text_direction => :bt, + :stagger_x_labels => false, + :x_label_format => "%Y-%m", + :y_label_format => "%Y-%m", + :min_x_value => FIRST_MONTH, + :timescale_divisions => TIMESCALE_DIVISIONS, + :add_popups => true, + :popup_format => "%Y-%m", + :area_fill => false, + :min_y_value => 0, + :number_format => "%d", + :show_lines => false, + } + graph = TimeSeries.new(defaults.merge(opts)) + graph.add_data( + data: yield.count.to_a.flatten, + template: "%Y-%m", + ) + graph.burn_svg_only + } + end +end diff --git a/app/views/home/about.html.erb b/app/views/home/about.html.erb index 8676c00c..b0a02091 100644 --- a/app/views/home/about.html.erb +++ b/app/views/home/about.html.erb @@ -148,7 +148,8 @@

The Lobsters community is in a sweet spot that it's large enough to be worth asking questions about and small enough the answers make sense. - If you're curious about stats, Peter is happy to run queries against the database and Rails/MySQL/nginx logs (but not write them for you), + We have some basic stats available, + and Peter is happy to run queries against the database and Rails/MySQL/nginx logs (but not write them for you), as long as they don't reveal personal info like IPs, browsing, and voting or create “worst-of” leaderboards celebrating most-downvoted users/comments/stories. If you're an academic researcher, please be like MIT, not like UChicago secretly experimenting on maintainers without IRB review.

diff --git a/app/views/stats/index.html.erb b/app/views/stats/index.html.erb new file mode 100644 index 00000000..07e73a19 --- /dev/null +++ b/app/views/stats/index.html.erb @@ -0,0 +1,28 @@ +
+ +

New users by month

+<%= raw @users_graph %> + +

Stories submitted by month

+<%= raw @stories_graph %> + +

Comments posted by month

+<%= raw @comments_graph %> + +

Votes cast by month

+<%= raw @votes_graph %> + +

+Want more info? +Write a query. +

+ + + +
diff --git a/config/application.rb b/config/application.rb index 7c426b2d..9d846338 100644 --- a/config/application.rb +++ b/config/application.rb @@ -58,6 +58,7 @@ module Lobsters config.after_initialize do require "#{Rails.root}/lib/monkey.rb" + require "#{Rails.root}/lib/time_series.rb" end config.generators do |g| diff --git a/config/routes.rb b/config/routes.rb index a36b9feb..5a6f8bb6 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -209,5 +209,7 @@ Rails.application.routes.draw do get "/about" => "home#about" get "/chat" => "home#chat" + get "/stats" => "stats#index" + post '/csp-violation-report' => 'csp#violation_report' end diff --git a/lib/time_series.rb b/lib/time_series.rb new file mode 100644 index 00000000..6ca5a89d --- /dev/null +++ b/lib/time_series.rb @@ -0,0 +1,21 @@ +class TimeSeries < SVG::Graph::TimeSeries + include ActionView::Helpers::NumberHelper + + # these two methods are a patch on the gem's lack of time zone awareness + def format x, y, description + [ + Time.at(x).utc.strftime(popup_format), + number_with_delimiter(y), + description, + ].compact.join(', ') + end + + def get_x_labels + get_x_values.collect {|v| Time.at(v).utc.strftime(x_label_format) } + end + + # improves y axis labels with commas + def get_y_labels + get_y_values.collect {|v| number_with_delimiter(v.to_i) } + end +end