diff --git a/Gemfile b/Gemfile
index 81a431c..9aed1b3 100644
--- a/Gemfile
+++ b/Gemfile
@@ -28,6 +28,11 @@ gem 'jquery-rails'
# Build JSON APIs with ease. Read more: https://github.com/rails/jbuilder
gem 'jbuilder', '~> 1.2'
+# Versioning
+gem 'paper_trail', '~> 4.0.0.rc'
+
+gem "highcharts-rails", "~> 4.1.5"
+
group :doc do
# bundle exec rake doc:rails generates the API under doc/api.
gem 'sdoc', require: false
diff --git a/Gemfile.lock b/Gemfile.lock
index 2127645..24d369e 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -49,6 +49,8 @@ GEM
erubis (2.7.0)
execjs (1.4.0)
multi_json (~> 1.0)
+ highcharts-rails (4.1.5)
+ railties (>= 3.1)
hike (1.2.3)
i18n (0.6.4)
jbuilder (1.5.0)
@@ -70,6 +72,10 @@ GEM
mime-types (1.23)
minitest (4.7.5)
multi_json (1.7.8)
+ paper_trail (4.0.0.rc1)
+ activerecord (>= 3.0, < 6.0)
+ activesupport (>= 3.0, < 6.0)
+ request_store (~> 1.1.0)
pg (0.15.1)
polyglot (0.3.3)
postmark (1.0.1)
@@ -103,6 +109,7 @@ GEM
rdoc (3.12.2)
json (~> 1.4)
ref (1.0.5)
+ request_store (1.1.0)
sass (3.2.10)
sass-rails (4.0.0)
railties (>= 4.0.0.beta, < 5.0)
@@ -141,9 +148,11 @@ PLATFORMS
DEPENDENCIES
coffee-rails (~> 4.0.0)
+ highcharts-rails (~> 4.1.5)
jbuilder (~> 1.2)
jquery-rails
less-rails
+ paper_trail (~> 4.0.0.rc)
pg
postmark-rails
rails (= 4.0.0)
@@ -154,3 +163,6 @@ DEPENDENCIES
therubyracer
twitter-bootstrap-rails!
uglifier (>= 1.3.0)
+
+BUNDLED WITH
+ 1.10.1
diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js
index 06170c8..15e3c0c 100644
--- a/app/assets/javascripts/application.js
+++ b/app/assets/javascripts/application.js
@@ -13,4 +13,7 @@
//= require jquery
//= require jquery_ujs
//= require twitter/bootstrap
+//= require highcharts
+//= require highcharts/highcharts-more
+//= require highcharts/themes/dark-unica
//= require_tree .
diff --git a/app/assets/javascripts/papers.js.coffee b/app/assets/javascripts/papers.js.coffee
index 0038185..d5f77be 100644
--- a/app/assets/javascripts/papers.js.coffee
+++ b/app/assets/javascripts/papers.js.coffee
@@ -19,39 +19,47 @@ totalWordsIdentifier = "num_words";
distinctLevels = [50, 100, 200, 300, 500, 800, 1000, 1250, 1500];
distinctIdentifier = "different_words";
+paper_etag = undefined
+
@updateSketches = (id) ->
- $.ajax id.toString(),
- type: 'GET'
- dataType: 'json'
- error: (jqXHR, textStatus, errorThrown) ->
- console.log "WARNING: Couldn't get current stats"
- success: (data, textStatus, jqXHR) ->
- console.log "SUCCESS: Got current stats, updating sketches..."
+ $.ajax "#{id.toString()}.json",
+ type: 'GET'
+ dataType: 'json'
+ error: (jqXHR, textStatus, errorThrown) ->
+ console.log "WARNING: Couldn't get current stats"
+ success: (data, textStatus, jqXHR) ->
+ if paper_etag != jqXHR.getResponseHeader("Etag")
+ console.log "SUCCESS: Got current stats, updating sketches..."
+ for sketch of sketches
+ updateSketch(sketch, data)
- for sketch of sketches
- updateSketch(sketch, data)
+ if @chart
+ updateTimeline(data["history"])
+ else
+ initTimeline(data["history"])
+ paper_etag = jqXHR.getResponseHeader("Etag")
@updateSketch = (sketch, data) ->
- # Sketches aren't loaded when document is ready
- # --> wait for them to load, since Processing.js provides no callback
- timer = 0
- timeout = 2000
- clearInterval(mem)
- mem = setInterval ->
- instance = Processing.getInstanceById(sketch);
- if instance
- if not sketches[sketch]
- # Load this sketch for the first time
- initSketch(sketch, instance)
- instance.update($.parseJSON(data["stats"]));
- clearInterval(mem);
- else
- timer += 10
- if timer > timeout
- console.log("WARNING: Failed to load sketch");
- clearInterval(mem);
- , 10
+ # Sketches aren't loaded when document is ready
+ # --> wait for them to load, since Processing.js provides no callback
+ timer = 0
+ timeout = 2000
+ clearInterval(mem)
+ mem = setInterval ->
+ instance = Processing.getInstanceById(sketch);
+ if instance
+ if not sketches[sketch]
+ # Load this sketch for the first time
+ initSketch(sketch, instance)
+ instance.update(data["stats"]);
+ clearInterval(mem);
+ else
+ timer += 10
+ if timer > timeout
+ console.log("WARNING: Failed to load sketch");
+ clearInterval(mem);
+ , 10
@initSketch = (sketch, instance) ->
@@ -64,14 +72,89 @@ distinctIdentifier = "different_words";
instance.setLevels(distinctLevels);
else # Do nothing
- sketches[sketch] = 1
+ sketches[sketch] = 1
+
+updateTimeline = (data) ->
+ words = []
+ pages = []
+
+ $.each data, (key,value) ->
+ words.push([Date.parse(value.time), value.words])
+ pages.push([Date.parse(value.time), value.pages])
+
+ @chart.series[0].update
+ data: words
+ @chart.series[1].update
+ data: pages
+initTimeline = (data) ->
+ words = []
+ pages = []
+
+ $.each data, (key,value) ->
+ words.push([Date.parse(value.time), value.words])
+ pages.push([Date.parse(value.time), value.pages])
+
+ @chart = $('#timeline').highcharts
+ chart:
+ type: 'spline'
+ zoomType: 'x'
+ backgroundColor:'black'
+ title: text: ''
+ xAxis:
+ type: 'datetime'
+ dateTimeLabelFormats:
+ month: '%e. %b'
+ year: '%Y'
+ title: text: 'Date'
+ yAxis: [
+ {
+ title: text: 'Words'
+ min: 0
+ }
+ {
+ title: text: 'Pages'
+ min: 0
+ }
+ ]
+ tooltip:
+ headerFormat: '{series.name}
'
+ pointFormat: '{point.x:%e. %b}: {point.y:.2f}'
+ plotOptions: spline: marker: enabled: true
+ series: [
+ {
+ name: 'Words'
+ data: words
+ yAxis: 0
+ zoneAxis: 'x'
+ zones: [
+ { value: words[words.length - 2][0] }
+ { dashStyle: 'dot' }
+ ]
+ connectNulls: true
+ }
+ {
+ name: 'Pages'
+ data: pages
+ yAxis: 1
+ zoneAxis: 'x'
+ zones: [
+ { value: pages[pages.length - 2][0] }
+ { dashStyle: 'dot' }
+ ]
+ connectNulls: true
+ }
+ ]
$ ->
- if $('.visualization').length > 0 # See if we're on a paper_show page
- paper_id = $('#paper_id').html()
- updateSketches(paper_id)
+ if $('.visualization').length > 0 # See if we're on a paper_show page
+ Highcharts.setOptions
+ global:
+ useUTC: false
+
+ paper_id = $('#paper_id').html()
+ updateSketches(paper_id)
- setInterval ->
- updateSketches(paper_id)
- , update_interval
+ setInterval ->
+ updateSketches(paper_id)
+ , update_interval
diff --git a/app/controllers/papers_controller.rb b/app/controllers/papers_controller.rb
index cd5ef91..bbcc4b2 100644
--- a/app/controllers/papers_controller.rb
+++ b/app/controllers/papers_controller.rb
@@ -11,6 +11,7 @@ def index
# GET /papers/1
# GET /papers/1.json
def show
+ fresh_when last_modified: @paper.updated_at.utc, etag: @paper
end
# GET /papers/new
diff --git a/app/models/paper.rb b/app/models/paper.rb
index e05e849..81101c5 100644
--- a/app/models/paper.rb
+++ b/app/models/paper.rb
@@ -1,3 +1,46 @@
class Paper < ActiveRecord::Base
has_many :applause
+
+ has_paper_trail skip: [:created_at, :updated_at]
+
+ def history
+ history = [start]
+ history += versions.map do |version|
+ {
+ time: version.created_at,
+ words: JSON.parse(version.reify.stats)['num_words'],
+ pages: JSON.parse(version.reify.stats)['pages']
+ }
+ end
+ history.append current
+ history.append goal
+ end
+
+ def start
+ {
+ time: start_date,
+ words: 0,
+ pages: 0
+ }
+ end
+
+ def current
+ {
+ time: updated_at,
+ words: JSON.parse(stats)['num_words'],
+ pages: JSON.parse(stats)['pages']
+ }
+ end
+
+ def goal
+ goal = {
+ time: end_date,
+ words: nil,
+ pages: nil
+ }
+ if end_date && goal_value && %w(pages words).include?(goal_type)
+ goal[goal_type.to_sym] = goal_value
+ end
+ goal
+ end
end
diff --git a/app/views/papers/show.html.erb b/app/views/papers/show.html.erb
index 0eea8ba..fac0319 100644
--- a/app/views/papers/show.html.erb
+++ b/app/views/papers/show.html.erb
@@ -6,7 +6,7 @@
<%= render "applause" %>
I'm currently typing ... !
<% end %>