Fix conflicts
This commit is contained in:
@@ -1,4 +1,6 @@
|
|||||||
language: ruby
|
language: ruby
|
||||||
|
addons:
|
||||||
|
postgresql: "9.4"
|
||||||
rvm:
|
rvm:
|
||||||
- "2.2.2"
|
- "2.2.2"
|
||||||
cache: bundler
|
cache: bundler
|
||||||
|
|||||||
3
Gemfile
3
Gemfile
@@ -36,6 +36,9 @@ gem 'initialjs-rails'
|
|||||||
gem 'unicorn'
|
gem 'unicorn'
|
||||||
gem 'paranoia'
|
gem 'paranoia'
|
||||||
|
|
||||||
|
gem 'ahoy_matey' # stats
|
||||||
|
gem 'groupdate' # group temporary data
|
||||||
|
|
||||||
group :development, :test do
|
group :development, :test do
|
||||||
# Call 'byebug' anywhere in the code to stop execution and get a debugger console
|
# Call 'byebug' anywhere in the code to stop execution and get a debugger console
|
||||||
gem 'byebug'
|
gem 'byebug'
|
||||||
|
|||||||
21
Gemfile.lock
21
Gemfile.lock
@@ -44,12 +44,23 @@ GEM
|
|||||||
awesome_nested_set (>= 3.0)
|
awesome_nested_set (>= 3.0)
|
||||||
acts_as_votable (0.10.0)
|
acts_as_votable (0.10.0)
|
||||||
addressable (2.3.8)
|
addressable (2.3.8)
|
||||||
|
ahoy_matey (1.2.0)
|
||||||
|
addressable
|
||||||
|
browser (>= 0.4.0)
|
||||||
|
errbase
|
||||||
|
geocoder
|
||||||
|
rails
|
||||||
|
referer-parser (>= 0.3.0)
|
||||||
|
request_store
|
||||||
|
user_agent_parser
|
||||||
|
uuidtools
|
||||||
arel (6.0.3)
|
arel (6.0.3)
|
||||||
awesome_nested_set (3.0.2)
|
awesome_nested_set (3.0.2)
|
||||||
activerecord (>= 4.0.0, < 5)
|
activerecord (>= 4.0.0, < 5)
|
||||||
bcrypt (3.1.10)
|
bcrypt (3.1.10)
|
||||||
binding_of_caller (0.7.2)
|
binding_of_caller (0.7.2)
|
||||||
debug_inspector (>= 0.0.1)
|
debug_inspector (>= 0.0.1)
|
||||||
|
browser (0.9.1)
|
||||||
builder (3.2.2)
|
builder (3.2.2)
|
||||||
byebug (5.0.0)
|
byebug (5.0.0)
|
||||||
columnize (= 0.9.0)
|
columnize (= 0.9.0)
|
||||||
@@ -116,6 +127,7 @@ GEM
|
|||||||
email_spec (1.6.0)
|
email_spec (1.6.0)
|
||||||
launchy (~> 2.1)
|
launchy (~> 2.1)
|
||||||
mail (~> 2.2)
|
mail (~> 2.2)
|
||||||
|
errbase (0.0.3)
|
||||||
erubis (2.7.0)
|
erubis (2.7.0)
|
||||||
execjs (2.5.2)
|
execjs (2.5.2)
|
||||||
factory_girl (4.5.0)
|
factory_girl (4.5.0)
|
||||||
@@ -135,8 +147,11 @@ GEM
|
|||||||
fuubar (2.0.0)
|
fuubar (2.0.0)
|
||||||
rspec (~> 3.0)
|
rspec (~> 3.0)
|
||||||
ruby-progressbar (~> 1.4)
|
ruby-progressbar (~> 1.4)
|
||||||
|
geocoder (1.2.9)
|
||||||
globalid (0.3.6)
|
globalid (0.3.6)
|
||||||
activesupport (>= 4.1.0)
|
activesupport (>= 4.1.0)
|
||||||
|
groupdate (2.4.0)
|
||||||
|
activesupport (>= 3)
|
||||||
highline (1.7.3)
|
highline (1.7.3)
|
||||||
http-cookie (1.0.2)
|
http-cookie (1.0.2)
|
||||||
domain_name (~> 0.5)
|
domain_name (~> 0.5)
|
||||||
@@ -221,6 +236,8 @@ GEM
|
|||||||
thor (>= 0.18.1, < 2.0)
|
thor (>= 0.18.1, < 2.0)
|
||||||
raindrops (0.15.0)
|
raindrops (0.15.0)
|
||||||
rake (10.4.2)
|
rake (10.4.2)
|
||||||
|
referer-parser (0.3.0)
|
||||||
|
request_store (1.2.0)
|
||||||
responders (2.1.0)
|
responders (2.1.0)
|
||||||
railties (>= 4.2.0, < 5)
|
railties (>= 4.2.0, < 5)
|
||||||
rest-client (1.8.0)
|
rest-client (1.8.0)
|
||||||
@@ -299,6 +316,8 @@ GEM
|
|||||||
kgio (~> 2.6)
|
kgio (~> 2.6)
|
||||||
rack
|
rack
|
||||||
raindrops (~> 0.7)
|
raindrops (~> 0.7)
|
||||||
|
user_agent_parser (2.2.0)
|
||||||
|
uuidtools (2.1.5)
|
||||||
warden (1.2.3)
|
warden (1.2.3)
|
||||||
rack (>= 1.0)
|
rack (>= 1.0)
|
||||||
web-console (2.2.1)
|
web-console (2.2.1)
|
||||||
@@ -319,6 +338,7 @@ DEPENDENCIES
|
|||||||
acts-as-taggable-on
|
acts-as-taggable-on
|
||||||
acts_as_commentable_with_threading
|
acts_as_commentable_with_threading
|
||||||
acts_as_votable
|
acts_as_votable
|
||||||
|
ahoy_matey
|
||||||
byebug
|
byebug
|
||||||
cancancan
|
cancancan
|
||||||
capistrano (= 3.4.0)
|
capistrano (= 3.4.0)
|
||||||
@@ -336,6 +356,7 @@ DEPENDENCIES
|
|||||||
foundation-rails
|
foundation-rails
|
||||||
foundation_rails_helper
|
foundation_rails_helper
|
||||||
fuubar
|
fuubar
|
||||||
|
groupdate
|
||||||
i18n-tasks
|
i18n-tasks
|
||||||
initialjs-rails
|
initialjs-rails
|
||||||
jquery-rails
|
jquery-rails
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ Las herramientas utilizadas para el frontend no están cerradas aún. Los estilo
|
|||||||
|
|
||||||
## Configuración para desarrollo y tests
|
## Configuración para desarrollo y tests
|
||||||
|
|
||||||
Prerequisitos: tener instalado git, ImageMagick, Ruby 2.2.2, la gema `bundler`, y una librería moderna de PostgreSQL.
|
Prerequisitos: tener instalado git, ImageMagick, Ruby 2.2.2, la gema `bundler`, y PostgreSQL (9.4 o superior).
|
||||||
|
|
||||||
```
|
```
|
||||||
cd participacion
|
cd participacion
|
||||||
|
|||||||
@@ -17,6 +17,10 @@
|
|||||||
//= require ckeditor/init
|
//= require ckeditor/init
|
||||||
//= require social-share-button
|
//= require social-share-button
|
||||||
//= require initial
|
//= require initial
|
||||||
|
//= require ahoy
|
||||||
|
//= require d3
|
||||||
|
//= require c3
|
||||||
|
//= require c3ext
|
||||||
//= require app
|
//= require app
|
||||||
//= require_tree .
|
//= require_tree .
|
||||||
|
|
||||||
@@ -25,6 +29,7 @@ var initialize_modules = function() {
|
|||||||
App.Users.initialize();
|
App.Users.initialize();
|
||||||
App.Votes.initialize();
|
App.Votes.initialize();
|
||||||
App.Tags.initialize();
|
App.Tags.initialize();
|
||||||
|
App.Stats.initialize();
|
||||||
};
|
};
|
||||||
|
|
||||||
$(function(){
|
$(function(){
|
||||||
|
|||||||
11
app/assets/javascripts/stats.js.coffee
Normal file
11
app/assets/javascripts/stats.js.coffee
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
# Helper for generate C3.js graphs
|
||||||
|
#----------------------------------------------------------------------
|
||||||
|
|
||||||
|
buildGraph = (el) ->
|
||||||
|
url = $(el).data 'graph'
|
||||||
|
conf = bindto: el, data: {x: 'x', url: url, mimeType: 'json'}, axis: { x: {type: 'timeseries',tick: { format: '%Y-%m-%d' } }}
|
||||||
|
graph = c3.generate conf
|
||||||
|
|
||||||
|
App.Stats =
|
||||||
|
initialize: ->
|
||||||
|
buildGraph(g) for g in $("[data-graph]")
|
||||||
@@ -8,3 +8,4 @@
|
|||||||
@import "variables";
|
@import "variables";
|
||||||
@import "participacion";
|
@import "participacion";
|
||||||
@import "debates";
|
@import "debates";
|
||||||
|
@import "c3";
|
||||||
|
|||||||
4
app/controllers/api/api_controller.rb
Normal file
4
app/controllers/api/api_controller.rb
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
class Api::ApiController < ApplicationController
|
||||||
|
before_action :authenticate_user!
|
||||||
|
protect_from_forgery with: :null_session
|
||||||
|
end
|
||||||
22
app/controllers/api/stats_controller.rb
Normal file
22
app/controllers/api/stats_controller.rb
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
class Api::StatsController < Api::ApiController
|
||||||
|
def show
|
||||||
|
unless params[:events].present? || params[:visits].present?
|
||||||
|
return render json: {}, status: :bad_request
|
||||||
|
end
|
||||||
|
|
||||||
|
ds = Ahoy::DataSource.new
|
||||||
|
|
||||||
|
if params[:events].present?
|
||||||
|
event_types = params[:events].split ','
|
||||||
|
event_types.each do |event|
|
||||||
|
ds.add event.titleize, Ahoy::Event.where(name: event).group_by_day(:time).count
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if params[:visits].present?
|
||||||
|
ds.add "Visits", Visit.group_by_day(:started_at).count
|
||||||
|
end
|
||||||
|
|
||||||
|
render json: ds.build
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -26,7 +26,9 @@ class DebatesController < ApplicationController
|
|||||||
def create
|
def create
|
||||||
@debate = Debate.new(debate_params)
|
@debate = Debate.new(debate_params)
|
||||||
@debate.author = current_user
|
@debate.author = current_user
|
||||||
|
|
||||||
if @debate.save_with_captcha
|
if @debate.save_with_captcha
|
||||||
|
ahoy.track :debate_created, debate_id: @debate.id
|
||||||
redirect_to @debate, notice: t('flash.actions.create.notice', resource_name: 'Debate')
|
redirect_to @debate, notice: t('flash.actions.create.notice', resource_name: 'Debate')
|
||||||
else
|
else
|
||||||
load_featured_tags
|
load_featured_tags
|
||||||
|
|||||||
5
app/controllers/stats_controller.rb
Normal file
5
app/controllers/stats_controller.rb
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
class StatsController < ApplicationController
|
||||||
|
def show
|
||||||
|
@event_types = Ahoy::Event.select(:name).uniq.pluck(:name)
|
||||||
|
end
|
||||||
|
end
|
||||||
16
app/helpers/stats_helper.rb
Normal file
16
app/helpers/stats_helper.rb
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
module StatsHelper
|
||||||
|
def events_chart_tag(events, opt={})
|
||||||
|
events = events.join(',') if events.is_a? Array
|
||||||
|
opt[:data] ||= {}
|
||||||
|
opt[:data][:graph] = api_stats_path(events: events)
|
||||||
|
content_tag :div, "", opt
|
||||||
|
end
|
||||||
|
|
||||||
|
def visits_chart_tag(opt={})
|
||||||
|
events = events.join(',') if events.is_a? Array
|
||||||
|
opt[:data] ||= {}
|
||||||
|
opt[:data][:graph] = api_stats_path(visits: true)
|
||||||
|
content_tag :div, "", opt
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
49
app/models/ahoy/data_source.rb
Normal file
49
app/models/ahoy/data_source.rb
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
# This class combines multiple collections with shared keys into a
|
||||||
|
# hash of collections compatible with C3.js charts
|
||||||
|
#----------------------------------------------------------------------
|
||||||
|
|
||||||
|
module Ahoy
|
||||||
|
class DataSource
|
||||||
|
|
||||||
|
# Adds a collection with the datasource
|
||||||
|
# Name is the name of the collection and will be showed in the
|
||||||
|
# chart
|
||||||
|
def add(name, collection)
|
||||||
|
collections.push data: collection, name: name
|
||||||
|
collection.each{ |k,v| add_key k }
|
||||||
|
end
|
||||||
|
|
||||||
|
def build
|
||||||
|
data = { x: [] }
|
||||||
|
keys.each do |k|
|
||||||
|
# Add the key with a valid date format
|
||||||
|
data[:x].push k.strftime("%Y-%m-%d")
|
||||||
|
|
||||||
|
# Add the value for each column, or 0 if not present
|
||||||
|
collections.each do |col|
|
||||||
|
data[col[:name]] ||= []
|
||||||
|
count = col[:data][k] || 0
|
||||||
|
data[col[:name]].push count
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return data
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def collections
|
||||||
|
@collections ||= []
|
||||||
|
end
|
||||||
|
|
||||||
|
def keys
|
||||||
|
@keys ||= []
|
||||||
|
end
|
||||||
|
|
||||||
|
def add_key(key)
|
||||||
|
keys.push(key) unless keys.include? key
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
8
app/models/ahoy/event.rb
Normal file
8
app/models/ahoy/event.rb
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
module Ahoy
|
||||||
|
class Event < ActiveRecord::Base
|
||||||
|
self.table_name = "ahoy_events"
|
||||||
|
|
||||||
|
belongs_to :visit
|
||||||
|
belongs_to :user
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -22,6 +22,9 @@ class Debate < ActiveRecord::Base
|
|||||||
before_validation :sanitize_description
|
before_validation :sanitize_description
|
||||||
before_validation :sanitize_tag_list
|
before_validation :sanitize_tag_list
|
||||||
|
|
||||||
|
# Ahoy setup
|
||||||
|
visitable # Ahoy will automatically assign visit_id on create
|
||||||
|
|
||||||
def self.search(params)
|
def self.search(params)
|
||||||
if params[:tag]
|
if params[:tag]
|
||||||
tagged_with(params[:tag])
|
tagged_with(params[:tag])
|
||||||
|
|||||||
4
app/models/visit.rb
Normal file
4
app/models/visit.rb
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
class Visit < ActiveRecord::Base
|
||||||
|
has_many :ahoy_events, class_name: "Ahoy::Event"
|
||||||
|
belongs_to :user
|
||||||
|
end
|
||||||
13
app/views/stats/show.html.erb
Normal file
13
app/views/stats/show.html.erb
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<h1>Stats</h1>
|
||||||
|
|
||||||
|
<h3>Visits</h3>
|
||||||
|
<%= visits_chart_tag id: "visits" %>
|
||||||
|
|
||||||
|
<h3>Combined</h3>
|
||||||
|
<%= events_chart_tag @event_types, id: 'combined' %>
|
||||||
|
|
||||||
|
|
||||||
|
<% @event_types.each do |event_type| %>
|
||||||
|
<h3><%= event_type.titleize %></h3>
|
||||||
|
<%= events_chart_tag event_type %>
|
||||||
|
<% end %>
|
||||||
9
config/initializers/ahoy.rb
Normal file
9
config/initializers/ahoy.rb
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
class Ahoy::Store < Ahoy::Stores::ActiveRecordStore
|
||||||
|
|
||||||
|
# Track user IP
|
||||||
|
def track_event(name, properties, options)
|
||||||
|
super do |event|
|
||||||
|
event.ip = request.ip
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -16,6 +16,11 @@ Rails.application.routes.draw do
|
|||||||
end
|
end
|
||||||
|
|
||||||
resource :account, controller: "account", only: [:show, :update]
|
resource :account, controller: "account", only: [:show, :update]
|
||||||
|
resource :stats, only: [:show]
|
||||||
|
|
||||||
|
namespace :api do
|
||||||
|
resource :stats, only: [:show]
|
||||||
|
end
|
||||||
|
|
||||||
namespace :admin do
|
namespace :admin do
|
||||||
root to: "dashboard#index"
|
root to: "dashboard#index"
|
||||||
|
|||||||
56
db/migrate/20150804144230_create_visits.rb
Normal file
56
db/migrate/20150804144230_create_visits.rb
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
class CreateVisits < ActiveRecord::Migration
|
||||||
|
def change
|
||||||
|
create_table :visits, id: false do |t|
|
||||||
|
t.uuid :id, default: nil, primary_key: true
|
||||||
|
t.uuid :visitor_id, default: nil
|
||||||
|
|
||||||
|
# the rest are recommended but optional
|
||||||
|
# simply remove the columns you don't want
|
||||||
|
|
||||||
|
# standard
|
||||||
|
t.string :ip
|
||||||
|
t.text :user_agent
|
||||||
|
t.text :referrer
|
||||||
|
t.text :landing_page
|
||||||
|
|
||||||
|
# user
|
||||||
|
t.integer :user_id
|
||||||
|
# add t.string :user_type if polymorphic
|
||||||
|
|
||||||
|
# traffic source
|
||||||
|
t.string :referring_domain
|
||||||
|
t.string :search_keyword
|
||||||
|
|
||||||
|
# technology
|
||||||
|
t.string :browser
|
||||||
|
t.string :os
|
||||||
|
t.string :device_type
|
||||||
|
t.integer :screen_height
|
||||||
|
t.integer :screen_width
|
||||||
|
|
||||||
|
# location
|
||||||
|
t.string :country
|
||||||
|
t.string :region
|
||||||
|
t.string :city
|
||||||
|
t.string :postal_code
|
||||||
|
t.decimal :latitude
|
||||||
|
t.decimal :longitude
|
||||||
|
|
||||||
|
# utm parameters
|
||||||
|
t.string :utm_source
|
||||||
|
t.string :utm_medium
|
||||||
|
t.string :utm_term
|
||||||
|
t.string :utm_content
|
||||||
|
t.string :utm_campaign
|
||||||
|
|
||||||
|
# native apps
|
||||||
|
# t.string :platform
|
||||||
|
# t.string :app_version
|
||||||
|
# t.string :os_version
|
||||||
|
|
||||||
|
t.timestamp :started_at
|
||||||
|
end
|
||||||
|
|
||||||
|
add_index :visits, [:user_id]
|
||||||
|
end
|
||||||
|
end
|
||||||
20
db/migrate/20150804144231_create_ahoy_events.rb
Normal file
20
db/migrate/20150804144231_create_ahoy_events.rb
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
class CreateAhoyEvents < ActiveRecord::Migration
|
||||||
|
def change
|
||||||
|
create_table :ahoy_events, id: false do |t|
|
||||||
|
t.uuid :id, default: nil, primary_key: true
|
||||||
|
t.uuid :visit_id, default: nil
|
||||||
|
|
||||||
|
# user
|
||||||
|
t.integer :user_id
|
||||||
|
# add t.string :user_type if polymorphic
|
||||||
|
|
||||||
|
t.string :name
|
||||||
|
t.jsonb :properties
|
||||||
|
t.timestamp :time
|
||||||
|
end
|
||||||
|
|
||||||
|
add_index :ahoy_events, [:visit_id]
|
||||||
|
add_index :ahoy_events, [:user_id]
|
||||||
|
add_index :ahoy_events, [:time]
|
||||||
|
end
|
||||||
|
end
|
||||||
5
db/migrate/20150808100936_add_ip_to_ahoy_event.rb
Normal file
5
db/migrate/20150808100936_add_ip_to_ahoy_event.rb
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
class AddIpToAhoyEvent < ActiveRecord::Migration
|
||||||
|
def change
|
||||||
|
add_column :ahoy_events, :ip, :string
|
||||||
|
end
|
||||||
|
end
|
||||||
5
db/migrate/20150808102442_add_visit_id_to_debate.rb
Normal file
5
db/migrate/20150808102442_add_visit_id_to_debate.rb
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
class AddVisitIdToDebate < ActiveRecord::Migration
|
||||||
|
def change
|
||||||
|
add_column :debates, :visit_id, :string
|
||||||
|
end
|
||||||
|
end
|
||||||
44
db/schema.rb
44
db/schema.rb
@@ -22,6 +22,19 @@ ActiveRecord::Schema.define(version: 20150817150457) do
|
|||||||
|
|
||||||
add_index "administrators", ["user_id"], name: "index_administrators_on_user_id", using: :btree
|
add_index "administrators", ["user_id"], name: "index_administrators_on_user_id", using: :btree
|
||||||
|
|
||||||
|
create_table "ahoy_events", id: :uuid, default: nil, force: :cascade do |t|
|
||||||
|
t.uuid "visit_id"
|
||||||
|
t.integer "user_id"
|
||||||
|
t.string "name"
|
||||||
|
t.jsonb "properties"
|
||||||
|
t.datetime "time"
|
||||||
|
t.string "ip"
|
||||||
|
end
|
||||||
|
|
||||||
|
add_index "ahoy_events", ["time"], name: "index_ahoy_events_on_time", using: :btree
|
||||||
|
add_index "ahoy_events", ["user_id"], name: "index_ahoy_events_on_user_id", using: :btree
|
||||||
|
add_index "ahoy_events", ["visit_id"], name: "index_ahoy_events_on_visit_id", using: :btree
|
||||||
|
|
||||||
create_table "comments", force: :cascade do |t|
|
create_table "comments", force: :cascade do |t|
|
||||||
t.integer "commentable_id"
|
t.integer "commentable_id"
|
||||||
t.string "commentable_type"
|
t.string "commentable_type"
|
||||||
@@ -49,6 +62,7 @@ ActiveRecord::Schema.define(version: 20150817150457) do
|
|||||||
t.datetime "created_at", null: false
|
t.datetime "created_at", null: false
|
||||||
t.datetime "updated_at", null: false
|
t.datetime "updated_at", null: false
|
||||||
t.datetime "hidden_at"
|
t.datetime "hidden_at"
|
||||||
|
t.string "visit_id"
|
||||||
end
|
end
|
||||||
|
|
||||||
add_index "debates", ["hidden_at"], name: "index_debates_on_hidden_at", using: :btree
|
add_index "debates", ["hidden_at"], name: "index_debates_on_hidden_at", using: :btree
|
||||||
@@ -125,6 +139,36 @@ ActiveRecord::Schema.define(version: 20150817150457) do
|
|||||||
add_index "users", ["email"], name: "index_users_on_email", unique: true, using: :btree
|
add_index "users", ["email"], name: "index_users_on_email", unique: true, using: :btree
|
||||||
add_index "users", ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true, using: :btree
|
add_index "users", ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true, using: :btree
|
||||||
|
|
||||||
|
create_table "visits", id: :uuid, default: nil, force: :cascade do |t|
|
||||||
|
t.uuid "visitor_id"
|
||||||
|
t.string "ip"
|
||||||
|
t.text "user_agent"
|
||||||
|
t.text "referrer"
|
||||||
|
t.text "landing_page"
|
||||||
|
t.integer "user_id"
|
||||||
|
t.string "referring_domain"
|
||||||
|
t.string "search_keyword"
|
||||||
|
t.string "browser"
|
||||||
|
t.string "os"
|
||||||
|
t.string "device_type"
|
||||||
|
t.integer "screen_height"
|
||||||
|
t.integer "screen_width"
|
||||||
|
t.string "country"
|
||||||
|
t.string "region"
|
||||||
|
t.string "city"
|
||||||
|
t.string "postal_code"
|
||||||
|
t.decimal "latitude"
|
||||||
|
t.decimal "longitude"
|
||||||
|
t.string "utm_source"
|
||||||
|
t.string "utm_medium"
|
||||||
|
t.string "utm_term"
|
||||||
|
t.string "utm_content"
|
||||||
|
t.string "utm_campaign"
|
||||||
|
t.datetime "started_at"
|
||||||
|
end
|
||||||
|
|
||||||
|
add_index "visits", ["user_id"], name: "index_visits_on_user_id", using: :btree
|
||||||
|
|
||||||
create_table "votes", force: :cascade do |t|
|
create_table "votes", force: :cascade do |t|
|
||||||
t.integer "votable_id"
|
t.integer "votable_id"
|
||||||
t.string "votable_type"
|
t.string "votable_type"
|
||||||
|
|||||||
97
spec/controllers/api/stats_controller_spec.rb
Normal file
97
spec/controllers/api/stats_controller_spec.rb
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
describe Api::StatsController do
|
||||||
|
|
||||||
|
# GET index
|
||||||
|
#----------------------------------------------------------------------
|
||||||
|
|
||||||
|
describe 'GET index' do
|
||||||
|
let(:user) { create :user }
|
||||||
|
|
||||||
|
context 'events or visits not present' do
|
||||||
|
it 'should respond with bad_request' do
|
||||||
|
sign_in user
|
||||||
|
get :show
|
||||||
|
expect(response).to_not be_ok
|
||||||
|
expect(response.status).to eq 400
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'events present' do
|
||||||
|
before :each do
|
||||||
|
time_1 = DateTime.parse("2015-01-01")
|
||||||
|
time_2 = DateTime.parse("2015-01-02")
|
||||||
|
time_3 = DateTime.parse("2015-01-03")
|
||||||
|
|
||||||
|
create :ahoy_event, name: 'foo', time: time_1
|
||||||
|
create :ahoy_event, name: 'foo', time: time_1
|
||||||
|
create :ahoy_event, name: 'foo', time: time_2
|
||||||
|
create :ahoy_event, name: 'bar', time: time_1
|
||||||
|
create :ahoy_event, name: 'bar', time: time_3
|
||||||
|
create :ahoy_event, name: 'bar', time: time_3
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'should return single events formated for working with c3.js' do
|
||||||
|
sign_in user
|
||||||
|
get :show, events: 'foo'
|
||||||
|
|
||||||
|
expect(response).to be_ok
|
||||||
|
|
||||||
|
data = JSON.parse(response.body)
|
||||||
|
expect(data).to eq "x"=>["2015-01-01", "2015-01-02"], "Foo"=>[2, 1]
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'should return combined comma separated events formated for working with c3.js' do
|
||||||
|
sign_in user
|
||||||
|
get :show, events: 'foo,bar'
|
||||||
|
|
||||||
|
expect(response).to be_ok
|
||||||
|
|
||||||
|
data = JSON.parse(response.body)
|
||||||
|
expect(data).to eq "x"=>["2015-01-01", "2015-01-02", "2015-01-03"], "Foo"=>[2, 1, 0], "Bar"=>[1, 0, 2]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'visits present' do
|
||||||
|
it 'should return visits formated for working with c3.js' do
|
||||||
|
time_1 = DateTime.parse("2015-01-01")
|
||||||
|
time_2 = DateTime.parse("2015-01-02")
|
||||||
|
|
||||||
|
create :visit, started_at: time_1
|
||||||
|
create :visit, started_at: time_1
|
||||||
|
create :visit, started_at: time_2
|
||||||
|
|
||||||
|
sign_in user
|
||||||
|
get :show, visits: true
|
||||||
|
|
||||||
|
expect(response).to be_ok
|
||||||
|
|
||||||
|
data = JSON.parse(response.body)
|
||||||
|
expect(data).to eq "x"=>["2015-01-01", "2015-01-02"], "Visits"=>[2, 1]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'visits and events present' do
|
||||||
|
it 'should return combined events and visits formated for working with c3.js' do
|
||||||
|
time_1 = DateTime.parse("2015-01-01")
|
||||||
|
time_2 = DateTime.parse("2015-01-02")
|
||||||
|
|
||||||
|
create :ahoy_event, name: 'foo', time: time_1
|
||||||
|
create :ahoy_event, name: 'foo', time: time_2
|
||||||
|
create :ahoy_event, name: 'foo', time: time_2
|
||||||
|
|
||||||
|
create :visit, started_at: time_1
|
||||||
|
create :visit, started_at: time_1
|
||||||
|
create :visit, started_at: time_2
|
||||||
|
|
||||||
|
sign_in user
|
||||||
|
get :show, events: 'foo', visits: true
|
||||||
|
|
||||||
|
expect(response).to be_ok
|
||||||
|
|
||||||
|
data = JSON.parse(response.body)
|
||||||
|
expect(data).to eq "x"=>["2015-01-01", "2015-01-02"], "Foo"=>[1, 2], "Visits"=>[2, 1]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
18
spec/controllers/debates_controller_spec.rb
Normal file
18
spec/controllers/debates_controller_spec.rb
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
describe DebatesController do
|
||||||
|
|
||||||
|
# create
|
||||||
|
#----------------------------------------------------------------------
|
||||||
|
|
||||||
|
describe 'POST create' do
|
||||||
|
let(:user) { create :user }
|
||||||
|
|
||||||
|
it 'should create an ahoy event' do
|
||||||
|
sign_in user
|
||||||
|
post :create, debate: { title: 'foo', description: 'foo bar', terms_of_service: 1 }
|
||||||
|
expect(Ahoy::Event.where(name: :debate_created).count).to eq 1
|
||||||
|
expect(Ahoy::Event.last.properties['debate_id']).to eq Debate.last.id
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -60,4 +60,14 @@ FactoryGirl.define do
|
|||||||
sequence(:value) { |n| "setting number #{n} value" }
|
sequence(:value) { |n| "setting number #{n} value" }
|
||||||
end
|
end
|
||||||
|
|
||||||
|
factory :ahoy_event, :class => Ahoy::Event do
|
||||||
|
id { SecureRandom.uuid }
|
||||||
|
time DateTime.now
|
||||||
|
sequence(:name) {|n| "Event #{n} type"}
|
||||||
|
end
|
||||||
|
|
||||||
|
factory :visit do
|
||||||
|
id { SecureRandom.uuid }
|
||||||
|
started_at DateTime.now
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
36
spec/models/ahoy/data_source_spec.rb
Normal file
36
spec/models/ahoy/data_source_spec.rb
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
describe Ahoy::DataSource do
|
||||||
|
describe '#build' do
|
||||||
|
before :each do
|
||||||
|
time_1 = DateTime.parse("2015-01-01")
|
||||||
|
time_2 = DateTime.parse("2015-01-02")
|
||||||
|
time_3 = DateTime.parse("2015-01-03")
|
||||||
|
|
||||||
|
create :ahoy_event, name: 'foo', time: time_1
|
||||||
|
create :ahoy_event, name: 'foo', time: time_1
|
||||||
|
create :ahoy_event, name: 'foo', time: time_2
|
||||||
|
create :ahoy_event, name: 'bar', time: time_1
|
||||||
|
create :ahoy_event, name: 'bar', time: time_3
|
||||||
|
create :ahoy_event, name: 'bar', time: time_3
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'should work without data sources' do
|
||||||
|
ds = Ahoy::DataSource.new
|
||||||
|
expect(ds.build).to eq x: []
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'should work with single data sources' do
|
||||||
|
ds = Ahoy::DataSource.new
|
||||||
|
ds.add 'foo', Ahoy::Event.where(name: 'foo').group_by_day(:time).count
|
||||||
|
expect(ds.build).to eq :x=>["2015-01-01", "2015-01-02"], "foo"=>[2, 1]
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'should combine data sources' do
|
||||||
|
ds = Ahoy::DataSource.new
|
||||||
|
ds.add 'foo', Ahoy::Event.where(name: 'foo').group_by_day(:time).count
|
||||||
|
ds.add 'bar', Ahoy::Event.where(name: 'bar').group_by_day(:time).count
|
||||||
|
expect(ds.build).to eq :x=>["2015-01-01", "2015-01-02", "2015-01-03"], "foo"=>[2, 1, 0], "bar"=>[1, 0, 2]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -8,6 +8,7 @@ RSpec.configure do |config|
|
|||||||
|
|
||||||
config.filter_run :focus
|
config.filter_run :focus
|
||||||
config.run_all_when_everything_filtered = true
|
config.run_all_when_everything_filtered = true
|
||||||
|
config.include Devise::TestHelpers, :type => :controller
|
||||||
config.include FactoryGirl::Syntax::Methods
|
config.include FactoryGirl::Syntax::Methods
|
||||||
config.include(EmailSpec::Helpers)
|
config.include(EmailSpec::Helpers)
|
||||||
config.include(EmailSpec::Matchers)
|
config.include(EmailSpec::Matchers)
|
||||||
|
|||||||
7049
vendor/assets/javascripts/c3.js
vendored
Normal file
7049
vendor/assets/javascripts/c3.js
vendored
Normal file
File diff suppressed because it is too large
Load Diff
380
vendor/assets/javascripts/c3ext.js
vendored
Normal file
380
vendor/assets/javascripts/c3ext.js
vendored
Normal file
@@ -0,0 +1,380 @@
|
|||||||
|
var c3ext = {};
|
||||||
|
c3ext.generate = function (options) {
|
||||||
|
|
||||||
|
if (options.zoom2 != null) {
|
||||||
|
zoom2_reducers = options.zoom2.reducers || {};
|
||||||
|
zoom2_enabled = options.zoom2.enabled;
|
||||||
|
_zoom2_factor = options.zoom2.factor || 1;
|
||||||
|
_zoom2_maxItems = options.zoom2.maxItems;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!zoom2_enabled) {
|
||||||
|
return c3.generate(options);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
var originalData = Q.copy(options.data);
|
||||||
|
var zoom2_reducers;
|
||||||
|
var zoom2_enabled;
|
||||||
|
var _zoom2_maxItems;
|
||||||
|
|
||||||
|
if (_zoom2_maxItems == null) {
|
||||||
|
var el = d3.select(options.bindto)[0][0];
|
||||||
|
if (el != null) {
|
||||||
|
var availWidth = el.clientWidth;
|
||||||
|
|
||||||
|
var pointSize = 20;
|
||||||
|
_zoom2_maxItems = Math.ceil(availWidth / pointSize);
|
||||||
|
}
|
||||||
|
if (_zoom2_maxItems == null || _zoom2_maxItems < 10) {
|
||||||
|
_zoom2_maxItems = 10;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onZoomChanged(e) {
|
||||||
|
refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
var zoom2 = c3ext.ZoomBehavior({ changed: onZoomChanged, bindto: options.bindto });
|
||||||
|
|
||||||
|
zoom2.enhance = function () {
|
||||||
|
_zoom2_maxItems *= 2;
|
||||||
|
var totalItems = zoom2.getZoom().totalItems;
|
||||||
|
if (_zoom2_maxItems > totalItems)
|
||||||
|
_zoom2_maxItems = totalItems;
|
||||||
|
refresh();
|
||||||
|
}
|
||||||
|
zoom2.dehance = function () {
|
||||||
|
_zoom2_maxItems = Math.ceil(_zoom2_maxItems / 2) + 1;
|
||||||
|
refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
zoom2.maxItems = function () { return _zoom2_maxItems; };
|
||||||
|
function zoomAndReduceData(list, zoomRange, func, maxItems) {
|
||||||
|
//var maxItems = 10;//Math.ceil(10 * zoomFactor);
|
||||||
|
var list2 = list.slice(zoomRange[0], zoomRange[1]);
|
||||||
|
var chunkSize = 1;
|
||||||
|
var list3 = list2;
|
||||||
|
if (list3.length > maxItems) {
|
||||||
|
var chunkSize = Math.ceil(list2.length / maxItems);
|
||||||
|
list3 = list3.splitIntoChunksOf(chunkSize).map(func);
|
||||||
|
}
|
||||||
|
//console.log("x" + getCurrentZoomLevel() + ", maxItems=" + maxItems + " chunkSize=" + chunkSize + " totalBefore=" + list2.length + ", totalAfter=" + list3.length);
|
||||||
|
return list3;
|
||||||
|
}
|
||||||
|
|
||||||
|
function first(t) { return t[0]; }
|
||||||
|
|
||||||
|
var getDataForZoom = function (data) {
|
||||||
|
if (data.columns == null || data.columns.length == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var zoomInfo = zoom2.getZoom();
|
||||||
|
if (zoomInfo.totalItems != data.columns[0].length - 1) {
|
||||||
|
zoom2.setOptions({ totalItems: data.columns[0].length - 1 });
|
||||||
|
zoomInfo = zoom2.getZoom();
|
||||||
|
}
|
||||||
|
data.columns = originalData.columns.map(function (column) {
|
||||||
|
var name = column[0];
|
||||||
|
var reducer = zoom2_reducers[name] || first; //by default take the first
|
||||||
|
|
||||||
|
var values = column.slice(1);
|
||||||
|
var newValues = zoomAndReduceData(values, zoomInfo.currentZoom, reducer, _zoom2_maxItems);
|
||||||
|
return [name].concat(newValues);
|
||||||
|
});
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
|
getDataForZoom(options.data);
|
||||||
|
var chart = c3.generate(options);
|
||||||
|
var _chart_load_org = chart.load.bind(chart);
|
||||||
|
chart.zoom2 = zoom2;
|
||||||
|
chart.load = function (data) {
|
||||||
|
if (data.unload) {
|
||||||
|
unload(data.unload);
|
||||||
|
delete data.unload;
|
||||||
|
}
|
||||||
|
Q.copy(data, originalData);
|
||||||
|
refresh();
|
||||||
|
}
|
||||||
|
chart.unload = function (names) {
|
||||||
|
unload(names);
|
||||||
|
refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
function unload(names) {
|
||||||
|
originalData.columns.removeAll(function (t) { names.contains(t); });
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function refresh() {
|
||||||
|
var data = Q.copy(originalData)
|
||||||
|
getDataForZoom(data);
|
||||||
|
_chart_load_org(data);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
return chart;
|
||||||
|
}
|
||||||
|
|
||||||
|
c3ext.ZoomBehavior = function (options) {
|
||||||
|
var zoom = { __type: "ZoomBehavior" };
|
||||||
|
|
||||||
|
var _zoom2_factor;
|
||||||
|
var _left;
|
||||||
|
var totalItems;
|
||||||
|
var currentZoom;
|
||||||
|
var bindto = options.bindto;
|
||||||
|
var _zoomChanged = options.changed || function () { };
|
||||||
|
var element;
|
||||||
|
var mousewheelTimer;
|
||||||
|
var deltaY = 0;
|
||||||
|
var leftRatio = 0;
|
||||||
|
|
||||||
|
|
||||||
|
zoom.setOptions = function (options) {
|
||||||
|
if (options == null)
|
||||||
|
options = {};
|
||||||
|
_zoom2_factor = options.factor || 1;
|
||||||
|
_left = 0;
|
||||||
|
totalItems = options.totalItems || 0;
|
||||||
|
currentZoom = [0, totalItems];
|
||||||
|
_zoomChanged = options.changed || _zoomChanged;
|
||||||
|
}
|
||||||
|
|
||||||
|
zoom.setOptions(options);
|
||||||
|
|
||||||
|
|
||||||
|
function verifyZoom(newZoom) {
|
||||||
|
//newZoom.sort();
|
||||||
|
if (newZoom[1] > totalItems) {
|
||||||
|
var diff = newZoom[1] - totalItems;
|
||||||
|
newZoom[0] -= diff;
|
||||||
|
newZoom[1] -= diff;
|
||||||
|
}
|
||||||
|
if (newZoom[0] < 0) {
|
||||||
|
var diff = newZoom[0] * -1;
|
||||||
|
newZoom[0] += diff;
|
||||||
|
newZoom[1] += diff;
|
||||||
|
}
|
||||||
|
if (newZoom[1] > totalItems)
|
||||||
|
newZoom[1] = totalItems;
|
||||||
|
if (newZoom[0] < 0)
|
||||||
|
newZoom[0] = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function zoomAndPan(zoomFactor, left) {
|
||||||
|
var itemsToShow = Math.ceil(totalItems / zoomFactor);
|
||||||
|
var newZoom = [left, left + itemsToShow];
|
||||||
|
verifyZoom(newZoom);
|
||||||
|
currentZoom = newZoom;
|
||||||
|
onZoomChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
function onZoomChanged() {
|
||||||
|
if (_zoomChanged != null)
|
||||||
|
_zoomChanged(zoom.getZoom());
|
||||||
|
}
|
||||||
|
function applyZoomAndPan() {
|
||||||
|
zoomAndPan(_zoom2_factor, _left);
|
||||||
|
}
|
||||||
|
function getItemsToShow() {
|
||||||
|
var itemsToShow = Math.ceil(totalItems / _zoom2_factor);
|
||||||
|
return itemsToShow;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
zoom.getZoom = function () {
|
||||||
|
return { totalItems: totalItems, currentZoom: currentZoom.slice() };
|
||||||
|
}
|
||||||
|
|
||||||
|
zoom.factor = function (factor, skipDraw) {
|
||||||
|
if (arguments.length == 0)
|
||||||
|
return _zoom2_factor;
|
||||||
|
_zoom2_factor = factor;
|
||||||
|
if (_zoom2_factor < 1)
|
||||||
|
_zoom2_factor = 1;
|
||||||
|
if (skipDraw)
|
||||||
|
return;
|
||||||
|
applyZoomAndPan();
|
||||||
|
}
|
||||||
|
zoom.left = function (left, skipDraw) {
|
||||||
|
if (arguments.length == 0)
|
||||||
|
return _left;
|
||||||
|
_left = left;
|
||||||
|
if (_left < 0)
|
||||||
|
_left = 0;
|
||||||
|
var pageSize = getItemsToShow();
|
||||||
|
//_left += pageSize;
|
||||||
|
if (_left + pageSize > totalItems)
|
||||||
|
_left = totalItems - pageSize;
|
||||||
|
console.log({ left: _left, pageSize: pageSize });
|
||||||
|
if (skipDraw)
|
||||||
|
return;
|
||||||
|
applyZoomAndPan();
|
||||||
|
}
|
||||||
|
|
||||||
|
zoom.zoomAndPanByRatio = function (zoomRatio, panRatio) {
|
||||||
|
|
||||||
|
var pageSize = getItemsToShow();
|
||||||
|
var leftOffset = Math.round(pageSize * panRatio);
|
||||||
|
var mouseLeft = _left + leftOffset;
|
||||||
|
zoom.factor(zoom.factor() * zoomRatio, true);
|
||||||
|
|
||||||
|
var finalLeft = mouseLeft;
|
||||||
|
if (zoomRatio != 1) {
|
||||||
|
var pageSize2 = getItemsToShow();
|
||||||
|
var leftOffset2 = Math.round(pageSize2 * panRatio);
|
||||||
|
finalLeft = mouseLeft - leftOffset2;
|
||||||
|
}
|
||||||
|
zoom.left(finalLeft, true);
|
||||||
|
applyZoomAndPan();
|
||||||
|
}
|
||||||
|
|
||||||
|
zoom.zoomIn = function () {
|
||||||
|
zoom.zoomAndPanByRatio(2, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
zoom.zoomOut = function () {
|
||||||
|
zoom.zoomAndPanByRatio(0.5, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
zoom.panLeft = function () {
|
||||||
|
zoom.zoomAndPanByRatio(1, -1);
|
||||||
|
}
|
||||||
|
zoom.panRight = function () {
|
||||||
|
zoom.zoomAndPanByRatio(1, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
zoom.reset = function () {
|
||||||
|
_left = 0;
|
||||||
|
_zoom2_factor = 1;
|
||||||
|
applyZoomAndPan();
|
||||||
|
}
|
||||||
|
|
||||||
|
function doZoom() {
|
||||||
|
if (deltaY != 0) {
|
||||||
|
var maxDelta = 10;
|
||||||
|
var multiply = (maxDelta + deltaY) / maxDelta;
|
||||||
|
//var factor = chart.zoom2.factor()*multiply;
|
||||||
|
//factor= Math.ceil(factor*100) / 100;
|
||||||
|
console.log({ deltaY: deltaY, multiply: multiply });
|
||||||
|
zoom.zoomAndPanByRatio(multiply, leftRatio);//0.5);//leftRatio);
|
||||||
|
deltaY = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function element_mousewheel(e) {
|
||||||
|
deltaY += e.deltaY;
|
||||||
|
leftRatio = (e.offsetX - 70) / (e.currentTarget.offsetWidth - 70);
|
||||||
|
//console.log({ "e.offsetX": e.offsetX, "e.currentTarget.offsetWidth": e.currentTarget.offsetWidth, leftRatio: leftRatio });
|
||||||
|
mousewheelTimer.set(150);
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bindto != null) {
|
||||||
|
element = $(options.bindto);
|
||||||
|
if (element.mousewheel) {
|
||||||
|
mousewheelTimer = new Timer(doZoom);
|
||||||
|
element.mousewheel(element_mousewheel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return zoom;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof (Q) == "undefined") {
|
||||||
|
var Q = function () {
|
||||||
|
};
|
||||||
|
|
||||||
|
Q.copy = function (src, target, options, depth) {
|
||||||
|
///<summary>Copies an object into a target object, recursively cloning any object or array in the way, overwrite=true will overwrite a primitive field value even if exists</summary>
|
||||||
|
///<param name="src" />
|
||||||
|
///<param name="target" />
|
||||||
|
///<param name="options" type="Object">{ overwrite:false }</param>
|
||||||
|
///<returns type="Object">The copied object</returns>
|
||||||
|
if (depth == null)
|
||||||
|
depth = 0;
|
||||||
|
if (depth == 100) {
|
||||||
|
console.warn("Q.copy is in depth of 100 - possible circular reference")
|
||||||
|
}
|
||||||
|
options = options || { overwrite: false };
|
||||||
|
if (src == target || src == null)
|
||||||
|
return target;
|
||||||
|
if (typeof (src) != "object") {
|
||||||
|
if (options.overwrite || target == null)
|
||||||
|
return src;
|
||||||
|
return target;
|
||||||
|
}
|
||||||
|
if (typeof (src.clone) == "function") {
|
||||||
|
if (options.overwrite || target == null)
|
||||||
|
return src.clone();
|
||||||
|
return target;
|
||||||
|
}
|
||||||
|
if (target == null) {
|
||||||
|
if (src instanceof Array)
|
||||||
|
target = [];
|
||||||
|
else
|
||||||
|
target = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (src instanceof Array) {
|
||||||
|
for (var i = 0; i < src.length; i++) {
|
||||||
|
var item = src[i];
|
||||||
|
var item2 = target[i];
|
||||||
|
item2 = Q.copy(item, item2, options, depth + 1);
|
||||||
|
target[i] = item2;
|
||||||
|
}
|
||||||
|
target.splice(src.length, target.length - src.length);
|
||||||
|
return target;
|
||||||
|
}
|
||||||
|
for (var p in src) {
|
||||||
|
var value = src[p];
|
||||||
|
var value2 = target[p];
|
||||||
|
value2 = Q.copy(value, value2, options, depth + 1);
|
||||||
|
target[p] = value2;
|
||||||
|
}
|
||||||
|
return target;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (typeof (Timer) == "undefined") {
|
||||||
|
var Timer = function (action, ms) {
|
||||||
|
this.action = action;
|
||||||
|
if (ms != null)
|
||||||
|
this.set(ms);
|
||||||
|
}
|
||||||
|
|
||||||
|
Timer.prototype.set = function (ms) {
|
||||||
|
if (ms == null)
|
||||||
|
ms = this._ms;
|
||||||
|
else
|
||||||
|
this._ms = ms;
|
||||||
|
this.clear();
|
||||||
|
if (ms == null)
|
||||||
|
return;
|
||||||
|
this.timeout = window.setTimeout(this.onTick.bind(this), ms);
|
||||||
|
}
|
||||||
|
|
||||||
|
Timer.prototype.onTick = function () {
|
||||||
|
this.clear();
|
||||||
|
this.action();
|
||||||
|
}
|
||||||
|
|
||||||
|
Timer.prototype.clear = function (ms) {
|
||||||
|
if (this.timeout == null)
|
||||||
|
return;
|
||||||
|
window.clearTimeout(this.timeout);
|
||||||
|
this.timeout = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (typeof(Array.prototype.splitIntoChunksOf)=="undefined") {
|
||||||
|
Array.prototype.splitIntoChunksOf = function (countInEachChunk) {
|
||||||
|
var chunks = Math.ceil(this.length / countInEachChunk);
|
||||||
|
var list = [];
|
||||||
|
for (var i = 0; i < this.length; i += countInEachChunk) {
|
||||||
|
list.push(this.slice(i, i + countInEachChunk));
|
||||||
|
}
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
}
|
||||||
9503
vendor/assets/javascripts/d3.js
vendored
Normal file
9503
vendor/assets/javascripts/d3.js
vendored
Normal file
File diff suppressed because it is too large
Load Diff
163
vendor/assets/stylesheets/c3.css
vendored
Normal file
163
vendor/assets/stylesheets/c3.css
vendored
Normal file
@@ -0,0 +1,163 @@
|
|||||||
|
/*-- Chart --*/
|
||||||
|
.c3 svg {
|
||||||
|
font: 10px sans-serif;
|
||||||
|
-webkit-tap-highlight-color: rgba(0, 0, 0, 0); }
|
||||||
|
|
||||||
|
.c3 path, .c3 line {
|
||||||
|
fill: none;
|
||||||
|
stroke: #000; }
|
||||||
|
|
||||||
|
.c3 text {
|
||||||
|
-webkit-user-select: none;
|
||||||
|
-moz-user-select: none;
|
||||||
|
user-select: none; }
|
||||||
|
|
||||||
|
.c3-legend-item-tile, .c3-xgrid-focus, .c3-ygrid, .c3-event-rect, .c3-bars path {
|
||||||
|
shape-rendering: crispEdges; }
|
||||||
|
|
||||||
|
.c3-chart-arc path {
|
||||||
|
stroke: #fff; }
|
||||||
|
|
||||||
|
.c3-chart-arc text {
|
||||||
|
fill: #fff;
|
||||||
|
font-size: 13px; }
|
||||||
|
|
||||||
|
/*-- Axis --*/
|
||||||
|
/*-- Grid --*/
|
||||||
|
.c3-grid line {
|
||||||
|
stroke: #aaa; }
|
||||||
|
|
||||||
|
.c3-grid text {
|
||||||
|
fill: #aaa; }
|
||||||
|
|
||||||
|
.c3-xgrid, .c3-ygrid {
|
||||||
|
stroke-dasharray: 3 3; }
|
||||||
|
|
||||||
|
/*-- Text on Chart --*/
|
||||||
|
.c3-text.c3-empty {
|
||||||
|
fill: #808080;
|
||||||
|
font-size: 2em; }
|
||||||
|
|
||||||
|
/*-- Line --*/
|
||||||
|
.c3-line {
|
||||||
|
stroke-width: 1px; }
|
||||||
|
|
||||||
|
/*-- Point --*/
|
||||||
|
.c3-circle._expanded_ {
|
||||||
|
stroke-width: 1px;
|
||||||
|
stroke: white; }
|
||||||
|
|
||||||
|
.c3-selected-circle {
|
||||||
|
fill: white;
|
||||||
|
stroke-width: 2px; }
|
||||||
|
|
||||||
|
/*-- Bar --*/
|
||||||
|
.c3-bar {
|
||||||
|
stroke-width: 0; }
|
||||||
|
|
||||||
|
.c3-bar._expanded_ {
|
||||||
|
fill-opacity: 0.75; }
|
||||||
|
|
||||||
|
/*-- Focus --*/
|
||||||
|
.c3-target.c3-focused {
|
||||||
|
opacity: 1; }
|
||||||
|
|
||||||
|
.c3-target.c3-focused path.c3-line, .c3-target.c3-focused path.c3-step {
|
||||||
|
stroke-width: 2px; }
|
||||||
|
|
||||||
|
.c3-target.c3-defocused {
|
||||||
|
opacity: 0.3 !important; }
|
||||||
|
|
||||||
|
/*-- Region --*/
|
||||||
|
.c3-region {
|
||||||
|
fill: steelblue;
|
||||||
|
fill-opacity: 0.1; }
|
||||||
|
|
||||||
|
/*-- Brush --*/
|
||||||
|
.c3-brush .extent {
|
||||||
|
fill-opacity: 0.1; }
|
||||||
|
|
||||||
|
/*-- Select - Drag --*/
|
||||||
|
/*-- Legend --*/
|
||||||
|
.c3-legend-item {
|
||||||
|
font-size: 12px; }
|
||||||
|
|
||||||
|
.c3-legend-item-hidden {
|
||||||
|
opacity: 0.15; }
|
||||||
|
|
||||||
|
.c3-legend-background {
|
||||||
|
opacity: 0.75;
|
||||||
|
fill: white;
|
||||||
|
stroke: lightgray;
|
||||||
|
stroke-width: 1; }
|
||||||
|
|
||||||
|
/*-- Title --*/
|
||||||
|
.c3-title {
|
||||||
|
font: 14px sans-serif; }
|
||||||
|
|
||||||
|
/*-- Tooltip --*/
|
||||||
|
.c3-tooltip-container {
|
||||||
|
z-index: 10; }
|
||||||
|
|
||||||
|
.c3-tooltip {
|
||||||
|
border-collapse: collapse;
|
||||||
|
border-spacing: 0;
|
||||||
|
background-color: #fff;
|
||||||
|
empty-cells: show;
|
||||||
|
-webkit-box-shadow: 7px 7px 12px -9px #777777;
|
||||||
|
-moz-box-shadow: 7px 7px 12px -9px #777777;
|
||||||
|
box-shadow: 7px 7px 12px -9px #777777;
|
||||||
|
opacity: 0.9; }
|
||||||
|
|
||||||
|
.c3-tooltip tr {
|
||||||
|
border: 1px solid #CCC; }
|
||||||
|
|
||||||
|
.c3-tooltip th {
|
||||||
|
background-color: #aaa;
|
||||||
|
font-size: 14px;
|
||||||
|
padding: 2px 5px;
|
||||||
|
text-align: left;
|
||||||
|
color: #FFF; }
|
||||||
|
|
||||||
|
.c3-tooltip td {
|
||||||
|
font-size: 13px;
|
||||||
|
padding: 3px 6px;
|
||||||
|
background-color: #fff;
|
||||||
|
border-left: 1px dotted #999; }
|
||||||
|
|
||||||
|
.c3-tooltip td > span {
|
||||||
|
display: inline-block;
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
margin-right: 6px; }
|
||||||
|
|
||||||
|
.c3-tooltip td.value {
|
||||||
|
text-align: right; }
|
||||||
|
|
||||||
|
/*-- Area --*/
|
||||||
|
.c3-area {
|
||||||
|
stroke-width: 0;
|
||||||
|
opacity: 0.2; }
|
||||||
|
|
||||||
|
/*-- Arc --*/
|
||||||
|
.c3-chart-arcs-title {
|
||||||
|
dominant-baseline: middle;
|
||||||
|
font-size: 1.3em; }
|
||||||
|
|
||||||
|
.c3-chart-arcs .c3-chart-arcs-background {
|
||||||
|
fill: #e0e0e0;
|
||||||
|
stroke: none; }
|
||||||
|
|
||||||
|
.c3-chart-arcs .c3-chart-arcs-gauge-unit {
|
||||||
|
fill: #000;
|
||||||
|
font-size: 16px; }
|
||||||
|
|
||||||
|
.c3-chart-arcs .c3-chart-arcs-gauge-max {
|
||||||
|
fill: #777; }
|
||||||
|
|
||||||
|
.c3-chart-arcs .c3-chart-arcs-gauge-min {
|
||||||
|
fill: #777; }
|
||||||
|
|
||||||
|
.c3-chart-arc .c3-gauge-value {
|
||||||
|
fill: #000;
|
||||||
|
/* font-size: 28px !important;*/ }
|
||||||
Reference in New Issue
Block a user