chart-candy 0.0.7

Sign up to get free protection for your applications and to get access to all the features.
Files changed (34) hide show
  1. data/Gemfile +4 -0
  2. data/LICENSE +7 -0
  3. data/README.md +109 -0
  4. data/Rakefile +11 -0
  5. data/app/assets/javascripts/chart_candy/base.coffee +16 -0
  6. data/app/assets/javascripts/chart_candy/counter.coffee +50 -0
  7. data/app/assets/javascripts/chart_candy/donut.coffee +162 -0
  8. data/app/assets/javascripts/chart_candy/index.js +4 -0
  9. data/app/assets/javascripts/chart_candy/line.coffee +338 -0
  10. data/app/assets/stylesheets/chart_candy/base.css.scss +66 -0
  11. data/app/assets/stylesheets/chart_candy/counter.css.scss +11 -0
  12. data/app/assets/stylesheets/chart_candy/donut.css.scss +34 -0
  13. data/app/assets/stylesheets/chart_candy/index.css +6 -0
  14. data/app/assets/stylesheets/chart_candy/line.css.scss +107 -0
  15. data/app/controllers/candy_charts_controller.rb +60 -0
  16. data/config/locales/fr.yml +36 -0
  17. data/config/routes.rb +3 -0
  18. data/lib/chart-candy.rb +23 -0
  19. data/lib/chart-candy/authentication.rb +45 -0
  20. data/lib/chart-candy/base_chart.rb +11 -0
  21. data/lib/chart-candy/builder.rb +46 -0
  22. data/lib/chart-candy/builder/base.rb +70 -0
  23. data/lib/chart-candy/builder/counter.rb +12 -0
  24. data/lib/chart-candy/builder/donut.rb +73 -0
  25. data/lib/chart-candy/builder/line.rb +114 -0
  26. data/lib/chart-candy/builder/xls_builder.rb +159 -0
  27. data/lib/chart-candy/engine.rb +5 -0
  28. data/lib/chart-candy/helpers.rb +169 -0
  29. data/lib/chart-candy/implants.rb +10 -0
  30. data/lib/chart-candy/implants/railtie.rb +17 -0
  31. data/spec/chart-candy_spec.rb +11 -0
  32. data/spec/spec_helper.rb +12 -0
  33. data/vendor/assets/javascripts/d3.js +4 -0
  34. metadata +118 -0
@@ -0,0 +1,70 @@
1
+ class ChartCandy::Builder::Base
2
+ def initialize(id, options={})
3
+ options.reverse_merge! from: nil, to: nil, step: nil
4
+
5
+ @from = options[:from] ? Time.parse(options[:from]) : nil
6
+ @to = options[:to] ? Time.parse(options[:to]) : Time.now
7
+
8
+ @chart = { id: id }
9
+ @chart[:step] = options[:step] if options[:step]
10
+ @chart[:title] = t('title')
11
+ @chart[:period] = ChartCandy::Builder.period(@from, @to, step: @chart[:step]) if @from
12
+ end
13
+
14
+ def close_chart
15
+ # Hooks before closing a chart
16
+ end
17
+
18
+ def filename
19
+ name = [title.parameterize]
20
+ name << @from.strftime('%Y%m%d') if @from
21
+ name << @to.strftime('%Y%m%d') if @to
22
+
23
+ return name.compact.join('-')
24
+ end
25
+
26
+ def id
27
+ @chart[:id]
28
+ end
29
+
30
+ def l(date, options={})
31
+ options.reverse_merge!(format: :date_long)
32
+
33
+ return ChartCandy.localize(date, options)
34
+ end
35
+
36
+ def period
37
+ @chart[:period]
38
+ end
39
+
40
+ def set_period_from_data(data)
41
+ @from = data.first
42
+ @to = data.last
43
+
44
+ @chart[:step] = ChartCandy::Builder.get_step_from_interval(data[1] - data[0]) if not @chart[:step]
45
+
46
+ @chart[:period] = ChartCandy::Builder.period @from, @to, step: @chart[:step]
47
+ end
48
+
49
+ def t(path, vars={})
50
+ vars.reverse_merge! :default => ''
51
+
52
+ ChartCandy.translate("#{id.gsub('-', '_')}.#{path}", vars)
53
+ end
54
+
55
+ def title
56
+ @chart[:title]
57
+ end
58
+
59
+ def to_json
60
+ close_chart
61
+
62
+ return @chart.to_json
63
+ end
64
+
65
+ def to_xls
66
+ close_chart
67
+
68
+ return ChartCandy::Builder::XlsBuilder.chart_to_xls @chart
69
+ end
70
+ end
@@ -0,0 +1,12 @@
1
+ class ChartCandy::Builder::Counter < ChartCandy::Builder::Base
2
+
3
+ def initialize(id, options={})
4
+ super
5
+
6
+ @chart.merge! nature: 'count', data: []
7
+ end
8
+
9
+ def add_primary(id, value)
10
+ @chart[:data] << { nature: :primary, label: t("data.#{id}.label"), id: id, value: value }
11
+ end
12
+ end
@@ -0,0 +1,73 @@
1
+ class ChartCandy::Builder::Donut < ChartCandy::Builder::Base
2
+
3
+ def initialize(id, options={})
4
+ super
5
+
6
+ @chart.merge! hole: [], label: t('label'), nature: 'donut', slices: [], show_label: true, unit: :number, value: t('value')
7
+ end
8
+
9
+ def add_hole_item(name, value)
10
+ @chart[:hole] = [t('hole.title')].flatten if hole.empty?
11
+ hole << t("hole.#{name}", value: value)
12
+ end
13
+
14
+ def add_slice(name, value, options={})
15
+ options.reverse_merge! txt_vars: {}
16
+
17
+ return if value.to_i <= 0
18
+
19
+ value = value.round(2) if money?
20
+ valuef = money? ? format_money(value) : value
21
+
22
+ options[:txt_vars][:value] = valuef
23
+
24
+ label_str = t("slices.#{name}.label", options[:txt_vars])
25
+ tooltip = t("slices.#{name}.tooltip", options[:txt_vars])
26
+
27
+ @chart[:slices] << { label: label_str, percent: 0, tooltip: tooltip, value: value, valuef: valuef }
28
+ end
29
+
30
+ def close_chart
31
+ total = @chart[:slices].sum{ |s| s[:value] }
32
+
33
+ total = total.round(2) if money?
34
+
35
+ @chart[:total] = { label: 'Total', value: total }
36
+
37
+ fill_percents
38
+ end
39
+
40
+ def format_money(value)
41
+ sprintf("%0.02f", (value.to_f).round(2)).gsub('.', ',') + ' $'
42
+ end
43
+
44
+ def hole
45
+ @chart[:hole]
46
+ end
47
+
48
+ def money?
49
+ @chart[:unit] == :money
50
+ end
51
+
52
+ def show_label=(active)
53
+ @chart[:show_label] = active
54
+ end
55
+
56
+ def show_label
57
+ @chart[:show_label]
58
+ end
59
+
60
+ def unit=(unit_sym)
61
+ @chart[:unit] = unit_sym.to_sym if [:number, :money].include? unit_sym.to_sym
62
+ end
63
+
64
+ def unit
65
+ @chart[:unit]
66
+ end
67
+
68
+ private
69
+
70
+ def fill_percents
71
+ @chart[:slices].each { |s| s[:percent] = (s[:value].to_f * 100 / @chart[:total][:value]).round(2) }
72
+ end
73
+ end
@@ -0,0 +1,114 @@
1
+ class ChartCandy::Builder::Line < ChartCandy::Builder::Base
2
+
3
+ def initialize(id, options={})
4
+ super
5
+
6
+ @chart.merge! axis: {}, legend: nil, lines: [], nature: 'line', tooltip: true
7
+ end
8
+
9
+ def add_dot(dot, id, x_name, y_name)
10
+ {
11
+ x: dot[x_name],
12
+ y: dot[y_name],
13
+ label_x: add_dot_label(id, dot[x_name], @chart[:axis][:x][:nature]),
14
+ label_y: add_dot_label(id, dot[y_name], @chart[:axis][:y][:nature])
15
+ }
16
+ end
17
+
18
+ def add_dot_label(id, value, nature)
19
+ case nature
20
+ when :date then add_dot_label_date value
21
+ when :money then add_dot_label_money value
22
+ else value.to_s + ' ' + t("lines.#{id}.unit")
23
+ end
24
+ end
25
+
26
+ def add_dot_label_date(date)
27
+ case @chart[:step]
28
+ when 'day' then l(date, format: :date_long)
29
+ when 'week' then ChartCandy.translate('date.week') + ' ' + l(date, format: :date_long).strip
30
+ when 'month' then l(date, format: :date_without_day).capitalize
31
+ else l(date, format: :date_long)
32
+ end
33
+ end
34
+
35
+ def add_dot_label_money(amount)
36
+ sprintf("%0.02f", amount.round(2)).gsub('.', ',') + ' $'
37
+ end
38
+
39
+ def add_line(id, original_data, options={})
40
+ options.reverse_merge! axis_y: "left", txt_vars: {}, key_x: "time", key_y: "value"
41
+
42
+ data = original_data.map{ |d| add_dot(d, id, options[:key_x], options[:key_y]) }
43
+
44
+ [:x, :y].each do |key|
45
+ [:min, :max].each { |m| @chart[:axis][key][m] = to_money_format(@chart[:axis][key][m]) } if money? key
46
+ end
47
+
48
+ data = original_data.map do |d|
49
+ [:key_x, :key_y].each { |key| d[options[key]] = to_money_format(d[options[key]]) if money?(key[-1,1]) }
50
+ add_dot(d, id, options[:key_x], options[:key_y])
51
+ end
52
+
53
+ @chart[:lines] << { axis_y: options[:axis_y], data: data, label: t("lines.#{id}.label", options[:txt_vars]), unit: t("lines.#{id}.unit"), total: get_total(data) }
54
+ end
55
+
56
+ def add_x_axis(nature, original_data, options={})
57
+ options.reverse_merge! key: "time"
58
+
59
+ data = original_data.map{ |d| d[options[:key]] }
60
+
61
+ set_period_from_data data if not @from and nature == :date
62
+
63
+ @chart[:axis][:x] = { nature: nature, label: t("axis.x.label"), min: data.min, max: data.max, max_ticks: data.length }
64
+ end
65
+
66
+ def add_y_axis(nature, original_data, options={})
67
+ options.reverse_merge! key: 'value', max: nil, min: nil
68
+
69
+ data = original_data.map{ |d| d[options[:key]] }
70
+
71
+ min = options[:min] ? options[:min] : data.min
72
+ max = options[:max] ? options[:max] : data.max
73
+
74
+ @chart[:axis][:y] = { nature: nature, label: t('axis.y.label'), min: min, max: max, max_ticks: data.length }
75
+ end
76
+
77
+ def close_chart
78
+ super
79
+
80
+ @chart[:legend] = (@chart[:lines].length > 1) if @chart[:legend].nil?
81
+ end
82
+
83
+ def date_based?
84
+ @chart[:axis][:x] and @chart[:axis][:x][:nature] == :date
85
+ end
86
+
87
+ def get_total(data)
88
+ { label: 'Total', value: data.sum{ |d| d[:y] } }
89
+ end
90
+
91
+ def legend=(active)
92
+ @chart[:legend] = active
93
+ end
94
+
95
+ def legend
96
+ @chart[:legend]
97
+ end
98
+
99
+ def money?(key)
100
+ @chart[:axis][key.to_sym][:nature] == :money
101
+ end
102
+
103
+ def to_money_format(value)
104
+ (BigDecimal.new(value) / 100).round(2)
105
+ end
106
+
107
+ def tooltip=(active)
108
+ @chart[:tooltip] = active
109
+ end
110
+
111
+ def tooltip
112
+ @chart[:tooltip]
113
+ end
114
+ end
@@ -0,0 +1,159 @@
1
+ require 'spreadsheet'
2
+
3
+ class ChartCandy::Builder::XlsBuilder
4
+ attr_reader :current_row, :current_column, :workbook
5
+
6
+ def self.chart_to_xls(chart)
7
+ xls = self.new(chart)
8
+ xls.generate
9
+
10
+ return xls.workbook
11
+ end
12
+
13
+ def initialize(chart)
14
+ @chart = chart
15
+ @workbook = Spreadsheet::Workbook.new
16
+ @sheet = @workbook.create_worksheet
17
+ @formats = build_formats
18
+
19
+ @current_row = -1
20
+ @current_row_format = :normal
21
+ @current_column = 0
22
+
23
+ @columns_width = []
24
+ @max_column_width = 50
25
+ @default_column_width = 10
26
+ end
27
+
28
+ def add_align_right_formats(formats)
29
+ align_right = {}
30
+
31
+ formats.each do |k,origin|
32
+ new_format = origin.dup
33
+ new_format.horizontal_align = :right
34
+
35
+ align_right["#{k}_right".to_sym] = new_format
36
+ end
37
+
38
+ formats.merge! align_right
39
+ end
40
+
41
+ def build_formats
42
+ f = {}
43
+
44
+ f[:h1] = Spreadsheet::Format.new(weight: :bold, size: 16, horizontal_align: :left, vertical_align: :middle)
45
+ f[:h2] = Spreadsheet::Format.new(weight: :normal, size: 12, horizontal_align: :left, vertical_align: :middle)
46
+ f[:h3] = Spreadsheet::Format.new(weight: :normal, size: 9, horizontal_align: :left, vertical_align: :middle)
47
+ f[:h4] = Spreadsheet::Format.new(weight: :bold, size: 8, horizontal_align: :left, vertical_align: :middle)
48
+ f[:th] = Spreadsheet::Format.new(weight: :bold, size: 8, horizontal_align: :center, vertical_align: :middle, pattern_fg_color: :xls_color_19, pattern: 1)
49
+ f[:th_foot] = Spreadsheet::Format.new(weight: :bold, size: 8, horizontal_align: :left, vertical_align: :middle, pattern_fg_color: :xls_color_19, pattern: 1)
50
+ f[:normal] = Spreadsheet::Format.new(size: 8, horizontal_align: :left , vertical_align: :middle)
51
+
52
+ add_align_right_formats f
53
+
54
+ return f
55
+ end
56
+
57
+ def cell(content=nil, options={})
58
+ options.reverse_merge! format: nil, nature: :text
59
+
60
+ row_obj = @sheet.row(current_row)
61
+
62
+ row_obj.set_format current_column, @formats[options[:format]] if options[:format]
63
+ set_cell_format options[:nature]
64
+ row_obj.height = row_obj.format(0).font.size * 1.6
65
+
66
+ parsed_content = format_data(content)
67
+
68
+ @sheet[current_row, current_column] = parsed_content
69
+
70
+ @columns_width[current_column] = parsed_content.to_s.length if @columns_width[current_column].to_i < parsed_content.to_s.length
71
+
72
+ @current_column += 1
73
+ end
74
+
75
+ def format_data(data)
76
+ case
77
+ when data.is_a?(Time) then data.strftime('%d-%m-%Y')
78
+ else data
79
+ end
80
+ end
81
+
82
+ def generate
83
+ header
84
+
85
+ case @chart[:nature]
86
+ when 'line' then generate_chart_line_table
87
+ when 'donut' then generate_chart_donut_table
88
+ end
89
+
90
+ set_columns_width
91
+ end
92
+
93
+ def generate_chart_line_table
94
+ row @chart[:axis][:x][:label], :th
95
+
96
+ @chart[:lines].map { |l| cell l[:label] }
97
+
98
+ @chart[:lines][0][:data].each_with_index do |l,i|
99
+ row
100
+
101
+ cell l[:x], nature: @chart[:axis][:x][:nature]
102
+
103
+ @chart[:lines].each { |line| cell line[:data][i][:y], nature: @chart[:axis][:y][:nature] }
104
+ end
105
+
106
+ row @chart[:lines][0][:total][:label], :th_foot
107
+
108
+ @chart[:lines].map { |l| cell l[:total][:value], nature: :number }
109
+ end
110
+
111
+ def generate_chart_donut_table
112
+ row [@chart[:label], @chart[:value]], :th
113
+
114
+ @chart[:slices].map { |s| row [s[:label], s[:value]] }
115
+
116
+ row [@chart[:total][:label], @chart[:total][:value]], :th_foot
117
+ end
118
+
119
+ def header
120
+ row @chart[:title], :h1
121
+ row @chart[:period], :h3 if @chart[:period]
122
+
123
+ reset_columns_width
124
+
125
+ skip_row
126
+ end
127
+
128
+ def reset_columns_width
129
+ @columns_width = Array.new(@columns_width.length, @default_column_width)
130
+ end
131
+
132
+ def row(data=[], format = :normal)
133
+ @current_column = 0
134
+ @current_row += 1
135
+
136
+ @sheet.row(current_row).default_format = @formats[format]
137
+ @current_row_format = format
138
+
139
+ [data].flatten.each { |d| cell(d) }
140
+ end
141
+
142
+ def set_cell_format(nature)
143
+ if nature == :number
144
+ f = "#{@current_row_format}_right".gsub('right_right', 'right').to_sym
145
+
146
+ @sheet.row(current_row).set_format(current_column, @formats[f])
147
+ end
148
+ end
149
+
150
+ def set_columns_width
151
+ @columns_width.each_with_index do |c,i|
152
+ @sheet.column(i).width = (c.to_i < @default_column_width) ? @default_column_width : c.to_i
153
+ end
154
+ end
155
+
156
+ def skip_row
157
+ @current_row += 1
158
+ end
159
+ end
@@ -0,0 +1,5 @@
1
+ module ChartCandy
2
+ class Engine < Rails::Engine
3
+ #auto wire
4
+ end
5
+ end
@@ -0,0 +1,169 @@
1
+ module ChartCandy::Helpers
2
+ def counter_chart(id, options={})
3
+ ChartCandyTagHelper.new(self, id, options[:from], options[:to], options[:step]).counter(options)
4
+ end
5
+
6
+ def d3_include_tag
7
+ ('<![if ! lt IE 9]>' + javascript_include_tag("d3") + '<![endif]>').html_safe
8
+ end
9
+
10
+ def donut_chart(id, options={})
11
+ ChartCandyTagHelper.new(self, id, options[:from], options[:to], options[:step]).donut(options)
12
+ end
13
+
14
+ def line_chart(id, options={})
15
+ ChartCandyTagHelper.new(self, id, options[:from], options[:to], options[:step]).line(options)
16
+ end
17
+
18
+ def excel_chart_button(id, options={})
19
+ ChartCandyTagHelper.new(self, id, options[:from], options[:to], options[:step]).excel_chart_button(options)
20
+ end
21
+
22
+ class ChartCandyTagHelper
23
+ def initialize(rails_helpers, id, from, to, step)
24
+ @rails_helpers = rails_helpers
25
+ @id = id
26
+ @from = from
27
+ @to = to
28
+ @step = step || 'month'
29
+ end
30
+
31
+ def counter(options={})
32
+ options.reverse_merge! update_every: 1.minute, tools: nil
33
+
34
+ chart 'counter', options
35
+ end
36
+
37
+ def excel_chart_button(options={})
38
+ build_url 'line', options if not @url
39
+
40
+ tool_export_xls options[:label]
41
+ end
42
+
43
+ def line(options={})
44
+ options.reverse_merge! tools: {}
45
+ options[:tools].reverse_merge! export_xls: true, step: true, template: true
46
+
47
+ chart 'line', options
48
+ end
49
+
50
+ def donut(options={})
51
+ options.reverse_merge! tools: {}
52
+ options[:tools].reverse_merge! export_xls: true, step: false, template: true
53
+
54
+ chart 'donut', options
55
+ end
56
+
57
+ private
58
+
59
+ def build_url(nature, options={})
60
+ params = { from: @from, format: 'json', id: @id, nature: nature, nonce: SecureRandom.hex(20), step: @step, timestamp: Time.now.utc.iso8601, to: @to, version: 'v1' }
61
+
62
+ options.each { |k,v| params[k] = v if not ['class', 'tools'].include? k.to_s }
63
+
64
+ params[:token] = build_url_token(params)
65
+
66
+ @url = @rails_helpers.candy_chart_url params
67
+ end
68
+
69
+ def build_url_token(params)
70
+ compacted_params = ChartCandy::Authentication.compact_params(params)
71
+
72
+ url = @rails_helpers.candy_charts_url + compacted_params
73
+
74
+ return ChartCandy::Authentication.tokenize(url)
75
+ end
76
+
77
+ def chart(nature, options={})
78
+ options.reverse_merge! class: ""
79
+ options[:class] += " wrapper-chart chart-#{nature}"
80
+
81
+ build_url nature, options
82
+
83
+ content = ''
84
+ content += title_tag
85
+ content += chart_tools(nature, options[:tools]) if options[:tools]
86
+ content += content_tag(:div, content_tag(:div, '', class: 'chart') + content_tag(:div, '', class: 'table'), class: 'templates')
87
+
88
+ wrapper_options = { id: @id, class: options[:class], 'data-chart-candy' => nature, 'data-url' => @url}
89
+ wrapper_options['data-update-delay'] = options[:update_every].to_i if options[:update_every]
90
+
91
+ return content_tag(:div, content.html_safe, wrapper_options)
92
+ end
93
+
94
+ def chart_select_tag(name, choices, selection)
95
+ if form_candy?
96
+ candy.select(name, choices, selection)
97
+ else
98
+ select_tag(name, options_for_select(choices, selection), id: nil)
99
+ end
100
+ end
101
+
102
+ def chart_switch_tag(name, choices, selection)
103
+ if form_candy?
104
+ candy.switch(name, choices, selection)
105
+ else
106
+ select_tag(name, options_for_select(choices, selection), id: nil)
107
+ end
108
+ end
109
+
110
+ def chart_tools(nature, options={})
111
+ content = form_tag(@url) do
112
+ tools = ''
113
+ tools += tool_export_xls if options[:export_xls]
114
+ tools += tool_step if options[:step]
115
+ tools += tool_template if options[:template]
116
+
117
+ tools.html_safe
118
+ end
119
+
120
+ return content_tag(:div, content.html_safe, class: 'tools')
121
+ end
122
+
123
+ def form_candy?
124
+ begin
125
+ (candy ? true : false)
126
+ rescue
127
+ false
128
+ end
129
+ end
130
+
131
+ def t(path)
132
+ ChartCandy.translate(path)
133
+ end
134
+
135
+ def title_tag
136
+ content_tag(:h2, t("#{@id.underscore}.title").html_safe, class: 'title-chart')
137
+ end
138
+
139
+ def tool_export_xls(label=nil)
140
+ label = t('base.xls_export') if not label
141
+
142
+ content = link_to(content_tag(:span, label, class: 'text'), @url.gsub('.json', '.xls'), class: 'button', title: t('base.xls_export'))
143
+
144
+ return content_tag(:div, content.html_safe, class: 'tool holder-export-xls')
145
+ end
146
+
147
+ def tool_step
148
+ choices = ['day', 'week', 'month'].map{ |c| [t("base.steps.#{c}"), c] }
149
+
150
+ return content_tag(:div, chart_select_tag('step', choices, 'month'), class: 'tool holder-step')
151
+ end
152
+
153
+ def tool_template
154
+ choices = ['chart', 'table'].map { |c| [t("base.template.#{c}"), c] }
155
+
156
+ return content_tag(:div, chart_switch_tag('template', choices, 'chart'), class: 'tool holder-template')
157
+ end
158
+
159
+ def method_missing(*args, &block)
160
+ if [:candy, :content_tag, :form_tag, :link_to, :options_for_select, :select_tag].include?(args.first)
161
+ return @rails_helpers.send(*args, &block)
162
+ else
163
+ raise NoMethodError.new("undefined local variable or method '#{args.first}' for #{self.class}")
164
+ end
165
+ end
166
+ end
167
+
168
+ ::ActionView::Base.send :include, self
169
+ end