wayneeseguin-dynamic_reports 0.0.2

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.
@@ -0,0 +1,268 @@
1
+ # DynamicReports
2
+ #
3
+ # Dynamic Reporting Engine for Ruby / Rails
4
+ module DynamicReports
5
+
6
+ # DynamicReports::Report
7
+ #
8
+ class Report
9
+ @@default_engine = "erb"
10
+
11
+ attr_accessor :name, :title, :sub_title, :columns, :charts, :records, :template, :class_name, :styles
12
+
13
+ # views accessor, array of view paths.
14
+ def views
15
+ @views
16
+ end
17
+
18
+ # options accessor, all report options and configuration is stored in this.
19
+ def options
20
+ @options
21
+ end
22
+
23
+ class << self
24
+ # Views setter and accessor.
25
+ def views(*array)
26
+ @views ||= ["#{File::dirname(File::expand_path(__FILE__))}/views/"]
27
+ unless array.empty?
28
+ @views.unshift(array)
29
+ @views.flatten!
30
+ @views.uniq!
31
+ else
32
+ @views
33
+ end
34
+ end
35
+
36
+ # class level options accessor
37
+ def options
38
+ @options ||= {}
39
+ end
40
+
41
+ # class level name accessor & setter
42
+ #
43
+ # Set the name of the report, for example:
44
+ #
45
+ # name "Orders Report"
46
+ #
47
+ def name(value = nil)
48
+ if value
49
+ options[:name] = value
50
+ else
51
+ # TODO: snake_case_the_name
52
+ options[:name] || self.class.name
53
+ end
54
+ end
55
+
56
+ # Accessor used to set the title:
57
+ #
58
+ # title "All orders for the account"
59
+ #
60
+ # Or to access the already set title:
61
+ #
62
+ # OrdersReport.title
63
+ #
64
+ # #=> "All orders for the account"
65
+ #
66
+ def title(value = nil)
67
+ value ? options[:title] = value : options[:title]
68
+ end
69
+
70
+ # Accessor used to set the sub title:
71
+ #
72
+ # sub_title "All orders for the account"
73
+ #
74
+ # Or to access the already set sub title:
75
+ #
76
+ # OrdersReport.sub_title
77
+ #
78
+ # #=> "All orders for the account"
79
+ #
80
+ def sub_title(value = nil)
81
+ value ? options[:sub_title] = value : options[:sub_title]
82
+ end
83
+
84
+ def styles
85
+ options[:styles] ||= false
86
+ end
87
+
88
+ def class_name(value = nil)
89
+ if value
90
+ options[:class_name] = value
91
+ elsif options[:class_name].empty?
92
+ options[:class_name] = self.to_s
93
+ else
94
+ options[:class_name]
95
+ end
96
+ end
97
+
98
+ # Accessor for the template to use for the report.
99
+ #
100
+ # Example:
101
+ #
102
+ # template :orders # => renders orders.html.erb (erb is the default engine)
103
+ #
104
+ # Used without argument returns the template set for the report.
105
+ #
106
+ # OrdersReport.template # => :orders
107
+ #
108
+ def template(value = nil)
109
+ if value
110
+ @template = value
111
+ options[:template] = @template
112
+ end
113
+ @template ||= nil
114
+ end
115
+
116
+ # Accessor for columns
117
+ #
118
+ # Pass an array of symbols to define columns on the report
119
+ #
120
+ # Example:
121
+ #
122
+ # columns :total, :created_at
123
+ #
124
+ # Calling the class method with no arguments will return an array with the defined columns.
125
+ #
126
+ # Example:
127
+ #
128
+ # OrdersReport.columns
129
+ #
130
+ # # => [:total, :created_at]
131
+ #
132
+ def columns(*array)
133
+ unless array.empty?
134
+ if (array.class == Array)
135
+ options[:columns] = array
136
+ else
137
+ raise "Report columns must be specified."
138
+ end
139
+ else
140
+ options[:columns]
141
+ end
142
+ end
143
+
144
+ # Return the chart with the specified name, if it exists. nil otherwise.
145
+ def chart_with_name(name)
146
+ options[:charts].to_a.detect{ |c| c.name == name.to_sym }
147
+ end
148
+
149
+ # Return an array of charts defined for the report.
150
+ def charts(object=nil)
151
+ options[:charts] ||= []
152
+ options[:charts] << object if object
153
+ options[:charts]
154
+ end
155
+
156
+ # Define a chart for the report
157
+ #
158
+ # Example:
159
+ # chart :PV_Visits, {:grxl => 'xxxxx'} do
160
+ # title "Pageviews and Visits"
161
+ # columns [:pageviews, :visits]
162
+ # no_labels false
163
+ # label_column "recorded_at"
164
+ # width "400"
165
+ # height "350"
166
+ # type "line"
167
+ # end
168
+ #
169
+ def chart(name, *chart_options, &block)
170
+ chart_options = chart_options.shift || {}
171
+ charts(Chart.configure(name, chart_options, &block))
172
+ end
173
+
174
+ # Method for instanciating a report instance on a set of given records.
175
+ #
176
+ # Example:
177
+ #
178
+ # OrdersReport.on(@records)
179
+ #
180
+ # Where @records is an array of
181
+ #
182
+ # * Objects that respond to methods for all columns defined on the report
183
+ # * Hashes that have keys corresponding to all columns defined on the report
184
+ #
185
+ # This will return an instance of the OrdersReport bound to @records
186
+ #
187
+ def on(records)
188
+ new(records, @options)
189
+ end
190
+
191
+ #--
192
+ # Methods for definining a sub report
193
+ #def link_column
194
+ #end
195
+ #def link_rows
196
+ #end
197
+
198
+ end
199
+
200
+ # Instantiate the report on a set of records.
201
+ #
202
+ # Example:
203
+ #
204
+ # OrdersReport.new(@records)
205
+ #
206
+ # options is a set of optional overrides for
207
+ #
208
+ # * views - Used to override the class defined views.
209
+ # * template - Used to override the class defined template.
210
+ #
211
+ def initialize(records, *new_options)
212
+ new_options = new_options.shift || {}
213
+ @records = records
214
+
215
+ @views = self.class.views
216
+ @views.unshift(new_options.delete(:views)) if new_options[:views]
217
+ @views.flatten!
218
+ @views.uniq!
219
+
220
+ @template = self.class.template
221
+ @template = new_options.delete(:template) if new_options[:template]
222
+
223
+ @options = self.class.options.merge!(new_options)
224
+ @options.each_pair do |key,value|
225
+ if key == "chart"
226
+ # TODO: erh?
227
+ self.chart(value[:name],{},value)
228
+ else
229
+ instance_variable_set("@#{key}".to_sym, value)
230
+ end
231
+ end
232
+ end
233
+
234
+ # Convert an instance of a report bound to a records set to an html view
235
+ #
236
+ # Example
237
+ #
238
+ # OrdersReport.on(@records).to_html
239
+ #
240
+ # [options]
241
+ # :engine - one of :erb, :haml, :csv, :pdf
242
+ #
243
+ # Note: CSV & PDF forthcoming.
244
+ #
245
+ def to_html(*options)
246
+ view = View.new(self)
247
+ # todo: this is not clean...
248
+ options = (options.shift || {}).merge!(@options || {})
249
+ # todo: if rails is loaded set the default engine: dynamicreports::report.default_engine
250
+ engine = options.delete(:engine) || @@default_engine
251
+ options[:template] = self.class.template
252
+ view.__send__("#{engine}", options)
253
+ end
254
+
255
+ # Not Implemented Yet
256
+ def to_csv
257
+ # TODO: Write csv hook
258
+ end
259
+
260
+ # Not Implemented Yet
261
+ def to_pdf
262
+ # TODO: Write pdf hook
263
+ end
264
+
265
+ end
266
+
267
+ end
268
+
@@ -0,0 +1,178 @@
1
+ module DynamicReports
2
+ # Options are:
3
+ # :layout If set to false, no layout is rendered, otherwise
4
+ # the specified layout is used
5
+ # :locals A hash with local variables that should be available
6
+ # in the template
7
+ module Templates
8
+ def csv(options={}, locals={})
9
+ template = options.delete(:template)
10
+ render :csv, template, options.merge!(:content_type => "csv"), locals
11
+ end
12
+
13
+ def erb(options={}, locals={})
14
+ require "erb"
15
+ template = options.delete(:template)
16
+ render :erb, template, options, locals
17
+ end
18
+
19
+ def haml(options={}, locals={})
20
+ require_warn("Haml") unless defined?(::Haml::Engine)
21
+ template = options.delete(:template)
22
+ render :haml, template, options, locals
23
+ end
24
+
25
+ def builder(options={}, locals={}, &block)
26
+ require_warn("Builder") unless defined?(::Builder)
27
+ template = options.delete(:template)
28
+ options, template = template, nil if template.is_a?(Hash)
29
+ template = lambda { block } if template.nil?
30
+ render :builder, template, options, locals
31
+ end
32
+
33
+ # TODO: Add Report Helpers for injection
34
+ def titleize(object)
35
+ object.to_s.split('_').each{ |word| word.capitalize! }.join(' ')
36
+ end
37
+
38
+ def commify(object)
39
+ if object.is_a?(Numeric)
40
+ object.to_s.gsub(/(\d)(?=(\d{3})+$)/,'\1,')
41
+ else
42
+ object
43
+ end
44
+ end
45
+
46
+ def chart_url(chart,report)
47
+ columns = chart.columns ? chart.columns : report.columns
48
+ chart_type = chart.type.nil? ? :line : chart.type.to_sym
49
+ case chart_type
50
+ when :line
51
+ Charts.line_chart(chart,columns,report)
52
+ when :pie
53
+ Charts.pie_chart(chart,columns,report)
54
+ when :bar
55
+ Charts.bar_column_chart(chart,columns,report,:vertical)
56
+ when :column
57
+ Charts.bar_column_chart(chart,columns,report,:horizontal)
58
+ else
59
+ raise StandardError => "Unknown chart type '#{chart.type}'."
60
+ end
61
+
62
+ end
63
+
64
+ private
65
+
66
+ def render(engine, template, options={}, locals={})
67
+ # merge app-level options
68
+ options = self.class.send(engine).merge(options) if self.class.respond_to?(engine)
69
+
70
+ # extract generic options
71
+ layout = options.delete(:layout)
72
+ layout = :default_layout if (layout.nil? || layout == true)
73
+ views = options.delete(:views) || self.views
74
+ content_type = options.delete(:content_type)
75
+ locals = options.delete(:locals) || locals || {}
76
+ locals.merge!(:report => @report, :options => options || {})
77
+
78
+ # render template
79
+ data, options[:filename], options[:line] = lookup_template(engine, template, views, content_type)
80
+ output = __send__("render_#{engine}", template, data, options, locals)
81
+ # render layout
82
+ # TODO: Fix Layout Rendering & Specify Layout
83
+ if layout
84
+ data, options[:filename], options[:line] = lookup_layout(engine, layout, views, content_type)
85
+ if data
86
+ output = __send__("render_#{engine}", layout, data, options, {}) { output }
87
+ end
88
+ end
89
+ output
90
+ end
91
+
92
+ def lookup_template(engine, template, views, content_type = nil, filename = nil, line = nil)
93
+ content_type = "html" if content_type.nil? or content_type.blank?
94
+ if (template.nil? || template == '')
95
+ template = :default_template
96
+ #views = DefaultReport.default_view_paths
97
+ end
98
+ case template
99
+ when Symbol
100
+ if cached = self.class.cached_templates[template]
101
+ lookup_template(engine, cached[:template], views, content_type, cached[:filename], cached[:line])
102
+ else
103
+ filename = "#{template}.#{content_type}.#{engine}"
104
+ dir = views.to_a.detect do |view|
105
+ ::File.exists?(::File.join(view, filename))
106
+ end
107
+ if dir
108
+ path = ::File.join(dir, filename)
109
+ else
110
+ path = ::File.join(::File::dirname(::File::expand_path(__FILE__)), "views","default_report.#{content_type}.#{engine}")
111
+ end
112
+ [ ::File.read(path), path, 1 ]
113
+ end
114
+ when Proc
115
+ filename, line = self.class.caller_locations.first if filename.nil?
116
+ [ template.call, filename, line.to_i ]
117
+ when String
118
+ filename, line = self.class.caller_locations.first if filename.nil?
119
+ [ template, filename, line.to_i ]
120
+ else
121
+ raise ArgumentError, "Template was not specified properly: '#{template}'."
122
+ end
123
+ end
124
+
125
+ def lookup_layout(engine, template, views, content_type)
126
+ lookup_template(engine, template, views, content_type)
127
+ rescue Errno::ENOENT
128
+ nil
129
+ end
130
+
131
+ def render_csv(template, data, options, locals, &block)
132
+ # TODO: Implement this.
133
+ end
134
+
135
+ def render_erb(template, data, options, locals, &block)
136
+ original_out_buf = defined?(@_out_buf) && @_out_buf
137
+ data = data.call if data.kind_of? Proc
138
+
139
+ instance = ::ERB.new(data, nil, nil, "@_out_buf")
140
+ locals_assigns = locals.to_a.collect { |k,v| "#{k} = locals[:#{k}]" }
141
+
142
+ filename = options.delete(:filename) || "(__ERB__)"
143
+ line = options.delete(:line) || 1
144
+ line -= 1 if instance.src =~ /^#coding:/
145
+
146
+ render_binding = binding
147
+ eval locals_assigns.join("\n"), render_binding
148
+ eval instance.src, render_binding, filename, line
149
+ @_out_buf, result = original_out_buf, @_out_buf
150
+ result
151
+ end
152
+
153
+ def render_haml(template, data, options, locals, &block)
154
+ ::Haml::Engine.new(data, options).render(self, locals, &block)
155
+ end
156
+
157
+ def render_builder(template, data, options, locals, &block)
158
+ options = { :indent => 2 }.merge(options)
159
+ filename = options.delete(:filename) || "<BUILDER>"
160
+ line = options.delete(:line) || 1
161
+ xml = ::Builder::XmlMarkup.new(options)
162
+ if data.respond_to?(:to_str)
163
+ eval data.to_str, binding, filename, line
164
+ elsif data.kind_of?(Proc)
165
+ data.call(xml)
166
+ end
167
+ xml.target!
168
+ end
169
+
170
+ def require_warn(engine)
171
+ warn "Auto-require of #{engine} is deprecated; add require \"#{engine}\" to your app."
172
+ require engine.downcase
173
+ end
174
+
175
+
176
+
177
+ end
178
+ end
@@ -0,0 +1,90 @@
1
+ require File.dirname(__FILE__) + '/base'
2
+ module GoogleChart
3
+ # Generates a Bar Chart. You can specify the alignment(horizontal or vertical) and whether you want the bars to be grouped or stacked
4
+ # ==== Examples
5
+ # bc = GoogleChart::BarChart.new('800x200', "Bar Chart", :vertical, false)
6
+ # bc.data "Trend 1", [5,4,3,1,3,5], '0000ff'
7
+ class BarChart < Base
8
+
9
+ attr_accessor :alignment, :stacked
10
+
11
+ # Specify the
12
+ # * +chart_size+ in WIDTHxHEIGHT format
13
+ # * +chart_title+ as a string
14
+ # * +alignment+ as either <tt>:vertical</tt> or <tt>:horizontal</tt>
15
+ # * +stacked+ should be +true+ if you want the bars to be stacked, false otherwise
16
+ def initialize(chart_size='300x200', chart_title=nil, alignment=:vertical, stacked=false) # :yield: self
17
+ super(chart_size, chart_title)
18
+ @alignment = alignment
19
+ @stacked = stacked
20
+ set_chart_type
21
+ self.show_legend = true
22
+ yield self if block_given?
23
+ end
24
+
25
+ # Set the alignment to either <tt>:vertical</tt> or <tt>:horizontal</tt>
26
+ def alignment=(value)
27
+ @alignment = value
28
+ set_chart_type
29
+ end
30
+
31
+ # If you want the bar chart to be stacked, set the value to <tt>true</tt>, otherwise set the value to <tt>false</tt> to group it.
32
+ def stacked=(value)
33
+ @stacked = value
34
+ set_chart_type
35
+ end
36
+
37
+ # Defines options for bar width, spacing between bars and between groups of bars. Applicable for bar charts.
38
+ # [+options+] : Options for the style, specifying things like line thickness and lengths of the line segment and blank portions
39
+ #
40
+ # ==== Options
41
+ # * <tt>:bar_width</tt>, Bar width in pixels
42
+ # * <tt>:bar_spacing</tt> (optional), space between bars in a group
43
+ # * <tt>:group_spacing</tt> (optional), space between groups
44
+ def width_spacing_options(options={})
45
+ options_str = "#{options[:bar_width]}"
46
+ options_str += ",#{options[:bar_spacing]}" if options[:bar_spacing]
47
+ options_str += ",#{options[:group_spacing]}" if options[:bar_spacing] and options[:group_spacing]
48
+ @bar_width_spacing_options = options_str
49
+ end
50
+
51
+ def process_data
52
+ if @stacked # Special handling of max value for stacked
53
+ unless @max_data # Unless max_data is explicitly set
54
+ @max_data = @data.inject([]) do |sum_arr, series|
55
+ series.each_with_index do |v,i|
56
+ if sum_arr[i] == nil
57
+ sum_arr[i] = v
58
+ else
59
+ sum_arr[i] += v
60
+ end
61
+ end
62
+ sum_arr
63
+ end.max
64
+ end
65
+ end
66
+
67
+ if @data.size > 1
68
+ join_encoded_data(@data.collect { |series|
69
+ encode_data(series, max_data_value)
70
+ })
71
+ else
72
+ encode_data(@data.flatten,max_data_value)
73
+ end
74
+ end
75
+
76
+ private
77
+ def set_chart_type
78
+ # Set chart type
79
+ if alignment == :vertical and stacked == false
80
+ self.chart_type = :bvg
81
+ elsif alignment == :vertical and stacked == true
82
+ self.chart_type = :bvs
83
+ elsif alignment == :horizontal and stacked == false
84
+ self.chart_type = :bhg
85
+ elsif alignment == :horizontal and stacked == true
86
+ self.chart_type = :bhs
87
+ end
88
+ end
89
+ end
90
+ end