chart-candy 0.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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