ar_to_chart 0.0.1 → 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore CHANGED
@@ -1,3 +1,4 @@
1
1
  pkg/*
2
2
  *.gem
3
3
  .bundle
4
+ *.DS_Store
@@ -0,0 +1,3 @@
1
+ 2010/11/1 ar_to_chart v0.0.1
2
+
3
+ * Initial conversion from plugin
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2010 Kip Cole
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,36 @@
1
+ h1. Description
2
+
3
+ p. ar_to_chart generates Highcharts-based charts from an ActiveRecord result set. For example:
4
+
5
+ bc. Product.all.to_chart(:price, :product)
6
+
7
+ p. Will generate HTML for a chart container and javascript to render the chart in the browser.
8
+
9
+ h2. Dependencies
10
+
11
+ ar_to_chart depends on:
12
+
13
+ * jQuery (jquery.com)
14
+ * Highcharts (www.highcharts.com)
15
+ * arToChart javascript object (found in the files directory of the gem). This is the javascript interface between ar_to_chart and the Highcharts library
16
+ * ar_to_chart.css (found in the files directory of the gem). This css defines the attributes used by the ar_to_chart javascript.
17
+
18
+ h2. Examples
19
+
20
+ p. Coming soon
21
+
22
+ h2. Options
23
+
24
+ p. Also coming soon
25
+
26
+ h1. License
27
+
28
+ (The MIT License)
29
+
30
+ Copyright © 2010 Kip Cole
31
+
32
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the ‘Software’), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
33
+
34
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
35
+
36
+ THE SOFTWARE IS PROVIDED ‘AS IS’, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -12,9 +12,8 @@ Gem::Specification.new do |s|
12
12
  s.summary = %q{Render an ActiveRecord result set as a chart}
13
13
  s.description = <<-EOF
14
14
  Defines Array#to_chart that will accept ActiveRecord result sets
15
- and render them as a chart. Currently assumes OpenFlashChart as the
16
- only supported charting engine. Protovis based charting coming
17
- soon.
15
+ and render them as a chart. Currently assumes Highcharts (highcharts.com)
16
+ as the charting engine.
18
17
  EOF
19
18
 
20
19
  s.rubyforge_project = "ar_to_chart"
@@ -0,0 +1,33 @@
1
+ /*
2
+ For Highcharts
3
+ These are sort of proxy classes. They are here just so we can access the
4
+ attributes in JS and apply them to charts.
5
+ */
6
+
7
+ /* Chart background */
8
+ #highcharts { background-color: #ddd; }
9
+
10
+ /* Used for axis text */
11
+ #highcharts-text {
12
+ color: black;
13
+ font-size: 90%;
14
+ font-weight: normal;
15
+ }
16
+
17
+ /* Area graph color for the area */
18
+ #highcharts-area {
19
+ color: rgb(25,135,213);
20
+ color: rgba(25,135,213, 0.3);
21
+ }
22
+
23
+ /* Chart line color */
24
+ #highcharts-line { color: rgb(25,135,213); }
25
+
26
+ /* Gridlines color */
27
+ #highcharts-gridlines { color: white;}
28
+
29
+ /* Axis line color */
30
+ #highcharts-axis { color: #777; }
31
+
32
+ /* X-Axis plotbands color */
33
+ #highcharts-xbands { color: #ccc }
@@ -0,0 +1,238 @@
1
+ function arToChart() {
2
+ // Set up the various colours we want to use here
3
+ var self = this;
4
+
5
+ // Add some hidden structure that we can then use to get color and style information from
6
+ var template = "<div style='display:none;width=1em'><p id=highcharts>&nbsp</p>" +
7
+ "<p id=highcharts-text>&npsb</p><p id=highcharts-area>&npsb</p>" +
8
+ "<p id=highcharts-line>&npsb</p><p id=highcharts-gridlines>&npsb</p>" +
9
+ "<p id=highcharts-axis>&npsb</p><p id=highcharts-xbands>&npsb</p></div>";
10
+ $('body').append(template);
11
+
12
+ this.font = {
13
+ size: $('#highcharts-text').css('font-size'),
14
+ family: $('#highcharts-text').css('font-family'),
15
+ weight: $('#highcharts-text').css('font-weight'),
16
+ color: $('#highcharts-text').css('color')
17
+ }
18
+
19
+ this.lineWeight = {
20
+ grid: 1,
21
+ axis: 1
22
+ }
23
+
24
+ this.colors = {
25
+ background: $('#highcharts').css('background-color'),
26
+ areaFill: $('#highcharts-area').css('color'),
27
+ areaOpacity: $('#highcharts-area').css('opacity'),
28
+ gridLines: $('#highcharts-gridlines').css('color'),
29
+ lineColor: $('#highcharts-line').css('color'),
30
+ xAxisLine: $('#highcharts-axis').css('color'),
31
+ yAxisLine: $('#highcharts-axis').css('color'),
32
+ plotBands: $('#highcharts-xbands').css('color')
33
+ };
34
+
35
+ function will_print() {
36
+ return window.location.href.match(/print=/) || pdf_export();
37
+ }
38
+
39
+ function pdf_export() {
40
+ return $('meta[name=pdf-output]').length > 0;
41
+ }
42
+
43
+ function getLineWidth(series) {
44
+ if (pdf_export) {
45
+ return 1;
46
+ } else {
47
+ return (series.length > 50) ? 1 : self.lineWeight.axis;
48
+ }
49
+ }
50
+
51
+ // Render an Area chart with one or more data series
52
+ this.area = function(container, categories, series_data, options) {
53
+
54
+ /* Put colors into plotbands */
55
+ if (options.x_plot_bands) {
56
+ $(options.x_plot_bands).each(function(index, item) {
57
+ item.color = self.colors.plotBands;
58
+ });
59
+ };
60
+
61
+ return chart = new Highcharts.Chart({
62
+ chart: {
63
+ credits: {
64
+ enabled: false
65
+ },
66
+ borderWidth: 0,
67
+ borderColor: self.colors.background,
68
+ renderTo: container,
69
+ defaultSeriesType: 'area',
70
+ backgroundColor: self.colors.background,
71
+ marginTop: 15,
72
+ zoomType: 'x'
73
+ },
74
+ navigation: {
75
+ buttonOptions: {
76
+ backgroundColor: self.colors.background
77
+ }
78
+ },
79
+ exporting: {
80
+ enabled: !will_print()
81
+ },
82
+ title: {
83
+ text: options.title || ''
84
+ },
85
+ subtitle: {
86
+ text: options.subtitle || ''
87
+ },
88
+ xAxis: {
89
+ type: (categories ? 'linear' : 'datetime'),
90
+ categories: categories,
91
+ gridLineColor: self.colors.gridLines,
92
+ gridLineWidth: (series_data[0].data.length > 50) ? 0 : self.lineWeight.grid,
93
+ lineColor: self.colors.xAxisLine,
94
+ lineWidth: getLineWidth(series_data[0].data),
95
+ title: {
96
+ text: options.x_axis || ''
97
+ },
98
+ labels: {
99
+ step: options.x_step || 1,
100
+ staggerLines: options.staggerLines || 1,
101
+ style: {
102
+ fontSize: self.font.size,
103
+ fontFamily: self.font.family,
104
+ fontWeight: self.font.weight,
105
+ color: self.font.color
106
+ },
107
+ formatter: function() {
108
+ return this.value;
109
+ }
110
+ },
111
+ plotBands: options.x_plot_bands
112
+ },
113
+ yAxis: {
114
+ gridLineColor: self.colors.gridLines,
115
+ gridLineWidth: self.lineWeight.grid,
116
+ lineColor: self.colors.yAxisLine,
117
+ lineWidth: self.lineWeight.axis,
118
+ title: {
119
+ text: options.y_axis || ''
120
+ },
121
+ labels: {
122
+ style: {
123
+ fontSize: self.font.size,
124
+ fontFamily: self.font.family,
125
+ fontWeight: self.font.weight,
126
+ color: self.font.color
127
+ },
128
+ formatter: function() {
129
+ return this.value;
130
+ }
131
+ }
132
+ },
133
+ tooltip: {
134
+ formatter: function() {
135
+ if (this.series.type == 'pie') {
136
+ return this.series.name + ':<br>' + this.point.name + ": " + Highcharts.numberFormat(this.y, 0)
137
+ } else {
138
+ return 'On: ' + this.x + "<br>" + this.series.name +': ' + Highcharts.numberFormat(this.y, 0)
139
+ }
140
+ }
141
+ },
142
+ legend: {
143
+ enabled: (series_data.size > 1)
144
+ },
145
+ plotOptions: {
146
+ series: {
147
+ enableMouseTracking: !will_print(),
148
+ shadow: false,
149
+ animation: !will_print()
150
+ },
151
+ area: {
152
+ fillColor: self.colors.areaFill,
153
+ color: self.colors.lineColor,
154
+ lineWidth: 3,
155
+ marker: {
156
+ enabled: false,
157
+ symbol: 'circle',
158
+ radius: 2,
159
+ states: {
160
+ hover: {
161
+ enabled: true
162
+ }
163
+ }
164
+ }
165
+ },
166
+ pie: {
167
+ allowPointSelect: true,
168
+ cursor: 'pointer',
169
+ dataLabels: {
170
+ enabled: true,
171
+ formatter: function() {
172
+ if (this.y > 0) return this.point.name + ": " + Highcharts.numberFormat(this.percentage, 1) +'%';
173
+ },
174
+ color: self.font.color
175
+ }
176
+ }
177
+ },
178
+ series: series_data
179
+ });
180
+ };
181
+
182
+ // Render a Pie chart
183
+ this.pie = function(container, categories, series_data, options) {
184
+ this.area(container, categories, series_data, options);
185
+ }
186
+
187
+ // Render Funnel chart
188
+ this.funnel = function(container, categories, series_data, options) {
189
+ return chart = new Highcharts.Chart({
190
+ chart: {
191
+ backgroundColor: self.colors.background,
192
+ borderWidth: 0,
193
+ borderColor: self.colors.background,
194
+ renderTo: container,
195
+ defaultSeriesType: 'funnel',
196
+ margin: [20, 100, 40, 180]
197
+ },
198
+ navigation: {
199
+ buttonOptions: {
200
+ backgroundColor: self.colors.background
201
+ }
202
+ },
203
+ title: {
204
+ text: options.title || ''
205
+ },
206
+ subtitle: {
207
+ text: options.subtitle || ''
208
+ },
209
+ plotArea: {
210
+ shadow: null,
211
+ borderWidth: null,
212
+ backgroundColor: null
213
+ },
214
+ tooltip: {
215
+ formatter: function() {
216
+ return '<b>'+ this.point.name +'</b>: '+ Highcharts.numberFormat(this.y, 0);
217
+ }
218
+ },
219
+ plotOptions: {
220
+ series: {
221
+ dataLabels: {
222
+ align: 'left',
223
+ x: -300,
224
+ enabled: true,
225
+ color: self.font.color,
226
+ formatter: function() {
227
+ return '<b>'+ this.point.name +'</b> ('+ Highcharts.numberFormat(this.point.y, 0) +')';
228
+ }
229
+ }
230
+ }
231
+ },
232
+ legend: {
233
+ enabled: false
234
+ },
235
+ series: series_data
236
+ });
237
+ };
238
+ };
@@ -1,3 +1,19 @@
1
+ require File.dirname(__FILE__) + '/ar_to_chart/charting/highcharts.rb'
2
+ require File.dirname(__FILE__) + '/ar_to_chart/charting/sparklines.rb'
3
+ require File.dirname(__FILE__) + '/ar_to_chart/charting/period.rb'
4
+ require File.dirname(__FILE__) + '/ar_to_chart/charting/highcharts/base'
5
+ require File.dirname(__FILE__) + '/ar_to_chart/charting/highcharts/area'
6
+ require File.dirname(__FILE__) + '/ar_to_chart/charting/highcharts/pie'
7
+ require File.dirname(__FILE__) + '/ar_to_chart/charting/highcharts/funnel'
8
+
9
+ require File.dirname(__FILE__) + '/ar_to_chart/active_record_array.rb'
10
+ Array.send :include, Charting::ActiveRecordArray
11
+
12
+ require File.dirname(__FILE__) + '/ar_to_chart/charting/transforms.rb'
13
+ ActiveRecord::Base.send :include, Charting::Transforms
14
+
15
+ I18n.load_path << Dir[ File.join(File.dirname(__FILE__), 'locale', '*.{rb,yml}') ]
16
+
1
17
  module ArToChart
2
18
  # Your code goes here...
3
19
  end
@@ -0,0 +1,74 @@
1
+ # Adds method to Array to allow output of flash-based charts from
2
+ # active record result sets
3
+ module Charting
4
+ module ActiveRecordArray
5
+ def self.included(base)
6
+ base.class_eval do
7
+ extend ClassMethods
8
+ include InstanceMethods
9
+ end
10
+ end
11
+
12
+ module InstanceMethods
13
+ # Render a chart based upon an ActiveRecord result set.
14
+ # Requires jQuery.
15
+ #
16
+ # ====Parameters
17
+ #
18
+ # columns: column name (or array of names) representing the
19
+ # Y-Axis
20
+ # label_column: The category (X-Axis) column
21
+ #
22
+ # ====Options
23
+ #
24
+ # See the options in Charting::Highcharts::Renderer
25
+ def to_chart(columns, label_column, options = {})
26
+ chart_object(columns, label_column, options).to_html
27
+ end
28
+
29
+ # Returns the chart container (the <div>) and the javascript
30
+ # separately so you can put the container where you want and
31
+ # the javascript at the end of the document
32
+ def to_container_and_script(column, label_column, options = {})
33
+ chart = chart_object(column, label_column, options)
34
+ return chart.container, chart.script
35
+ end
36
+
37
+ # Render a sparkline based upon an ActiveRecord result set.
38
+ # Requires jQuery and jQuery sparklines plugin.
39
+ #
40
+ # ====Parameters
41
+ #
42
+ # column: column name representing the
43
+ # Y-Axis
44
+ # label_column: The category (X-Axis) column
45
+ #
46
+ # ====Options
47
+ #
48
+ # See the options in Charting::Sparklines::Renderer
49
+ def to_sparkline(column, options = {})
50
+ sparkline_object(column, options).to_html
51
+ end
52
+
53
+ private
54
+ # Eventually we'll do charting engine selection here. For now
55
+ # only Highcharts or Sparklines
56
+ def chart_object(label_column, columns, options)
57
+ Charting::Highcharts::Renderer.new(self, columns, label_column, merged_options(options))
58
+ end
59
+
60
+ def sparkline_object(column, options)
61
+ Charting::Sparklines::Renderer.new(self, column, merged_options(options))
62
+ end
63
+
64
+ def merged_options(options)
65
+ {}.merge(options)
66
+ end
67
+
68
+ end
69
+
70
+ module ClassMethods
71
+
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,113 @@
1
+ module Charting
2
+ module Highcharts
3
+ class Renderer
4
+ DEFAULT_OPTIONS = {
5
+ :container_height => '200px',
6
+ :type => :area,
7
+ }
8
+ DEFAULT_CHARTING_OBJECT = 'arToChart'
9
+
10
+ attr_accessor :categories, :series, :options, :chart
11
+ cattr_accessor :charting_object
12
+
13
+ # Generate Highchart based charts. CSS is used
14
+ # for the colouring. See the file ar_to_chart.css included
15
+ # in the files directory of the gem.
16
+ #
17
+ # ====Parameters
18
+ #
19
+ # data_source: The active record result set
20
+ # category_column: The column used for the x-axis
21
+ # data_columns: one column name or an array of column names to be charted or a hash
22
+ # with pairs of :column_names => :series_type
23
+ # options: options hash
24
+ #
25
+ # ====Options
26
+ #
27
+ # :type Chart type to render. Defauly is :area. Other options
28
+ # are :pie and :funnel. Subclass Base to create other
29
+ # chart types available in Highcharts. The name of the subclass
30
+ # becomes the chart type.
31
+ # :container DOM id of the container to render to. Default is to
32
+ # generate a name.
33
+ # :container_height Requested height of the container. Defaults to 200px
34
+ # :charting_object The javascript object that is instantiated as the
35
+ # browser renderer. Defaults to arToChar (supplied in
36
+ # the files directory of the gem)
37
+ #
38
+ def initialize(data_source, category_column, data_columns, options = {})
39
+ @options = DEFAULT_OPTIONS.merge(options)
40
+ @options[:container] ||= generate_container_name
41
+ @options[:charting_object] ||= self.class.charting_object || DEFAULT_CHARTING_OBJECT
42
+ @data_columns = data_columns.respond_to?(:each) ? data_columns : [data_columns]
43
+ @chart = chart_class.new(data_source, category_column, @data_columns, @options)
44
+ end
45
+
46
+ # Returns the <div> into which the chart will be rendered.
47
+ def container
48
+ <<-EOF
49
+ <div id='#{container_id}' #{styles}"></div>
50
+ EOF
51
+ end
52
+
53
+ # Returns the javscript (without <script> tag) that is sent
54
+ # to the browser to render the chart. Note that there is a
55
+ # dependency on the ar_to_chart.js javascript library being
56
+ # loaded in the <head> of the document.
57
+ #
58
+ # See the ar_to_chart.js file included in the files directory
59
+ # of the gem.
60
+ def script
61
+ <<-EOF
62
+ $(document).ready(function() {
63
+ chart = new #{charting_object};
64
+ #{chart.to_js}
65
+ });
66
+ EOF
67
+ end
68
+
69
+ # Return the HTML of the container <div> and the
70
+ # script to render the chart.
71
+ def to_html
72
+ <<-EOF
73
+ #{container}
74
+ <script type="text/javascript">
75
+ #{script}
76
+ </script>
77
+ EOF
78
+ end
79
+
80
+ # Set configuration options. Currently no options
81
+ # are available.
82
+ def self.configure
83
+ yield self
84
+ end
85
+
86
+ private
87
+ def chart_class
88
+ @chart_class ||= "Charting::Highcharts::#{options[:type].to_s.titleize}".constantize
89
+ end
90
+
91
+ def container_id
92
+ @container_id ||= options[:container]
93
+ end
94
+
95
+ def styles
96
+ container_height ? "style='height:#{container_height}'" : ''
97
+ end
98
+
99
+ def container_height
100
+ options[:container_height]
101
+ end
102
+
103
+ def charting_object
104
+ options[:charting_object]
105
+ end
106
+
107
+ def generate_container_name
108
+ "chart_" + ActiveSupport::SecureRandom.hex(3)
109
+ end
110
+
111
+ end
112
+ end
113
+ end
@@ -0,0 +1,25 @@
1
+ module Charting
2
+ module Highcharts
3
+ class Area < Charting::Highcharts::Base
4
+
5
+ # Generate categories from ActiveRecord data
6
+ def categories
7
+ data_source.inject([]) do |categories, row|
8
+ categories << row.format_column(category_column).try(:strip_tags).try(:strip)
9
+ end
10
+ end
11
+
12
+ # Generate data series array (for each data column)
13
+ def series
14
+ data_columns.inject([]) do |series, column|
15
+ series_data = data_source.inject([]) do |series_data, row|
16
+ series_data << row[column].to_i
17
+ end
18
+ series << {:name => series_name(column), :data => series_data}
19
+ end
20
+ end
21
+
22
+ end
23
+ end
24
+ end
25
+
@@ -0,0 +1,130 @@
1
+ module Charting
2
+ module Highcharts
3
+ # Base class for chart renderers. Chart renderers should
4
+ # subclass rather than instantiate Base directly.
5
+ class Base
6
+ attr_reader :data_source, :category_column, :data_columns, :options
7
+
8
+ WEEKEND = [0,6] # The days of the week that are the weekend (Sun, Sat)
9
+ AXIS_START_UNIT = 0.5 # Where highcharts starts its x-axis in axis units
10
+ MAX_X_LABELS = 10 # Display no more than this number of category labels
11
+ DEFAULT_OPTIONS = {
12
+
13
+ }
14
+
15
+ # Initialize a new chart.
16
+ #
17
+ # ====Parameters
18
+ #
19
+ # data_source: Array of ActiveRecord objects for rendering as a chart
20
+ # category_column: The name of the column which is the category (X) axis
21
+ # data_columns: The name (or array of names) of the columns which are
22
+ # the data series.
23
+ # options: Options hash
24
+ #
25
+ # ====Options
26
+ #
27
+ # title: Chart title (default: none)
28
+ # subtitle: Subtitle (default: none)
29
+ # x_axis_title: X-Axis title (default: none)
30
+ # y_axis_title: Y-Axis title (defualt: none)
31
+ # x_step: X-Axis labels are printed every 'x' steps
32
+ # Requires Highcharts beta 2.1
33
+ # x_plot_bands: Plot bands in Ruby Hash format according to the
34
+ # Highcharts documented format
35
+ # linearize: Linearize the data to ensure continuous
36
+ # series on the X-Axis.
37
+ # weekend_plot_bands: Creates plot-bands for the weekends (Saturday/Sunday)
38
+ # container: DOM id for the container div. Default is to generate one.
39
+
40
+ def initialize(data_source, category_column, data_columns, options = {})
41
+ @options = DEFAULT_OPTIONS.merge(options)
42
+ @category_column = category_column
43
+ @data_columns = data_columns
44
+ @data_source = data_source
45
+ @data_source = linearize if @options.delete(:linearize)
46
+ @options[:x_step] ||= (@data_source.size.to_f / MAX_X_LABELS.to_f).round
47
+ end
48
+
49
+ def chart_options
50
+ {
51
+ :title => options[:title],
52
+ :subtitle => options[:subtitle],
53
+ :x_axis => options[:x_axis_title],
54
+ :y_axis => options[:y_axis_title],
55
+ :x_step => options[:x_step],
56
+ :x_plot_bands => weekend_plot_bands
57
+ }
58
+ end
59
+
60
+ # Define in concrete subclass
61
+ def series
62
+ nil
63
+ end
64
+
65
+ # Define in concrete subclass
66
+ def categories
67
+ nil
68
+ end
69
+
70
+ # Returns a plotbands definition for the data source
71
+ # Only if the category column is a date or datetime
72
+ def weekend_plot_bands
73
+ return unless options[:weekend_plot_bands] && data_source.first[category_column].respond_to?(:to_date)
74
+ x_plot_bands = data_source.inject_with_index([]) do |plot_bands, row, index|
75
+ if WEEKEND.include?(row[category_column].to_date.wday)
76
+ plot_bands << {:from => (index + AXIS_START_UNIT - 1), :to => (index + AXIS_START_UNIT)}
77
+ end
78
+ plot_bands
79
+ end
80
+ end
81
+
82
+ def series_name(column)
83
+ data_source.first.class.human_attribute_name(column)
84
+ end
85
+
86
+ def chart_type
87
+ @chart_type ||= self.class.name.split('::').last.downcase
88
+ end
89
+
90
+ def container
91
+ options[:container]
92
+ end
93
+
94
+ def to_js
95
+ <<-EOF
96
+ chart.#{chart_type}('#{container}',
97
+ #{categories.to_json},
98
+ #{series.to_json},
99
+ #{chart_options.to_json}
100
+ );
101
+ EOF
102
+ end
103
+
104
+ # When we retrieve datat from the data base it may have gaps where not
105
+ # results are available. For charting we want to have a linearized
106
+ # series of data so here we plug the gaps.
107
+ #
108
+ # Punt that the categeory column tells us enough to know what the data
109
+ # should be.
110
+ def linearize
111
+ return data_source unless range = Charting::Period.range_from(options)
112
+ range = data_source.first[category_column]..range.last if range.first > data_source.first[category_column]
113
+ klass = data_source.first.class
114
+ index = 0
115
+ range.inject(Array.new) do |linear_data, data_point|
116
+ if data_source[index] && data_source[index][category_column] == data_point
117
+ linear_data << data_source[index]
118
+ index += 1
119
+ else
120
+ new_row = klass.new(category_column => data_point)
121
+ data_columns.each {|column| new_row[column] = 0 }
122
+ yield new_row if block_given?
123
+ linear_data << new_row
124
+ end
125
+ linear_data
126
+ end
127
+ end
128
+ end
129
+ end
130
+ end
@@ -0,0 +1,10 @@
1
+ module Charting
2
+ module Highcharts
3
+ # Produces a Funnel graph. Subclasses Highcharts::Pie
4
+ # since the data format is pretty much the same, only needs
5
+ # different name.
6
+ class Funnel < Charting::Highcharts::Pie
7
+
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,22 @@
1
+ module Charting
2
+ module Highcharts
3
+ class Pie < Charting::Highcharts::Base
4
+
5
+ # Generate data series array (for each data column)
6
+ def series
7
+ data_columns.inject([]) do |series, column|
8
+ series_data = data_source.inject([]) do |series_data, row|
9
+ series_data << [row.format_column(category_column).strip_tags.strip, row[column].to_i]
10
+ end
11
+ series << {:type => chart_type, :name => series_name(column), :data => series_data}
12
+ end
13
+ end
14
+
15
+ # No categories for pie charts
16
+ def categories
17
+ nil
18
+ end
19
+
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,157 @@
1
+ # Parse parameters (usually from a request via ActionController) into
2
+ # chain of named scopes
3
+ #
4
+ # Format of query parameters:
5
+ # => by=dim1,dim2,dim3 dimensions
6
+ # => metric=metric1 a metric (only one at this time)
7
+ # => from=
8
+ # => to=
9
+
10
+ module Charting
11
+ class Period
12
+ def from_params(params = {})
13
+ default_from = (today - 30.days).beginning_of_day
14
+ default_to = today.end_of_day
15
+ if params[:period]
16
+ from, to = from_param(params[:period], default_from..default_to)
17
+ else
18
+ to = date_from_param(params[:to], default_to)
19
+ from = date_from_param(params[:from], default_from)
20
+ end
21
+ from..to
22
+ end
23
+
24
+ def date_from_param(date, default)
25
+ return default unless date
26
+ return date.to_date if date.is_a?(Date) || date.is_a?(Time)
27
+ Time.parse(date) rescue default
28
+ end
29
+
30
+ def from_param(period, default)
31
+ return default.first, default.last unless period
32
+ period_range = case period.to_sym
33
+ when :today then today.beginning_of_day..today.end_of_day
34
+ when :yesterday then yesterday.beginning_of_day..yesterday.end_of_day
35
+ when :this_week then first_day_of_this_week.beginning_of_day..last_day_of_this_week.end_of_day
36
+ when :this_month then first_day_of_this_month.beginning_of_day..last_day_of_this_month.end_of_day
37
+ when :this_year then first_day_of_this_year.beginning_of_day..last_day_of_this_year.end_of_day
38
+ when :last_week then first_day_of_last_week.beginning_of_day..last_day_of_last_week.end_of_day
39
+ when :last_month then first_day_of_last_month.beginning_of_day..last_day_of_last_month.end_of_day
40
+ when :last_year then first_day_of_last_year.beginning_of_day..last_day_of_last_year.end_of_day
41
+ when :last_30_days then (today - 30.days).beginning_of_day..today.end_of_day
42
+ when :last_12_months then (today - 12.months).beginning_of_day..today.end_of_day
43
+ when :lifetime then beginning_of_epoch.beginning_of_day..today.end_of_day;
44
+ else default
45
+ end
46
+ return period_range.first, period_range.last
47
+ end
48
+
49
+ def range_from(params)
50
+ period = self.from_params(params)
51
+ case params[:time_group]
52
+ when 'date'
53
+ period.first.to_date..period.last.to_date
54
+ when 'day_of_week'
55
+ 0..6
56
+ when 'day_of_month'
57
+ 1..31
58
+ when 'hour'
59
+ 0..24
60
+ when 'month'
61
+ 1..12
62
+ when 'year'
63
+ period.first.to_date.year..period.last.to_date.year
64
+ end
65
+ end
66
+
67
+ # Basic markers
68
+ def today
69
+ Time.zone.now.to_date
70
+ end
71
+
72
+ def yesterday
73
+ today - 1.day
74
+ end
75
+
76
+ def tomorrow
77
+ today + 1.day
78
+ end
79
+
80
+ def beginning_of_epoch
81
+ today - 20.years
82
+ end
83
+
84
+ # This week
85
+ def first_day_of_this_week
86
+ today - today.wday.days
87
+ end
88
+
89
+ def last_day_of_this_week
90
+ first_day_of_this_week + 7.days
91
+ end
92
+
93
+ # This month
94
+ def first_day_of_this_month
95
+ Date.new(today.year, today.month, 1)
96
+ end
97
+
98
+ def last_day_of_this_month
99
+ first_day_of_this_month + 1.month - 1.day
100
+ end
101
+
102
+ # Last week
103
+ def first_day_of_last_week
104
+ first_day_of_this_week - 7.days
105
+ end
106
+
107
+ def last_day_of_last_week
108
+ first_day_of_last_week + 7.days
109
+ end
110
+
111
+ # Last month
112
+ def first_day_of_last_month
113
+ last_month = Date.today - 1.month
114
+ Date.new(last_month.year, last_month.month, 1)
115
+ end
116
+
117
+ def last_day_of_last_month
118
+ first_day_of_last_month + 1.month - 1.day
119
+ end
120
+
121
+ # This year
122
+ def first_day_of_this_year
123
+ Date.new(today.year, 1, 1)
124
+ end
125
+
126
+ def last_day_of_this_year
127
+ Date.new(today.year, 12, 31)
128
+ end
129
+
130
+ # Last year
131
+ def first_day_of_last_year
132
+ Date.new(today.year - 1, 1, 1)
133
+ end
134
+
135
+ def last_day_of_last_year
136
+ Date.new(first_day_of_last_year.year, 12, 31)
137
+ end
138
+
139
+ # Clear the saved instance which we should do at the start
140
+ # of each Rails request
141
+ def self.clear
142
+ Thread.current[:period2] = nil
143
+ end
144
+
145
+ # Called when we're configured as a before_filter which we need to
146
+ # ensure our thread data is ours and no left over from somebody else
147
+ def self.filter(controller)
148
+ clear
149
+ end
150
+
151
+ # Arrange for instance methods to be called as if class methods. Make threadsafe.
152
+ def self.method_missing(method, *args)
153
+ Thread.current[:period2] = new unless Thread.current[:period2]
154
+ self.instance_methods.include?(method.to_s) ? Thread.current[:period2].send(method, *args) : super
155
+ end
156
+ end
157
+ end
@@ -0,0 +1,65 @@
1
+ module Charting
2
+ module Sparklines
3
+ class Renderer
4
+ DEFAULT_OPTIONS = {
5
+ :type => :line,
6
+ }
7
+
8
+ attr_accessor :series, :options, :chart
9
+
10
+ # Generate jQuery sparkline based charts.
11
+ #
12
+ # Ensure jquery-sparkine library is loaded before use, for example:
13
+ #
14
+ # <script src="/javascripts/jquery.sparkline.js" type="text/javascript"></script>
15
+ #
16
+ # And that you invoke somethink like:
17
+ #
18
+ # /* Initialize sparklines */
19
+ # $(document).ready(function() {
20
+ # $('.sparkline').sparkline();
21
+ # });
22
+ #
23
+ # in your document script.
24
+ #
25
+ # Styling in CSS is required for the <span> container which
26
+ # has a class of 'sparkline'
27
+ #
28
+ # ====Parameters
29
+ #
30
+ # data_source: The active record result set
31
+ # data_column: one column name to be charted
32
+ # options: options hash
33
+ #
34
+ # ====Options
35
+ #
36
+ # Currently no options for Sparklines.
37
+ def initialize(data_source, data_column, options = {})
38
+ @options = DEFAULT_OPTIONS.merge(options)
39
+ @options[:container] ||= generate_container_name
40
+ @data_column = data_column
41
+ @chart = chart_class.new(data_source, @data_column, @options)
42
+ end
43
+
44
+ # Output the HTML representation of the chart
45
+ def to_html
46
+ @chart.to_html
47
+ end
48
+
49
+ # Convenience method for configuring the chart.
50
+ def self.configure
51
+ yield self
52
+ end
53
+
54
+ private
55
+ def chart_class
56
+ @chart_class ||= "Charting::Sparklines::#{options[:type].to_s.titleize}".constantize
57
+ end
58
+
59
+ def generate_container_name
60
+ "sparkline_" + ActiveSupport::SecureRandom.hex(3)
61
+ end
62
+
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,54 @@
1
+ module Charting
2
+ module Sparklines
3
+ class Base
4
+ attr_reader :data_source, :data_column, :options
5
+
6
+ DEFAULT_OPTIONS = {}
7
+
8
+ def initialize(data_source, data_column, options = {})
9
+ @data_source = data_source
10
+ @data_column = data_column
11
+ @options = DEFAULT_OPTIONS.merge(options)
12
+ end
13
+
14
+ # No options currently defined
15
+ def chart_options
16
+ {}
17
+ end
18
+
19
+ # Define in concrete subclass
20
+ def series
21
+ []
22
+ end
23
+
24
+ # The name of a data series
25
+ def series_name
26
+ data_source.first.class.human_attribute_name(data_column)
27
+ end
28
+
29
+ # The chart type (derived from the class name)
30
+ def chart_type
31
+ @chart_type ||= self.class.name.split('::').last.downcase
32
+ end
33
+
34
+ # Render the chart HTML. Requires jQuery and jQuery sparklines plugin
35
+ def to_html
36
+ <<-EOF
37
+ <span id="#{container_id}" class="#{chart_css_class}">
38
+ #{series.join(',')}
39
+ </span>
40
+ EOF
41
+ end
42
+
43
+ private
44
+ def chart_css_class
45
+ "spark#{chart_type}"
46
+ end
47
+
48
+ def container_id
49
+ @container_id ||= options[:container]
50
+ end
51
+
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,12 @@
1
+ module Charting
2
+ module Sparklines
3
+ class Line < Charting::Sparklines::Base
4
+
5
+ def series
6
+ @chart_series ||= data_source.map{|row| row[data_column].to_i }
7
+ end
8
+
9
+ end
10
+ end
11
+ end
12
+
@@ -0,0 +1,51 @@
1
+ # Adds method to ActiveRecord to support some chart
2
+ # transformations
3
+ module Charting
4
+ module Transforms
5
+ def self.included(base)
6
+ base.class_eval do
7
+ extend ClassMethods
8
+ include InstanceMethods
9
+ const_set(:DEFAULT_PIVOT_COLUMNS, {
10
+ :attribute => :attribute, :value => :value
11
+ })
12
+ end
13
+ end
14
+
15
+ module InstanceMethods
16
+ # Takes one row and returns an array of objects of the same class
17
+ # where columns are pivoted to become rows. The new rows are of the
18
+ # same class as the current instance.
19
+ #
20
+ # ====Arguments
21
+ #
22
+ # The columns that become the row/column pairs in the result array.
23
+ # An empty list means pivot all attributes.
24
+ #
25
+ # ====Options
26
+ #
27
+ # Specify the optional column names for the result array. The defaults
28
+ # are:
29
+ #
30
+ # :category => :category
31
+ # :value => value
32
+ def pivot(*args)
33
+ options = (args.last.is_a?(Hash) ? args.pop : {}).merge(self.class.const_get(:DEFAULT_PIVOT_COLUMNS))
34
+ args.flatten!
35
+ attributes = args.size > 0 ? args : self.attributes.map(&:first)
36
+ klass = self.class
37
+ return attributes.inject([]) do |rows, arg|
38
+ row = klass.new
39
+ row[options[:attribute]] = arg.to_s
40
+ row[options[:value]] = self[arg].to_i
41
+ rows << row
42
+ rows
43
+ end
44
+ end
45
+ end
46
+
47
+ module ClassMethods
48
+
49
+ end
50
+ end
51
+ end
@@ -1,3 +1,3 @@
1
1
  module ArToChart
2
- VERSION = "0.0.1"
2
+ VERSION = "0.0.2"
3
3
  end
@@ -0,0 +1,3 @@
1
+ en:
2
+ charts:
3
+ not_set: Not Set
@@ -0,0 +1,4 @@
1
+ # desc "Explaining what the task does"
2
+ # task :ar_to_chart do
3
+ # # Task goes here
4
+ # end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ar_to_chart
3
3
  version: !ruby/object:Gem::Version
4
- hash: 29
4
+ hash: 27
5
5
  prerelease: false
6
6
  segments:
7
7
  - 0
8
8
  - 0
9
- - 1
10
- version: 0.0.1
9
+ - 2
10
+ version: 0.0.2
11
11
  platform: ruby
12
12
  authors:
13
13
  - Kip Cole
@@ -15,11 +15,11 @@ autorequire:
15
15
  bindir: bin
16
16
  cert_chain: []
17
17
 
18
- date: 2010-10-14 00:00:00 +08:00
18
+ date: 2010-11-02 00:00:00 +08:00
19
19
  default_executable:
20
20
  dependencies: []
21
21
 
22
- description: " Defines Array#to_chart that will accept ActiveRecord result sets\n and render them as a chart. Currently assumes OpenFlashChart as the\n only supported charting engine. Protovis based charting coming\n soon.\n"
22
+ description: " Defines Array#to_chart that will accept ActiveRecord result sets\n and render them as a chart. Currently assumes Highcharts (highcharts.com)\n as the charting engine.\n"
23
23
  email:
24
24
  - kipcole9@gmail.com
25
25
  executables: []
@@ -30,11 +30,30 @@ extra_rdoc_files: []
30
30
 
31
31
  files:
32
32
  - .gitignore
33
+ - CHANGELOG
33
34
  - Gemfile
35
+ - MIT-LICENSE
36
+ - README.textile
34
37
  - Rakefile
35
38
  - ar_to_chart.gemspec
39
+ - files/ar_to_chart.css
40
+ - files/ar_to_chart.js
36
41
  - lib/ar_to_chart.rb
42
+ - lib/ar_to_chart/.DS_Store
43
+ - lib/ar_to_chart/active_record_array.rb
44
+ - lib/ar_to_chart/charting/highcharts.rb
45
+ - lib/ar_to_chart/charting/highcharts/area.rb
46
+ - lib/ar_to_chart/charting/highcharts/base.rb
47
+ - lib/ar_to_chart/charting/highcharts/funnel.rb
48
+ - lib/ar_to_chart/charting/highcharts/pie.rb
49
+ - lib/ar_to_chart/charting/period.rb
50
+ - lib/ar_to_chart/charting/sparklines.rb
51
+ - lib/ar_to_chart/charting/sparklines/base.rb
52
+ - lib/ar_to_chart/charting/sparklines/line.rb
53
+ - lib/ar_to_chart/charting/transforms.rb
37
54
  - lib/ar_to_chart/version.rb
55
+ - lib/locale/to_chart-en.yml
56
+ - lib/tasks/at_to_chart_tasks.rake
38
57
  has_rdoc: true
39
58
  homepage: http://rubygems.org/gems/ar_to_chart
40
59
  licenses: []