From c9cae9421a46a8f435fc704d2a8753eeab391709 Mon Sep 17 00:00:00 2001 From: Jyri-Petteri Paloposki Date: Sun, 12 May 2019 14:10:59 +0300 Subject: [PATCH 01/15] #1153: Few of the graphs converted to use Chartkick --- Gemfile | 2 + Gemfile.lock | 2 + app/assets/javascripts/application.js | 3 + app/controllers/stats_controller.rb | 86 ---------- app/models/stats/actions.rb | 155 +++++++++++++++++- app/models/stats/contexts.rb | 10 -- app/views/stats/_actions.html.erb | 18 ++ app/views/stats/_contexts.html.erb | 14 +- .../actions_day_of_week_30days_data.html.erb | 16 -- .../actions_day_of_week_all_data.html.erb | 16 -- .../actions_time_of_day_30days_data.html.erb | 23 --- .../actions_time_of_day_all_data.html.erb | 23 --- app/views/stats/pie_chart_data.html.erb | 9 - 13 files changed, 185 insertions(+), 192 deletions(-) delete mode 100644 app/views/stats/actions_day_of_week_30days_data.html.erb delete mode 100644 app/views/stats/actions_day_of_week_all_data.html.erb delete mode 100644 app/views/stats/actions_time_of_day_30days_data.html.erb delete mode 100644 app/views/stats/actions_time_of_day_all_data.html.erb delete mode 100644 app/views/stats/pie_chart_data.html.erb diff --git a/Gemfile b/Gemfile index d66c2924..68e83a6f 100644 --- a/Gemfile +++ b/Gemfile @@ -88,3 +88,5 @@ group :test do # get test coverage info on codeclimate gem "codeclimate-test-reporter", "1.0.7", group: :test, require: nil end + +gem "chartkick" diff --git a/Gemfile.lock b/Gemfile.lock index 0c28b987..932d8a9a 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -66,6 +66,7 @@ GEM activesupport (>= 3.0.0) uniform_notifier (~> 1.11) byebug (11.0.1) + chartkick (3.0.2) childprocess (0.9.0) ffi (~> 1.0, >= 1.0.11) climate_control (0.2.0) @@ -284,6 +285,7 @@ DEPENDENCIES bootstrap-sass (= 3.4.1) bullet byebug + chartkick codeclimate-test-reporter (= 1.0.7) coffee-rails (~> 4.2.0) database_cleaner diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js index 38c7800b..a472d2f8 100644 --- a/app/assets/javascripts/application.js +++ b/app/assets/javascripts/application.js @@ -36,3 +36,6 @@ //= require jquery.truncator //= require superfish //= require supersubs + +//= require Chart.bundle +//= require chartkick diff --git a/app/controllers/stats_controller.rb b/app/controllers/stats_controller.rb index f217a18f..2f6aaf84 100644 --- a/app/controllers/stats_controller.rb +++ b/app/controllers/stats_controller.rb @@ -164,23 +164,6 @@ class StatsController < ApplicationController render :layout => false end - def actions_open_per_week_data - @actions_started = current_user.todos.created_after(@today-53.weeks). - select("todos.created_at, todos.completed_at"). - reorder("todos.created_at DESC") - - @max_weeks = difference_in_weeks(@today, @actions_started.last.created_at) - - # cut off chart at 52 weeks = one year - @count = [52, @max_weeks].min - - @actions_open_per_week_array = convert_to_weeks_running_from_today_array(@actions_started, @max_weeks+1) - @actions_open_per_week_array = cut_off_array(@actions_open_per_week_array, @count) - @max_actions = (@actions_open_per_week_array.max or 0) - - render :layout => false - end - def context_total_actions_data actions = Stats::TopContextsQuery.new(current_user).result @@ -196,75 +179,6 @@ class StatsController < ApplicationController render :pie_chart_data, :layout => false end - def actions_day_of_week_all_data - @actions_creation_day = current_user.todos.select("created_at") - @actions_completion_day = current_user.todos.completed.select("completed_at") - - # convert to array and fill in non-existing days - @actions_creation_day_array = Array.new(7) { |i| 0} - @actions_creation_day.each { |t| @actions_creation_day_array[ t.created_at.wday ] += 1 } - @max = @actions_creation_day_array.max - - # convert to array and fill in non-existing days - @actions_completion_day_array = Array.new(7) { |i| 0} - @actions_completion_day.each { |t| @actions_completion_day_array[ t.completed_at.wday ] += 1 } - @max = @actions_completion_day_array.max - - render :layout => false - end - - def actions_day_of_week_30days_data - @actions_creation_day = current_user.todos.created_after(@cut_off_month).select("created_at") - @actions_completion_day = current_user.todos.completed_after(@cut_off_month).select("completed_at") - - # convert to hash to be able to fill in non-existing days - @max=0 - @actions_creation_day_array = Array.new(7) { |i| 0} - @actions_creation_day.each { |r| @actions_creation_day_array[ r.created_at.wday ] += 1 } - - # convert to hash to be able to fill in non-existing days - @actions_completion_day_array = Array.new(7) { |i| 0} - @actions_completion_day.each { |r| @actions_completion_day_array[r.completed_at.wday] += 1 } - - @max = [@actions_creation_day_array.max, @actions_completion_day_array.max].max - - render :layout => false - end - - def actions_time_of_day_all_data - @actions_creation_hour = current_user.todos.select("created_at") - @actions_completion_hour = current_user.todos.completed.select("completed_at") - - # convert to hash to be able to fill in non-existing days - @actions_creation_hour_array = Array.new(24) { |i| 0} - @actions_creation_hour.each{|r| @actions_creation_hour_array[r.created_at.hour] += 1 } - - # convert to hash to be able to fill in non-existing days - @actions_completion_hour_array = Array.new(24) { |i| 0} - @actions_completion_hour.each{|r| @actions_completion_hour_array[r.completed_at.hour] += 1 } - - @max = [@actions_creation_hour_array.max, @actions_completion_hour_array.max].max - - render :layout => false - end - - def actions_time_of_day_30days_data - @actions_creation_hour = current_user.todos.created_after(@cut_off_month).select("created_at") - @actions_completion_hour = current_user.todos.completed_after(@cut_off_month).select("completed_at") - - # convert to hash to be able to fill in non-existing days - @actions_creation_hour_array = Array.new(24) { |i| 0} - @actions_creation_hour.each{|r| @actions_creation_hour_array[r.created_at.hour] += 1 } - - # convert to hash to be able to fill in non-existing days - @actions_completion_hour_array = Array.new(24) { |i| 0} - @actions_completion_hour.each{|r| @actions_completion_hour_array[r.completed_at.hour] += 1 } - - @max = [@actions_creation_hour_array.max, @max = @actions_completion_hour_array.max].max - - render :layout => false - end - def show_selected_actions_from_chart @page_title = t('stats.action_selection_title') @count = 99 diff --git a/app/models/stats/actions.rb b/app/models/stats/actions.rb index 212aa374..85179394 100644 --- a/app/models/stats/actions.rb +++ b/app/models/stats/actions.rb @@ -6,6 +6,12 @@ module Stats attr_reader :user def initialize(user) @user = user + + @today = Time.zone.now.utc.beginning_of_day + @cut_off_year = 12.months.ago.beginning_of_day + @cut_off_year_plus3 = 15.months.ago.beginning_of_day + @cut_off_month = 1.month.ago.beginning_of_day + @cut_off_30days = 30.days.ago.beginning_of_day end def ttc @@ -42,16 +48,121 @@ module Stats @timing_charts ||= %w{ actions_visible_running_time_data actions_running_time_data - actions_open_per_week_data - actions_day_of_week_all_data - actions_day_of_week_30days_data - actions_time_of_day_all_data - actions_time_of_day_30days_data }.map do |action| Stats::Chart.new(action) end end + def running_time_data + @actions_running_time = @user.todos.not_completed.select("created_at").reorder("created_at DESC") + + # convert to array and fill in non-existing weeks with 0 + @max_weeks = difference_in_weeks(@today, @actions_running_time.last.created_at) + @actions_running_per_week_array = convert_to_weeks_from_today_array(@actions_running_time, @max_weeks+1, :created_at) + + # cut off chart at 52 weeks = one year + @count = [52, @max_weeks].min + + # convert to new array to hold max @cut_off elems + 1 for sum of actions after @cut_off + @actions_running_time_array = cut_off_array_with_sum(@actions_running_per_week_array, @count) + @max_actions = @actions_running_time_array.max + + # get percentage done cumulative + @cum_percent_done = convert_to_cumulative_array(@actions_running_time_array, @actions_running_time.count ) + end + + def open_per_week_data + @actions_started = @user.todos.created_after(@today-53.weeks). + select("todos.created_at, todos.completed_at"). + reorder("todos.created_at DESC") + + @max_weeks = difference_in_weeks(@today, @actions_started.last.created_at) + + # cut off chart at 52 weeks = one year + @count = [52, @max_weeks].min + + @actions_open_per_week_array = convert_to_weeks_running_from_today_array(@actions_started, @max_weeks+1) + @actions_open_per_week_array = cut_off_array(@actions_open_per_week_array, @count) + + return @actions_open_per_week_array.each_with_index.map { |total, week| [week, total] } + end + + def day_of_week_all_data + @actions_creation_day = @user.todos.select("created_at") + @actions_completion_day = @user.todos.completed.select("completed_at") + + # convert to array and fill in non-existing days + @actions_creation_day_array = Array.new(7) { |i| 0} + @actions_creation_day.each { |t| @actions_creation_day_array[ t.created_at.wday ] += 1 } + @max = @actions_creation_day_array.max + + # convert to array and fill in non-existing days + @actions_completion_day_array = Array.new(7) { |i| 0} + @actions_completion_day.each { |t| @actions_completion_day_array[ t.completed_at.wday ] += 1 } + + # FIXME: Day of week as string instead of number + return [ + {name: "Created", data: @actions_creation_day_array.each_with_index.map { |total, day| [day, total] } }, + {name: "Completed", data: @actions_completion_day_array.each_with_index.map { |total, day| [day, total] } } + ] + end + + def day_of_week_30days_data + @actions_creation_day = @user.todos.created_after(@cut_off_month).select("created_at") + @actions_completion_day = @user.todos.completed_after(@cut_off_month).select("completed_at") + + # convert to hash to be able to fill in non-existing days + @max=0 + @actions_creation_day_array = Array.new(7) { |i| 0} + @actions_creation_day.each { |r| @actions_creation_day_array[ r.created_at.wday ] += 1 } + + # convert to hash to be able to fill in non-existing days + @actions_completion_day_array = Array.new(7) { |i| 0} + @actions_completion_day.each { |r| @actions_completion_day_array[r.completed_at.wday] += 1 } + + # FIXME: Day of week as string instead of number + return [ + {name: "Created", data: @actions_creation_day_array.each_with_index.map { |total, day| [day, total] } }, + {name: "Completed", data: @actions_completion_day_array.each_with_index.map { |total, day| [day, total] } } + ] + end + + def time_of_day_all_data + @actions_creation_hour = @user.todos.select("created_at") + @actions_completion_hour = @user.todos.completed.select("completed_at") + + # convert to hash to be able to fill in non-existing days + @actions_creation_hour_array = Array.new(24) { |i| 0} + @actions_creation_hour.each{|r| @actions_creation_hour_array[r.created_at.hour] += 1 } + + # convert to hash to be able to fill in non-existing days + @actions_completion_hour_array = Array.new(24) { |i| 0} + @actions_completion_hour.each{|r| @actions_completion_hour_array[r.completed_at.hour] += 1 } + + return [ + {name: "Created", data: @actions_creation_hour_array.each_with_index.map { |total, hour| [hour, total] } }, + {name: "Completed", data: @actions_completion_hour_array.each_with_index.map { |total, hour| [hour, total] } } + ] + end + + def time_of_day_30days_data + @actions_creation_hour = @user.todos.created_after(@cut_off_month).select("created_at") + @actions_completion_hour = @user.todos.completed_after(@cut_off_month).select("completed_at") + + # convert to hash to be able to fill in non-existing days + @actions_creation_hour_array = Array.new(24) { |i| 0} + @actions_creation_hour.each{|r| @actions_creation_hour_array[r.created_at.hour] += 1 } + + # convert to hash to be able to fill in non-existing days + @actions_completion_hour_array = Array.new(24) { |i| 0} + @actions_completion_hour.each{|r| @actions_completion_hour_array[r.completed_at.hour] += 1 } + + return [ + {name: "Created", data: @actions_creation_hour_array.each_with_index.map { |total, hour| [hour, total] } }, + {name: "Completed", data: @actions_completion_hour_array.each_with_index.map { |total, hour| [hour, total] } } + ] + end + private def one_year @@ -73,5 +184,39 @@ module Stats def completed @completed ||= user.todos.completed.select("completed_at, created_at") end + + # assumes date1 > date2 + def difference_in_days(date1, date2) + return ((date1.utc.at_midnight-date2.utc.at_midnight)/SECONDS_PER_DAY).to_i + end + + # assumes date1 > date2 + def difference_in_weeks(date1, date2) + return difference_in_days(date1, date2) / 7 + end + + # uses the supplied block to determine array of indexes in hash + # the block should return an array of indexes each is added to the hash and summed + def convert_to_array(records, upper_bound) + a = Array.new(upper_bound, 0) + records.each { |r| (yield r).each { |i| a[i] += 1 if a[i] } } + a + end + + def convert_to_weeks_running_from_today_array(records, array_size) + return convert_to_array(records, array_size) { |r| week_indexes_of(r) } + end + + def cut_off_array(array, cut_off) + return Array.new(cut_off){|i| array[i]||0} + end + + def week_indexes_of(record) + a = [] + start_week = difference_in_weeks(@today, record.created_at) + end_week = record.completed_at ? difference_in_weeks(@today, record.completed_at) : 0 + end_week.upto(start_week) { |i| a << i }; + return a + end end end diff --git a/app/models/stats/contexts.rb b/app/models/stats/contexts.rb index 1aa04e38..488663ca 100644 --- a/app/models/stats/contexts.rb +++ b/app/models/stats/contexts.rb @@ -13,15 +13,5 @@ module Stats def running_actions @running_actions ||= Stats::TopContextsQuery.new(user, :limit => 5, :running => true).result end - - def charts - @charts = %w{ - context_total_actions_data - context_running_actions_data - }.map do |action| - Stats::Chart.new(action, :height => 325) - end - end - end end diff --git a/app/views/stats/_actions.html.erb b/app/views/stats/_actions.html.erb index 7c48350b..d1259ca0 100644 --- a/app/views/stats/_actions.html.erb +++ b/app/views/stats/_actions.html.erb @@ -15,3 +15,21 @@ render :partial => 'chart', :locals => {:chart => chart} -%><% end %> +

Current running time of all incomplete actions

+<%= column_chart actions.running_time_data %> + +

Active (visible and hidden) next actions per week

+<%= column_chart actions.open_per_week_data, xtitle: "Weeks ago" %> + +

Day of week (all actions)

+<%= column_chart actions.day_of_week_all_data %> + +

Day of week (past 30 days)

+<%= column_chart actions.day_of_week_30days_data %> + +

Time of day (all actions)

+<%= column_chart actions.time_of_day_all_data %> + +

Time of day (last 30 days)

+<%= column_chart actions.time_of_day_30days_data %> + diff --git a/app/views/stats/_contexts.html.erb b/app/views/stats/_contexts.html.erb index 2f9502e1..e0af588a 100644 --- a/app/views/stats/_contexts.html.erb +++ b/app/views/stats/_contexts.html.erb @@ -1,9 +1,15 @@ -<% contexts.charts.each do |chart| %><%= - render :partial => 'chart', :locals => {:chart => chart} --%><% end %> -
+

Spread of actions for all contexts

+<%= pie_chart Stats::TopContextsQuery.new(current_user).result.map { |context| + [context.name, context.total] +} %> + +

Spread of actions for visible contexts

+<%= pie_chart Stats::TopContextsQuery.new(current_user, :running => true).result.map { |context| + [context.name, context.total] +} %> + <%= render :partial => 'contexts_list', :locals => {:contexts => contexts.actions, :key => 'contexts'} -%> <%= render :partial => 'contexts_list', :locals => {:contexts => contexts.running_actions, :key => 'visible_contexts_with_incomplete_actions'} -%> diff --git a/app/views/stats/actions_day_of_week_30days_data.html.erb b/app/views/stats/actions_day_of_week_30days_data.html.erb deleted file mode 100644 index 75adf7bb..00000000 --- a/app/views/stats/actions_day_of_week_30days_data.html.erb +++ /dev/null @@ -1,16 +0,0 @@ -&title=<%= t('stats.actions_dow_30days_title') %>,{font-size:16},& -&y_legend=<%= t('stats.actions_dow_30days_legend.number_of_actions') %>,10,0x736AFF& -&x_legend=<%= t('stats.actions_dow_30days_legend.day_of_week') %>,10,0x736AFF& -&y_ticks=5,10,5& -&filled_bar=50,0x9933CC,0x8010A0,<%= t('stats.labels.created') %>,8& -&filled_bar_2=50,0x0066CC,0x0066CC,<%= t('stats.labels.completed') %>,8& -&values=<%= @actions_creation_day_array.join(",") %>& -&values_2=<%= @actions_completion_day_array.join(",") %>& -&x_labels=<%= t('date.day_names').join(",") %>& -&y_min=0& -<% - # add one to @max for people who have no actions completed yet. - # OpenFlashChart cannot handle y_max=0 --%> -&y_max=<%=@max+1 -%>& -&x_label_style=9,,2,1& \ No newline at end of file diff --git a/app/views/stats/actions_day_of_week_all_data.html.erb b/app/views/stats/actions_day_of_week_all_data.html.erb deleted file mode 100644 index 24e3ad6f..00000000 --- a/app/views/stats/actions_day_of_week_all_data.html.erb +++ /dev/null @@ -1,16 +0,0 @@ -&title=<%= t('stats.actions_day_of_week_title') %>,{font-size:16},& -&y_legend=<%= t('stats.actions_day_of_week_legend.number_of_actions') %>,10,0x736AFF& -&x_legend=<%= t('stats.actions_day_of_week_legend.day_of_week') %>,10,0x736AFF& -&y_ticks=5,10,5& -&filled_bar=50,0x9933CC,0x8010A0,<%= t('stats.labels.created') %>,8& -&filled_bar_2=50,0x0066CC,0x0066CC,<%= t('stats.labels.completed') %>,8& -&values=<%= @actions_creation_day_array.join(",") %>& -&values_2=<%= @actions_completion_day_array.join(",") %>& -&x_labels=<%= t('date.day_names').join(",") %>& -&y_min=0& -<% - # add one to @max for people who have no actions completed yet. - # OpenFlashChart cannot handle y_max=0 --%> -&y_max=<%=@max+1 -%>& -&x_label_style=9,,2,1& \ No newline at end of file diff --git a/app/views/stats/actions_time_of_day_30days_data.html.erb b/app/views/stats/actions_time_of_day_30days_data.html.erb deleted file mode 100644 index 49f6fa48..00000000 --- a/app/views/stats/actions_time_of_day_30days_data.html.erb +++ /dev/null @@ -1,23 +0,0 @@ -&title=<%= t('stats.tod30') %>,{font-size:16},& -&y_legend=<%= t('stats.tod30_legend.number_of_actions') %>,12,0x736AFF& -&x_legend=<%= t('stats.tod30_legend.time_of_day') %>,12,0x736AFF& -&y_ticks=5,10,5& -&filled_bar=50,0x9933CC,0x8010A0,<%= t('stats.labels.created') %>,8& -&filled_bar_2=50,0x0066CC,0x0066CC,<%= t('stats.labels.completed') %>,8& -&values=<% -0.upto 22 do |i| -%> - <%=@actions_creation_hour_array[i] -%>, -<% end -%><%=@actions_creation_hour_array[23]%>& -&values_2=<% -0.upto 22 do |i| -%> - <%=@actions_completion_hour_array[i] -%>, -<% end -%><%=@actions_completion_hour_array[23]%>& -&x_labels= <% -0.upto 22 do |i| -%> - <%=i-%>, -<% end -%>23& -&y_min=0& -<% # add one to @max for people who have no actions completed yet. - # OpenFlashChart cannot handle y_max=0 -%> -&y_max=<%=@max+1 -%>& -&x_label_style=9,,1,1& diff --git a/app/views/stats/actions_time_of_day_all_data.html.erb b/app/views/stats/actions_time_of_day_all_data.html.erb deleted file mode 100644 index 4abb52e9..00000000 --- a/app/views/stats/actions_time_of_day_all_data.html.erb +++ /dev/null @@ -1,23 +0,0 @@ -&title=<%= t('stats.time_of_day') %>,{font-size:16},& -&y_legend=<%= t('stats.time_of_day_legend.number_of_actions') %>,12,0x736AFF& -&x_legend=<%= t('stats.time_of_day_legend.time_of_day') %>,12,0x736AFF& -&y_ticks=5,10,5& -&filled_bar=50,0x9933CC,0x8010A0,<%= t('stats.labels.created') %>,8& -&filled_bar_2=50,0x0066CC,0x0066CC,<%= t('stats.labels.completed') %>,8& -&values=<% -0.upto 22 do |i| -%> - <%=@actions_creation_hour_array[i] -%>, -<% end -%><%=@actions_creation_hour_array[23]%>& -&values_2=<% -0.upto 22 do |i| -%> - <%=@actions_completion_hour_array[i] -%>, -<% end -%><%=@actions_completion_hour_array[23]%>& -&x_labels= <% -0.upto 22 do |i| -%> - <%=i-%>, -<% end -%>23& -&y_min=0& -<% # add one to @max for people who have no actions completed yet. - # OpenFlashChart cannot handle y_max=0 -%> -&y_max=<%=@max+1 -%>& -&x_label_style=9,,1,1& diff --git a/app/views/stats/pie_chart_data.html.erb b/app/views/stats/pie_chart_data.html.erb deleted file mode 100644 index 31beb727..00000000 --- a/app/views/stats/pie_chart_data.html.erb +++ /dev/null @@ -1,9 +0,0 @@ -&title=<%= @data.title %>,{font-size:16}& -&pie=<%= @data.alpha %>,#505050,{font-size: 12px; color: #404040;}& -&x_axis_steps=1& &y_ticks=5,10,5& &line=3,#87421F& &y_min=0& &y_max=20& -&values=<%= @data.values.join(",") %>& -&pie_labels=<%= @data.labels.join(",") %>& -&links=<%= @data.ids.map{|id| context_path(id)}.join(",") %>& -&colours=#d01f3c,#356aa0,#C79810,#c61fd0,#1fc6d0,#1fd076,#72d01f,#c6d01f,#d0941f,#40941f& -&tool_tip=#x_label#: #val#%25& -&x_label_style=9,,2,1& From dec82fd26c6f447096643f4861001584d9248899 Mon Sep 17 00:00:00 2001 From: Jyri-Petteri Paloposki Date: Fri, 17 May 2019 19:34:49 +0300 Subject: [PATCH 02/15] #1153: Change to using Chart.js with a basic RoR library instead of Chartkick because Chartkick doesn't support combo charts. --- Gemfile | 2 +- Gemfile.lock | 5 +- app/assets/javascripts/application.js | 4 +- .../chartjs-plugin-colorschemes.min.js | 11 +++ app/models/stats/actions.rb | 70 ++++++++++++++----- app/views/stats/_actions.html.erb | 37 ++++++---- app/views/stats/_contexts.html.erb | 43 +++++++++--- 7 files changed, 131 insertions(+), 41 deletions(-) create mode 100644 app/assets/javascripts/chartjs-plugin-colorschemes.min.js diff --git a/Gemfile b/Gemfile index 68e83a6f..7e6f55f0 100644 --- a/Gemfile +++ b/Gemfile @@ -89,4 +89,4 @@ group :test do gem "codeclimate-test-reporter", "1.0.7", group: :test, require: nil end -gem "chartkick" +gem 'chartjs-ror' diff --git a/Gemfile.lock b/Gemfile.lock index 932d8a9a..49e0d59b 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -66,7 +66,8 @@ GEM activesupport (>= 3.0.0) uniform_notifier (~> 1.11) byebug (11.0.1) - chartkick (3.0.2) + chartjs-ror (3.6.4) + rails (>= 3.1) childprocess (0.9.0) ffi (~> 1.0, >= 1.0.11) climate_control (0.2.0) @@ -285,7 +286,7 @@ DEPENDENCIES bootstrap-sass (= 3.4.1) bullet byebug - chartkick + chartjs-ror codeclimate-test-reporter (= 1.0.7) coffee-rails (~> 4.2.0) database_cleaner diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js index a472d2f8..375f0bdd 100644 --- a/app/assets/javascripts/application.js +++ b/app/assets/javascripts/application.js @@ -37,5 +37,5 @@ //= require superfish //= require supersubs -//= require Chart.bundle -//= require chartkick +//= require Chart.min +//= require chartjs-plugin-colorschemes.min diff --git a/app/assets/javascripts/chartjs-plugin-colorschemes.min.js b/app/assets/javascripts/chartjs-plugin-colorschemes.min.js new file mode 100644 index 00000000..82ee0703 --- /dev/null +++ b/app/assets/javascripts/chartjs-plugin-colorschemes.min.js @@ -0,0 +1,11 @@ +/* + * @license + * chartjs-plugin-colorschemes + * https://github.com/nagix/chartjs-plugin-colorschemes/ + * Version: 0.3.0 + * + * Copyright 2019 Akihiko Kusanagi + * Released under the MIT license + * https://github.com/nagix/chartjs-plugin-colorschemes/blob/master/LICENSE.md + */ +!function(f,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e(require("chart.js")):"function"==typeof define&&define.amd?define(["chart.js"],e):f["chartjs-plugin-colorschemes"]=e(f.Chart)}(this,function(f){"use strict";f=f&&f.hasOwnProperty("default")?f.default:f;var o={brewer:{YlGn3:["#f7fcb9","#addd8e","#31a354"],YlGn4:["#ffffcc","#c2e699","#78c679","#238443"],YlGn5:["#ffffcc","#c2e699","#78c679","#31a354","#006837"],YlGn6:["#ffffcc","#d9f0a3","#addd8e","#78c679","#31a354","#006837"],YlGn7:["#ffffcc","#d9f0a3","#addd8e","#78c679","#41ab5d","#238443","#005a32"],YlGn8:["#ffffe5","#f7fcb9","#d9f0a3","#addd8e","#78c679","#41ab5d","#238443","#005a32"],YlGn9:["#ffffe5","#f7fcb9","#d9f0a3","#addd8e","#78c679","#41ab5d","#238443","#006837","#004529"],YlGnBu3:["#edf8b1","#7fcdbb","#2c7fb8"],YlGnBu4:["#ffffcc","#a1dab4","#41b6c4","#225ea8"],YlGnBu5:["#ffffcc","#a1dab4","#41b6c4","#2c7fb8","#253494"],YlGnBu6:["#ffffcc","#c7e9b4","#7fcdbb","#41b6c4","#2c7fb8","#253494"],YlGnBu7:["#ffffcc","#c7e9b4","#7fcdbb","#41b6c4","#1d91c0","#225ea8","#0c2c84"],YlGnBu8:["#ffffd9","#edf8b1","#c7e9b4","#7fcdbb","#41b6c4","#1d91c0","#225ea8","#0c2c84"],YlGnBu9:["#ffffd9","#edf8b1","#c7e9b4","#7fcdbb","#41b6c4","#1d91c0","#225ea8","#253494","#081d58"],GnBu3:["#e0f3db","#a8ddb5","#43a2ca"],GnBu4:["#f0f9e8","#bae4bc","#7bccc4","#2b8cbe"],GnBu5:["#f0f9e8","#bae4bc","#7bccc4","#43a2ca","#0868ac"],GnBu6:["#f0f9e8","#ccebc5","#a8ddb5","#7bccc4","#43a2ca","#0868ac"],GnBu7:["#f0f9e8","#ccebc5","#a8ddb5","#7bccc4","#4eb3d3","#2b8cbe","#08589e"],GnBu8:["#f7fcf0","#e0f3db","#ccebc5","#a8ddb5","#7bccc4","#4eb3d3","#2b8cbe","#08589e"],GnBu9:["#f7fcf0","#e0f3db","#ccebc5","#a8ddb5","#7bccc4","#4eb3d3","#2b8cbe","#0868ac","#084081"],BuGn3:["#e5f5f9","#99d8c9","#2ca25f"],BuGn4:["#edf8fb","#b2e2e2","#66c2a4","#238b45"],BuGn5:["#edf8fb","#b2e2e2","#66c2a4","#2ca25f","#006d2c"],BuGn6:["#edf8fb","#ccece6","#99d8c9","#66c2a4","#2ca25f","#006d2c"],BuGn7:["#edf8fb","#ccece6","#99d8c9","#66c2a4","#41ae76","#238b45","#005824"],BuGn8:["#f7fcfd","#e5f5f9","#ccece6","#99d8c9","#66c2a4","#41ae76","#238b45","#005824"],BuGn9:["#f7fcfd","#e5f5f9","#ccece6","#99d8c9","#66c2a4","#41ae76","#238b45","#006d2c","#00441b"],PuBuGn3:["#ece2f0","#a6bddb","#1c9099"],PuBuGn4:["#f6eff7","#bdc9e1","#67a9cf","#02818a"],PuBuGn5:["#f6eff7","#bdc9e1","#67a9cf","#1c9099","#016c59"],PuBuGn6:["#f6eff7","#d0d1e6","#a6bddb","#67a9cf","#1c9099","#016c59"],PuBuGn7:["#f6eff7","#d0d1e6","#a6bddb","#67a9cf","#3690c0","#02818a","#016450"],PuBuGn8:["#fff7fb","#ece2f0","#d0d1e6","#a6bddb","#67a9cf","#3690c0","#02818a","#016450"],PuBuGn9:["#fff7fb","#ece2f0","#d0d1e6","#a6bddb","#67a9cf","#3690c0","#02818a","#016c59","#014636"],PuBu3:["#ece7f2","#a6bddb","#2b8cbe"],PuBu4:["#f1eef6","#bdc9e1","#74a9cf","#0570b0"],PuBu5:["#f1eef6","#bdc9e1","#74a9cf","#2b8cbe","#045a8d"],PuBu6:["#f1eef6","#d0d1e6","#a6bddb","#74a9cf","#2b8cbe","#045a8d"],PuBu7:["#f1eef6","#d0d1e6","#a6bddb","#74a9cf","#3690c0","#0570b0","#034e7b"],PuBu8:["#fff7fb","#ece7f2","#d0d1e6","#a6bddb","#74a9cf","#3690c0","#0570b0","#034e7b"],PuBu9:["#fff7fb","#ece7f2","#d0d1e6","#a6bddb","#74a9cf","#3690c0","#0570b0","#045a8d","#023858"],BuPu3:["#e0ecf4","#9ebcda","#8856a7"],BuPu4:["#edf8fb","#b3cde3","#8c96c6","#88419d"],BuPu5:["#edf8fb","#b3cde3","#8c96c6","#8856a7","#810f7c"],BuPu6:["#edf8fb","#bfd3e6","#9ebcda","#8c96c6","#8856a7","#810f7c"],BuPu7:["#edf8fb","#bfd3e6","#9ebcda","#8c96c6","#8c6bb1","#88419d","#6e016b"],BuPu8:["#f7fcfd","#e0ecf4","#bfd3e6","#9ebcda","#8c96c6","#8c6bb1","#88419d","#6e016b"],BuPu9:["#f7fcfd","#e0ecf4","#bfd3e6","#9ebcda","#8c96c6","#8c6bb1","#88419d","#810f7c","#4d004b"],RdPu3:["#fde0dd","#fa9fb5","#c51b8a"],RdPu4:["#feebe2","#fbb4b9","#f768a1","#ae017e"],RdPu5:["#feebe2","#fbb4b9","#f768a1","#c51b8a","#7a0177"],RdPu6:["#feebe2","#fcc5c0","#fa9fb5","#f768a1","#c51b8a","#7a0177"],RdPu7:["#feebe2","#fcc5c0","#fa9fb5","#f768a1","#dd3497","#ae017e","#7a0177"],RdPu8:["#fff7f3","#fde0dd","#fcc5c0","#fa9fb5","#f768a1","#dd3497","#ae017e","#7a0177"],RdPu9:["#fff7f3","#fde0dd","#fcc5c0","#fa9fb5","#f768a1","#dd3497","#ae017e","#7a0177","#49006a"],PuRd3:["#e7e1ef","#c994c7","#dd1c77"],PuRd4:["#f1eef6","#d7b5d8","#df65b0","#ce1256"],PuRd5:["#f1eef6","#d7b5d8","#df65b0","#dd1c77","#980043"],PuRd6:["#f1eef6","#d4b9da","#c994c7","#df65b0","#dd1c77","#980043"],PuRd7:["#f1eef6","#d4b9da","#c994c7","#df65b0","#e7298a","#ce1256","#91003f"],PuRd8:["#f7f4f9","#e7e1ef","#d4b9da","#c994c7","#df65b0","#e7298a","#ce1256","#91003f"],PuRd9:["#f7f4f9","#e7e1ef","#d4b9da","#c994c7","#df65b0","#e7298a","#ce1256","#980043","#67001f"],OrRd3:["#fee8c8","#fdbb84","#e34a33"],OrRd4:["#fef0d9","#fdcc8a","#fc8d59","#d7301f"],OrRd5:["#fef0d9","#fdcc8a","#fc8d59","#e34a33","#b30000"],OrRd6:["#fef0d9","#fdd49e","#fdbb84","#fc8d59","#e34a33","#b30000"],OrRd7:["#fef0d9","#fdd49e","#fdbb84","#fc8d59","#ef6548","#d7301f","#990000"],OrRd8:["#fff7ec","#fee8c8","#fdd49e","#fdbb84","#fc8d59","#ef6548","#d7301f","#990000"],OrRd9:["#fff7ec","#fee8c8","#fdd49e","#fdbb84","#fc8d59","#ef6548","#d7301f","#b30000","#7f0000"],YlOrRd3:["#ffeda0","#feb24c","#f03b20"],YlOrRd4:["#ffffb2","#fecc5c","#fd8d3c","#e31a1c"],YlOrRd5:["#ffffb2","#fecc5c","#fd8d3c","#f03b20","#bd0026"],YlOrRd6:["#ffffb2","#fed976","#feb24c","#fd8d3c","#f03b20","#bd0026"],YlOrRd7:["#ffffb2","#fed976","#feb24c","#fd8d3c","#fc4e2a","#e31a1c","#b10026"],YlOrRd8:["#ffffcc","#ffeda0","#fed976","#feb24c","#fd8d3c","#fc4e2a","#e31a1c","#b10026"],YlOrRd9:["#ffffcc","#ffeda0","#fed976","#feb24c","#fd8d3c","#fc4e2a","#e31a1c","#bd0026","#800026"],YlOrBr3:["#fff7bc","#fec44f","#d95f0e"],YlOrBr4:["#ffffd4","#fed98e","#fe9929","#cc4c02"],YlOrBr5:["#ffffd4","#fed98e","#fe9929","#d95f0e","#993404"],YlOrBr6:["#ffffd4","#fee391","#fec44f","#fe9929","#d95f0e","#993404"],YlOrBr7:["#ffffd4","#fee391","#fec44f","#fe9929","#ec7014","#cc4c02","#8c2d04"],YlOrBr8:["#ffffe5","#fff7bc","#fee391","#fec44f","#fe9929","#ec7014","#cc4c02","#8c2d04"],YlOrBr9:["#ffffe5","#fff7bc","#fee391","#fec44f","#fe9929","#ec7014","#cc4c02","#993404","#662506"],Purples3:["#efedf5","#bcbddc","#756bb1"],Purples4:["#f2f0f7","#cbc9e2","#9e9ac8","#6a51a3"],Purples5:["#f2f0f7","#cbc9e2","#9e9ac8","#756bb1","#54278f"],Purples6:["#f2f0f7","#dadaeb","#bcbddc","#9e9ac8","#756bb1","#54278f"],Purples7:["#f2f0f7","#dadaeb","#bcbddc","#9e9ac8","#807dba","#6a51a3","#4a1486"],Purples8:["#fcfbfd","#efedf5","#dadaeb","#bcbddc","#9e9ac8","#807dba","#6a51a3","#4a1486"],Purples9:["#fcfbfd","#efedf5","#dadaeb","#bcbddc","#9e9ac8","#807dba","#6a51a3","#54278f","#3f007d"],Blues3:["#deebf7","#9ecae1","#3182bd"],Blues4:["#eff3ff","#bdd7e7","#6baed6","#2171b5"],Blues5:["#eff3ff","#bdd7e7","#6baed6","#3182bd","#08519c"],Blues6:["#eff3ff","#c6dbef","#9ecae1","#6baed6","#3182bd","#08519c"],Blues7:["#eff3ff","#c6dbef","#9ecae1","#6baed6","#4292c6","#2171b5","#084594"],Blues8:["#f7fbff","#deebf7","#c6dbef","#9ecae1","#6baed6","#4292c6","#2171b5","#084594"],Blues9:["#f7fbff","#deebf7","#c6dbef","#9ecae1","#6baed6","#4292c6","#2171b5","#08519c","#08306b"],Greens3:["#e5f5e0","#a1d99b","#31a354"],Greens4:["#edf8e9","#bae4b3","#74c476","#238b45"],Greens5:["#edf8e9","#bae4b3","#74c476","#31a354","#006d2c"],Greens6:["#edf8e9","#c7e9c0","#a1d99b","#74c476","#31a354","#006d2c"],Greens7:["#edf8e9","#c7e9c0","#a1d99b","#74c476","#41ab5d","#238b45","#005a32"],Greens8:["#f7fcf5","#e5f5e0","#c7e9c0","#a1d99b","#74c476","#41ab5d","#238b45","#005a32"],Greens9:["#f7fcf5","#e5f5e0","#c7e9c0","#a1d99b","#74c476","#41ab5d","#238b45","#006d2c","#00441b"],Oranges3:["#fee6ce","#fdae6b","#e6550d"],Oranges4:["#feedde","#fdbe85","#fd8d3c","#d94701"],Oranges5:["#feedde","#fdbe85","#fd8d3c","#e6550d","#a63603"],Oranges6:["#feedde","#fdd0a2","#fdae6b","#fd8d3c","#e6550d","#a63603"],Oranges7:["#feedde","#fdd0a2","#fdae6b","#fd8d3c","#f16913","#d94801","#8c2d04"],Oranges8:["#fff5eb","#fee6ce","#fdd0a2","#fdae6b","#fd8d3c","#f16913","#d94801","#8c2d04"],Oranges9:["#fff5eb","#fee6ce","#fdd0a2","#fdae6b","#fd8d3c","#f16913","#d94801","#a63603","#7f2704"],Reds3:["#fee0d2","#fc9272","#de2d26"],Reds4:["#fee5d9","#fcae91","#fb6a4a","#cb181d"],Reds5:["#fee5d9","#fcae91","#fb6a4a","#de2d26","#a50f15"],Reds6:["#fee5d9","#fcbba1","#fc9272","#fb6a4a","#de2d26","#a50f15"],Reds7:["#fee5d9","#fcbba1","#fc9272","#fb6a4a","#ef3b2c","#cb181d","#99000d"],Reds8:["#fff5f0","#fee0d2","#fcbba1","#fc9272","#fb6a4a","#ef3b2c","#cb181d","#99000d"],Reds9:["#fff5f0","#fee0d2","#fcbba1","#fc9272","#fb6a4a","#ef3b2c","#cb181d","#a50f15","#67000d"],Greys3:["#f0f0f0","#bdbdbd","#636363"],Greys4:["#f7f7f7","#cccccc","#969696","#525252"],Greys5:["#f7f7f7","#cccccc","#969696","#636363","#252525"],Greys6:["#f7f7f7","#d9d9d9","#bdbdbd","#969696","#636363","#252525"],Greys7:["#f7f7f7","#d9d9d9","#bdbdbd","#969696","#737373","#525252","#252525"],Greys8:["#ffffff","#f0f0f0","#d9d9d9","#bdbdbd","#969696","#737373","#525252","#252525"],Greys9:["#ffffff","#f0f0f0","#d9d9d9","#bdbdbd","#969696","#737373","#525252","#252525","#000000"],PuOr3:["#f1a340","#f7f7f7","#998ec3"],PuOr4:["#e66101","#fdb863","#b2abd2","#5e3c99"],PuOr5:["#e66101","#fdb863","#f7f7f7","#b2abd2","#5e3c99"],PuOr6:["#b35806","#f1a340","#fee0b6","#d8daeb","#998ec3","#542788"],PuOr7:["#b35806","#f1a340","#fee0b6","#f7f7f7","#d8daeb","#998ec3","#542788"],PuOr8:["#b35806","#e08214","#fdb863","#fee0b6","#d8daeb","#b2abd2","#8073ac","#542788"],PuOr9:["#b35806","#e08214","#fdb863","#fee0b6","#f7f7f7","#d8daeb","#b2abd2","#8073ac","#542788"],PuOr10:["#7f3b08","#b35806","#e08214","#fdb863","#fee0b6","#d8daeb","#b2abd2","#8073ac","#542788","#2d004b"],PuOr11:["#7f3b08","#b35806","#e08214","#fdb863","#fee0b6","#f7f7f7","#d8daeb","#b2abd2","#8073ac","#542788","#2d004b"],BrBG3:["#d8b365","#f5f5f5","#5ab4ac"],BrBG4:["#a6611a","#dfc27d","#80cdc1","#018571"],BrBG5:["#a6611a","#dfc27d","#f5f5f5","#80cdc1","#018571"],BrBG6:["#8c510a","#d8b365","#f6e8c3","#c7eae5","#5ab4ac","#01665e"],BrBG7:["#8c510a","#d8b365","#f6e8c3","#f5f5f5","#c7eae5","#5ab4ac","#01665e"],BrBG8:["#8c510a","#bf812d","#dfc27d","#f6e8c3","#c7eae5","#80cdc1","#35978f","#01665e"],BrBG9:["#8c510a","#bf812d","#dfc27d","#f6e8c3","#f5f5f5","#c7eae5","#80cdc1","#35978f","#01665e"],BrBG10:["#543005","#8c510a","#bf812d","#dfc27d","#f6e8c3","#c7eae5","#80cdc1","#35978f","#01665e","#003c30"],BrBG11:["#543005","#8c510a","#bf812d","#dfc27d","#f6e8c3","#f5f5f5","#c7eae5","#80cdc1","#35978f","#01665e","#003c30"],PRGn3:["#af8dc3","#f7f7f7","#7fbf7b"],PRGn4:["#7b3294","#c2a5cf","#a6dba0","#008837"],PRGn5:["#7b3294","#c2a5cf","#f7f7f7","#a6dba0","#008837"],PRGn6:["#762a83","#af8dc3","#e7d4e8","#d9f0d3","#7fbf7b","#1b7837"],PRGn7:["#762a83","#af8dc3","#e7d4e8","#f7f7f7","#d9f0d3","#7fbf7b","#1b7837"],PRGn8:["#762a83","#9970ab","#c2a5cf","#e7d4e8","#d9f0d3","#a6dba0","#5aae61","#1b7837"],PRGn9:["#762a83","#9970ab","#c2a5cf","#e7d4e8","#f7f7f7","#d9f0d3","#a6dba0","#5aae61","#1b7837"],PRGn10:["#40004b","#762a83","#9970ab","#c2a5cf","#e7d4e8","#d9f0d3","#a6dba0","#5aae61","#1b7837","#00441b"],PRGn11:["#40004b","#762a83","#9970ab","#c2a5cf","#e7d4e8","#f7f7f7","#d9f0d3","#a6dba0","#5aae61","#1b7837","#00441b"],PiYG3:["#e9a3c9","#f7f7f7","#a1d76a"],PiYG4:["#d01c8b","#f1b6da","#b8e186","#4dac26"],PiYG5:["#d01c8b","#f1b6da","#f7f7f7","#b8e186","#4dac26"],PiYG6:["#c51b7d","#e9a3c9","#fde0ef","#e6f5d0","#a1d76a","#4d9221"],PiYG7:["#c51b7d","#e9a3c9","#fde0ef","#f7f7f7","#e6f5d0","#a1d76a","#4d9221"],PiYG8:["#c51b7d","#de77ae","#f1b6da","#fde0ef","#e6f5d0","#b8e186","#7fbc41","#4d9221"],PiYG9:["#c51b7d","#de77ae","#f1b6da","#fde0ef","#f7f7f7","#e6f5d0","#b8e186","#7fbc41","#4d9221"],PiYG10:["#8e0152","#c51b7d","#de77ae","#f1b6da","#fde0ef","#e6f5d0","#b8e186","#7fbc41","#4d9221","#276419"],PiYG11:["#8e0152","#c51b7d","#de77ae","#f1b6da","#fde0ef","#f7f7f7","#e6f5d0","#b8e186","#7fbc41","#4d9221","#276419"],RdBu3:["#ef8a62","#f7f7f7","#67a9cf"],RdBu4:["#ca0020","#f4a582","#92c5de","#0571b0"],RdBu5:["#ca0020","#f4a582","#f7f7f7","#92c5de","#0571b0"],RdBu6:["#b2182b","#ef8a62","#fddbc7","#d1e5f0","#67a9cf","#2166ac"],RdBu7:["#b2182b","#ef8a62","#fddbc7","#f7f7f7","#d1e5f0","#67a9cf","#2166ac"],RdBu8:["#b2182b","#d6604d","#f4a582","#fddbc7","#d1e5f0","#92c5de","#4393c3","#2166ac"],RdBu9:["#b2182b","#d6604d","#f4a582","#fddbc7","#f7f7f7","#d1e5f0","#92c5de","#4393c3","#2166ac"],RdBu10:["#67001f","#b2182b","#d6604d","#f4a582","#fddbc7","#d1e5f0","#92c5de","#4393c3","#2166ac","#053061"],RdBu11:["#67001f","#b2182b","#d6604d","#f4a582","#fddbc7","#f7f7f7","#d1e5f0","#92c5de","#4393c3","#2166ac","#053061"],RdGy3:["#ef8a62","#ffffff","#999999"],RdGy4:["#ca0020","#f4a582","#bababa","#404040"],RdGy5:["#ca0020","#f4a582","#ffffff","#bababa","#404040"],RdGy6:["#b2182b","#ef8a62","#fddbc7","#e0e0e0","#999999","#4d4d4d"],RdGy7:["#b2182b","#ef8a62","#fddbc7","#ffffff","#e0e0e0","#999999","#4d4d4d"],RdGy8:["#b2182b","#d6604d","#f4a582","#fddbc7","#e0e0e0","#bababa","#878787","#4d4d4d"],RdGy9:["#b2182b","#d6604d","#f4a582","#fddbc7","#ffffff","#e0e0e0","#bababa","#878787","#4d4d4d"],RdGy10:["#67001f","#b2182b","#d6604d","#f4a582","#fddbc7","#e0e0e0","#bababa","#878787","#4d4d4d","#1a1a1a"],RdGy11:["#67001f","#b2182b","#d6604d","#f4a582","#fddbc7","#ffffff","#e0e0e0","#bababa","#878787","#4d4d4d","#1a1a1a"],RdYlBu3:["#fc8d59","#ffffbf","#91bfdb"],RdYlBu4:["#d7191c","#fdae61","#abd9e9","#2c7bb6"],RdYlBu5:["#d7191c","#fdae61","#ffffbf","#abd9e9","#2c7bb6"],RdYlBu6:["#d73027","#fc8d59","#fee090","#e0f3f8","#91bfdb","#4575b4"],RdYlBu7:["#d73027","#fc8d59","#fee090","#ffffbf","#e0f3f8","#91bfdb","#4575b4"],RdYlBu8:["#d73027","#f46d43","#fdae61","#fee090","#e0f3f8","#abd9e9","#74add1","#4575b4"],RdYlBu9:["#d73027","#f46d43","#fdae61","#fee090","#ffffbf","#e0f3f8","#abd9e9","#74add1","#4575b4"],RdYlBu10:["#a50026","#d73027","#f46d43","#fdae61","#fee090","#e0f3f8","#abd9e9","#74add1","#4575b4","#313695"],RdYlBu11:["#a50026","#d73027","#f46d43","#fdae61","#fee090","#ffffbf","#e0f3f8","#abd9e9","#74add1","#4575b4","#313695"],Spectral3:["#fc8d59","#ffffbf","#99d594"],Spectral4:["#d7191c","#fdae61","#abdda4","#2b83ba"],Spectral5:["#d7191c","#fdae61","#ffffbf","#abdda4","#2b83ba"],Spectral6:["#d53e4f","#fc8d59","#fee08b","#e6f598","#99d594","#3288bd"],Spectral7:["#d53e4f","#fc8d59","#fee08b","#ffffbf","#e6f598","#99d594","#3288bd"],Spectral8:["#d53e4f","#f46d43","#fdae61","#fee08b","#e6f598","#abdda4","#66c2a5","#3288bd"],Spectral9:["#d53e4f","#f46d43","#fdae61","#fee08b","#ffffbf","#e6f598","#abdda4","#66c2a5","#3288bd"],Spectral10:["#9e0142","#d53e4f","#f46d43","#fdae61","#fee08b","#e6f598","#abdda4","#66c2a5","#3288bd","#5e4fa2"],Spectral11:["#9e0142","#d53e4f","#f46d43","#fdae61","#fee08b","#ffffbf","#e6f598","#abdda4","#66c2a5","#3288bd","#5e4fa2"],RdYlGn3:["#fc8d59","#ffffbf","#91cf60"],RdYlGn4:["#d7191c","#fdae61","#a6d96a","#1a9641"],RdYlGn5:["#d7191c","#fdae61","#ffffbf","#a6d96a","#1a9641"],RdYlGn6:["#d73027","#fc8d59","#fee08b","#d9ef8b","#91cf60","#1a9850"],RdYlGn7:["#d73027","#fc8d59","#fee08b","#ffffbf","#d9ef8b","#91cf60","#1a9850"],RdYlGn8:["#d73027","#f46d43","#fdae61","#fee08b","#d9ef8b","#a6d96a","#66bd63","#1a9850"],RdYlGn9:["#d73027","#f46d43","#fdae61","#fee08b","#ffffbf","#d9ef8b","#a6d96a","#66bd63","#1a9850"],RdYlGn10:["#a50026","#d73027","#f46d43","#fdae61","#fee08b","#d9ef8b","#a6d96a","#66bd63","#1a9850","#006837"],RdYlGn11:["#a50026","#d73027","#f46d43","#fdae61","#fee08b","#ffffbf","#d9ef8b","#a6d96a","#66bd63","#1a9850","#006837"],Accent3:["#7fc97f","#beaed4","#fdc086"],Accent4:["#7fc97f","#beaed4","#fdc086","#ffff99"],Accent5:["#7fc97f","#beaed4","#fdc086","#ffff99","#386cb0"],Accent6:["#7fc97f","#beaed4","#fdc086","#ffff99","#386cb0","#f0027f"],Accent7:["#7fc97f","#beaed4","#fdc086","#ffff99","#386cb0","#f0027f","#bf5b17"],Accent8:["#7fc97f","#beaed4","#fdc086","#ffff99","#386cb0","#f0027f","#bf5b17","#666666"],"Dark2-3":["#1b9e77","#d95f02","#7570b3"],"Dark2-4":["#1b9e77","#d95f02","#7570b3","#e7298a"],"Dark2-5":["#1b9e77","#d95f02","#7570b3","#e7298a","#66a61e"],"Dark2-6":["#1b9e77","#d95f02","#7570b3","#e7298a","#66a61e","#e6ab02"],"Dark2-7":["#1b9e77","#d95f02","#7570b3","#e7298a","#66a61e","#e6ab02","#a6761d"],"Dark2-8":["#1b9e77","#d95f02","#7570b3","#e7298a","#66a61e","#e6ab02","#a6761d","#666666"],Paired3:["#a6cee3","#1f78b4","#b2df8a"],Paired4:["#a6cee3","#1f78b4","#b2df8a","#33a02c"],Paired5:["#a6cee3","#1f78b4","#b2df8a","#33a02c","#fb9a99"],Paired6:["#a6cee3","#1f78b4","#b2df8a","#33a02c","#fb9a99","#e31a1c"],Paired7:["#a6cee3","#1f78b4","#b2df8a","#33a02c","#fb9a99","#e31a1c","#fdbf6f"],Paired8:["#a6cee3","#1f78b4","#b2df8a","#33a02c","#fb9a99","#e31a1c","#fdbf6f","#ff7f00"],Paired9:["#a6cee3","#1f78b4","#b2df8a","#33a02c","#fb9a99","#e31a1c","#fdbf6f","#ff7f00","#cab2d6"],Paired10:["#a6cee3","#1f78b4","#b2df8a","#33a02c","#fb9a99","#e31a1c","#fdbf6f","#ff7f00","#cab2d6","#6a3d9a"],Paired11:["#a6cee3","#1f78b4","#b2df8a","#33a02c","#fb9a99","#e31a1c","#fdbf6f","#ff7f00","#cab2d6","#6a3d9a","#ffff99"],Paired12:["#a6cee3","#1f78b4","#b2df8a","#33a02c","#fb9a99","#e31a1c","#fdbf6f","#ff7f00","#cab2d6","#6a3d9a","#ffff99","#b15928"],"Pastel1-3":["#fbb4ae","#b3cde3","#ccebc5"],"Pastel1-4":["#fbb4ae","#b3cde3","#ccebc5","#decbe4"],"Pastel1-5":["#fbb4ae","#b3cde3","#ccebc5","#decbe4","#fed9a6"],"Pastel1-6":["#fbb4ae","#b3cde3","#ccebc5","#decbe4","#fed9a6","#ffffcc"],"Pastel1-7":["#fbb4ae","#b3cde3","#ccebc5","#decbe4","#fed9a6","#ffffcc","#e5d8bd"],"Pastel1-8":["#fbb4ae","#b3cde3","#ccebc5","#decbe4","#fed9a6","#ffffcc","#e5d8bd","#fddaec"],"Pastel1-9":["#fbb4ae","#b3cde3","#ccebc5","#decbe4","#fed9a6","#ffffcc","#e5d8bd","#fddaec","#f2f2f2"],"Pastel2-3":["#b3e2cd","#fdcdac","#cbd5e8"],"Pastel2-4":["#b3e2cd","#fdcdac","#cbd5e8","#f4cae4"],"Pastel2-5":["#b3e2cd","#fdcdac","#cbd5e8","#f4cae4","#e6f5c9"],"Pastel2-6":["#b3e2cd","#fdcdac","#cbd5e8","#f4cae4","#e6f5c9","#fff2ae"],"Pastel2-7":["#b3e2cd","#fdcdac","#cbd5e8","#f4cae4","#e6f5c9","#fff2ae","#f1e2cc"],"Pastel2-8":["#b3e2cd","#fdcdac","#cbd5e8","#f4cae4","#e6f5c9","#fff2ae","#f1e2cc","#cccccc"],"Set1-3":["#e41a1c","#377eb8","#4daf4a"],"Set1-4":["#e41a1c","#377eb8","#4daf4a","#984ea3"],"Set1-5":["#e41a1c","#377eb8","#4daf4a","#984ea3","#ff7f00"],"Set1-6":["#e41a1c","#377eb8","#4daf4a","#984ea3","#ff7f00","#ffff33"],"Set1-7":["#e41a1c","#377eb8","#4daf4a","#984ea3","#ff7f00","#ffff33","#a65628"],"Set1-8":["#e41a1c","#377eb8","#4daf4a","#984ea3","#ff7f00","#ffff33","#a65628","#f781bf"],"Set1-9":["#e41a1c","#377eb8","#4daf4a","#984ea3","#ff7f00","#ffff33","#a65628","#f781bf","#999999"],"Set2-3":["#66c2a5","#fc8d62","#8da0cb"],"Set2-4":["#66c2a5","#fc8d62","#8da0cb","#e78ac3"],"Set2-5":["#66c2a5","#fc8d62","#8da0cb","#e78ac3","#a6d854"],"Set2-6":["#66c2a5","#fc8d62","#8da0cb","#e78ac3","#a6d854","#ffd92f"],"Set2-7":["#66c2a5","#fc8d62","#8da0cb","#e78ac3","#a6d854","#ffd92f","#e5c494"],"Set2-8":["#66c2a5","#fc8d62","#8da0cb","#e78ac3","#a6d854","#ffd92f","#e5c494","#b3b3b3"],"Set3-3":["#8dd3c7","#ffffb3","#bebada"],"Set3-4":["#8dd3c7","#ffffb3","#bebada","#fb8072"],"Set3-5":["#8dd3c7","#ffffb3","#bebada","#fb8072","#80b1d3"],"Set3-6":["#8dd3c7","#ffffb3","#bebada","#fb8072","#80b1d3","#fdb462"],"Set3-7":["#8dd3c7","#ffffb3","#bebada","#fb8072","#80b1d3","#fdb462","#b3de69"],"Set3-8":["#8dd3c7","#ffffb3","#bebada","#fb8072","#80b1d3","#fdb462","#b3de69","#fccde5"],"Set3-9":["#8dd3c7","#ffffb3","#bebada","#fb8072","#80b1d3","#fdb462","#b3de69","#fccde5","#d9d9d9"],"Set3-10":["#8dd3c7","#ffffb3","#bebada","#fb8072","#80b1d3","#fdb462","#b3de69","#fccde5","#d9d9d9","#bc80bd"],"Set3-11":["#8dd3c7","#ffffb3","#bebada","#fb8072","#80b1d3","#fdb462","#b3de69","#fccde5","#d9d9d9","#bc80bd","#ccebc5"],"Set3-12":["#8dd3c7","#ffffb3","#bebada","#fb8072","#80b1d3","#fdb462","#b3de69","#fccde5","#d9d9d9","#bc80bd","#ccebc5","#ffed6f"]},office:{Adjacency6:["#a9a57c","#9cbebd","#d2cb6c","#95a39d","#c89f5d","#b1a089"],Advantage6:["#663366","#330f42","#666699","#999966","#f7901e","#a3a101"],Angles6:["#797b7e","#f96a1b","#08a1d9","#7c984a","#c2ad8d","#506e94"],Apex6:["#ceb966","#9cb084","#6bb1c9","#6585cf","#7e6bc9","#a379bb"],Apothecary6:["#93a299","#cf543f","#b5ae53","#848058","#e8b54d","#786c71"],Aspect6:["#f07f09","#9f2936","#1b587c","#4e8542","#604878","#c19859"],Atlas6:["#f81b02","#fc7715","#afbf41","#50c49f","#3b95c4","#b560d4"],Austin6:["#94c600","#71685a","#ff6700","#909465","#956b43","#fea022"],Badge6:["#f8b323","#656a59","#46b2b5","#8caa7e","#d36f68","#826276"],Banded6:["#ffc000","#a5d028","#08cc78","#f24099","#828288","#f56617"],Basis6:["#f09415","#c1b56b","#4baf73","#5aa6c0","#d17df9","#fa7e5c"],Berlin6:["#a6b727","#df5327","#fe9e00","#418ab3","#d7d447","#818183"],BlackTie6:["#6f6f74","#a7b789","#beae98","#92a9b9","#9c8265","#8d6974"],Blue6:["#0f6fc6","#009dd9","#0bd0d9","#10cf9b","#7cca62","#a5c249"],BlueGreen6:["#3494ba","#58b6c0","#75bda7","#7a8c8e","#84acb6","#2683c6"],BlueII6:["#1cade4","#2683c6","#27ced7","#42ba97","#3e8853","#62a39f"],BlueRed6:["#4a66ac","#629dd1","#297fd5","#7f8fa9","#5aa2ae","#9d90a0"],BlueWarm6:["#4a66ac","#629dd1","#297fd5","#7f8fa9","#5aa2ae","#9d90a0"],Breeze6:["#2c7c9f","#244a58","#e2751d","#ffb400","#7eb606","#c00000"],Capital6:["#4b5a60","#9c5238","#504539","#c1ad79","#667559","#bad6ad"],Celestial6:["#ac3ec1","#477bd1","#46b298","#90ba4c","#dd9d31","#e25247"],Circuit6:["#9acd4c","#faa93a","#d35940","#b258d3","#63a0cc","#8ac4a7"],Civic6:["#d16349","#ccb400","#8cadae","#8c7b70","#8fb08c","#d19049"],Clarity6:["#93a299","#ad8f67","#726056","#4c5a6a","#808da0","#79463d"],Codex6:["#990000","#efab16","#78ac35","#35aca2","#4083cf","#0d335e"],Composite6:["#98c723","#59b0b9","#deae00","#b77bb4","#e0773c","#a98d63"],Concourse6:["#2da2bf","#da1f28","#eb641b","#39639d","#474b78","#7d3c4a"],Couture6:["#9e8e5c","#a09781","#85776d","#aeafa9","#8d878b","#6b6149"],Crop6:["#8c8d86","#e6c069","#897b61","#8dab8e","#77a2bb","#e28394"],Damask6:["#9ec544","#50bea3","#4a9ccc","#9a66ca","#c54f71","#de9c3c"],Depth6:["#41aebd","#97e9d5","#a2cf49","#608f3d","#f4de3a","#fcb11c"],Dividend6:["#4d1434","#903163","#b2324b","#969fa7","#66b1ce","#40619d"],Droplet6:["#2fa3ee","#4bcaad","#86c157","#d99c3f","#ce6633","#a35dd1"],Elemental6:["#629dd1","#297fd5","#7f8fa9","#4a66ac","#5aa2ae","#9d90a0"],Equity6:["#d34817","#9b2d1f","#a28e6a","#956251","#918485","#855d5d"],Essential6:["#7a7a7a","#f5c201","#526db0","#989aac","#dc5924","#b4b392"],Excel16:["#9999ff","#993366","#ffffcc","#ccffff","#660066","#ff8080","#0066cc","#ccccff","#000080","#ff00ff","#ffff00","#0000ff","#800080","#800000","#008080","#0000ff"],Executive6:["#6076b4","#9c5252","#e68422","#846648","#63891f","#758085"],Exhibit6:["#3399ff","#69ffff","#ccff33","#3333ff","#9933ff","#ff33ff"],Expo6:["#fbc01e","#efe1a2","#fa8716","#be0204","#640f10","#7e13e3"],Facet6:["#90c226","#54a021","#e6b91e","#e76618","#c42f1a","#918655"],Feathered6:["#606372","#79a8a4","#b2ad8f","#ad8082","#dec18c","#92a185"],Flow6:["#0f6fc6","#009dd9","#0bd0d9","#10cf9b","#7cca62","#a5c249"],Focus6:["#ffb91d","#f97817","#6de304","#ff0000","#732bea","#c913ad"],Folio6:["#294171","#748cbc","#8e887c","#834736","#5a1705","#a0a16a"],Formal6:["#907f76","#a46645","#cd9c47","#9a92cd","#7d639b","#733678"],Forte6:["#c70f0c","#dd6b0d","#faa700","#93e50d","#17c7ba","#0a96e4"],Foundry6:["#72a376","#b0ccb0","#a8cdd7","#c0beaf","#cec597","#e8b7b7"],Frame6:["#40bad2","#fab900","#90bb23","#ee7008","#1ab39f","#d5393d"],Gallery6:["#b71e42","#de478e","#bc72f0","#795faf","#586ea6","#6892a0"],Genesis6:["#80b606","#e29f1d","#2397e2","#35aca2","#5430bb","#8d34e0"],Grayscale6:["#dddddd","#b2b2b2","#969696","#808080","#5f5f5f","#4d4d4d"],Green6:["#549e39","#8ab833","#c0cf3a","#029676","#4ab5c4","#0989b1"],GreenYellow6:["#99cb38","#63a537","#37a76f","#44c1a3","#4eb3cf","#51c3f9"],Grid6:["#c66951","#bf974d","#928b70","#87706b","#94734e","#6f777d"],Habitat6:["#f8c000","#f88600","#f83500","#8b723d","#818b3d","#586215"],Hardcover6:["#873624","#d6862d","#d0be40","#877f6c","#972109","#aeb795"],Headlines6:["#439eb7","#e28b55","#dcb64d","#4ca198","#835b82","#645135"],Horizon6:["#7e97ad","#cc8e60","#7a6a60","#b4936d","#67787b","#9d936f"],Infusion6:["#8c73d0","#c2e8c4","#c5a6e8","#b45ec7","#9fdafb","#95c5b0"],Inkwell6:["#860908","#4a0505","#7a500a","#c47810","#827752","#b5bb83"],Inspiration6:["#749805","#bacc82","#6e9ec2","#2046a5","#5039c6","#7411d0"],Integral6:["#1cade4","#2683c6","#27ced7","#42ba97","#3e8853","#62a39f"],Ion6:["#b01513","#ea6312","#e6b729","#6aac90","#5f9c9d","#9e5e9b"],IonBoardroom6:["#b31166","#e33d6f","#e45f3c","#e9943a","#9b6bf2","#d53dd0"],Kilter6:["#76c5ef","#fea022","#ff6700","#70a525","#a5d848","#20768c"],Madison6:["#a1d68b","#5ec795","#4dadcf","#cdb756","#e29c36","#8ec0c1"],MainEvent6:["#b80e0f","#a6987d","#7f9a71","#64969f","#9b75b2","#80737a"],Marquee6:["#418ab3","#a6b727","#f69200","#838383","#fec306","#df5327"],Median6:["#94b6d2","#dd8047","#a5ab81","#d8b25c","#7ba79d","#968c8c"],Mesh6:["#6f6f6f","#bfbfa5","#dcd084","#e7bf5f","#e9a039","#cf7133"],Metail6:["#6283ad","#324966","#5b9ea4","#1d5b57","#1b4430","#2f3c35"],Metro6:["#7fd13b","#ea157a","#feb80a","#00addc","#738ac8","#1ab39f"],Metropolitan6:["#50b4c8","#a8b97f","#9b9256","#657689","#7a855d","#84ac9d"],Module6:["#f0ad00","#60b5cc","#e66c7d","#6bb76d","#e88651","#c64847"],NewsPrint6:["#ad0101","#726056","#ac956e","#808da9","#424e5b","#730e00"],Office6:["#5b9bd5","#ed7d31","#a5a5a5","#ffc000","#4472c4","#70ad47"],"Office2007-2010-6":["#4f81bd","#c0504d","#9bbb59","#8064a2","#4bacc6","#f79646"],Opulent6:["#b83d68","#ac66bb","#de6c36","#f9b639","#cf6da4","#fa8d3d"],Orange6:["#e48312","#bd582c","#865640","#9b8357","#c2bc80","#94a088"],OrangeRed6:["#d34817","#9b2d1f","#a28e6a","#956251","#918485","#855d5d"],Orbit6:["#f2d908","#9de61e","#0d8be6","#c61b1b","#e26f08","#8d35d1"],Organic6:["#83992a","#3c9770","#44709d","#a23c33","#d97828","#deb340"],Oriel6:["#fe8637","#7598d9","#b32c16","#f5cd2d","#aebad5","#777c84"],Origin6:["#727ca3","#9fb8cd","#d2da7a","#fada7a","#b88472","#8e736a"],Paper6:["#a5b592","#f3a447","#e7bc29","#d092a7","#9c85c0","#809ec2"],Parallax6:["#30acec","#80c34f","#e29d3e","#d64a3b","#d64787","#a666e1"],Parcel6:["#f6a21d","#9bafb5","#c96731","#9ca383","#87795d","#a0988c"],Perception6:["#a2c816","#e07602","#e4c402","#7dc1ef","#21449b","#a2b170"],Perspective6:["#838d9b","#d2610c","#80716a","#94147c","#5d5ad2","#6f6c7d"],Pixel6:["#ff7f01","#f1b015","#fbec85","#d2c2f1","#da5af4","#9d09d1"],Plaza6:["#990000","#580101","#e94a00","#eb8f00","#a4a4a4","#666666"],Precedent6:["#993232","#9b6c34","#736c5d","#c9972b","#c95f2b","#8f7a05"],Pushpin6:["#fda023","#aa2b1e","#71685c","#64a73b","#eb5605","#b9ca1a"],Quotable6:["#00c6bb","#6feba0","#b6df5e","#efb251","#ef755f","#ed515c"],Red6:["#a5300f","#d55816","#e19825","#b19c7d","#7f5f52","#b27d49"],RedOrange6:["#e84c22","#ffbd47","#b64926","#ff8427","#cc9900","#b22600"],RedViolet6:["#e32d91","#c830cc","#4ea6dc","#4775e7","#8971e1","#d54773"],Retrospect6:["#e48312","#bd582c","#865640","#9b8357","#c2bc80","#94a088"],Revolution6:["#0c5986","#ddf53d","#508709","#bf5e00","#9c0001","#660075"],Saddle6:["#c6b178","#9c5b14","#71b2bc","#78aa5d","#867099","#4c6f75"],Savon6:["#1cade4","#2683c6","#27ced7","#42ba97","#3e8853","#62a39f"],Sketchbook6:["#a63212","#e68230","#9bb05e","#6b9bc7","#4e66b2","#8976ac"],Sky6:["#073779","#8fd9fb","#ffcc00","#eb6615","#c76402","#b523b4"],Slate6:["#bc451b","#d3ba68","#bb8640","#ad9277","#a55a43","#ad9d7b"],Slice6:["#052f61","#a50e82","#14967c","#6a9e1f","#e87d37","#c62324"],Slipstream6:["#4e67c8","#5eccf3","#a7ea52","#5dceaf","#ff8021","#f14124"],SOHO6:["#61625e","#964d2c","#66553e","#848058","#afa14b","#ad7d4d"],Solstice6:["#3891a7","#feb80a","#c32d2e","#84aa33","#964305","#475a8d"],Spectrum6:["#990000","#ff6600","#ffba00","#99cc00","#528a02","#333333"],Story6:["#1d86cd","#732e9a","#b50b1b","#e8950e","#55992b","#2c9c89"],Studio6:["#f7901e","#fec60b","#9fe62f","#4ea5d1","#1c4596","#542d90"],Summer6:["#51a6c2","#51c2a9","#7ec251","#e1dc53","#b54721","#a16bb1"],Technic6:["#6ea0b0","#ccaf0a","#8d89a4","#748560","#9e9273","#7e848d"],Thatch6:["#759aa5","#cfc60d","#99987f","#90ac97","#ffad1c","#b9ab6f"],Tradition6:["#6b4a0b","#790a14","#908342","#423e5c","#641345","#748a2f"],Travelogue6:["#b74d21","#a32323","#4576a3","#615d9a","#67924b","#bf7b1b"],Trek6:["#f0a22e","#a5644e","#b58b80","#c3986d","#a19574","#c17529"],Twilight6:["#e8bc4a","#83c1c6","#e78d35","#909ce1","#839c41","#cc5439"],Urban6:["#53548a","#438086","#a04da3","#c4652d","#8b5d3d","#5c92b5"],UrbanPop6:["#86ce24","#00a2e6","#fac810","#7d8f8c","#d06b20","#958b8b"],VaporTrail6:["#df2e28","#fe801a","#e9bf35","#81bb42","#32c7a9","#4a9bdc"],Venture6:["#9eb060","#d09a08","#f2ec86","#824f1c","#511818","#553876"],Verve6:["#ff388c","#e40059","#9c007f","#68007f","#005bd3","#00349e"],View6:["#6f6f74","#92a9b9","#a7b789","#b9a489","#8d6374","#9b7362"],Violet6:["#ad84c6","#8784c7","#5d739a","#6997af","#84acb6","#6f8183"],VioletII6:["#92278f","#9b57d3","#755dd9","#665eb8","#45a5ed","#5982db"],Waveform6:["#31b6fd","#4584d3","#5bd078","#a5d028","#f5c040","#05e0db"],Wisp6:["#a53010","#de7e18","#9f8351","#728653","#92aa4c","#6aac91"],WoodType6:["#d34817","#9b2d1f","#a28e6a","#956251","#918485","#855d5d"],Yellow6:["#ffca08","#f8931d","#ce8d3e","#ec7016","#e64823","#9c6a6a"],YellowOrange6:["#f0a22e","#a5644e","#b58b80","#c3986d","#a19574","#c17529"]},tableau:{Tableau10:["#4E79A7","#F28E2B","#E15759","#76B7B2","#59A14F","#EDC948","#B07AA1","#FF9DA7","#9C755F","#BAB0AC"],Tableau20:["#4E79A7","#A0CBE8","#F28E2B","#FFBE7D","#59A14F","#8CD17D","#B6992D","#F1CE63","#499894","#86BCB6","#E15759","#FF9D9A","#79706E","#BAB0AC","#D37295","#FABFD2","#B07AA1","#D4A6C8","#9D7660","#D7B5A6"],ColorBlind10:["#1170aa","#fc7d0b","#a3acb9","#57606c","#5fa2ce","#c85200","#7b848f","#a3cce9","#ffbc79","#c8d0d9"],SeattleGrays5:["#767f8b","#b3b7b8","#5c6068","#d3d3d3","#989ca3"],Traffic9:["#b60a1c","#e39802","#309143","#e03531","#f0bd27","#51b364","#ff684c","#ffda66","#8ace7e"],MillerStone11:["#4f6980","#849db1","#a2ceaa","#638b66","#bfbb60","#f47942","#fbb04e","#b66353","#d7ce9f","#b9aa97","#7e756d"],SuperfishelStone10:["#6388b4","#ffae34","#ef6f6a","#8cc2ca","#55ad89","#c3bc3f","#bb7693","#baa094","#a9b5ae","#767676"],NurielStone9:["#8175aa","#6fb899","#31a1b3","#ccb22b","#a39fc9","#94d0c0","#959c9e","#027b8e","#9f8f12"],JewelBright9:["#eb1e2c","#fd6f30","#f9a729","#f9d23c","#5fbb68","#64cdcc","#91dcea","#a4a4d5","#bbc9e5"],Summer8:["#bfb202","#b9ca5d","#cf3e53","#f1788d","#00a2b3","#97cfd0","#f3a546","#f7c480"],Winter10:["#90728f","#b9a0b4","#9d983d","#cecb76","#e15759","#ff9888","#6b6b6b","#bab2ae","#aa8780","#dab6af"],GreenOrangeTeal12:["#4e9f50","#87d180","#ef8a0c","#fcc66d","#3ca8bc","#98d9e4","#94a323","#c3ce3d","#a08400","#f7d42a","#26897e","#8dbfa8"],RedBlueBrown12:["#466f9d","#91b3d7","#ed444a","#feb5a2","#9d7660","#d7b5a6","#3896c4","#a0d4ee","#ba7e45","#39b87f","#c8133b","#ea8783"],PurplePinkGray12:["#8074a8","#c6c1f0","#c46487","#ffbed1","#9c9290","#c5bfbe","#9b93c9","#ddb5d5","#7c7270","#f498b6","#b173a0","#c799bc"],HueCircle19:["#1ba3c6","#2cb5c0","#30bcad","#21B087","#33a65c","#57a337","#a2b627","#d5bb21","#f8b620","#f89217","#f06719","#e03426","#f64971","#fc719e","#eb73b3","#ce69be","#a26dc2","#7873c0","#4f7cba"],OrangeBlue7:["#9e3d22","#d45b21","#f69035","#d9d5c9","#77acd3","#4f81af","#2b5c8a"],RedGreen7:["#a3123a","#e33f43","#f8816b","#ced7c3","#73ba67","#44914e","#24693d"],GreenBlue7:["#24693d","#45934d","#75bc69","#c9dad2","#77a9cf","#4e7fab","#2a5783"],RedBlue7:["#a90c38","#e03b42","#f87f69","#dfd4d1","#7eaed3","#5383af","#2e5a87"],RedBlack7:["#ae123a","#e33e43","#f8816b","#d9d9d9","#a0a7a8","#707c83","#49525e"],GoldPurple7:["#ad9024","#c1a33b","#d4b95e","#e3d8cf","#d4a3c3","#c189b0","#ac7299"],RedGreenGold7:["#be2a3e","#e25f48","#f88f4d","#f4d166","#90b960","#4b9b5f","#22763f"],SunsetSunrise7:["#33608c","#9768a5","#e7718a","#f6ba57","#ed7846","#d54c45","#b81840"],OrangeBlueWhite7:["#9e3d22","#e36621","#fcad52","#ffffff","#95c5e1","#5b8fbc","#2b5c8a"],RedGreenWhite7:["#ae123a","#ee574d","#fdac9e","#ffffff","#91d183","#539e52","#24693d"],GreenBlueWhite7:["#24693d","#529c51","#8fd180","#ffffff","#95c1dd","#598ab5","#2a5783"],RedBlueWhite7:["#a90c38","#ec534b","#feaa9a","#ffffff","#9ac4e1","#5c8db8","#2e5a87"],RedBlackWhite7:["#ae123a","#ee574d","#fdac9d","#ffffff","#bdc0bf","#7d888d","#49525e"],OrangeBlueLight7:["#ffcc9e","#f9d4b6","#f0dccd","#e5e5e5","#dae1ea","#cfdcef","#c4d8f3"],Temperature7:["#529985","#6c9e6e","#99b059","#dbcf47","#ebc24b","#e3a14f","#c26b51"],BlueGreen7:["#feffd9","#f2fabf","#dff3b2","#c4eab1","#94d6b7","#69c5be","#41b7c4"],BlueLight7:["#e5e5e5","#e0e3e8","#dbe1ea","#d5dfec","#d0dcef","#cadaf1","#c4d8f3"],OrangeLight7:["#e5e5e5","#ebe1d9","#f0ddcd","#f5d9c2","#f9d4b6","#fdd0aa","#ffcc9e"],Blue20:["#b9ddf1","#afd6ed","#a5cfe9","#9bc7e4","#92c0df","#89b8da","#80b0d5","#79aacf","#72a3c9","#6a9bc3","#6394be","#5b8cb8","#5485b2","#4e7fac","#4878a6","#437a9f","#3d6a98","#376491","#305d8a","#2a5783"],Orange20:["#ffc685","#fcbe75","#f9b665","#f7ae54","#f5a645","#f59c3c","#f49234","#f2882d","#f07e27","#ee7422","#e96b20","#e36420","#db5e20","#d25921","#ca5422","#c14f22","#b84b23","#af4623","#a64122","#9e3d22"],Green20:["#b3e0a6","#a5db96","#98d687","#8ed07f","#85ca77","#7dc370","#75bc69","#6eb663","#67af5c","#61a956","#59a253","#519c51","#49964f","#428f4d","#398949","#308344","#2b7c40","#27763d","#256f3d","#24693d"],Red20:["#ffbeb2","#feb4a6","#fdab9b","#fca290","#fb9984","#fa8f79","#f9856e","#f77b66","#f5715d","#f36754","#f05c4d","#ec5049","#e74545","#e13b42","#da323f","#d3293d","#ca223c","#c11a3b","#b8163a","#ae123a"],Purple20:["#eec9e5","#eac1df","#e6b9d9","#e0b2d2","#daabcb","#d5a4c4","#cf9dbe","#ca96b8","#c48fb2","#be89ac","#b882a6","#b27ba1","#aa759d","#a27099","#9a6a96","#926591","#8c5f86","#865986","#81537f","#7c4d79"],Brown20:["#eedbbd","#ecd2ad","#ebc994","#eac085","#e8b777","#e5ae6c","#e2a562","#de9d5a","#d99455","#d38c54","#ce8451","#c9784d","#c47247","#c16941","#bd6036","#b85636","#b34d34","#ad4433","#a63d32","#9f3632"],Gray20:["#d5d5d5","#cdcecd","#c5c7c6","#bcbfbe","#b4b7b7","#acb0b1","#a4a9ab","#9ca3a4","#939c9e","#8b9598","#848e93","#7c878d","#758087","#6e7a81","#67737c","#616c77","#5b6570","#555f6a","#4f5864","#49525e"],GrayWarm20:["#dcd4d0","#d4ccc8","#cdc4c0","#c5bdb9","#beb6b2","#b7afab","#b0a7a4","#a9a09d","#a29996","#9b938f","#948c88","#8d8481","#867e7b","#807774","#79706e","#736967","#6c6260","#665c51","#5f5654","#59504e"],BlueTeal20:["#bce4d8","#aedcd5","#a1d5d2","#95cecf","#89c8cc","#7ec1ca","#72bac6","#66b2c2","#59acbe","#4ba5ba","#419eb6","#3b96b2","#358ead","#3586a7","#347ea1","#32779b","#316f96","#2f6790","#2d608a","#2c5985"],OrangeGold20:["#f4d166","#f6c760","#f8bc58","#f8b252","#f7a84a","#f69e41","#f49538","#f38b2f","#f28026","#f0751e","#eb6c1c","#e4641e","#de5d1f","#d75521","#cf4f22","#c64a22","#bc4623","#b24223","#a83e24","#9e3a26"],GreenGold20:["#f4d166","#e3cd62","#d3c95f","#c3c55d","#b2c25b","#a3bd5a","#93b958","#84b457","#76af56","#67a956","#5aa355","#4f9e53","#479751","#40914f","#3a8a4d","#34844a","#2d7d45","#257740","#1c713b","#146c36"],RedGold21:["#f4d166","#f5c75f","#f6bc58","#f7b254","#f9a750","#fa9d4f","#fa9d4f","#fb934d","#f7894b","#f47f4a","#f0774a","#eb6349","#e66549","#e15c48","#dc5447","#d64c45","#d04344","#ca3a42","#c43141","#bd273f","#b71d3e"],Classic10:["#1f77b4","#ff7f0e","#2ca02c","#d62728","#9467bd","#8c564b","#e377c2","#7f7f7f","#bcbd22","#17becf"],ClassicMedium10:["#729ece","#ff9e4a","#67bf5c","#ed665d","#ad8bc9","#a8786e","#ed97ca","#a2a2a2","#cdcc5d","#6dccda"],ClassicLight10:["#aec7e8","#ffbb78","#98df8a","#ff9896","#c5b0d5","#c49c94","#f7b6d2","#c7c7c7","#dbdb8d","#9edae5"],Classic20:["#1f77b4","#aec7e8","#ff7f0e","#ffbb78","#2ca02c","#98df8a","#d62728","#ff9896","#9467bd","#c5b0d5","#8c564b","#c49c94","#e377c2","#f7b6d2","#7f7f7f","#c7c7c7","#bcbd22","#dbdb8d","#17becf","#9edae5"],ClassicGray5:["#60636a","#a5acaf","#414451","#8f8782","#cfcfcf"],ClassicColorBlind10:["#006ba4","#ff800e","#ababab","#595959","#5f9ed1","#c85200","#898989","#a2c8ec","#ffbc79","#cfcfcf"],ClassicTrafficLight9:["#b10318","#dba13a","#309343","#d82526","#ffc156","#69b764","#f26c64","#ffdd71","#9fcd99"],ClassicPurpleGray6:["#7b66d2","#dc5fbd","#94917b","#995688","#d098ee","#d7d5c5"],ClassicPurpleGray12:["#7b66d2","#a699e8","#dc5fbd","#ffc0da","#5f5a41","#b4b19b","#995688","#d898ba","#ab6ad5","#d098ee","#8b7c6e","#dbd4c5"],ClassicGreenOrange6:["#32a251","#ff7f0f","#3cb7cc","#ffd94a","#39737c","#b85a0d"],ClassicGreenOrange12:["#32a251","#acd98d","#ff7f0f","#ffb977","#3cb7cc","#98d9e4","#b85a0d","#ffd94a","#39737c","#86b4a9","#82853b","#ccc94d"],ClassicBlueRed6:["#2c69b0","#f02720","#ac613c","#6ba3d6","#ea6b73","#e9c39b"],ClassicBlueRed12:["#2c69b0","#b5c8e2","#f02720","#ffb6b0","#ac613c","#e9c39b","#6ba3d6","#b5dffd","#ac8763","#ddc9b4","#bd0a36","#f4737a"],ClassicCyclic13:["#1f83b4","#12a2a8","#2ca030","#78a641","#bcbd22","#ffbf50","#ffaa0e","#ff7f0e","#d63a3a","#c7519c","#ba43b4","#8a60b0","#6f63bb"],ClassicGreen7:["#bccfb4","#94bb83","#69a761","#339444","#27823b","#1a7232","#09622a"],ClassicGray13:["#c3c3c3","#b2b2b2","#a2a2a2","#929292","#838383","#747474","#666666","#585858","#4b4b4b","#3f3f3f","#333333","#282828","#1e1e1e"],ClassicBlue7:["#b4d4da","#7bc8e2","#67add4","#3a87b7","#1c73b1","#1c5998","#26456e"],ClassicRed9:["#eac0bd","#f89a90","#f57667","#e35745","#d8392c","#cf1719","#c21417","#b10c1d","#9c0824"],ClassicOrange7:["#f0c294","#fdab67","#fd8938","#f06511","#d74401","#a33202","#7b3014"],ClassicAreaRed11:["#f5cac7","#fbb3ab","#fd9c8f","#fe8b7a","#fd7864","#f46b55","#ea5e45","#e04e35","#d43e25","#c92b14","#bd1100"],ClassicAreaGreen11:["#dbe8b4","#c3e394","#acdc7a","#9ad26d","#8ac765","#7abc5f","#6cae59","#60a24d","#569735","#4a8c1c","#3c8200"],ClassicAreaBrown11:["#f3e0c2","#f6d29c","#f7c577","#f0b763","#e4aa63","#d89c63","#cc8f63","#c08262","#bb7359","#bb6348","#bb5137"],ClassicRedGreen11:["#9c0824","#bd1316","#d11719","#df513f","#fc8375","#cacaca","#a2c18f","#69a761","#2f8e41","#1e7735","#09622a"],ClassicRedBlue11:["#9c0824","#bd1316","#d11719","#df513f","#fc8375","#cacaca","#67add4","#3a87b7","#1c73b1","#1c5998","#26456e"],ClassicRedBlack11:["#9c0824","#bd1316","#d11719","#df513f","#fc8375","#cacaca","#9b9b9b","#777777","#565656","#383838","#1e1e1e"],ClassicAreaRedGreen21:["#bd1100","#c82912","#d23a21","#dc4930","#e6583e","#ef654d","#f7705b","#fd7e6b","#fe8e7e","#fca294","#e9dabe","#c7e298","#b1de7f","#a0d571","#90cb68","#82c162","#75b65d","#69aa56","#5ea049","#559633","#4a8c1c"],ClassicOrangeBlue13:["#7b3014","#a33202","#d74401","#f06511","#fd8938","#fdab67","#cacaca","#7bc8e2","#67add4","#3a87b7","#1c73b1","#1c5998","#26456e"],ClassicGreenBlue11:["#09622a","#1e7735","#2f8e41","#69a761","#a2c18f","#cacaca","#67add4","#3a87b7","#1c73b1","#1c5998","#26456e"],ClassicRedWhiteGreen11:["#9c0824","#b41f27","#cc312b","#e86753","#fcb4a5","#ffffff","#b9d7b7","#74af72","#428f49","#297839","#09622a"],ClassicRedWhiteBlack11:["#9c0824","#b41f27","#cc312b","#e86753","#fcb4a5","#ffffff","#bfbfbf","#838383","#575757","#393939","#1e1e1e"],ClassicOrangeWhiteBlue11:["#7b3014","#a84415","#d85a13","#fb8547","#ffc2a1","#ffffff","#b7cde2","#6a9ec5","#3679a8","#2e5f8a","#26456e"],ClassicRedWhiteBlackLight10:["#ffc2c5","#ffd1d3","#ffe0e1","#fff0f0","#ffffff","#f3f3f3","#e8e8e8","#dddddd","#d1d1d1","#c6c6c6"],ClassicOrangeWhiteBlueLight11:["#ffcc9e","#ffd6b1","#ffe0c5","#ffead8","#fff5eb","#ffffff","#f3f7fd","#e8effa","#dce8f8","#d0e0f6","#c4d8f3"],ClassicRedWhiteGreenLight11:["#ffb2b6","#ffc2c5","#ffd1d3","#ffe0e1","#fff0f0","#ffffff","#f1faed","#e3f5db","#d5f0ca","#c6ebb8","#b7e6a7"],ClassicRedGreenLight11:["#ffb2b6","#fcbdc0","#f8c7c9","#f2d1d2","#ecdbdc","#e5e5e5","#dde6d9","#d4e6cc","#cae6c0","#c1e6b4","#b7e6a7"]}},n=f.helpers,c=2===f.DatasetController.prototype.removeHoverStyle.length;f.defaults.global.plugins.colorschemes={scheme:"brewer.Paired12",fillAlpha:.5,reverse:!1};var e={id:"colorschemes",beforeUpdate:function(d,c){var a,b,r,l,f=c.scheme.split("."),e=o[f[0]];return e&&(a=e[f[1]],b=a.length,a&&d.config.data.datasets.forEach(function(f,e){switch(r=e%b,l=a[c.reverse?b-r-1:r],f.colorschemes={},f.type||d.config.type){case"line":case"radar":case"scatter":void 0===f.backgroundColor&&(f.backgroundColor=n.color(l).alpha(c.fillAlpha).rgbString(),f.colorschemes.backgroundColor=!0),void 0===f.borderColor&&(f.borderColor=l,f.colorschemes.borderColor=!0),void 0===f.pointBackgroundColor&&(f.pointBackgroundColor=n.color(l).alpha(c.fillAlpha).rgbString(),f.colorschemes.pointBackgroundColor=!0),void 0===f.pointBorderColor&&(f.pointBorderColor=l,f.colorschemes.pointBorderColor=!0);break;case"doughnut":case"pie":void 0===f.backgroundColor&&(f.backgroundColor=f.data.map(function(f,e){return r=e%b,a[c.reverse?b-r-1:r]}),f.colorschemes.backgroundColor=!0);break;default:void 0===f.backgroundColor&&(f.backgroundColor=l,f.colorschemes.backgroundColor=!0)}})),!0},afterUpdate:function(f){f.config.data.datasets.forEach(function(f){f.colorschemes&&(f.colorschemes.backgroundColor&&delete f.backgroundColor,f.colorschemes.borderColor&&delete f.borderColor,f.colorschemes.pointBackgroundColor&&delete f.pointBackgroundColor,f.colorschemes.pointBorderColor&&delete f.pointBorderColor,delete f.colorschemes)})},beforeEvent:function(f,e,d){return c&&this.beforeUpdate(f,d),!0},afterEvent:function(f){c&&this.afterUpdate(f)}};return f.colorschemes=o,f.plugins.register(e),e}); \ No newline at end of file diff --git a/app/models/stats/actions.rb b/app/models/stats/actions.rb index 85179394..ffeb6b53 100644 --- a/app/models/stats/actions.rb +++ b/app/models/stats/actions.rb @@ -69,6 +69,11 @@ module Stats # get percentage done cumulative @cum_percent_done = convert_to_cumulative_array(@actions_running_time_array, @actions_running_time.count ) + + return [ + {name: "Percentage", data: @cum_percent_done.each_with_index.map { |total, week| [week, total] } , type: "line"}, + {name: "Actions", data: @actions_running_time_array.each_with_index.map { |total, week| [week, total] } } + ] end def open_per_week_data @@ -101,10 +106,13 @@ module Stats @actions_completion_day.each { |t| @actions_completion_day_array[ t.completed_at.wday ] += 1 } # FIXME: Day of week as string instead of number - return [ - {name: "Created", data: @actions_creation_day_array.each_with_index.map { |total, day| [day, total] } }, - {name: "Completed", data: @actions_completion_day_array.each_with_index.map { |total, day| [day, total] } } - ] + return { + datasets: [ + {label: "Created", data: @actions_creation_day_array.map { |total| [total] } }, + {label: "Completed", data: @actions_completion_day_array.map { |total| [total] } } + ], + labels: @actions_creation_day_array.each_with_index.map { |total, day| [day] } + } end def day_of_week_30days_data @@ -121,10 +129,13 @@ module Stats @actions_completion_day.each { |r| @actions_completion_day_array[r.completed_at.wday] += 1 } # FIXME: Day of week as string instead of number - return [ - {name: "Created", data: @actions_creation_day_array.each_with_index.map { |total, day| [day, total] } }, - {name: "Completed", data: @actions_completion_day_array.each_with_index.map { |total, day| [day, total] } } - ] + return { + datasets: [ + {label: "Created", data: @actions_creation_day_array.map { |total| [total] } }, + {label: "Completed", data: @actions_completion_day_array.map { |total| [total] } } + ], + labels: @actions_creation_day_array.each_with_index.map { |total, day| [day] } + } end def time_of_day_all_data @@ -139,10 +150,13 @@ module Stats @actions_completion_hour_array = Array.new(24) { |i| 0} @actions_completion_hour.each{|r| @actions_completion_hour_array[r.completed_at.hour] += 1 } - return [ - {name: "Created", data: @actions_creation_hour_array.each_with_index.map { |total, hour| [hour, total] } }, - {name: "Completed", data: @actions_completion_hour_array.each_with_index.map { |total, hour| [hour, total] } } - ] + return { + datasets: [ + {label: "Created", data: @actions_creation_hour_array.map { |total| [total] } }, + {label: "Completed", data: @actions_completion_hour_array.map { |total| [total] } } + ], + labels: @actions_creation_hour_array.each_with_index.map { |total, hour| [hour] } + } end def time_of_day_30days_data @@ -157,10 +171,13 @@ module Stats @actions_completion_hour_array = Array.new(24) { |i| 0} @actions_completion_hour.each{|r| @actions_completion_hour_array[r.completed_at.hour] += 1 } - return [ - {name: "Created", data: @actions_creation_hour_array.each_with_index.map { |total, hour| [hour, total] } }, - {name: "Completed", data: @actions_completion_hour_array.each_with_index.map { |total, hour| [hour, total] } } - ] + return { + datasets: [ + {label: "Created", data: @actions_creation_hour_array.map { |total| [total] } }, + {label: "Completed", data: @actions_completion_hour_array.map { |total| [total] } } + ], + labels: @actions_creation_hour_array.each_with_index.map { |total, hour| [hour] } + } end private @@ -218,5 +235,26 @@ module Stats end_week.upto(start_week) { |i| a << i }; return a end + + def convert_to_weeks_from_today_array(records, array_size, date_method_on_todo) + return convert_to_array(records, array_size) { |r| [difference_in_weeks(@today, r.send(date_method_on_todo))]} + end + + def cut_off_array_with_sum(array, cut_off) + # +1 to hold sum of rest + a = Array.new(cut_off+1){|i| array[i]||0} + # add rest of array to last elem + a[cut_off] += array.inject(:+) - a.inject(:+) + return a + end + + def convert_to_cumulative_array(array, max) + # calculate fractions + a = Array.new(array.size){|i| array[i]*100.0/max} + # make cumulative + 1.upto(array.size-1){ |i| a[i] += a[i-1] } + return a + end + end end diff --git a/app/views/stats/_actions.html.erb b/app/views/stats/_actions.html.erb index d1259ca0..6e2d37e2 100644 --- a/app/views/stats/_actions.html.erb +++ b/app/views/stats/_actions.html.erb @@ -1,4 +1,17 @@ <%= render :partial => 'time_to_complete', :locals => {:ttc => actions.ttc} -%> +<% +options = { + width: "400px", + height: "400px", + maintainAspectRatio: false, + responsive: false, + plugins: { + colorschemes: { + scheme: 'brewer.Paired12' + } + } +} +%>

<%= t('stats.actions_actions_avg_created_30days', :count => (actions.created_last30days*10.0/30.0).round/10.0 )%> <%= t('stats.actions_avg_completed_30days', :count => (actions.done_last30days*10.0/30.0).round/10.0 )%> @@ -15,21 +28,21 @@ render :partial => 'chart', :locals => {:chart => chart} -%><% end %> -

Current running time of all incomplete actions

-<%= column_chart actions.running_time_data %> +<% + Rails.logger.info actions.running_time_data +%> -

Active (visible and hidden) next actions per week

-<%= column_chart actions.open_per_week_data, xtitle: "Weeks ago" %> +<%= bar_chart actions.running_time_data, library: { :series => { 0 => {type: "line"} } }, title: "Current running time of all incomplete actions" %> -

Day of week (all actions)

-<%= column_chart actions.day_of_week_all_data %> +
-

Day of week (past 30 days)

-<%= column_chart actions.day_of_week_30days_data %> +<%= bar_chart actions.open_per_week_data, options.merge({scales: {yAxes: [{ scaleLabel: { display: true, labelString: 'Weeks ago'}}]}, 'title': {'display': true, 'text': 'Active (visible and hidden) next actions per week'}}) %> -

Time of day (all actions)

-<%= column_chart actions.time_of_day_all_data %> +<%= bar_chart actions.day_of_week_all_data, options.merge({'title': {'display': true, 'text': 'Day of week (all actions)'}}) %> -

Time of day (last 30 days)

-<%= column_chart actions.time_of_day_30days_data %> +<%= bar_chart actions.day_of_week_30days_data, options.merge({'title': {'display': true, 'text': 'Day of week (past 30 days)'}}) %> + +<%= bar_chart actions.time_of_day_all_data, options.merge({'title': {'display': true, 'text': 'Time of day (all actions)'}}) %> + +<%= bar_chart actions.time_of_day_30days_data, options.merge({'title': {'display': true, 'text': 'Time of day (last 30 days)'}}) %> diff --git a/app/views/stats/_contexts.html.erb b/app/views/stats/_contexts.html.erb index e0af588a..2f1d2aa7 100644 --- a/app/views/stats/_contexts.html.erb +++ b/app/views/stats/_contexts.html.erb @@ -1,14 +1,41 @@
-

Spread of actions for all contexts

-<%= pie_chart Stats::TopContextsQuery.new(current_user).result.map { |context| - [context.name, context.total] -} %> +<% data = { + datasets: [{ + data: Array.new + }], + labels: Array.new +} +Stats::TopContextsQuery.new(current_user).result.map { |context| + data[:datasets][0][:data].append(context.total) + data[:labels].append(context.name) +} +options = { + width: "400px", + height: "400px", + maintainAspectRatio: false, + responsive: false, + plugins: { + colorschemes: { + scheme: 'brewer.Paired12' + } + } +} +%> +<%= pie_chart data, options.merge({'title': {'display': true, 'text': 'Spread of actions for all contexts'}}) %> -

Spread of actions for visible contexts

-<%= pie_chart Stats::TopContextsQuery.new(current_user, :running => true).result.map { |context| - [context.name, context.total] -} %> +<% data = { + datasets: [{ + data: Array.new + }], + labels: Array.new + } +Stats::TopContextsQuery.new(current_user, :running => true).result.map { |context| + data[:datasets][0][:data].append(context.total) + data[:labels].append(context.name) +} +%> +<%= pie_chart data, options.merge({'title': {'display': true, 'text': 'Spread of actions for visible contexts'}}) %> <%= render :partial => 'contexts_list', :locals => {:contexts => contexts.actions, :key => 'contexts'} -%> From f873a93eb37859fa605b566f33e4c74e7ba1013f Mon Sep 17 00:00:00 2001 From: Jyri-Petteri Paloposki Date: Sun, 19 May 2019 13:04:33 +0300 Subject: [PATCH 03/15] #1153: Convert rest of the stats to use ChartJS --- app/assets/stylesheets/legacy.scss | 2 +- app/controllers/stats_controller.rb | 208 ------------------------- app/models/stats/actions.rb | 228 +++++++++++++++++++++++----- app/views/stats/_actions.html.erb | 21 ++- app/views/stats/_contexts.html.erb | 2 + 5 files changed, 206 insertions(+), 255 deletions(-) diff --git a/app/assets/stylesheets/legacy.scss b/app/assets/stylesheets/legacy.scss index dd9519eb..3031810f 100644 --- a/app/assets/stylesheets/legacy.scss +++ b/app/assets/stylesheets/legacy.scss @@ -1266,7 +1266,7 @@ button.positive, .widgets a.positive{ background-color:black; } -.stats_content .open-flash-chart, .stats_content .stats_module { +.stats_content .chart, .stats_content .stats_module { float: left; width: 450px; margin-right:20px; diff --git a/app/controllers/stats_controller.rb b/app/controllers/stats_controller.rb index 2f6aaf84..70bf697b 100644 --- a/app/controllers/stats_controller.rb +++ b/app/controllers/stats_controller.rb @@ -11,42 +11,6 @@ class StatsController < ApplicationController @stats = Stats::UserStats.new(current_user) end - def actions_done_last12months_data - # get actions created and completed in the past 12+3 months. +3 for running - # - outermost set of entries needed for these calculations - actions_last12months = current_user.todos.created_or_completed_after(@cut_off_year_plus3).select("completed_at,created_at") - - # convert to array and fill in non-existing months - @actions_done_last12months_array = put_events_into_month_buckets(actions_last12months, 13, :completed_at) - @actions_created_last12months_array = put_events_into_month_buckets(actions_last12months, 13, :created_at) - - # find max for graph in both arrays - @max = (@actions_done_last12months_array + @actions_created_last12months_array).max - - # find running avg - done_in_last_15_months = put_events_into_month_buckets(actions_last12months, 16, :completed_at) - created_in_last_15_months = put_events_into_month_buckets(actions_last12months, 16, :created_at) - - @actions_done_avg_last12months_array = compute_running_avg_array(done_in_last_15_months, 13) - @actions_created_avg_last12months_array = compute_running_avg_array(created_in_last_15_months, 13) - - # interpolate avg for current month. - @interpolated_actions_created_this_month = interpolate_avg_for_current_month(@actions_created_last12months_array) - @interpolated_actions_done_this_month = interpolate_avg_for_current_month(@actions_done_last12months_array) - - @created_count_array = Array.new(13, actions_last12months.created_after(@cut_off_year).count(:all)/12.0) - @done_count_array = Array.new(13, actions_last12months.completed_after(@cut_off_year).count(:all)/12.0) - render :layout => false - end - - def interpolate_avg_for_current_month(set) - (set[0]*(1/percent_of_month) + set[1] + set[2]) / 3.0 - end - - def percent_of_month - Time.zone.now.day / Time.zone.now.end_of_month.day.to_f - end - def actions_done_last_years @page_title = t('stats.index_title') @chart = Stats::Chart.new('actions_done_lastyears_data', :height => 400, :width => 900) @@ -80,98 +44,6 @@ class StatsController < ApplicationController render :layout => false end - def actions_done_last30days_data - # get actions created and completed in the past 30 days. - @actions_done_last30days = current_user.todos.completed_after(@cut_off_30days).select("completed_at") - @actions_created_last30days = current_user.todos.created_after(@cut_off_30days).select("created_at") - - # convert to array. 30+1 to have 30 complete days and one current day [0] - @actions_done_last30days_array = convert_to_days_from_today_array(@actions_done_last30days, 31, :completed_at) - @actions_created_last30days_array = convert_to_days_from_today_array(@actions_created_last30days, 31, :created_at) - - # find max for graph in both hashes - @max = [@actions_done_last30days_array.max, @actions_created_last30days_array.max].max - - render :layout => false - end - - def actions_completion_time_data - @actions_completion_time = current_user.todos.completed.select("completed_at, created_at").reorder("completed_at DESC" ) - - # convert to array and fill in non-existing weeks with 0 - @max_weeks = @actions_completion_time.last ? difference_in_weeks(@today, @actions_completion_time.last.completed_at) : 1 - @actions_completed_per_week_array = convert_to_weeks_running_array(@actions_completion_time, @max_weeks+1) - - # stop the chart after 10 weeks - @count = [10, @max_weeks].min - - # convert to new array to hold max @cut_off elems + 1 for sum of actions after @cut_off - @actions_completion_time_array = cut_off_array_with_sum(@actions_completed_per_week_array, @count) - @max_actions = @actions_completion_time_array.max - - # get percentage done cumulative - @cum_percent_done = convert_to_cumulative_array(@actions_completion_time_array, @actions_completion_time.count(:all)) - - render :layout => false - end - - def actions_running_time_data - @actions_running_time = current_user.todos.not_completed.select("created_at").reorder("created_at DESC") - - # convert to array and fill in non-existing weeks with 0 - @max_weeks = difference_in_weeks(@today, @actions_running_time.last.created_at) - @actions_running_per_week_array = convert_to_weeks_from_today_array(@actions_running_time, @max_weeks+1, :created_at) - - # cut off chart at 52 weeks = one year - @count = [52, @max_weeks].min - - # convert to new array to hold max @cut_off elems + 1 for sum of actions after @cut_off - @actions_running_time_array = cut_off_array_with_sum(@actions_running_per_week_array, @count) - @max_actions = @actions_running_time_array.max - - # get percentage done cumulative - @cum_percent_done = convert_to_cumulative_array(@actions_running_time_array, @actions_running_time.count ) - - render :layout => false - end - - def actions_visible_running_time_data - # running means - # - not completed (completed_at must be null) - # visible means - # - actions not part of a hidden project - # - actions not part of a hidden context - # - actions not deferred (show_from must be null) - # - actions not pending/blocked - - @actions_running_time = current_user.todos.not_completed.not_hidden.not_deferred_or_blocked. - select("todos.created_at"). - reorder("todos.created_at DESC") - - @max_weeks = difference_in_weeks(@today, @actions_running_time.last.created_at) - @actions_running_per_week_array = convert_to_weeks_from_today_array(@actions_running_time, @max_weeks+1, :created_at) - - # cut off chart at 52 weeks = one year - @count = [52, @max_weeks].min - - # convert to new array to hold max @cut_off elems + 1 for sum of actions after @cut_off - @actions_running_time_array = cut_off_array_with_sum(@actions_running_per_week_array, @count) - @max_actions = @actions_running_time_array.max - - # get percentage done cumulative - @cum_percent_done = convert_to_cumulative_array(@actions_running_time_array, @actions_running_time.count ) - - render :layout => false - end - - def context_total_actions_data - actions = Stats::TopContextsQuery.new(current_user).result - - @data = Stats::PieChartData.new(actions, t('stats.spread_of_actions_for_all_context'), 70) - - render :pie_chart_data, :layout => false - end - def context_running_actions_data actions = Stats::TopContextsQuery.new(current_user, :running => true).result @data = Stats::PieChartData.new(actions, t('stats.spread_of_running_actions_for_visible_contexts'), 60) @@ -298,84 +170,4 @@ class StatsController < ApplicationController def put_events_into_month_buckets(records, array_size, date_method_on_todo) convert_to_array(records.select { |x| x.send(date_method_on_todo) }, array_size) { |r| [difference_in_months(@today, r.send(date_method_on_todo))]} end - - def convert_to_days_from_today_array(records, array_size, date_method_on_todo) - return convert_to_array(records, array_size){ |r| [difference_in_days(@today, r.send(date_method_on_todo))]} - end - - def convert_to_weeks_from_today_array(records, array_size, date_method_on_todo) - return convert_to_array(records, array_size) { |r| [difference_in_weeks(@today, r.send(date_method_on_todo))]} - end - - def convert_to_weeks_running_array(records, array_size) - return convert_to_array(records, array_size) { |r| [difference_in_weeks(r.completed_at, r.created_at)]} - end - - def convert_to_weeks_running_from_today_array(records, array_size) - return convert_to_array(records, array_size) { |r| week_indexes_of(r) } - end - - def week_indexes_of(record) - a = [] - start_week = difference_in_weeks(@today, record.created_at) - end_week = record.completed_at ? difference_in_weeks(@today, record.completed_at) : 0 - end_week.upto(start_week) { |i| a << i }; - return a - end - - # returns a new array containing all elems of array up to cut_off and - # adds the sum of the rest of array to the last elem - def cut_off_array_with_sum(array, cut_off) - # +1 to hold sum of rest - a = Array.new(cut_off+1){|i| array[i]||0} - # add rest of array to last elem - a[cut_off] += array.inject(:+) - a.inject(:+) - return a - end - - def cut_off_array(array, cut_off) - return Array.new(cut_off){|i| array[i]||0} - end - - def convert_to_cumulative_array(array, max) - # calculate fractions - a = Array.new(array.size){|i| array[i]*100.0/max} - # make cumulative - 1.upto(array.size-1){ |i| a[i] += a[i-1] } - return a - end - - # assumes date1 > date2 - # this results in the number of months before the month of date1, not taking days into account, so diff of 31-dec and 1-jan is 1 month! - def difference_in_months(date1, date2) - return (date1.utc.year - date2.utc.year)*12 + (date1.utc.month - date2.utc.month) - end - - # assumes date1 > date2 - def difference_in_days(date1, date2) - return ((date1.utc.at_midnight-date2.utc.at_midnight)/SECONDS_PER_DAY).to_i - end - - # assumes date1 > date2 - def difference_in_weeks(date1, date2) - return difference_in_days(date1, date2) / 7 - end - - def three_month_avg(set, i) - (set.fetch(i){ 0 } + set.fetch(i+1){ 0 } + set.fetch(i+2){ 0 }) / 3.0 - end - - def set_three_month_avg(set,upper_bound) - (0..upper_bound-1).map { |i| three_month_avg(set, i) } - end - - # sets "null" on first column and - if necessary - cleans up last two columns, which may have insufficient data - def compute_running_avg_array(set, upper_bound) - result = set_three_month_avg(set, upper_bound) - result[upper_bound-1] = result[upper_bound-1] * 3 if upper_bound == set.length - result[upper_bound-2] = result[upper_bound-2] * 3 / 2 if upper_bound > 1 and upper_bound == set.length - result[0] = "null" - result - end # unsolved, not triggered, edge case for set.length == upper_bound + 1 - end diff --git a/app/models/stats/actions.rb b/app/models/stats/actions.rb index ffeb6b53..5ffcd12c 100644 --- a/app/models/stats/actions.rb +++ b/app/models/stats/actions.rb @@ -34,23 +34,99 @@ module Stats @sum_actions_created_last12months ||= new_since(one_year) end - def completion_charts - @completion_charts ||= %w{ - actions_done_last30days_data - actions_done_last12months_data - actions_completion_time_data - }.map do |action| - Stats::Chart.new(action) - end + def done_last12months_data + # get actions created and completed in the past 12+3 months. +3 for running + # - outermost set of entries needed for these calculations + actions_last12months = @user.todos.created_or_completed_after(@cut_off_year_plus3).select("completed_at,created_at") + + # convert to array and fill in non-existing months + @actions_done_last12months_array = put_events_into_month_buckets(actions_last12months, 13, :completed_at) + @actions_created_last12months_array = put_events_into_month_buckets(actions_last12months, 13, :created_at) + + # find max for graph in both arrays + @max = (@actions_done_last12months_array + @actions_created_last12months_array).max + + # find running avg + done_in_last_15_months = put_events_into_month_buckets(actions_last12months, 16, :completed_at) + created_in_last_15_months = put_events_into_month_buckets(actions_last12months, 16, :created_at) + + @actions_done_avg_last12months_array = compute_running_avg_array(done_in_last_15_months, 13) + @actions_created_avg_last12months_array = compute_running_avg_array(created_in_last_15_months, 13) + + # interpolate avg for current month. + @interpolated_actions_created_this_month = interpolate_avg_for_current_month(@actions_created_last12months_array) + @interpolated_actions_done_this_month = interpolate_avg_for_current_month(@actions_done_last12months_array) + + @created_count_array = Array.new(13, actions_last12months.created_after(@cut_off_year).count(:all)/12.0) + @done_count_array = Array.new(13, actions_last12months.completed_after(@cut_off_year).count(:all)/12.0) + + return { + datasets: [ + {label: "Avg created", data: @created_count_array.map { |total| [total] }, type: "line"}, + {label: "Avg completed", data: @done_count_array.map { |total| [total] }, type: "line"}, + {label: "3 months avg completed", data: @actions_done_avg_last12months_array.map { |total| [total] }, type: "line"}, + {label: "3 months avg created", data: @actions_created_avg_last12months_array.map { |total| [total] }, type: "line"}, + {label: "Created", data: @actions_created_last12months_array.map { |total| [total] } }, + {label: "Completed", data: @actions_done_last12months_array.map { |total| [total] } } + ], + labels: @actions_done_avg_last12months_array.each_with_index.map { |total, month| [month] } + } end - def timing_charts - @timing_charts ||= %w{ - actions_visible_running_time_data - actions_running_time_data - }.map do |action| - Stats::Chart.new(action) - end + def done_last30days_data + # get actions created and completed in the past 30 days. + @actions_done_last30days = @user.todos.completed_after(@cut_off_30days).select("completed_at") + @actions_created_last30days = @user.todos.created_after(@cut_off_30days).select("created_at") + + # convert to array. 30+1 to have 30 complete days and one current day [0] + @actions_done_last30days_array = convert_to_days_from_today_array(@actions_done_last30days, 31, :completed_at) + @actions_created_last30days_array = convert_to_days_from_today_array(@actions_created_last30days, 31, :created_at) + + # find max for graph in both hashes + @max = [@actions_done_last30days_array.max, @actions_created_last30days_array.max].max + + created_count_array = Array.new(30){ |i| @actions_created_last30days.size/30.0 } + done_count_array = Array.new(30){ |i| @actions_done_last30days.size/30.0 } + # TODO: make the strftime i18n proof + # TODO: Fix this, broke during transition from Flash-based stats. +# time_labels = Array.new(30){ |i| l(Time.zone.now-i.days, :format => :stats) } + + return { + datasets: [ + {label: "Avg created", data: created_count_array.map { |total| [total] }, type: "line"}, + {label: "Avg completed", data: done_count_array.map { |total| [total] }, type: "line"}, + {label: "Created", data: @actions_created_last30days_array.map { |total| [total] } }, + {label: "Completed", data: @actions_done_last30days_array.map { |total| [total] } } + ], +# labels: time_labels + labels: @actions_done_last30days_array.each_with_index.map { |total, days| [days] } + } + end + + def completion_time_data + @actions_completion_time = @user.todos.completed.select("completed_at, created_at").reorder("completed_at DESC" ) + + # convert to array and fill in non-existing weeks with 0 + @max_weeks = @actions_completion_time.last ? difference_in_weeks(@today, @actions_completion_time.last.completed_at) : 1 + @actions_completed_per_week_array = convert_to_weeks_running_array(@actions_completion_time, @max_weeks+1) + + # stop the chart after 10 weeks + @count = [10, @max_weeks].min + + # convert to new array to hold max @cut_off elems + 1 for sum of actions after @cut_off + @actions_completion_time_array = cut_off_array_with_sum(@actions_completed_per_week_array, @count) + @max_actions = @actions_completion_time_array.max + + # get percentage done cumulative + @cum_percent_done = convert_to_cumulative_array(@actions_completion_time_array, @actions_completion_time.count(:all)) + + return { + datasets: [ + {label: "Percentage", data: @cum_percent_done.map { |total| [total] }, type: "line"}, + {label: "Actions", data: @actions_completion_time_array.map { |total| [total] } } + ], + labels: @actions_completion_time_array.each_with_index.map { |total, week| [week] } + } end def running_time_data @@ -70,10 +146,48 @@ module Stats # get percentage done cumulative @cum_percent_done = convert_to_cumulative_array(@actions_running_time_array, @actions_running_time.count ) - return [ - {name: "Percentage", data: @cum_percent_done.each_with_index.map { |total, week| [week, total] } , type: "line"}, - {name: "Actions", data: @actions_running_time_array.each_with_index.map { |total, week| [week, total] } } - ] + return { + datasets: [ + {label: "Percentage", data: @cum_percent_done.map { |total| [total] }, type: "line"}, + {label: "Actions", data: @actions_running_time_array.map { |total| [total] } } + ], + labels: @actions_running_time_array.each_with_index.map { |total, week| [week] } + } + end + + def visible_running_time_data + # running means + # - not completed (completed_at must be null) + # visible means + # - actions not part of a hidden project + # - actions not part of a hidden context + # - actions not deferred (show_from must be null) + # - actions not pending/blocked + + @actions_running_time = @user.todos.not_completed.not_hidden.not_deferred_or_blocked. + select("todos.created_at"). + reorder("todos.created_at DESC") + + @max_weeks = difference_in_weeks(@today, @actions_running_time.last.created_at) + @actions_running_per_week_array = convert_to_weeks_from_today_array(@actions_running_time, @max_weeks+1, :created_at) + + # cut off chart at 52 weeks = one year + @count = [52, @max_weeks].min + + # convert to new array to hold max @cut_off elems + 1 for sum of actions after @cut_off + @actions_running_time_array = cut_off_array_with_sum(@actions_running_per_week_array, @count) + @max_actions = @actions_running_time_array.max + + # get percentage done cumulative + @cum_percent_done = convert_to_cumulative_array(@actions_running_time_array, @actions_running_time.count ) + + return { + datasets: [ + {label: "Percentage", data: @cum_percent_done.map { |total| [total] }, type: "line"}, + {label: "Actions", data: @actions_running_time_array.map { |total| [total] } } + ], + labels: @actions_running_time_array.each_with_index.map { |total, week| [week] } + } end def open_per_week_data @@ -89,7 +203,12 @@ module Stats @actions_open_per_week_array = convert_to_weeks_running_from_today_array(@actions_started, @max_weeks+1) @actions_open_per_week_array = cut_off_array(@actions_open_per_week_array, @count) - return @actions_open_per_week_array.each_with_index.map { |total, week| [week, total] } + return { + datasets: [ + {label: "Actions", data: @actions_open_per_week_array.map { |total| [total] } } + ], + labels: @actions_open_per_week_array.each_with_index.map { |total, week| [week] } + } end def day_of_week_all_data @@ -202,14 +321,12 @@ module Stats @completed ||= user.todos.completed.select("completed_at, created_at") end - # assumes date1 > date2 - def difference_in_days(date1, date2) - return ((date1.utc.at_midnight-date2.utc.at_midnight)/SECONDS_PER_DAY).to_i + def interpolate_avg_for_current_month(set) + (set[0]*(1/percent_of_month) + set[1] + set[2]) / 3.0 end - - # assumes date1 > date2 - def difference_in_weeks(date1, date2) - return difference_in_days(date1, date2) / 7 + + def percent_of_month + Time.zone.now.day / Time.zone.now.end_of_month.day.to_f end # uses the supplied block to determine array of indexes in hash @@ -220,12 +337,24 @@ module Stats a end - def convert_to_weeks_running_from_today_array(records, array_size) - return convert_to_array(records, array_size) { |r| week_indexes_of(r) } + def put_events_into_month_buckets(records, array_size, date_method_on_todo) + convert_to_array(records.select { |x| x.send(date_method_on_todo) }, array_size) { |r| [difference_in_months(@today, r.send(date_method_on_todo))]} end - def cut_off_array(array, cut_off) - return Array.new(cut_off){|i| array[i]||0} + def convert_to_days_from_today_array(records, array_size, date_method_on_todo) + return convert_to_array(records, array_size){ |r| [difference_in_days(@today, r.send(date_method_on_todo))]} + end + + def convert_to_weeks_from_today_array(records, array_size, date_method_on_todo) + return convert_to_array(records, array_size) { |r| [difference_in_weeks(@today, r.send(date_method_on_todo))]} + end + + def convert_to_weeks_running_array(records, array_size) + return convert_to_array(records, array_size) { |r| [difference_in_weeks(r.completed_at, r.created_at)]} + end + + def convert_to_weeks_running_from_today_array(records, array_size) + return convert_to_array(records, array_size) { |r| week_indexes_of(r) } end def week_indexes_of(record) @@ -236,10 +365,6 @@ module Stats return a end - def convert_to_weeks_from_today_array(records, array_size, date_method_on_todo) - return convert_to_array(records, array_size) { |r| [difference_in_weeks(@today, r.send(date_method_on_todo))]} - end - def cut_off_array_with_sum(array, cut_off) # +1 to hold sum of rest a = Array.new(cut_off+1){|i| array[i]||0} @@ -248,6 +373,10 @@ module Stats return a end + def cut_off_array(array, cut_off) + return Array.new(cut_off){|i| array[i]||0} + end + def convert_to_cumulative_array(array, max) # calculate fractions a = Array.new(array.size){|i| array[i]*100.0/max} @@ -256,5 +385,34 @@ module Stats return a end + def difference_in_months(date1, date2) + return (date1.utc.year - date2.utc.year)*12 + (date1.utc.month - date2.utc.month) + end + + # assumes date1 > date2 + def difference_in_days(date1, date2) + return ((date1.utc.at_midnight-date2.utc.at_midnight)/SECONDS_PER_DAY).to_i + end + + # assumes date1 > date2 + def difference_in_weeks(date1, date2) + return difference_in_days(date1, date2) / 7 + end + + def three_month_avg(set, i) + (set.fetch(i){ 0 } + set.fetch(i+1){ 0 } + set.fetch(i+2){ 0 }) / 3.0 + end + + def set_three_month_avg(set,upper_bound) + (0..upper_bound-1).map { |i| three_month_avg(set, i) } + end + + def compute_running_avg_array(set, upper_bound) + result = set_three_month_avg(set, upper_bound) + result[upper_bound-1] = result[upper_bound-1] * 3 if upper_bound == set.length + result[upper_bound-2] = result[upper_bound-2] * 3 / 2 if upper_bound > 1 and upper_bound == set.length + result[0] = "null" + result + end # unsolved, not triggered, edge case for set.length == upper_bound + 1 end end diff --git a/app/views/stats/_actions.html.erb b/app/views/stats/_actions.html.erb index 6e2d37e2..f3458eb9 100644 --- a/app/views/stats/_actions.html.erb +++ b/app/views/stats/_actions.html.erb @@ -12,36 +12,35 @@ options = { } } %> -

<%= t('stats.actions_actions_avg_created_30days', :count => (actions.created_last30days*10.0/30.0).round/10.0 )%> <%= t('stats.actions_avg_completed_30days', :count => (actions.done_last30days*10.0/30.0).round/10.0 )%> <%= t('stats.actions_avg_created', :count => (actions.created_last12months*10.0/12.0).round/10.0 )%> <%= t('stats.actions_avg_completed', :count => (actions.done_last12months*10.0/12.0).round/10.0 )%>

-<% actions.completion_charts.each do |chart| %><%= - render :partial => 'chart', :locals => {:chart => chart} --%><% end %> +<%= bar_chart actions.done_last30days_data, options.merge({'title': {'display': true, 'text': 'Actions in the last 30 days'}}) %> + +<%= bar_chart actions.done_last12months_data, options.merge({'title': {'display': true, 'text': 'Actions in the last 12 months'}}) %> + +<%= bar_chart actions.completion_time_data, options.merge({'title': {'display': true, 'text': 'Completion time (all completed actions)'}}) %>
-<% actions.timing_charts.each do |chart| %><%= - render :partial => 'chart', :locals => {:chart => chart} --%><% end %> - <% - Rails.logger.info actions.running_time_data +# TODO: There should be separate scales for percentage and amount of tasks so that the max of both is in the top of the chart. %> +<%= bar_chart actions.visible_running_time_data, options.merge({'title': {'display': true, 'text': 'Current running time of incomplete visible actions'}}) %> -<%= bar_chart actions.running_time_data, library: { :series => { 0 => {type: "line"} } }, title: "Current running time of all incomplete actions" %> +<%= bar_chart actions.running_time_data, options.merge({'title': {'display': true, 'text': 'Current running time of all incomplete actions'}}) %>
- <%= bar_chart actions.open_per_week_data, options.merge({scales: {yAxes: [{ scaleLabel: { display: true, labelString: 'Weeks ago'}}]}, 'title': {'display': true, 'text': 'Active (visible and hidden) next actions per week'}}) %> <%= bar_chart actions.day_of_week_all_data, options.merge({'title': {'display': true, 'text': 'Day of week (all actions)'}}) %> <%= bar_chart actions.day_of_week_30days_data, options.merge({'title': {'display': true, 'text': 'Day of week (past 30 days)'}}) %> +
+ <%= bar_chart actions.time_of_day_all_data, options.merge({'title': {'display': true, 'text': 'Time of day (all actions)'}}) %> <%= bar_chart actions.time_of_day_30days_data, options.merge({'title': {'display': true, 'text': 'Time of day (last 30 days)'}}) %> diff --git a/app/views/stats/_contexts.html.erb b/app/views/stats/_contexts.html.erb index 2f1d2aa7..b454aecd 100644 --- a/app/views/stats/_contexts.html.erb +++ b/app/views/stats/_contexts.html.erb @@ -37,6 +37,8 @@ Stats::TopContextsQuery.new(current_user, :running => true).result.map { |contex %> <%= pie_chart data, options.merge({'title': {'display': true, 'text': 'Spread of actions for visible contexts'}}) %> +
+ <%= render :partial => 'contexts_list', :locals => {:contexts => contexts.actions, :key => 'contexts'} -%> <%= render :partial => 'contexts_list', :locals => {:contexts => contexts.running_actions, :key => 'visible_contexts_with_incomplete_actions'} -%> From 0a106aac5e1f537092a80b41543c5696b2efcb93 Mon Sep 17 00:00:00 2001 From: Jyri-Petteri Paloposki Date: Sun, 19 May 2019 13:26:09 +0300 Subject: [PATCH 04/15] #1153: Remove unnecessary Flash-stats related templates --- .../actions_completion_time_data.html.erb | 26 ---------------- .../actions_done_last12months_data.html.erb | 31 ------------------- .../actions_done_last30days_data.html.erb | 26 ---------------- .../stats/actions_open_per_week_data.html.erb | 18 ----------- .../stats/actions_running_time_data.html.erb | 30 ------------------ ...actions_visible_running_time_data.html.erb | 30 ------------------ 6 files changed, 161 deletions(-) delete mode 100644 app/views/stats/actions_completion_time_data.html.erb delete mode 100644 app/views/stats/actions_done_last12months_data.html.erb delete mode 100644 app/views/stats/actions_done_last30days_data.html.erb delete mode 100644 app/views/stats/actions_open_per_week_data.html.erb delete mode 100644 app/views/stats/actions_running_time_data.html.erb delete mode 100644 app/views/stats/actions_visible_running_time_data.html.erb diff --git a/app/views/stats/actions_completion_time_data.html.erb b/app/views/stats/actions_completion_time_data.html.erb deleted file mode 100644 index c70912f0..00000000 --- a/app/views/stats/actions_completion_time_data.html.erb +++ /dev/null @@ -1,26 +0,0 @@ -<%- -time_labels = Array.new(@count){ |i| "#{i}-#{i+1}" } -time_labels[0] = t('stats.within_one') -time_labels[@count] = "> #{@count}" --%> -&title=<%= t('stats.action_completion_time_title') %>,{font-size:16},& -&y_legend=<%= t('stats.legend.actions') %>,10,0x8010A0& -&y2_legend=<%= t('stats.legend.percentage') %>,10,0xFF0000& -&x_legend=<%= t('stats.legend.running_time') %>,12,0x736AFF& -&y_ticks=5,10,5& -&filled_bar=50,0x9933CC,0x8010A0& -&values=<%= @actions_completion_time_array.join(",")%>& -&line_2=2,0xFF0000& -&values_2=<%= @cum_percent_done.join(",")%>& -&x_labels=<%= time_labels.join(",")%>& -&y_min=0& -<% -# add one to @max for people who have no actions completed yet. -# OpenFlashChart cannot handle y_max=0 --%> -&y_max=<%=1+@max_actions+@max_actions/10-%>& -&show_y2=true& -&y2_lines=2& -&y2_min=0& -&y2_max=100& -&x_label_style=9,,2,1& diff --git a/app/views/stats/actions_done_last12months_data.html.erb b/app/views/stats/actions_done_last12months_data.html.erb deleted file mode 100644 index c6ca9176..00000000 --- a/app/views/stats/actions_done_last12months_data.html.erb +++ /dev/null @@ -1,31 +0,0 @@ -<%- - url = url_for :controller => 'stats', :action => 'actions_done_last_years' --%> -&title=<%= t('stats.actions_lastyear_title') %>,{font-size:16},& -&y_legend=<%= t('stats.legend.number_of_actions') %>,12,0x736AFF& -&x_legend=<%= t('stats.legend.months_ago') %>,12,0x736AFF& -&y_ticks=5,10,5& -&filled_bar=50,0x9933CC,0x8010A0,<%= t('stats.labels.created')%>,9& -&filled_bar_2=50,0x0066CC,0x0066CC,<%= t('stats.labels.completed') %>,9& -&line_3=2,0x00FF00, <%= t('stats.labels.avg_created') %>, 9& -&line_4=2,0xFF0000, <%= t('stats.labels.avg_completed') %>, 9& -&line_5=2,0x007700, <%= t('stats.labels.month_avg_created', :months => 3) %>, 9& -&line_6=2,0xAA0000, <%= t('stats.labels.month_avg_completed', :months => 3) %>, 9& -&line_7=1,0xAA0000& -&line_8=1,0x007700& -&values=<%= @actions_created_last12months_array.join(",")%>& -&links=<%= Array.new(13,url).join(",") %>& -&links_2=<%= Array.new(13,url).join(",") %>& -&values_2=<%= @actions_done_last12months_array.join(",")%>& -&values_3=<%= @created_count_array.join(",")%>& -&values_4=<%= @done_count_array.join(",")%>& -&values_5=<%= @actions_created_avg_last12months_array.join(",")%>& -&values_6=<%= @actions_done_avg_last12months_array.join(",")%>& -&values_7=<%= @interpolated_actions_created_this_month%>,<%=@actions_done_avg_last12months_array[1]%>& -&values_8=<%= @interpolated_actions_done_this_month%>,<%=@actions_created_avg_last12months_array[1]%>& -&x_labels=<%= array_of_month_labels(@done_count_array.size).join(",")%>& -&y_min=0& -<% # add one to @max for people who have no actions completed yet. - # OpenFlashChart cannot handle y_max=0 -%> -&y_max=<%=@max+@max/10+1-%>& -&x_label_style=9,,2,& diff --git a/app/views/stats/actions_done_last30days_data.html.erb b/app/views/stats/actions_done_last30days_data.html.erb deleted file mode 100644 index 11991146..00000000 --- a/app/views/stats/actions_done_last30days_data.html.erb +++ /dev/null @@ -1,26 +0,0 @@ -<%- -created_count_array = Array.new(30){ |i| @actions_created_last30days.size/30.0 } -done_count_array = Array.new(30){ |i| @actions_done_last30days.size/30.0 } -# TODO: make the strftime i18n proof -time_labels = Array.new(30){ |i| l(Time.zone.now-i.days, :format => :stats) } --%> -&title=<%= t('stats.actions_30days_title') %>,{font-size:16},& -&y_legend=<%= t('stats.legend.number_of_actions') %>,12,0x736AFF& -&x_legend=<%= t('stats.legend.number_of_days') %>,12,0x736AFF& -&y_ticks=5,10,5& -&filled_bar=50,0x9933CC,0x8010A0,<%= t('stats.labels.created') %>,9& -&filled_bar_2=50,0x0066CC,0x0066CC,<%= t('stats.labels.completed') %>,9& -&line_3=3,0x00FF00, <%= t('stats.labels.avg_created') %>, 9& -&line_4=3,0xFF0000, <%= t('stats.labels.avg_completed') %>, 9& -&values=<%= @actions_created_last30days_array.join(",")%>& -&values_2=<%= @actions_done_last30days_array.join(",")%>& -&values_3=<%= created_count_array.join(",")%>& -&values_4=<%= done_count_array.join(",")%>& -&x_labels=<%= time_labels.join(",")%>& -&y_min=0& -<% # max + 10% for some extra space at the top - # add one to @max for people who have no actions completed yet. - # OpenFlashChart cannot handle y_max=0 --%> -&y_max=<%=@max+@max/10+1 -%>& -&x_label_style=9,,2,3& \ No newline at end of file diff --git a/app/views/stats/actions_open_per_week_data.html.erb b/app/views/stats/actions_open_per_week_data.html.erb deleted file mode 100644 index e9e93bed..00000000 --- a/app/views/stats/actions_open_per_week_data.html.erb +++ /dev/null @@ -1,18 +0,0 @@ -<%- -time_labels = Array.new(@count+1){ |i| "#{i}-#{i+1}" } -time_labels[0] = "< 1" --%> -&title=<%= t('stats.open_per_week') %>,{font-size:16},& -&y_legend=<%= t('stats.open_per_week_legend.actions') %>,10,0x736AFF& -&x_legend=<%= t('stats.open_per_week_legend.weeks') %>,11,0x736AFF& -&y_ticks=5,10,5& -&filled_bar=50,0x9933CC,0x8010A0& -&values=<%= @actions_open_per_week_array.join(",") -%>& -&x_labels=<%= time_labels.join(",")%>& -&y_min=0& -<% - # add one to @max for people who have no actions completed yet. - # OpenFlashChart cannot handle y_max=0 --%> -&y_max=<%=1+@max_actions+@max_actions/10-%>& -&x_label_style=9,,2,2& \ No newline at end of file diff --git a/app/views/stats/actions_running_time_data.html.erb b/app/views/stats/actions_running_time_data.html.erb deleted file mode 100644 index 6126b31a..00000000 --- a/app/views/stats/actions_running_time_data.html.erb +++ /dev/null @@ -1,30 +0,0 @@ -<%- -url_labels = Array.new(@count){ |i| url_for(:controller => 'stats', :action => 'show_selected_actions_from_chart', :index => i, :id=> "art") } -url_labels[@count]=url_for(:controller => 'stats', :action => 'show_selected_actions_from_chart', :index => @count, :id=> "art_end") - -time_labels = Array.new(@count){ |i| "#{i}-#{i+1}" } -time_labels[0] = "< 1" -time_labels[@count] = "> #{@count}" --%> -&title=<%= t('stats.running_time_all') %>,{font-size:16},& -&y_legend=<%= t('stats.running_time_all_legend.actions') %>,10,0x736AFF& -&y2_legend=<%= t('stats.running_time_all_legend.percentage') %>,10,0xFF0000& -&x_legend=<%= t('stats.running_time_all_legend.running_time') %>,11,0x736AFF& -&y_ticks=5,10,5& -&filled_bar=50,0x9933CC,0x8010A0& -&values=<%= @actions_running_time_array.join(",") -%>& -&links=<%= url_labels.join(",") %>& -&line_2=2,0xFF0000& -&values_2=<%= @cum_percent_done.join(",") %>& -&x_labels=<%= time_labels.join(",") %> & -&y_min=0& -<% - # add one to @max for people who have no actions completed yet. - # OpenFlashChart cannot handle y_max=0 --%> -&y_max=<%=1+@max_actions+@max_actions/10-%>& -&x_label_style=9,,2,2& -&show_y2=true& -&y2_lines=2& -&y2_min=0& -&y2_max=100& diff --git a/app/views/stats/actions_visible_running_time_data.html.erb b/app/views/stats/actions_visible_running_time_data.html.erb deleted file mode 100644 index c68969ec..00000000 --- a/app/views/stats/actions_visible_running_time_data.html.erb +++ /dev/null @@ -1,30 +0,0 @@ -<%- -url_labels = Array.new(@count){ |i| url_for(:controller => 'stats', :action => 'show_selected_actions_from_chart', :index => i, :id=> "avrt") } -url_labels[@count]=url_for(:controller => 'stats', :action => 'show_selected_actions_from_chart', :index => @count, :id=> "avrt_end") - -time_labels = Array.new(@count){ |i| "#{i}-#{i+1}" } -time_labels[0] = "< 1" -time_labels[@count] = "> #{@count}" --%> -&title=<%= t('stats.current_running_time_of_incomplete_visible_actions') %>,{font-size:16},& -&y_legend=<%= t('stats.running_time_legend.actions') %>,10,0x736AFF& -&y2_legend=<%= t('stats.running_time_legend.percentage') %>,10,0xFF0000& -&x_legend=<%= t('stats.running_time_legend.weeks') %>,11,0x736AFF& -&y_ticks=5,10,5& -&filled_bar=50,0x9933CC,0x8010A0& -&values=<%= @actions_running_time_array.join(",") -%>& -&links=<%= url_labels.join(",") %>& -&line_2=2,0xFF0000& -&values_2=<%= @cum_percent_done.join(",") -%>& -&x_labels=<%= time_labels.join(",")%>& -&y_min=0& -<% - # add one to @max for people who have no actions completed yet. - # OpenFlashChart cannot handle y_max=0 --%> -&y_max=<%=1+@max_actions+@max_actions/10-%>& -&x_label_style=9,,2,2& -&show_y2=true& -&y2_lines=2& -&y2_min=0& -&y2_max=100& From 0b326e17d85bd273fa64b3cf3be8fdab4667eb83 Mon Sep 17 00:00:00 2001 From: Jyri-Petteri Paloposki Date: Sun, 19 May 2019 16:40:00 +0300 Subject: [PATCH 05/15] #1153: Use translations properly, add missing labels, fix bugs and add links to charts --- app/controllers/stats_controller.rb | 17 +++-- app/models/stats/actions.rb | 85 ++++++++++++----------- app/views/stats/_actions.html.erb | 103 +++++++++++++++++++++++----- app/views/stats/_contexts.html.erb | 30 +++++--- 4 files changed, 160 insertions(+), 75 deletions(-) diff --git a/app/controllers/stats_controller.rb b/app/controllers/stats_controller.rb index 70bf697b..39b1c908 100644 --- a/app/controllers/stats_controller.rb +++ b/app/controllers/stats_controller.rb @@ -44,13 +44,6 @@ class StatsController < ApplicationController render :layout => false end - def context_running_actions_data - actions = Stats::TopContextsQuery.new(current_user, :running => true).result - @data = Stats::PieChartData.new(actions, t('stats.spread_of_running_actions_for_visible_contexts'), 60) - - render :pie_chart_data, :layout => false - end - def show_selected_actions_from_chart @page_title = t('stats.action_selection_title') @count = 99 @@ -170,4 +163,14 @@ class StatsController < ApplicationController def put_events_into_month_buckets(records, array_size, date_method_on_todo) convert_to_array(records.select { |x| x.send(date_method_on_todo) }, array_size) { |r| [difference_in_months(@today, r.send(date_method_on_todo))]} end + + # assumes date1 > date2 + def difference_in_days(date1, date2) + return ((date1.utc.at_midnight-date2.utc.at_midnight)/SECONDS_PER_DAY).to_i + end + + # assumes date1 > date2 + def difference_in_weeks(date1, date2) + return difference_in_days(date1, date2) / 7 + end end diff --git a/app/models/stats/actions.rb b/app/models/stats/actions.rb index 5ffcd12c..9b0cd924 100644 --- a/app/models/stats/actions.rb +++ b/app/models/stats/actions.rb @@ -54,6 +54,7 @@ module Stats @actions_created_avg_last12months_array = compute_running_avg_array(created_in_last_15_months, 13) # interpolate avg for current month. + # FIXME: These should also be used. @interpolated_actions_created_this_month = interpolate_avg_for_current_month(@actions_created_last12months_array) @interpolated_actions_done_this_month = interpolate_avg_for_current_month(@actions_done_last12months_array) @@ -62,14 +63,14 @@ module Stats return { datasets: [ - {label: "Avg created", data: @created_count_array.map { |total| [total] }, type: "line"}, - {label: "Avg completed", data: @done_count_array.map { |total| [total] }, type: "line"}, - {label: "3 months avg completed", data: @actions_done_avg_last12months_array.map { |total| [total] }, type: "line"}, - {label: "3 months avg created", data: @actions_created_avg_last12months_array.map { |total| [total] }, type: "line"}, - {label: "Created", data: @actions_created_last12months_array.map { |total| [total] } }, - {label: "Completed", data: @actions_done_last12months_array.map { |total| [total] } } + {label: I18n.t('stats.labels.avg_created'), data: @created_count_array.map { |total| [total] }, type: "line"}, + {label: I18n.t('stats.labels.avg_completed'), data: @done_count_array.map { |total| [total] }, type: "line"}, + {label: I18n.t('stats.labels.month_avg_completed', :months => 3), data: @actions_done_avg_last12months_array.map { |total| [total] }, type: "line"}, + {label: I18n.t('stats.labels.month_avg_created', :months => 3), data: @actions_created_avg_last12months_array.map { |total| [total] }, type: "line"}, + {label: I18n.t('stats.labels.created'), data: @actions_created_last12months_array.map { |total| [total] } }, + {label: I18n.t('stats.labels.completed'), data: @actions_done_last12months_array.map { |total| [total] } }, ], - labels: @actions_done_avg_last12months_array.each_with_index.map { |total, month| [month] } + labels: array_of_month_labels(@done_count_array.size), } end @@ -88,18 +89,16 @@ module Stats created_count_array = Array.new(30){ |i| @actions_created_last30days.size/30.0 } done_count_array = Array.new(30){ |i| @actions_done_last30days.size/30.0 } # TODO: make the strftime i18n proof - # TODO: Fix this, broke during transition from Flash-based stats. -# time_labels = Array.new(30){ |i| l(Time.zone.now-i.days, :format => :stats) } + time_labels = Array.new(30){ |i| I18n.l(Time.zone.now-i.days, :format => :stats) } return { datasets: [ - {label: "Avg created", data: created_count_array.map { |total| [total] }, type: "line"}, - {label: "Avg completed", data: done_count_array.map { |total| [total] }, type: "line"}, - {label: "Created", data: @actions_created_last30days_array.map { |total| [total] } }, - {label: "Completed", data: @actions_done_last30days_array.map { |total| [total] } } + {label: I18n.t('stats.labels.avg_created'), data: created_count_array.map { |total| [total] }, type: "line"}, + {label: I18n.t('stats.labels.completed'), data: done_count_array.map { |total| [total] }, type: "line"}, + {label: I18n.t('stats.labels.created'), data: @actions_created_last30days_array.map { |total| [total] } }, + {label: I18n.t('stats.labels.completed'), data: @actions_done_last30days_array.map { |total| [total] } }, ], -# labels: time_labels - labels: @actions_done_last30days_array.each_with_index.map { |total, days| [days] } + labels: time_labels, } end @@ -122,10 +121,10 @@ module Stats return { datasets: [ - {label: "Percentage", data: @cum_percent_done.map { |total| [total] }, type: "line"}, - {label: "Actions", data: @actions_completion_time_array.map { |total| [total] } } + {label: I18n.t('stats.legend.percentage'), data: @cum_percent_done.map { |total| [total] }, type: "line"}, + {label: I18n.t('stats.legend.actions'), data: @actions_completion_time_array.map { |total| [total] } }, ], - labels: @actions_completion_time_array.each_with_index.map { |total, week| [week] } + labels: @actions_completion_time_array.each_with_index.map { |total, week| [week] }, } end @@ -148,10 +147,10 @@ module Stats return { datasets: [ - {label: "Percentage", data: @cum_percent_done.map { |total| [total] }, type: "line"}, - {label: "Actions", data: @actions_running_time_array.map { |total| [total] } } + {label: I18n.t('stats.running_time_all_legend.percentage'), data: @cum_percent_done.map { |total| [total] }, type: "line"}, + {label: I18n.t('stats.running_time_all_legend.actions'), data: @actions_running_time_array.map { |total| [total] } }, ], - labels: @actions_running_time_array.each_with_index.map { |total, week| [week] } + labels: @actions_running_time_array.each_with_index.map { |total, week| [week] }, } end @@ -183,10 +182,10 @@ module Stats return { datasets: [ - {label: "Percentage", data: @cum_percent_done.map { |total| [total] }, type: "line"}, - {label: "Actions", data: @actions_running_time_array.map { |total| [total] } } + {label: I18n.t('stats.running_time_legend.percentage'), data: @cum_percent_done.map { |total| [total] }, type: "line"}, + {label: I18n.t('stats.running_time_legend.actions'), data: @actions_running_time_array.map { |total| [total] } }, ], - labels: @actions_running_time_array.each_with_index.map { |total, week| [week] } + labels: @actions_running_time_array.each_with_index.map { |total, week| [week] }, } end @@ -205,9 +204,9 @@ module Stats return { datasets: [ - {label: "Actions", data: @actions_open_per_week_array.map { |total| [total] } } + {label: I18n.t('stats.open_per_week_legend.actions'), data: @actions_open_per_week_array.map { |total| [total] } }, ], - labels: @actions_open_per_week_array.each_with_index.map { |total, week| [week] } + labels: @actions_open_per_week_array.each_with_index.map { |total, week| [week] }, } end @@ -224,13 +223,12 @@ module Stats @actions_completion_day_array = Array.new(7) { |i| 0} @actions_completion_day.each { |t| @actions_completion_day_array[ t.completed_at.wday ] += 1 } - # FIXME: Day of week as string instead of number return { datasets: [ - {label: "Created", data: @actions_creation_day_array.map { |total| [total] } }, - {label: "Completed", data: @actions_completion_day_array.map { |total| [total] } } + {label: I18n.t('stats.labels.created'), data: @actions_creation_day_array.map { |total| [total] } }, + {label: I18n.t('stats.labels.completed'), data: @actions_completion_day_array.map { |total| [total] } }, ], - labels: @actions_creation_day_array.each_with_index.map { |total, day| [day] } + labels: I18n.t('date.day_names'), } end @@ -247,13 +245,12 @@ module Stats @actions_completion_day_array = Array.new(7) { |i| 0} @actions_completion_day.each { |r| @actions_completion_day_array[r.completed_at.wday] += 1 } - # FIXME: Day of week as string instead of number return { datasets: [ - {label: "Created", data: @actions_creation_day_array.map { |total| [total] } }, - {label: "Completed", data: @actions_completion_day_array.map { |total| [total] } } + {label: I18n.t('stats.labels.created'), data: @actions_creation_day_array.map { |total| [total] } }, + {label: I18n.t('stats.labels.completed'), data: @actions_completion_day_array.map { |total| [total] } }, ], - labels: @actions_creation_day_array.each_with_index.map { |total, day| [day] } + labels: I18n.t('date.day_names'), } end @@ -271,10 +268,10 @@ module Stats return { datasets: [ - {label: "Created", data: @actions_creation_hour_array.map { |total| [total] } }, - {label: "Completed", data: @actions_completion_hour_array.map { |total| [total] } } + {label: I18n.t('stats.labels.created'), data: @actions_creation_hour_array.map { |total| [total] } }, + {label: I18n.t('stats.labels.completed'), data: @actions_completion_hour_array.map { |total| [total] } }, ], - labels: @actions_creation_hour_array.each_with_index.map { |total, hour| [hour] } + labels: @actions_creation_hour_array.each_with_index.map { |total, hour| [hour] }, } end @@ -292,10 +289,10 @@ module Stats return { datasets: [ - {label: "Created", data: @actions_creation_hour_array.map { |total| [total] } }, - {label: "Completed", data: @actions_completion_hour_array.map { |total| [total] } } + {label: I18n.t('stats.labels.created'), data: @actions_creation_hour_array.map { |total| [total] } }, + {label: I18n.t('stats.labels.completed'), data: @actions_completion_hour_array.map { |total| [total] } }, ], - labels: @actions_creation_hour_array.each_with_index.map { |total, hour| [hour] } + labels: @actions_creation_hour_array.each_with_index.map { |total, hour| [hour] }, } end @@ -414,5 +411,13 @@ module Stats result[0] = "null" result end # unsolved, not triggered, edge case for set.length == upper_bound + 1 + + def month_label(i) + I18n.t('date.month_names')[ (Time.zone.now.mon - i -1 ) % 12 + 1 ] + end + + def array_of_month_labels(count) + Array.new(count) { |i| month_label(i) } + end end end diff --git a/app/views/stats/_actions.html.erb b/app/views/stats/_actions.html.erb index f3458eb9..f1604786 100644 --- a/app/views/stats/_actions.html.erb +++ b/app/views/stats/_actions.html.erb @@ -7,9 +7,9 @@ options = { responsive: false, plugins: { colorschemes: { - scheme: 'brewer.Paired12' - } - } + scheme: 'brewer.Paired12', + }, + }, } %>

<%= t('stats.actions_actions_avg_created_30days', :count => (actions.created_last30days*10.0/30.0).round/10.0 )%> @@ -17,31 +17,98 @@ options = { <%= t('stats.actions_avg_created', :count => (actions.created_last12months*10.0/12.0).round/10.0 )%> <%= t('stats.actions_avg_completed', :count => (actions.done_last12months*10.0/12.0).round/10.0 )%>

-<%= bar_chart actions.done_last30days_data, options.merge({'title': {'display': true, 'text': 'Actions in the last 30 days'}}) %> +<%= bar_chart actions.done_last30days_data, options.merge({ + scales: { + xAxes: [{ scaleLabel: { display: true, labelString: t('stats.legend.number_of_days')}}], + yAxes: [{ scaleLabel: { display: true, labelString: t('stats.legend.number_of_actions')}}], + }, + 'title': {'display': true, 'text': t('stats.actions_30days_title')}, +}) %> -<%= bar_chart actions.done_last12months_data, options.merge({'title': {'display': true, 'text': 'Actions in the last 12 months'}}) %> +<% +# TODO: Missing the first 3 month avg values because they're null? +%> +<%= bar_chart actions.done_last12months_data, options.merge({ + scales: { + xAxes: [{ scaleLabel: { display: true, labelString: t('stats.legend.months_ago')}}], + yAxes: [{ scaleLabel: { display: true, labelString: t('stats.legend.number_of_actions')}}], + }, + 'title': {'display': true, 'text': t('stats.actions_lastyear_title')}, + 'onClick': 'function() { window.location.href = "' + url_for(:controller => 'stats', :action => 'actions_done_last_years') + '"; }', + }) %> -<%= bar_chart actions.completion_time_data, options.merge({'title': {'display': true, 'text': 'Completion time (all completed actions)'}}) %> +<% +# TODO: There should be separate scales for percentage and amount of tasks so that the max of both is in the top of the chart, ie. the left y-axis should be "Percentage". +%> +<%= bar_chart actions.completion_time_data, options.merge({ + scales: { + xAxes: [{ scaleLabel: { display: true, labelString: t('stats.legend.running_time')}}], + yAxes: [{ scaleLabel: { display: true, labelString: t('stats.legend.actions')}}], + }, + 'title': {'display': true, 'text': t('stats.action_completion_time_title')}}) %>
<% -# TODO: There should be separate scales for percentage and amount of tasks so that the max of both is in the top of the chart. +# TODO: There should be separate scales for percentage and amount of tasks so that the max of both is in the top of the chart, ie. the left y-axis should be "Percentage". %> -<%= bar_chart actions.visible_running_time_data, options.merge({'title': {'display': true, 'text': 'Current running time of incomplete visible actions'}}) %> +<%= bar_chart actions.visible_running_time_data, options.merge({ + scales: { + xAxes: [{ scaleLabel: { display: true, labelString: t('stats.running_time_legend.weeks')}}], + yAxes: [{ scaleLabel: { display: true, labelString: t('stats.running_time_legend.actions')}}], + }, + 'title': {'display': true, 'text': t('stats.current_running_time_of_incomplete_visible_actions')}, + 'onClick': 'function(event, array) { window.location.href = "' + url_for(:controller => 'stats', :action => 'show_selected_actions_from_chart', :id => "art") + '?index=" + array[0]._index; }', +}) %> -<%= bar_chart actions.running_time_data, options.merge({'title': {'display': true, 'text': 'Current running time of all incomplete actions'}}) %> - -
-<%= bar_chart actions.open_per_week_data, options.merge({scales: {yAxes: [{ scaleLabel: { display: true, labelString: 'Weeks ago'}}]}, 'title': {'display': true, 'text': 'Active (visible and hidden) next actions per week'}}) %> - -<%= bar_chart actions.day_of_week_all_data, options.merge({'title': {'display': true, 'text': 'Day of week (all actions)'}}) %> - -<%= bar_chart actions.day_of_week_30days_data, options.merge({'title': {'display': true, 'text': 'Day of week (past 30 days)'}}) %> +<% +# TODO: There should be separate scales for percentage and amount of tasks so that the max of both is in the top of the chart, ie. the left y-axis should be "Percentage". +%> +<%= bar_chart actions.running_time_data, options.merge({ + scales: { + xAxes: [{ scaleLabel: { display: true, labelString: t('stats.running_time_all_legend.running_time')}}], + yAxes: [{ scaleLabel: { display: true, labelString: t('stats.running_time_legend.actions')}}], + }, + 'title': {'display': true, 'text': t('stats.running_time_all')}, + 'onClick': 'function(event, array) { window.location.href = "' + url_for(:controller => 'stats', :action => 'show_selected_actions_from_chart', :id => "avrt") + '?index=" + array[0]._index; }', +}) %>
-<%= bar_chart actions.time_of_day_all_data, options.merge({'title': {'display': true, 'text': 'Time of day (all actions)'}}) %> +<%= bar_chart actions.open_per_week_data, options.merge({ + scales: { + xAxes: [{ scaleLabel: { display: true, labelString: t('stats.open_per_week_legend.weeks')}}], + yAxes: [{ scaleLabel: { display: true, labelString: t('stats.open_per_week_legend.actions')}}], + }, + 'title': {'display': true, 'text': t('stats.open_per_week')}}) %> -<%= bar_chart actions.time_of_day_30days_data, options.merge({'title': {'display': true, 'text': 'Time of day (last 30 days)'}}) %> +<%= bar_chart actions.day_of_week_all_data, options.merge({ + scales: { + xAxes: [{ scaleLabel: { display: true, labelString: t('stats.actions_day_of_week_legend.day_of_week')}}], + yAxes: [{ scaleLabel: { display: true, labelString: t('stats.actions_day_of_week_legend.number_of_actions')}}], + }, + 'title': {'display': true, 'text': t('stats.actions_day_of_week_title')}}) %> + +<%= bar_chart actions.day_of_week_30days_data, options.merge({ + scales: { + xAxes: [{ scaleLabel: { display: true, labelString: t('stats.actions_dow_30days_legend.day_of_week')}}], + yAxes: [{ scaleLabel: { display: true, labelString: t('stats.actions_dow_30days_legend.number_of_actions')}}], + }, + 'title': {'display': true, 'text': t('stats.actions_dow_30days_title')}}) %> + +
+ +<%= bar_chart actions.time_of_day_all_data, options.merge({ + scales: { + xAxes: [{ scaleLabel: { display: true, labelString: t('stats.time_of_day_legend.time_of_day')}}], + yAxes: [{ scaleLabel: { display: true, labelString: t('stats.time_of_day_legend.number_of_actions')}}], + }, + 'title': {'display': true, 'text': t('stats.time_of_day')}}) %> + +<%= bar_chart actions.time_of_day_30days_data, options.merge({ + scales: { + xAxes: [{ scaleLabel: { display: true, labelString: t('stats.tod30_legend.time_of_day')}}], + yAxes: [{ scaleLabel: { display: true, labelString: t('stats.tod30_legend.number_of_actions')}}], + }, + 'title': {'display': true, 'text': t('stats.tod30')}}) %> diff --git a/app/views/stats/_contexts.html.erb b/app/views/stats/_contexts.html.erb index b454aecd..5301f604 100644 --- a/app/views/stats/_contexts.html.erb +++ b/app/views/stats/_contexts.html.erb @@ -2,13 +2,15 @@ <% data = { datasets: [{ - data: Array.new + data: Array.new, }], - labels: Array.new + labels: Array.new, + ids: Array.new, } Stats::TopContextsQuery.new(current_user).result.map { |context| data[:datasets][0][:data].append(context.total) data[:labels].append(context.name) + data[:ids].append(context.id) } options = { width: "400px", @@ -17,25 +19,33 @@ options = { responsive: false, plugins: { colorschemes: { - scheme: 'brewer.Paired12' - } - } + scheme: 'brewer.Paired12', + }, + }, } %> -<%= pie_chart data, options.merge({'title': {'display': true, 'text': 'Spread of actions for all contexts'}}) %> +<% #TODO: Move data handling to model. Show value as percentage %> +<%= pie_chart data, options.merge({ + 'title': {'display': true, 'text': t('stats.spread_of_actions_for_all_context')}, + 'onClick': 'function(event, array) { console.log(array); window.location.href = "' + url_for(:controller => 'contexts', :action => 'show', :id => -1).gsub('-1', '') + '" + array[0]._chart.chart.data.ids[array[0]._index]; }' +}) %> <% data = { datasets: [{ - data: Array.new + data: Array.new, }], - labels: Array.new - } + labels: Array.new, + ids: Array.new, +} Stats::TopContextsQuery.new(current_user, :running => true).result.map { |context| data[:datasets][0][:data].append(context.total) data[:labels].append(context.name) + data[:ids].append(context.id) } %> -<%= pie_chart data, options.merge({'title': {'display': true, 'text': 'Spread of actions for visible contexts'}}) %> +<%= pie_chart data, options.merge({ + 'onClick': 'function(event, array) { console.log(array); window.location.href = "' + url_for(:controller => 'contexts', :action => 'show', :id => -1).gsub('-1', '') + '" + array[0]._chart.chart.data.ids[array[0]._index]; }', + 'title': {'display': true, 'text': t('stats.spread_of_running_actions_for_visible_contexts')}}) %>
From f9370a9a4ac33b96d137b15dc114512908559fcb Mon Sep 17 00:00:00 2001 From: Jyri-Petteri Paloposki Date: Sun, 19 May 2019 16:51:26 +0300 Subject: [PATCH 06/15] #1153: Fix whitespace issues caused by Vim misconfiguration --- app/models/stats/actions.rb | 200 ++++++++++++++--------------- app/views/stats/_actions.html.erb | 18 +-- app/views/stats/_contexts.html.erb | 18 +-- 3 files changed, 118 insertions(+), 118 deletions(-) diff --git a/app/models/stats/actions.rb b/app/models/stats/actions.rb index 9b0cd924..795b3186 100644 --- a/app/models/stats/actions.rb +++ b/app/models/stats/actions.rb @@ -38,39 +38,39 @@ module Stats # get actions created and completed in the past 12+3 months. +3 for running # - outermost set of entries needed for these calculations actions_last12months = @user.todos.created_or_completed_after(@cut_off_year_plus3).select("completed_at,created_at") - + # convert to array and fill in non-existing months @actions_done_last12months_array = put_events_into_month_buckets(actions_last12months, 13, :completed_at) @actions_created_last12months_array = put_events_into_month_buckets(actions_last12months, 13, :created_at) - + # find max for graph in both arrays @max = (@actions_done_last12months_array + @actions_created_last12months_array).max - + # find running avg done_in_last_15_months = put_events_into_month_buckets(actions_last12months, 16, :completed_at) created_in_last_15_months = put_events_into_month_buckets(actions_last12months, 16, :created_at) - + @actions_done_avg_last12months_array = compute_running_avg_array(done_in_last_15_months, 13) @actions_created_avg_last12months_array = compute_running_avg_array(created_in_last_15_months, 13) - + # interpolate avg for current month. # FIXME: These should also be used. @interpolated_actions_created_this_month = interpolate_avg_for_current_month(@actions_created_last12months_array) @interpolated_actions_done_this_month = interpolate_avg_for_current_month(@actions_done_last12months_array) - + @created_count_array = Array.new(13, actions_last12months.created_after(@cut_off_year).count(:all)/12.0) @done_count_array = Array.new(13, actions_last12months.completed_after(@cut_off_year).count(:all)/12.0) return { - datasets: [ - {label: I18n.t('stats.labels.avg_created'), data: @created_count_array.map { |total| [total] }, type: "line"}, - {label: I18n.t('stats.labels.avg_completed'), data: @done_count_array.map { |total| [total] }, type: "line"}, - {label: I18n.t('stats.labels.month_avg_completed', :months => 3), data: @actions_done_avg_last12months_array.map { |total| [total] }, type: "line"}, - {label: I18n.t('stats.labels.month_avg_created', :months => 3), data: @actions_created_avg_last12months_array.map { |total| [total] }, type: "line"}, - {label: I18n.t('stats.labels.created'), data: @actions_created_last12months_array.map { |total| [total] } }, - {label: I18n.t('stats.labels.completed'), data: @actions_done_last12months_array.map { |total| [total] } }, - ], - labels: array_of_month_labels(@done_count_array.size), + datasets: [ + {label: I18n.t('stats.labels.avg_created'), data: @created_count_array.map { |total| [total] }, type: "line"}, + {label: I18n.t('stats.labels.avg_completed'), data: @done_count_array.map { |total| [total] }, type: "line"}, + {label: I18n.t('stats.labels.month_avg_completed', :months => 3), data: @actions_done_avg_last12months_array.map { |total| [total] }, type: "line"}, + {label: I18n.t('stats.labels.month_avg_created', :months => 3), data: @actions_created_avg_last12months_array.map { |total| [total] }, type: "line"}, + {label: I18n.t('stats.labels.created'), data: @actions_created_last12months_array.map { |total| [total] } }, + {label: I18n.t('stats.labels.completed'), data: @actions_done_last12months_array.map { |total| [total] } }, + ], + labels: array_of_month_labels(@done_count_array.size), } end @@ -78,7 +78,7 @@ module Stats # get actions created and completed in the past 30 days. @actions_done_last30days = @user.todos.completed_after(@cut_off_30days).select("completed_at") @actions_created_last30days = @user.todos.created_after(@cut_off_30days).select("created_at") - + # convert to array. 30+1 to have 30 complete days and one current day [0] @actions_done_last30days_array = convert_to_days_from_today_array(@actions_done_last30days, 31, :completed_at) @actions_created_last30days_array = convert_to_days_from_today_array(@actions_created_last30days, 31, :created_at) @@ -90,67 +90,67 @@ module Stats done_count_array = Array.new(30){ |i| @actions_done_last30days.size/30.0 } # TODO: make the strftime i18n proof time_labels = Array.new(30){ |i| I18n.l(Time.zone.now-i.days, :format => :stats) } - + return { - datasets: [ - {label: I18n.t('stats.labels.avg_created'), data: created_count_array.map { |total| [total] }, type: "line"}, - {label: I18n.t('stats.labels.completed'), data: done_count_array.map { |total| [total] }, type: "line"}, - {label: I18n.t('stats.labels.created'), data: @actions_created_last30days_array.map { |total| [total] } }, - {label: I18n.t('stats.labels.completed'), data: @actions_done_last30days_array.map { |total| [total] } }, - ], - labels: time_labels, + datasets: [ + {label: I18n.t('stats.labels.avg_created'), data: created_count_array.map { |total| [total] }, type: "line"}, + {label: I18n.t('stats.labels.completed'), data: done_count_array.map { |total| [total] }, type: "line"}, + {label: I18n.t('stats.labels.created'), data: @actions_created_last30days_array.map { |total| [total] } }, + {label: I18n.t('stats.labels.completed'), data: @actions_done_last30days_array.map { |total| [total] } }, + ], + labels: time_labels, } end def completion_time_data @actions_completion_time = @user.todos.completed.select("completed_at, created_at").reorder("completed_at DESC" ) - + # convert to array and fill in non-existing weeks with 0 @max_weeks = @actions_completion_time.last ? difference_in_weeks(@today, @actions_completion_time.last.completed_at) : 1 @actions_completed_per_week_array = convert_to_weeks_running_array(@actions_completion_time, @max_weeks+1) - + # stop the chart after 10 weeks @count = [10, @max_weeks].min - + # convert to new array to hold max @cut_off elems + 1 for sum of actions after @cut_off @actions_completion_time_array = cut_off_array_with_sum(@actions_completed_per_week_array, @count) @max_actions = @actions_completion_time_array.max - + # get percentage done cumulative @cum_percent_done = convert_to_cumulative_array(@actions_completion_time_array, @actions_completion_time.count(:all)) - + return { - datasets: [ - {label: I18n.t('stats.legend.percentage'), data: @cum_percent_done.map { |total| [total] }, type: "line"}, - {label: I18n.t('stats.legend.actions'), data: @actions_completion_time_array.map { |total| [total] } }, - ], - labels: @actions_completion_time_array.each_with_index.map { |total, week| [week] }, + datasets: [ + {label: I18n.t('stats.legend.percentage'), data: @cum_percent_done.map { |total| [total] }, type: "line"}, + {label: I18n.t('stats.legend.actions'), data: @actions_completion_time_array.map { |total| [total] } }, + ], + labels: @actions_completion_time_array.each_with_index.map { |total, week| [week] }, } end def running_time_data @actions_running_time = @user.todos.not_completed.select("created_at").reorder("created_at DESC") - + # convert to array and fill in non-existing weeks with 0 @max_weeks = difference_in_weeks(@today, @actions_running_time.last.created_at) @actions_running_per_week_array = convert_to_weeks_from_today_array(@actions_running_time, @max_weeks+1, :created_at) - + # cut off chart at 52 weeks = one year @count = [52, @max_weeks].min - + # convert to new array to hold max @cut_off elems + 1 for sum of actions after @cut_off @actions_running_time_array = cut_off_array_with_sum(@actions_running_per_week_array, @count) @max_actions = @actions_running_time_array.max - + # get percentage done cumulative @cum_percent_done = convert_to_cumulative_array(@actions_running_time_array, @actions_running_time.count ) return { - datasets: [ - {label: I18n.t('stats.running_time_all_legend.percentage'), data: @cum_percent_done.map { |total| [total] }, type: "line"}, - {label: I18n.t('stats.running_time_all_legend.actions'), data: @actions_running_time_array.map { |total| [total] } }, - ], - labels: @actions_running_time_array.each_with_index.map { |total, week| [week] }, + datasets: [ + {label: I18n.t('stats.running_time_all_legend.percentage'), data: @cum_percent_done.map { |total| [total] }, type: "line"}, + {label: I18n.t('stats.running_time_all_legend.actions'), data: @actions_running_time_array.map { |total| [total] } }, + ], + labels: @actions_running_time_array.each_with_index.map { |total, week| [week] }, } end @@ -162,30 +162,30 @@ module Stats # - actions not part of a hidden context # - actions not deferred (show_from must be null) # - actions not pending/blocked - + @actions_running_time = @user.todos.not_completed.not_hidden.not_deferred_or_blocked. select("todos.created_at"). reorder("todos.created_at DESC") - + @max_weeks = difference_in_weeks(@today, @actions_running_time.last.created_at) @actions_running_per_week_array = convert_to_weeks_from_today_array(@actions_running_time, @max_weeks+1, :created_at) - + # cut off chart at 52 weeks = one year @count = [52, @max_weeks].min - + # convert to new array to hold max @cut_off elems + 1 for sum of actions after @cut_off @actions_running_time_array = cut_off_array_with_sum(@actions_running_per_week_array, @count) @max_actions = @actions_running_time_array.max - + # get percentage done cumulative @cum_percent_done = convert_to_cumulative_array(@actions_running_time_array, @actions_running_time.count ) - + return { - datasets: [ - {label: I18n.t('stats.running_time_legend.percentage'), data: @cum_percent_done.map { |total| [total] }, type: "line"}, - {label: I18n.t('stats.running_time_legend.actions'), data: @actions_running_time_array.map { |total| [total] } }, - ], - labels: @actions_running_time_array.each_with_index.map { |total, week| [week] }, + datasets: [ + {label: I18n.t('stats.running_time_legend.percentage'), data: @cum_percent_done.map { |total| [total] }, type: "line"}, + {label: I18n.t('stats.running_time_legend.actions'), data: @actions_running_time_array.map { |total| [total] } }, + ], + labels: @actions_running_time_array.each_with_index.map { |total, week| [week] }, } end @@ -193,106 +193,106 @@ module Stats @actions_started = @user.todos.created_after(@today-53.weeks). select("todos.created_at, todos.completed_at"). reorder("todos.created_at DESC") - + @max_weeks = difference_in_weeks(@today, @actions_started.last.created_at) - + # cut off chart at 52 weeks = one year @count = [52, @max_weeks].min - + @actions_open_per_week_array = convert_to_weeks_running_from_today_array(@actions_started, @max_weeks+1) @actions_open_per_week_array = cut_off_array(@actions_open_per_week_array, @count) return { - datasets: [ + datasets: [ {label: I18n.t('stats.open_per_week_legend.actions'), data: @actions_open_per_week_array.map { |total| [total] } }, - ], - labels: @actions_open_per_week_array.each_with_index.map { |total, week| [week] }, + ], + labels: @actions_open_per_week_array.each_with_index.map { |total, week| [week] }, } end def day_of_week_all_data @actions_creation_day = @user.todos.select("created_at") @actions_completion_day = @user.todos.completed.select("completed_at") - + # convert to array and fill in non-existing days @actions_creation_day_array = Array.new(7) { |i| 0} - @actions_creation_day.each { |t| @actions_creation_day_array[ t.created_at.wday ] += 1 } + @actions_creation_day.each { |t| @actions_creation_day_array[ t.created_at.wday ] += 1 } @max = @actions_creation_day_array.max - + # convert to array and fill in non-existing days @actions_completion_day_array = Array.new(7) { |i| 0} - @actions_completion_day.each { |t| @actions_completion_day_array[ t.completed_at.wday ] += 1 } - + @actions_completion_day.each { |t| @actions_completion_day_array[ t.completed_at.wday ] += 1 } + return { - datasets: [ + datasets: [ {label: I18n.t('stats.labels.created'), data: @actions_creation_day_array.map { |total| [total] } }, - {label: I18n.t('stats.labels.completed'), data: @actions_completion_day_array.map { |total| [total] } }, - ], - labels: I18n.t('date.day_names'), + {label: I18n.t('stats.labels.completed'), data: @actions_completion_day_array.map { |total| [total] } }, + ], + labels: I18n.t('date.day_names'), } end def day_of_week_30days_data @actions_creation_day = @user.todos.created_after(@cut_off_month).select("created_at") @actions_completion_day = @user.todos.completed_after(@cut_off_month).select("completed_at") - + # convert to hash to be able to fill in non-existing days @max=0 @actions_creation_day_array = Array.new(7) { |i| 0} @actions_creation_day.each { |r| @actions_creation_day_array[ r.created_at.wday ] += 1 } - + # convert to hash to be able to fill in non-existing days @actions_completion_day_array = Array.new(7) { |i| 0} @actions_completion_day.each { |r| @actions_completion_day_array[r.completed_at.wday] += 1 } - + return { - datasets: [ - {label: I18n.t('stats.labels.created'), data: @actions_creation_day_array.map { |total| [total] } }, - {label: I18n.t('stats.labels.completed'), data: @actions_completion_day_array.map { |total| [total] } }, - ], - labels: I18n.t('date.day_names'), + datasets: [ + {label: I18n.t('stats.labels.created'), data: @actions_creation_day_array.map { |total| [total] } }, + {label: I18n.t('stats.labels.completed'), data: @actions_completion_day_array.map { |total| [total] } }, + ], + labels: I18n.t('date.day_names'), } end def time_of_day_all_data @actions_creation_hour = @user.todos.select("created_at") @actions_completion_hour = @user.todos.completed.select("completed_at") - + # convert to hash to be able to fill in non-existing days @actions_creation_hour_array = Array.new(24) { |i| 0} - @actions_creation_hour.each{|r| @actions_creation_hour_array[r.created_at.hour] += 1 } - + @actions_creation_hour.each{|r| @actions_creation_hour_array[r.created_at.hour] += 1 } + # convert to hash to be able to fill in non-existing days @actions_completion_hour_array = Array.new(24) { |i| 0} - @actions_completion_hour.each{|r| @actions_completion_hour_array[r.completed_at.hour] += 1 } - + @actions_completion_hour.each{|r| @actions_completion_hour_array[r.completed_at.hour] += 1 } + return { - datasets: [ - {label: I18n.t('stats.labels.created'), data: @actions_creation_hour_array.map { |total| [total] } }, - {label: I18n.t('stats.labels.completed'), data: @actions_completion_hour_array.map { |total| [total] } }, - ], - labels: @actions_creation_hour_array.each_with_index.map { |total, hour| [hour] }, + datasets: [ + {label: I18n.t('stats.labels.created'), data: @actions_creation_hour_array.map { |total| [total] } }, + {label: I18n.t('stats.labels.completed'), data: @actions_completion_hour_array.map { |total| [total] } }, + ], + labels: @actions_creation_hour_array.each_with_index.map { |total, hour| [hour] }, } end def time_of_day_30days_data @actions_creation_hour = @user.todos.created_after(@cut_off_month).select("created_at") @actions_completion_hour = @user.todos.completed_after(@cut_off_month).select("completed_at") - + # convert to hash to be able to fill in non-existing days @actions_creation_hour_array = Array.new(24) { |i| 0} - @actions_creation_hour.each{|r| @actions_creation_hour_array[r.created_at.hour] += 1 } - + @actions_creation_hour.each{|r| @actions_creation_hour_array[r.created_at.hour] += 1 } + # convert to hash to be able to fill in non-existing days @actions_completion_hour_array = Array.new(24) { |i| 0} - @actions_completion_hour.each{|r| @actions_completion_hour_array[r.completed_at.hour] += 1 } - + @actions_completion_hour.each{|r| @actions_completion_hour_array[r.completed_at.hour] += 1 } + return { - datasets: [ - {label: I18n.t('stats.labels.created'), data: @actions_creation_hour_array.map { |total| [total] } }, - {label: I18n.t('stats.labels.completed'), data: @actions_completion_hour_array.map { |total| [total] } }, - ], - labels: @actions_creation_hour_array.each_with_index.map { |total, hour| [hour] }, + datasets: [ + {label: I18n.t('stats.labels.created'), data: @actions_creation_hour_array.map { |total| [total] } }, + {label: I18n.t('stats.labels.completed'), data: @actions_completion_hour_array.map { |total| [total] } }, + ], + labels: @actions_creation_hour_array.each_with_index.map { |total, hour| [hour] }, } end @@ -333,7 +333,7 @@ module Stats records.each { |r| (yield r).each { |i| a[i] += 1 if a[i] } } a end - + def put_events_into_month_buckets(records, array_size, date_method_on_todo) convert_to_array(records.select { |x| x.send(date_method_on_todo) }, array_size) { |r| [difference_in_months(@today, r.send(date_method_on_todo))]} end @@ -390,7 +390,7 @@ module Stats def difference_in_days(date1, date2) return ((date1.utc.at_midnight-date2.utc.at_midnight)/SECONDS_PER_DAY).to_i end - + # assumes date1 > date2 def difference_in_weeks(date1, date2) return difference_in_days(date1, date2) / 7 @@ -415,7 +415,7 @@ module Stats def month_label(i) I18n.t('date.month_names')[ (Time.zone.now.mon - i -1 ) % 12 + 1 ] end - + def array_of_month_labels(count) Array.new(count) { |i| month_label(i) } end diff --git a/app/views/stats/_actions.html.erb b/app/views/stats/_actions.html.erb index f1604786..e6ff18be 100644 --- a/app/views/stats/_actions.html.erb +++ b/app/views/stats/_actions.html.erb @@ -1,15 +1,15 @@ <%= render :partial => 'time_to_complete', :locals => {:ttc => actions.ttc} -%> <% options = { - width: "400px", - height: "400px", - maintainAspectRatio: false, - responsive: false, - plugins: { - colorschemes: { - scheme: 'brewer.Paired12', - }, - }, + width: "400px", + height: "400px", + maintainAspectRatio: false, + responsive: false, + plugins: { + colorschemes: { + scheme: 'brewer.Paired12', + }, + }, } %>

<%= t('stats.actions_actions_avg_created_30days', :count => (actions.created_last30days*10.0/30.0).round/10.0 )%> diff --git a/app/views/stats/_contexts.html.erb b/app/views/stats/_contexts.html.erb index 5301f604..e48cec28 100644 --- a/app/views/stats/_contexts.html.erb +++ b/app/views/stats/_contexts.html.erb @@ -13,15 +13,15 @@ Stats::TopContextsQuery.new(current_user).result.map { |context| data[:ids].append(context.id) } options = { - width: "400px", - height: "400px", - maintainAspectRatio: false, - responsive: false, - plugins: { - colorschemes: { - scheme: 'brewer.Paired12', - }, - }, + width: "400px", + height: "400px", + maintainAspectRatio: false, + responsive: false, + plugins: { + colorschemes: { + scheme: 'brewer.Paired12', + }, + }, } %> <% #TODO: Move data handling to model. Show value as percentage %> From b040dfce51904246dbecde8cf01278ce60e9bbfc Mon Sep 17 00:00:00 2001 From: Jyri-Petteri Paloposki Date: Sun, 19 May 2019 17:39:38 +0300 Subject: [PATCH 07/15] #1153: Final checks against the old code, fixed the last mistakes --- app/models/stats/actions.rb | 25 ++++++++++++++++++++----- app/views/stats/_actions.html.erb | 6 +++--- 2 files changed, 23 insertions(+), 8 deletions(-) diff --git a/app/models/stats/actions.rb b/app/models/stats/actions.rb index 795b3186..56727dca 100644 --- a/app/models/stats/actions.rb +++ b/app/models/stats/actions.rb @@ -94,7 +94,7 @@ module Stats return { datasets: [ {label: I18n.t('stats.labels.avg_created'), data: created_count_array.map { |total| [total] }, type: "line"}, - {label: I18n.t('stats.labels.completed'), data: done_count_array.map { |total| [total] }, type: "line"}, + {label: I18n.t('stats.labels.avg_completed'), data: done_count_array.map { |total| [total] }, type: "line"}, {label: I18n.t('stats.labels.created'), data: @actions_created_last30days_array.map { |total| [total] } }, {label: I18n.t('stats.labels.completed'), data: @actions_done_last30days_array.map { |total| [total] } }, ], @@ -119,12 +119,16 @@ module Stats # get percentage done cumulative @cum_percent_done = convert_to_cumulative_array(@actions_completion_time_array, @actions_completion_time.count(:all)) + time_labels = Array.new(@count){ |i| "#{i}-#{i+1}" } + time_labels[0] = I18n.t('stats.within_one') + time_labels[@count] = "> #{@count}" + return { datasets: [ {label: I18n.t('stats.legend.percentage'), data: @cum_percent_done.map { |total| [total] }, type: "line"}, {label: I18n.t('stats.legend.actions'), data: @actions_completion_time_array.map { |total| [total] } }, ], - labels: @actions_completion_time_array.each_with_index.map { |total, week| [week] }, + labels: time_labels, } end @@ -145,12 +149,16 @@ module Stats # get percentage done cumulative @cum_percent_done = convert_to_cumulative_array(@actions_running_time_array, @actions_running_time.count ) + time_labels = Array.new(@count){ |i| "#{i}-#{i+1}" } + time_labels[0] = "< 1" + time_labels[@count] = "> #{@count}" + return { datasets: [ {label: I18n.t('stats.running_time_all_legend.percentage'), data: @cum_percent_done.map { |total| [total] }, type: "line"}, {label: I18n.t('stats.running_time_all_legend.actions'), data: @actions_running_time_array.map { |total| [total] } }, ], - labels: @actions_running_time_array.each_with_index.map { |total, week| [week] }, + labels: time_labels, } end @@ -180,12 +188,16 @@ module Stats # get percentage done cumulative @cum_percent_done = convert_to_cumulative_array(@actions_running_time_array, @actions_running_time.count ) + time_labels = Array.new(@count){ |i| "#{i}-#{i+1}" } + time_labels[0] = "< 1" + time_labels[@count] = "> #{@count}" + return { datasets: [ {label: I18n.t('stats.running_time_legend.percentage'), data: @cum_percent_done.map { |total| [total] }, type: "line"}, {label: I18n.t('stats.running_time_legend.actions'), data: @actions_running_time_array.map { |total| [total] } }, ], - labels: @actions_running_time_array.each_with_index.map { |total, week| [week] }, + labels: time_labels, } end @@ -202,11 +214,14 @@ module Stats @actions_open_per_week_array = convert_to_weeks_running_from_today_array(@actions_started, @max_weeks+1) @actions_open_per_week_array = cut_off_array(@actions_open_per_week_array, @count) + time_labels = Array.new(@count+1){ |i| "#{i}-#{i+1}" } + time_labels[0] = "< 1" + return { datasets: [ {label: I18n.t('stats.open_per_week_legend.actions'), data: @actions_open_per_week_array.map { |total| [total] } }, ], - labels: @actions_open_per_week_array.each_with_index.map { |total, week| [week] }, + labels: time_labels, } end diff --git a/app/views/stats/_actions.html.erb b/app/views/stats/_actions.html.erb index e6ff18be..170f61da 100644 --- a/app/views/stats/_actions.html.erb +++ b/app/views/stats/_actions.html.erb @@ -58,7 +58,7 @@ options = { yAxes: [{ scaleLabel: { display: true, labelString: t('stats.running_time_legend.actions')}}], }, 'title': {'display': true, 'text': t('stats.current_running_time_of_incomplete_visible_actions')}, - 'onClick': 'function(event, array) { window.location.href = "' + url_for(:controller => 'stats', :action => 'show_selected_actions_from_chart', :id => "art") + '?index=" + array[0]._index; }', + 'onClick': 'function(event, array) { window.location.href = "' + url_for(:controller => 'stats', :action => 'show_selected_actions_from_chart', :id => "avrt") + '?index=" + array[0]._index; }', }) %> <% @@ -67,10 +67,10 @@ options = { <%= bar_chart actions.running_time_data, options.merge({ scales: { xAxes: [{ scaleLabel: { display: true, labelString: t('stats.running_time_all_legend.running_time')}}], - yAxes: [{ scaleLabel: { display: true, labelString: t('stats.running_time_legend.actions')}}], + yAxes: [{ scaleLabel: { display: true, labelString: t('stats.running_time_all_legend.actions')}}], }, 'title': {'display': true, 'text': t('stats.running_time_all')}, - 'onClick': 'function(event, array) { window.location.href = "' + url_for(:controller => 'stats', :action => 'show_selected_actions_from_chart', :id => "avrt") + '?index=" + array[0]._index; }', + 'onClick': 'function(event, array) { window.location.href = "' + url_for(:controller => 'stats', :action => 'show_selected_actions_from_chart', :id => "art") + '?index=" + array[0]._index; }', }) %>
From 61ed4c33f316c3432b5a3b61bc22f92bb34d1bb6 Mon Sep 17 00:00:00 2001 From: Jyri-Petteri Paloposki Date: Sun, 19 May 2019 18:31:04 +0300 Subject: [PATCH 08/15] #1153: Remove the last of Flash charts --- Gemfile | 5 ++--- app/assets/swfs/expressInstall.swf | Bin 727 -> 0 bytes app/assets/swfs/open-flash-chart.swf | Bin 64600 -> 0 bytes app/views/stats/_chart.html.erb | 2 -- app/views/stats/index.html.erb | 2 -- 5 files changed, 2 insertions(+), 7 deletions(-) delete mode 100644 app/assets/swfs/expressInstall.swf delete mode 100644 app/assets/swfs/open-flash-chart.swf delete mode 100644 app/views/stats/_chart.html.erb diff --git a/Gemfile b/Gemfile index 7e6f55f0..5fa745ba 100644 --- a/Gemfile +++ b/Gemfile @@ -31,7 +31,6 @@ gem "will_paginate" gem "acts_as_list" gem "aasm", '~> 3.4.0' gem "htmlentities" -gem "swf_fu" gem "rails_autolink" gem 'puma', '~> 3.12' gem 'paperclip' @@ -39,6 +38,8 @@ gem 'paperclip' # To use ActiveModel has_secure_password gem 'bcrypt', '~> 3.1.7' +gem 'chartjs-ror' + # Turbolinks makes following links in your web application faster. Read more: https://github.com/rails/turbolinks # gem 'turbolinks' @@ -88,5 +89,3 @@ group :test do # get test coverage info on codeclimate gem "codeclimate-test-reporter", "1.0.7", group: :test, require: nil end - -gem 'chartjs-ror' diff --git a/app/assets/swfs/expressInstall.swf b/app/assets/swfs/expressInstall.swf deleted file mode 100644 index 0fbf8fca961e6319d84442248f6ba314797dccec..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 727 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 HtI>lMRC*G= diff --git a/app/views/stats/_chart.html.erb b/app/views/stats/_chart.html.erb deleted file mode 100644 index 2281dd2e..00000000 --- a/app/views/stats/_chart.html.erb +++ /dev/null @@ -1,2 +0,0 @@ -<% @swf_count ||= 0 -%> -

diff --git a/app/views/stats/index.html.erb b/app/views/stats/index.html.erb index ebb780fa..44c4f458 100644 --- a/app/views/stats/index.html.erb +++ b/app/views/stats/index.html.erb @@ -1,5 +1,3 @@ -<%= javascript_include_tag "swf_fu" %> -

<%= t('stats.totals') %>

From b7320a1de8d7a42ee575cfd525683a011852aa2e Mon Sep 17 00:00:00 2001 From: Jyri-Petteri Paloposki Date: Sun, 19 May 2019 18:40:23 +0300 Subject: [PATCH 09/15] #1153: Perhaps the final deletion of the old Flash charts code --- app/models/stats/pie_chart_data.rb | 86 ------------------------------ 1 file changed, 86 deletions(-) delete mode 100644 app/models/stats/pie_chart_data.rb diff --git a/app/models/stats/pie_chart_data.rb b/app/models/stats/pie_chart_data.rb deleted file mode 100644 index 9111c9b5..00000000 --- a/app/models/stats/pie_chart_data.rb +++ /dev/null @@ -1,86 +0,0 @@ -module Stats - class PieChartData - - attr_reader :all_totals, :alpha, :title - def initialize(all_totals, title, alpha) - @all_totals = all_totals - @title = title - @alpha = alpha - end - - def values - @values ||= Array.new(slices) do |i| - chart_totals[i]['total'] * 100 / sum - end - end - - def labels - @labels ||= Array.new(slices) do |i| - chart_totals[i]['name'].truncate(15, :omission => '...') - end - end - - def ids - @ids ||= Array.new(slices) do |i| - chart_totals[i]['id'] - end - end - - def sum - @sum ||= totals.inject(0) do |sum, total| - sum + total - end - end - - private - - def pie_cutoff - 10 - end - - def slices - @slices ||= [all_totals.size, pie_cutoff].min - end - - def subtotal(from, to) - totals[from..to].inject(0) do |sum, total| - sum + total - end - end - - def chart_totals - unless @chart_totals - @chart_totals = first_n_totals(10) - if all_totals.size > pie_cutoff - @chart_totals[-1] = other - end - end - @chart_totals - end - - def first_n_totals(n) - # create a duplicate so that we don't accidentally - # overwrite the original array - Array.new(slices) do |i| - { - 'name' => all_totals[i]['name'], - 'total' => all_totals[i]['total'], - 'id' => all_totals[i]['id'] - } - end - end - - def other - { - 'name' => I18n.t('stats.other_actions_label'), - 'id' => -1, - 'total' => subtotal(slices-1, all_totals.size-1) - } - end - - def totals - @totals ||= all_totals.map { |item| item['total'] } - end - - end -end From 313e6ee106c977943f9a69b7bf13aa71efa74270 Mon Sep 17 00:00:00 2001 From: Jyri-Petteri Paloposki Date: Sun, 19 May 2019 18:42:56 +0300 Subject: [PATCH 10/15] #1153: Remove resolved TODO comment --- app/views/stats/_actions.html.erb | 3 --- 1 file changed, 3 deletions(-) diff --git a/app/views/stats/_actions.html.erb b/app/views/stats/_actions.html.erb index 170f61da..1daf9b7f 100644 --- a/app/views/stats/_actions.html.erb +++ b/app/views/stats/_actions.html.erb @@ -25,9 +25,6 @@ options = { 'title': {'display': true, 'text': t('stats.actions_30days_title')}, }) %> -<% -# TODO: Missing the first 3 month avg values because they're null? -%> <%= bar_chart actions.done_last12months_data, options.merge({ scales: { xAxes: [{ scaleLabel: { display: true, labelString: t('stats.legend.months_ago')}}], From 458d46da9ebd5d87cf98db50dc4ad40da8dd44b4 Mon Sep 17 00:00:00 2001 From: Jyri-Petteri Paloposki Date: Sun, 19 May 2019 18:46:02 +0300 Subject: [PATCH 11/15] #1153: Remove debug output --- app/views/stats/_contexts.html.erb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/views/stats/_contexts.html.erb b/app/views/stats/_contexts.html.erb index e48cec28..54dfa730 100644 --- a/app/views/stats/_contexts.html.erb +++ b/app/views/stats/_contexts.html.erb @@ -27,7 +27,7 @@ options = { <% #TODO: Move data handling to model. Show value as percentage %> <%= pie_chart data, options.merge({ 'title': {'display': true, 'text': t('stats.spread_of_actions_for_all_context')}, - 'onClick': 'function(event, array) { console.log(array); window.location.href = "' + url_for(:controller => 'contexts', :action => 'show', :id => -1).gsub('-1', '') + '" + array[0]._chart.chart.data.ids[array[0]._index]; }' + 'onClick': 'function(event, array) { window.location.href = "' + url_for(:controller => 'contexts', :action => 'show', :id => -1).gsub('-1', '') + '" + array[0]._chart.chart.data.ids[array[0]._index]; }' }) %> <% data = { @@ -44,7 +44,7 @@ Stats::TopContextsQuery.new(current_user, :running => true).result.map { |contex } %> <%= pie_chart data, options.merge({ - 'onClick': 'function(event, array) { console.log(array); window.location.href = "' + url_for(:controller => 'contexts', :action => 'show', :id => -1).gsub('-1', '') + '" + array[0]._chart.chart.data.ids[array[0]._index]; }', + 'onClick': 'function(event, array) { window.location.href = "' + url_for(:controller => 'contexts', :action => 'show', :id => -1).gsub('-1', '') + '" + array[0]._chart.chart.data.ids[array[0]._index]; }', 'title': {'display': true, 'text': t('stats.spread_of_running_actions_for_visible_contexts')}}) %>
From 80aad1b4f2cfa4dda7d793e4760b21e0576f6981 Mon Sep 17 00:00:00 2001 From: Jyri-Petteri Paloposki Date: Mon, 20 May 2019 01:03:28 +0300 Subject: [PATCH 12/15] #1153: Remove unnecessary type change and test for stats chart templates and endpoints which were removed --- app/models/stats/actions.rb | 50 ++-- test/controllers/stats_controller_test.rb | 324 +--------------------- test/models/pie_chart_data_test.rb | 78 ------ 3 files changed, 26 insertions(+), 426 deletions(-) delete mode 100644 test/models/pie_chart_data_test.rb diff --git a/app/models/stats/actions.rb b/app/models/stats/actions.rb index 56727dca..2d862d46 100644 --- a/app/models/stats/actions.rb +++ b/app/models/stats/actions.rb @@ -63,12 +63,12 @@ module Stats return { datasets: [ - {label: I18n.t('stats.labels.avg_created'), data: @created_count_array.map { |total| [total] }, type: "line"}, - {label: I18n.t('stats.labels.avg_completed'), data: @done_count_array.map { |total| [total] }, type: "line"}, - {label: I18n.t('stats.labels.month_avg_completed', :months => 3), data: @actions_done_avg_last12months_array.map { |total| [total] }, type: "line"}, - {label: I18n.t('stats.labels.month_avg_created', :months => 3), data: @actions_created_avg_last12months_array.map { |total| [total] }, type: "line"}, - {label: I18n.t('stats.labels.created'), data: @actions_created_last12months_array.map { |total| [total] } }, - {label: I18n.t('stats.labels.completed'), data: @actions_done_last12months_array.map { |total| [total] } }, + {label: I18n.t('stats.labels.avg_created'), data: @created_count_array, type: "line"}, + {label: I18n.t('stats.labels.avg_completed'), data: @done_count_array, type: "line"}, + {label: I18n.t('stats.labels.month_avg_completed', :months => 3), data: @actions_done_avg_last12months_array, type: "line"}, + {label: I18n.t('stats.labels.month_avg_created', :months => 3), data: @actions_created_avg_last12months_array, type: "line"}, + {label: I18n.t('stats.labels.created'), data: @actions_created_last12months_array}, + {label: I18n.t('stats.labels.completed'), data: @actions_done_last12months_array}, ], labels: array_of_month_labels(@done_count_array.size), } @@ -93,10 +93,10 @@ module Stats return { datasets: [ - {label: I18n.t('stats.labels.avg_created'), data: created_count_array.map { |total| [total] }, type: "line"}, - {label: I18n.t('stats.labels.avg_completed'), data: done_count_array.map { |total| [total] }, type: "line"}, - {label: I18n.t('stats.labels.created'), data: @actions_created_last30days_array.map { |total| [total] } }, - {label: I18n.t('stats.labels.completed'), data: @actions_done_last30days_array.map { |total| [total] } }, + {label: I18n.t('stats.labels.avg_created'), data: created_count_array, type: "line"}, + {label: I18n.t('stats.labels.avg_completed'), data: done_count_array, type: "line"}, + {label: I18n.t('stats.labels.created'), data: @actions_created_last30days_array}, + {label: I18n.t('stats.labels.completed'), data: @actions_done_last30days_array}, ], labels: time_labels, } @@ -125,8 +125,8 @@ module Stats return { datasets: [ - {label: I18n.t('stats.legend.percentage'), data: @cum_percent_done.map { |total| [total] }, type: "line"}, - {label: I18n.t('stats.legend.actions'), data: @actions_completion_time_array.map { |total| [total] } }, + {label: I18n.t('stats.legend.percentage'), data: @cum_percent_done, type: "line"}, + {label: I18n.t('stats.legend.actions'), data: @actions_completion_time_array}, ], labels: time_labels, } @@ -155,8 +155,8 @@ module Stats return { datasets: [ - {label: I18n.t('stats.running_time_all_legend.percentage'), data: @cum_percent_done.map { |total| [total] }, type: "line"}, - {label: I18n.t('stats.running_time_all_legend.actions'), data: @actions_running_time_array.map { |total| [total] } }, + {label: I18n.t('stats.running_time_all_legend.percentage'), data: @cum_percent_done, type: "line"}, + {label: I18n.t('stats.running_time_all_legend.actions'), data: @actions_running_time_array}, ], labels: time_labels, } @@ -194,8 +194,8 @@ module Stats return { datasets: [ - {label: I18n.t('stats.running_time_legend.percentage'), data: @cum_percent_done.map { |total| [total] }, type: "line"}, - {label: I18n.t('stats.running_time_legend.actions'), data: @actions_running_time_array.map { |total| [total] } }, + {label: I18n.t('stats.running_time_legend.percentage'), data: @cum_percent_done, type: "line"}, + {label: I18n.t('stats.running_time_legend.actions'), data: @actions_running_time_array}, ], labels: time_labels, } @@ -219,7 +219,7 @@ module Stats return { datasets: [ - {label: I18n.t('stats.open_per_week_legend.actions'), data: @actions_open_per_week_array.map { |total| [total] } }, + {label: I18n.t('stats.open_per_week_legend.actions'), data: @actions_open_per_week_array}, ], labels: time_labels, } @@ -240,8 +240,8 @@ module Stats return { datasets: [ - {label: I18n.t('stats.labels.created'), data: @actions_creation_day_array.map { |total| [total] } }, - {label: I18n.t('stats.labels.completed'), data: @actions_completion_day_array.map { |total| [total] } }, + {label: I18n.t('stats.labels.created'), data: @actions_creation_day_array}, + {label: I18n.t('stats.labels.completed'), data: @actions_completion_day_array}, ], labels: I18n.t('date.day_names'), } @@ -262,8 +262,8 @@ module Stats return { datasets: [ - {label: I18n.t('stats.labels.created'), data: @actions_creation_day_array.map { |total| [total] } }, - {label: I18n.t('stats.labels.completed'), data: @actions_completion_day_array.map { |total| [total] } }, + {label: I18n.t('stats.labels.created'), data: @actions_creation_day_array}, + {label: I18n.t('stats.labels.completed'), data: @actions_completion_day_array}, ], labels: I18n.t('date.day_names'), } @@ -283,8 +283,8 @@ module Stats return { datasets: [ - {label: I18n.t('stats.labels.created'), data: @actions_creation_hour_array.map { |total| [total] } }, - {label: I18n.t('stats.labels.completed'), data: @actions_completion_hour_array.map { |total| [total] } }, + {label: I18n.t('stats.labels.created'), data: @actions_creation_hour_array}, + {label: I18n.t('stats.labels.completed'), data: @actions_completion_hour_array}, ], labels: @actions_creation_hour_array.each_with_index.map { |total, hour| [hour] }, } @@ -304,8 +304,8 @@ module Stats return { datasets: [ - {label: I18n.t('stats.labels.created'), data: @actions_creation_hour_array.map { |total| [total] } }, - {label: I18n.t('stats.labels.completed'), data: @actions_completion_hour_array.map { |total| [total] } }, + {label: I18n.t('stats.labels.created'), data: @actions_creation_hour_array}, + {label: I18n.t('stats.labels.completed'), data: @actions_completion_hour_array}, ], labels: @actions_creation_hour_array.each_with_index.map { |total, hour| [hour] }, } diff --git a/test/controllers/stats_controller_test.rb b/test/controllers/stats_controller_test.rb index 85ef0f8d..74edf489 100644 --- a/test/controllers/stats_controller_test.rb +++ b/test/controllers/stats_controller_test.rb @@ -1,5 +1,6 @@ require 'test_helper' +# TODO: Add more detailed testing of the charts. There are previously defined tests in VCS before the Flash to Chart.js change. class StatsControllerTest < ActionController::TestCase def test_get_index_when_not_logged_in @@ -13,35 +14,6 @@ class StatsControllerTest < ActionController::TestCase assert_response :success end - def test_get_charts - login_as(:admin_user) - %w{ - actions_done_last30days_data - actions_done_last12months_data - actions_completion_time_data - actions_visible_running_time_data - actions_running_time_data - actions_open_per_week_data - actions_day_of_week_all_data - actions_day_of_week_30days_data - actions_time_of_day_all_data - actions_time_of_day_30days_data - }.each do |action| - get action - assert_response :success - assert_template "stats/"+action - end - - %w{ - context_total_actions_data - context_running_actions_data - }.each do |action| - get action - assert_response :success - assert_template "stats/pie_chart_data" - end - end - def test_totals login_as(:admin_user) get :index @@ -102,300 +74,6 @@ class StatsControllerTest < ActionController::TestCase assert_response :success end - def test_actions_done_last12months_data - travel_to Time.local(2013, 1, 15) do - login_as(:admin_user) - @current_user = User.find(users(:admin_user).id) - @current_user.todos.delete_all - - given_todos_for_stats - - # When I get the chart data - get :actions_done_last12months_data - assert_response :success - - # Then the todos for the chart should be retrieved - #assert_not_nil assigns['actions_done_last12months'] - #assert_not_nil assigns['actions_created_last12months'] - #assert_equal 7, assigns['actions_created_last12months'].count, "very old todo should not be retrieved" - - # And they should be totalled in a hash - assert_equal 2, assigns['actions_created_last12months_array'][0], "there should be two todos in current month" - - assert_equal 1, assigns['actions_created_last12months_array'][1], "there should be one todo in previous month" - assert_equal 1, assigns['actions_created_last12months_array'][2], "there should be one todo in two month ago" - assert_equal 1, assigns['actions_created_last12months_array'][3], "there should be one todo in three month ago" - assert_equal 2, assigns['actions_created_last12months_array'][4], "there should be two todos (1 created & 1 done) in four month ago" - - assert_equal 1, assigns['actions_done_last12months_array'][1], "there should be one completed todo one-two months ago" - assert_equal 1, assigns['actions_done_last12months_array'][2], "there should be one completed todo two-three months ago" - assert_equal 1, assigns['actions_done_last12months_array'][4], "there should be one completed todo four-five months ago" - - # And they should be averaged over three months - assert_equal 2/3.0, assigns['actions_done_avg_last12months_array'][1], "fourth month should be excluded" - assert_equal 2/3.0, assigns['actions_done_avg_last12months_array'][2], "fourth month should be included" - - assert_equal (3)/3.0, assigns['actions_created_avg_last12months_array'][1], "one every month" - assert_equal (4)/3.0, assigns['actions_created_avg_last12months_array'][2], "two in fourth month" - - # And the current month should be interpolated - fraction = Time.zone.now.day.to_f / Time.zone.now.end_of_month.day.to_f - assert_equal (2*(1/fraction)+2)/3.0, assigns['interpolated_actions_created_this_month'], "two this month and one in the last two months" - assert_equal (2)/3.0, assigns['interpolated_actions_done_this_month'], "none this month and one two the last two months" - - # And totals should be calculated - assert_equal 2, assigns['max'], "max of created or completed todos in one month" - end - end - - def test_empty_last12months_data - travel_to Time.local(2013, 1, 15) do - login_as(:admin_user) - @current_user = User.find(users(:admin_user).id) - @current_user.todos.delete_all - given_todos_for_stats - get :actions_done_last12months_data - assert_response :success - end - end - - def test_out_of_bounds_events_for_last12months_data - login_as(:admin_user) - @current_user = User.find(users(:admin_user).id) - @current_user.todos.delete_all - create_todo_in_past(2.years) - create_todo_in_past(15.months) - - get :actions_done_last12months_data - assert_response :success - end - - def test_actions_done_last30days_data - login_as(:admin_user) - @current_user = User.find(users(:admin_user).id) - @current_user.todos.delete_all - - given_todos_for_stats - - # When I get the chart data - get :actions_done_last30days_data - assert_response :success - - # only tests relevant differences with actions_done_last_12months_data - - assert_equal 31, assigns['actions_done_last30days_array'].size, "30 complete days plus 1 for the current day" - assert_equal 2, assigns['max'], "two actions created on one day is max" - end - - def test_actions_done_lastyears_data - login_as(:admin_user) - @current_user = User.find(users(:admin_user).id) - @current_user.todos.delete_all - - given_todos_for_stats - - # When I get the chart data - get :actions_done_lastyears_data - assert_response :success - - # only tests difference with actions_done_last_12months_data - - # And the last two months are corrected - assert_equal 2/3.0, assigns['actions_done_avg_last_months_array'][23] - assert_equal 2/3.0, assigns['actions_done_avg_last_months_array'][24] - end - - def test_actions_completion_time_data - login_as(:admin_user) - @current_user = User.find(users(:admin_user).id) - @current_user.todos.delete_all - - given_todos_for_stats - - # When I get the chart data - get :actions_completion_time_data - assert_response :success - - # do not test stuff already implicitly tested in other tests - assert_equal 104, assigns['max_weeks'], "two years is 104 weeks (for completed_at)" - assert_equal 3, assigns['max_actions'], "3 completed within one week" - assert_equal 11, assigns['actions_completion_time_array'].size, "there should be 10 weeks of data + 1 for the rest" - assert_equal 1, assigns['actions_completion_time_array'][10], "there is one completed todo after the 10 weeks cut_off" - assert_equal 100.0, assigns['cum_percent_done'][10], "cumulative percentage should add up to 100%" - end - - def test_actions_running_time_data - login_as(:admin_user) - @current_user = User.find(users(:admin_user).id) - @current_user.todos.delete_all - - given_todos_for_stats - - # When I get the chart data - get :actions_running_time_data - assert_response :success - - # do not test stuff already implicitly tested in other tests - assert_equal 17, assigns['max_weeks'], "there are actions in the first 17 weeks of this year" - assert_equal 2, assigns['max_actions'], "2 actions running long together" - assert_equal 18, assigns['actions_running_time_array'].size, "there should be 17 weeks ( < cut_off) of data + 1 for the rest" - assert_equal 1, assigns['actions_running_time_array'][17], "there is one running todos in week 17 and zero after 17 weeks ( < cut off; ) " - assert_equal 100.0, assigns['cum_percent_done'][17], "cumulative percentage should add up to 100%" - end - - def test_actions_open_per_week_data - login_as(:admin_user) - @current_user = User.find(users(:admin_user).id) - @current_user.todos.delete_all - - given_todos_for_stats - - # When I get the chart data - get :actions_open_per_week_data - assert_response :success - - # do not test stuff already implicitly tested in other tests - assert_equal 17, assigns['max_weeks'], "there are actions in the first 17 weeks of this year" - assert_equal 4, assigns['max_actions'], "4 actions running together" - assert_equal 17, assigns['actions_open_per_week_array'].size, "there should be 17 weeks ( < cut_off) of data" - end - - def test_actions_visible_running_time_data - login_as(:admin_user) - @current_user = User.find(users(:admin_user).id) - @current_user.todos.delete_all - - given_todos_for_stats - # Given todo1 is deferred (i.e. not visible) - @todo_today1.show_from = Time.zone.now + 1.week - @todo_today1.save - - # When I get the chart data - get :actions_visible_running_time_data - assert_response :success - - # do not test stuff already implicitly tested in other tests - assert_equal 17, assigns['max_weeks'], "there are actions in the first 17 weeks of this year" - assert_equal 1, assigns['max_actions'], "1 action running long; 1 is deferred" - assert_equal 1, assigns['actions_running_time_array'][0], "there is one running todos and one deferred todo created in week 1" - assert_equal 18, assigns['actions_running_time_array'].size, "there should be 17 weeks ( < cut_off) of data + 1 for the rest" - assert_equal 1, assigns['actions_running_time_array'][17], "there is one running todos in week 17 and zero after 17 weeks ( < cut off; ) " - assert_equal 100.0, assigns['cum_percent_done'][17], "cumulative percentage should add up to 100%" - end - - def test_context_total_actions_data - login_as(:admin_user) - @current_user = User.find(users(:admin_user).id) - @current_user.todos.delete_all - - given_todos_for_stats - - # When I get the chart data - get :context_total_actions_data - assert_response :success - - assert_equal 9, assigns['data'].sum, "Nine todos in 1 context" - assert_equal 1, assigns['data'].values.size - - # Given 10 more todos in 10 different contexts - 1.upto(10) do |i| - context = @current_user.contexts.create!(:name => "context #{i}") - @current_user.todos.create!(:description => "created today with new context #{i}", :context => context) - end - - # When I get the chart data - get :context_total_actions_data - assert_response :success - - assert_equal 19, assigns['data'].sum, "added 10 todos" - assert_equal 10, assigns['data'].values.size, "pie slices limited to max 10" - assert_equal 10, assigns['data'].values[9], "pie slices limited to max 10; last pie contains sum of rest (in percentage)" - assert_equal "(others)", assigns['data'].labels[9], "pie slices limited to max 10; last slice contains label for others" - end - - def test_context_running_actions_data - login_as(:admin_user) - @current_user = User.find(users(:admin_user).id) - @current_user.todos.delete_all - - given_todos_for_stats - - # When I get the chart data - get :context_running_actions_data - assert_response :success - - assert_equal 4, assigns['data'].sum, "Four todos in 1 context" - assert_equal 1, assigns['data'].values.size - - # Given 10 more todos in 10 different contexts - 1.upto(10) do |i| - context = @current_user.contexts.create!(:name => "context #{i}") - @current_user.todos.create!(:description => "created today with new context #{i}", :context => context) - end - - # When I get the chart data - get :context_running_actions_data - assert_response :success - - assert_equal 10, assigns['data'].values.size, "pie slices limited to max 10" - assert_equal 14, assigns['data'].values[9], "pie slices limited to max 10; last pie contains sum of rest (in percentage)" - assert_equal "(others)", assigns['data'].labels[9], "pie slices limited to max 10; last slice contains label for others" - end - - def test_actions_day_of_week_all_data - login_as(:admin_user) - @current_user = User.find(users(:admin_user).id) - @current_user.todos.delete_all - - given_todos_for_stats - - # When I get the chart data - get :actions_day_of_week_all_data - assert_response :success - - # FIXME: testdata is relative from today, so not stable to test on day_of_week - # trivial not_nil tests - assert_not_nil assigns['max'] - assert_not_nil assigns['actions_creation_day_array'] - assert_not_nil assigns['actions_completion_day_array'] - end - - def test_actions_day_of_week_30days_data - login_as(:admin_user) - @current_user = User.find(users(:admin_user).id) - @current_user.todos.delete_all - - given_todos_for_stats - - # When I get the chart data - get :actions_day_of_week_30days_data - assert_response :success - - # FIXME: testdata is relative from today, so not stable to test on day_of_week - # trivial not_nil tests - assert_not_nil assigns['max'] - assert_not_nil assigns['actions_creation_day_array'] - assert_not_nil assigns['actions_completion_day_array'] - end - - def test_actions_time_of_day_all_data - login_as(:admin_user) - @current_user = User.find(users(:admin_user).id) - @current_user.todos.delete_all - - given_todos_for_stats - - # When I get the chart data - get :actions_time_of_day_all_data - assert_response :success - - # FIXME: testdata is relative from today, so not stable to test on day_of_week - # for now just trivial not_nil tests - assert_not_nil assigns['max'] - assert_not_nil assigns['actions_creation_hour_array'] - assert_not_nil assigns['actions_completion_hour_array'] - end - def test_show_selected_actions_from_chart_avrt login_as(:admin_user) @current_user = User.find(users(:admin_user).id) diff --git a/test/models/pie_chart_data_test.rb b/test/models/pie_chart_data_test.rb deleted file mode 100644 index e50c8db0..00000000 --- a/test/models/pie_chart_data_test.rb +++ /dev/null @@ -1,78 +0,0 @@ -require 'minimal_test_helper' -require 'app/models/stats/pie_chart_data' -require 'active_support/core_ext/string' - -class Stats::PieChartDataTest < Minitest::Test - - def test_with_0_items - data = Stats::PieChartData.new([], 'a chart', 50) - - assert_equal [], data.values - assert_equal [], data.labels - assert_equal [], data.ids - end - - def test_with_less_than_10_items - items = [ - {'id' => 1, 'name' => 'one', 'total' => 11}, - {'id' => 2, 'name' => 'two', 'total' => 4}, - {'id' => 3, 'name' => 'three', 'total' => 8}, - {'id' => 4, 'name' => 'four', 'total' => 13}, - {'id' => 5, 'name' => 'five', 'total' => 20}, - {'id' => 6, 'name' => 'six', 'total' => 17}, - {'id' => 7, 'name' => 'seven', 'total' => 5}, - {'id' => 8, 'name' => 'eight', 'total' => 1}, - {'id' => 9, 'name' => 'nine', 'total' => 6} - ] - - data = Stats::PieChartData.new(items, 'a chart', 50) - - assert_equal [12, 4, 9, 15, 23, 20, 5, 1, 7], data.values - assert_equal ["one", "two", "three", "four", "five", "six", "seven", "eight", "nine"], data.labels - assert_equal [1, 2, 3, 4, 5, 6, 7, 8, 9], data.ids - end - - def test_with_exactly_10_items - items = [ - {'id' => 1, 'name' => 'one', 'total' => 11}, - {'id' => 2, 'name' => 'two', 'total' => 4}, - {'id' => 3, 'name' => 'three', 'total' => 8}, - {'id' => 4, 'name' => 'four', 'total' => 13}, - {'id' => 5, 'name' => 'five', 'total' => 20}, - {'id' => 6, 'name' => 'six', 'total' => 17}, - {'id' => 7, 'name' => 'seven', 'total' => 5}, - {'id' => 8, 'name' => 'eight', 'total' => 1}, - {'id' => 9, 'name' => 'nine', 'total' => 6}, - {'id' => 10, 'name' => 'ten', 'total' => 19} - ] - - data = Stats::PieChartData.new(items, 'a chart', 50) - - assert_equal [10, 3, 7, 12, 19, 16, 4, 0, 5, 18], data.values - assert_equal ["one", "two", "three", "four", "five", "six", "seven", "eight", "nine", "ten"], data.labels - assert_equal [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], data.ids - end - - def test_with_more_than_10_items - items = [ - {'id' => 1, 'name' => 'one', 'total' => 11}, - {'id' => 2, 'name' => 'two', 'total' => 4}, - {'id' => 3, 'name' => 'three', 'total' => 8}, - {'id' => 4, 'name' => 'four', 'total' => 13}, - {'id' => 5, 'name' => 'five', 'total' => 20}, - {'id' => 6, 'name' => 'six', 'total' => 17}, - {'id' => 7, 'name' => 'seven', 'total' => 5}, - {'id' => 8, 'name' => 'eight', 'total' => 1}, - {'id' => 9, 'name' => 'nine', 'total' => 6}, - {'id' => 10, 'name' => 'ten', 'total' => 19}, - {'id' => 11, 'name' => 'eleven', 'total' => 14} - ] - - data = Stats::PieChartData.new(items, 'a chart', 50) - - assert_equal [9, 3, 6, 11, 16, 14, 4, 0, 5, 27], data.values - assert_equal ["one", "two", "three", "four", "five", "six", "seven", "eight", "nine", "(others)"], data.labels - assert_equal [1, 2, 3, 4, 5, 6, 7, 8, 9, -1], data.ids - end - -end From 088346ecb03fba5bbe9dbe6321cc251d8952df97 Mon Sep 17 00:00:00 2001 From: Jyri-Petteri Paloposki Date: Mon, 20 May 2019 01:21:55 +0300 Subject: [PATCH 13/15] One more decomissioned test file away --- test/controllers/context_actions_data_test.rb | 34 ------------------- 1 file changed, 34 deletions(-) delete mode 100644 test/controllers/context_actions_data_test.rb diff --git a/test/controllers/context_actions_data_test.rb b/test/controllers/context_actions_data_test.rb deleted file mode 100644 index 0bcaae6a..00000000 --- a/test/controllers/context_actions_data_test.rb +++ /dev/null @@ -1,34 +0,0 @@ -require 'test_helper' - -class ContextActionsDataTest < ActionController::TestCase - tests StatsController - - def test_total_with_more_than_10_items - login_as(:admin_user) - contexts = [ - {'id' => 1, 'name' => 'one', 'total' => 11}, - {'id' => 2, 'name' => 'two', 'total' => 4}, - {'id' => 3, 'name' => 'three', 'total' => 8} - ] - Stats::TopContextsQuery.any_instance.stubs(:result).returns contexts - - get :context_total_actions_data - - assert_equal [47, 17, 34], assigns[:data].values - end - - def test_running_actions - login_as(:admin_user) - contexts = [ - {'id' => 1, 'name' => 'one', 'total' => 11}, - {'id' => 2, 'name' => 'two', 'total' => 4}, - {'id' => 3, 'name' => 'three', 'total' => 8} - ] - Stats::TopContextsQuery.any_instance.stubs(:result).returns contexts - - get :context_running_actions_data - - assert_equal [47, 17, 34], assigns[:data].values - end - -end From ccb5e1e2f16789c99f15671cfd6bb16168a9b2ac Mon Sep 17 00:00:00 2001 From: Jyri-Petteri Paloposki Date: Mon, 20 May 2019 01:39:39 +0300 Subject: [PATCH 14/15] Remove the decomissioned chart view from the selection display listing --- app/views/stats/show_selection_from_chart.html.erb | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/app/views/stats/show_selection_from_chart.html.erb b/app/views/stats/show_selection_from_chart.html.erb index 80b40306..29e9dfdb 100644 --- a/app/views/stats/show_selection_from_chart.html.erb +++ b/app/views/stats/show_selection_from_chart.html.erb @@ -1,7 +1,5 @@ -<%= render :partial => 'chart', :locals => {:chart => @chart} -%> -

-<%= t('stats.click_to_update_actions') %> <%= raw t('stats.click_to_return', :link => link_to(t('stats.click_to_return_link'), stats_path)) %> +<%= raw t('stats.click_to_return', :link => link_to(t('stats.click_to_return_link'), stats_path)) %> <% unless @further -%> From a86a59b68ecf1c950c89fbe5a1de733e52f8cb45 Mon Sep 17 00:00:00 2001 From: Jyri-Petteri Paloposki Date: Tue, 21 May 2019 16:05:33 +0300 Subject: [PATCH 15/15] Removing unnecessary Gems --- Gemfile.lock | 4 ---- 1 file changed, 4 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 49e0d59b..4b77e528 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -245,9 +245,6 @@ GEM activesupport (>= 4.0) sprockets (>= 3.0.0) sqlite3 (1.4.1) - swf_fu (2.0.4) - coffee-script - rails (>= 3.1) terrapin (0.6.0) climate_control (>= 0.0.3, < 1.0) therubyracer (0.12.3) @@ -314,7 +311,6 @@ DEPENDENCIES simplecov spring sqlite3 - swf_fu therubyracer tolk (~> 3.1.0) uglifier (>= 1.3.0)