graphene 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE +13 -0
- data/README.rdoc +101 -0
- data/lib/graphene/graphene.rb +55 -0
- data/lib/graphene/gruff.rb +360 -0
- data/lib/graphene/gruff_helpers.rb +0 -0
- data/lib/graphene/lazy_enumerable.rb +33 -0
- data/lib/graphene/over_x.rb +49 -0
- data/lib/graphene/percentages.rb +36 -0
- data/lib/graphene/result_set.rb +59 -0
- data/lib/graphene/subtotals.rb +32 -0
- data/lib/graphene/tablizer.rb +15 -0
- data/lib/graphene/version.rb +4 -0
- data/lib/graphene.rb +21 -0
- metadata +89 -0
data/LICENSE
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
Copyright 2012 Jordan Hollinger
|
2
|
+
|
3
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
4
|
+
you may not use this file except in compliance with the License.
|
5
|
+
You may obtain a copy of the License at
|
6
|
+
|
7
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
8
|
+
|
9
|
+
Unless required by applicable law or agreed to in writing, software
|
10
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
11
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
12
|
+
See the License for the specific language governing permissions and
|
13
|
+
limitations under the License.
|
data/README.rdoc
ADDED
@@ -0,0 +1,101 @@
|
|
1
|
+
= Graphene
|
2
|
+
|
3
|
+
Graphene is a library for transforming collections of Ruby objects into subtotals, percentages, tables and graphs.
|
4
|
+
It's most useful feature is probably the Gruff helpers. True, some types of Gruff charts are pretty simple to begin
|
5
|
+
with, but others can require tedious boilerplate code to calculate percentages, well-spaced labels, and more.
|
6
|
+
Graphene hides all of that, accepting an array of objects and transforming them into a graph.
|
7
|
+
|
8
|
+
Read the full documentation at http://jordanhollinger.com/docs/graphene/.
|
9
|
+
|
10
|
+
Nobody likes you, Ruby 1.8. Now go away.
|
11
|
+
|
12
|
+
== Installation
|
13
|
+
|
14
|
+
$ [sudo] gem install graphene
|
15
|
+
# Or just add "graphene" to your Gemfile
|
16
|
+
|
17
|
+
Gruff is required only by Graphene's graphing component. Because of this, and because Gruff's dependencies can be difficult to install,
|
18
|
+
you will need to install Gruff manually. If you do not/cannot, Graphene's other components will continue to function.
|
19
|
+
|
20
|
+
# On Debian/Ubuntu
|
21
|
+
$ sudo apt-get install librmagick-ruby libmagickcore-dev libmagickwand-dev
|
22
|
+
|
23
|
+
$ [sudo] gem install rmagick gruff
|
24
|
+
# Or just add "rmagick" and "gruff" to your Gemfile
|
25
|
+
|
26
|
+
See http://nubyonrails.com/pages/gruff/ for more on Gruff.
|
27
|
+
|
28
|
+
== Percentages
|
29
|
+
|
30
|
+
# An array of objects which respond to methods like :browser, :platform, :date
|
31
|
+
logs = SomeLogParser.parse('/var/log/nginx/access.log.*')
|
32
|
+
|
33
|
+
percentages = Graphene.percentages(logs, :browser)
|
34
|
+
|
35
|
+
puts percentages.to_a
|
36
|
+
=> [["Firefox", 40.0], ["Chrome", 35.0], ["Internet Explorer", 25.0]]
|
37
|
+
|
38
|
+
percentages.each do |browser, count|
|
39
|
+
puts "There were #{count} hits from #{browser}"
|
40
|
+
end
|
41
|
+
|
42
|
+
# You can also calculate by multiple criteria, and may use lambdas instead of symbols
|
43
|
+
percentages = Graphene.percentages(logs, ->(l) { l.browser.downcase }, :platform)
|
44
|
+
|
45
|
+
puts percentages.to_a
|
46
|
+
=> [["chrome", "OS X", 40], ["internet explorer", "Windows", 25], ["firefox", "Windows", 25], ["firefox", "GNU/Linux", 8], ["chrome", "GNU/Linux", 2]]
|
47
|
+
|
48
|
+
See Graphene.percentages for more info.
|
49
|
+
|
50
|
+
== Subtotals
|
51
|
+
|
52
|
+
Same as percentages above, except that subtotals are returned instead. See Graphene.subtotals for more info.
|
53
|
+
|
54
|
+
== Tablizer
|
55
|
+
|
56
|
+
Integration with the tablizer gem provides quick ASCII tables.
|
57
|
+
|
58
|
+
puts percentages.tablize
|
59
|
+
=> +-----------------+------------+----------+
|
60
|
+
| Browser | Platform |Percentage|
|
61
|
+
+-----------------+------------+----------+
|
62
|
+
|Firefox |Windows |50.0 |
|
63
|
+
|Internet Explorer|Windows |20.0 |
|
64
|
+
|Safari |OS X |20.0 |
|
65
|
+
|Firefox |GNU/Linux |10.0 |
|
66
|
+
+-----------------+------------+----------+
|
67
|
+
|
68
|
+
== Graphs
|
69
|
+
|
70
|
+
Provides helpers for generating Gruff graphs. Requires the "gruff" Ruby gem.
|
71
|
+
|
72
|
+
# A pie chart of Firefox version shares
|
73
|
+
ff = logs.select { |e| e.browser == 'Firefox' }
|
74
|
+
Graphene.percentages(ff, :browser).pie_chart('/path/to/graph.png', 'FF Version Share')
|
75
|
+
|
76
|
+
# A line graph of daily browser numbers over time, tricked out with lots of options
|
77
|
+
Graphene.subtotals(logs, :browser).over(:date).line_graph('/path/to/graph.png') do |chart, labeler|
|
78
|
+
chart.title = 'Browser Share'
|
79
|
+
chart.font = '/path/to/awesome/font.ttf'
|
80
|
+
chart.theme = {
|
81
|
+
:colors => %w(orange purple green white red),
|
82
|
+
:marker_color => 'blue',
|
83
|
+
:background_colors => %w(black grey)
|
84
|
+
}
|
85
|
+
|
86
|
+
# Only show every 7th label, and make dates pretty
|
87
|
+
labeler.call(7) do |date|
|
88
|
+
date.strftime('%b %e')
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
See Graphene::OneDGraphs and Graphene::TwoDGraphs for more types and examples. See http://gruff.rubyforge.org/classes/Gruff/Base.html for more Gruff options.
|
93
|
+
|
94
|
+
== TODO
|
95
|
+
|
96
|
+
Graphene::OverX and the graph helpers needs the ability to fill in empty points
|
97
|
+
|
98
|
+
== License
|
99
|
+
Copyright 2012 Jordan Hollinger
|
100
|
+
|
101
|
+
Licensed under the Apache License
|
@@ -0,0 +1,55 @@
|
|
1
|
+
module Graphene
|
2
|
+
# Class for Graphene Exceptions
|
3
|
+
class GrapheneException < StandardError; end
|
4
|
+
|
5
|
+
# For the given resources, returns the share of the count that each attr(s) has.
|
6
|
+
#
|
7
|
+
# "resources" is an array of objects which responds to the "args" method(s).
|
8
|
+
#
|
9
|
+
# "args" is one or more method symbols or proc/lambda which each object in "resources" responds to.
|
10
|
+
# Subtotals will be calculated from the returned values.
|
11
|
+
#
|
12
|
+
# Returns an instance of Graphene::Subtotals, which implements Enumerable. Each member is
|
13
|
+
# an array of [attribute(s), count]
|
14
|
+
#
|
15
|
+
# Example, Browser Family share:
|
16
|
+
#
|
17
|
+
# Graphene.subtotals(logins, :browser_family).to_a
|
18
|
+
# => [['Firefox', 5040], ['Chrome', 1960], ['Internet Explorer', 1500], ['Safari', 1000], ['Unknown', 500]]
|
19
|
+
#
|
20
|
+
# Example, Browser/OS share, asking for symbols back:
|
21
|
+
#
|
22
|
+
# Graphene.subtotals(server_log_entries, :browser_sym, :os_sym).to_a
|
23
|
+
# => [[:firefox, :windows_7, 50.4, 5040], [:chrome, :osx, 19.6, 1960], [:msie, :windows_xp, 15, 1500], [:safari, :osx, 10, 1000], [:other, :other, 5, 100]]
|
24
|
+
#
|
25
|
+
def self.subtotals(resources, *args)
|
26
|
+
Subtotals.new(resources, *args)
|
27
|
+
end
|
28
|
+
|
29
|
+
# For the given "resources", returns the % share of the group that each attr(s) has.
|
30
|
+
#
|
31
|
+
# "resources" is an array of objects which responds to the "args" method(s).
|
32
|
+
#
|
33
|
+
# "args" is one or more method symbols or proc/lambda which each object in "resources" responds to.
|
34
|
+
# Percentages will be calculated from the returned values.
|
35
|
+
#
|
36
|
+
# "args" may have, as it's last member, :threshold => n, where n is the number of the lowest
|
37
|
+
# percentage you want returned.
|
38
|
+
#
|
39
|
+
# Returns an instance of Graphene::Percentages, which implements Enumerable. Each member is
|
40
|
+
# an array of [attribute(s), percentage]
|
41
|
+
#
|
42
|
+
# Example, Browser Family share:
|
43
|
+
#
|
44
|
+
# Graphene.percentages(logins, :browser_family).to_a
|
45
|
+
# => [['Firefox', 50.4], ['Chrome', 19.6], ['Internet Explorer', 15], ['Safari', 10], ['Unknown', 5]]
|
46
|
+
#
|
47
|
+
# Example, Browser/OS share, asking for symbols back:
|
48
|
+
#
|
49
|
+
# Graphene.percentages(server_log_entries, :browser_sym, :os_sym).to_a
|
50
|
+
# => [[:firefox, :windows_7, 50.4], [:chrome, :osx, 19.6], [:msie, :windows_xp, 15], [:safari, :osx, 10], [:other, :other, 5]]
|
51
|
+
#
|
52
|
+
def self.percentages(resources, *args)
|
53
|
+
Percentages.new(resources, *args)
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,360 @@
|
|
1
|
+
module Graphene
|
2
|
+
# Executes the given block if Gruff is available. Raises a GrapheneException if not.
|
3
|
+
def self.gruff
|
4
|
+
if defined? Gruff
|
5
|
+
yield if block_given?
|
6
|
+
else
|
7
|
+
raise GrapheneException, "Gruff integration is disabled because Gruff could not be loaded; install the \"gruff\" gem"
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
# Extends calculators with one-dimensional graphs, like pie charts.
|
12
|
+
module OneDGraphs
|
13
|
+
# Returns a Gruff::Pie object with the stats set.
|
14
|
+
#
|
15
|
+
# Optionally you may pass a file path and graph title. If you pass a file path, the graph will
|
16
|
+
# be written to file automatically. Otherwise, you would call "write('/path/to/graph.png')" on the
|
17
|
+
# returned graph object.
|
18
|
+
#
|
19
|
+
# If you pass a block, it will be called, giving you access to the Gruff::Pie object before it is
|
20
|
+
# written to file (that is, if you also passed a file path).
|
21
|
+
#
|
22
|
+
# Example 1:
|
23
|
+
#
|
24
|
+
# Graphene.percentages(logs, :browser).pie_chart('/path/to/browser-share.png', 'Browser Share')
|
25
|
+
#
|
26
|
+
# Example 2:
|
27
|
+
#
|
28
|
+
# Graphene.percentages(logs, :browser).pie_chart('/path/to/browser-share.png') do |pie|
|
29
|
+
# pie.title = 'Browser Share'
|
30
|
+
# pie.font = '/path/to/font.ttf'
|
31
|
+
# pie.theme = pie.theme_37signals
|
32
|
+
# end
|
33
|
+
#
|
34
|
+
# Example 3:
|
35
|
+
#
|
36
|
+
# blog = Graphene.percentages(logs, :browser).pie_chart.to_blob
|
37
|
+
#
|
38
|
+
def pie_chart(path=nil, title=nil, &block)
|
39
|
+
Graphene.gruff do
|
40
|
+
chart(Gruff::Pie.new, path, title, &block)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
alias_method :pie_graph, :pie_chart
|
44
|
+
|
45
|
+
# Returns a Gruff::Bar object with the stats set.
|
46
|
+
#
|
47
|
+
# Optionally you may pass a file path and chart title. If you pass a file path, the chart will
|
48
|
+
# be written to file automatically. Otherwise, you would call "write('/path/to/graph.png')" on the
|
49
|
+
# returned chart object.
|
50
|
+
#
|
51
|
+
# If you pass a block, it will be called, giving you access to the Gruff::Bar object before it is
|
52
|
+
# written to file (that is, if you also passed a file path).
|
53
|
+
#
|
54
|
+
# Example 1:
|
55
|
+
#
|
56
|
+
# Graphene.percentages(logs, :browser).bar_chart('/path/to/browser-share.png', 'Browser Share')
|
57
|
+
#
|
58
|
+
# Example 2:
|
59
|
+
#
|
60
|
+
# Graphene.percentages(logs, :browser).bar_chart('/path/to/browser-share.png') do |chart|
|
61
|
+
# chart.title = 'Browser Share'
|
62
|
+
# chart.font = '/path/to/font.ttf'
|
63
|
+
# chart.theme = chart.theme_37signals
|
64
|
+
# end
|
65
|
+
#
|
66
|
+
# Example 3:
|
67
|
+
#
|
68
|
+
# blog = Graphene.subtotals(logs, :browser).bar_chart.to_blob
|
69
|
+
#
|
70
|
+
def bar_chart(path=nil, title=nil, &block)
|
71
|
+
Graphene.gruff do
|
72
|
+
chart(Gruff::Bar.new, path, title, false, &block)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
alias_method :bar_graph, :bar_chart
|
76
|
+
|
77
|
+
# Returns a Gruff::StackedBar object with the stats set.
|
78
|
+
#
|
79
|
+
# Optionally you may pass a file path and chart title. If you pass a file path, the chart will
|
80
|
+
# be written to file automatically. Otherwise, you would call "write('/path/to/graph.png')" on the
|
81
|
+
# returned chart object.
|
82
|
+
#
|
83
|
+
# If you pass a block, it will be called, giving you access to the Gruff::StackedBar object before it is
|
84
|
+
# written to file (that is, if you also passed a file path).
|
85
|
+
#
|
86
|
+
# Example 1:
|
87
|
+
#
|
88
|
+
# Graphene.percentages(logs, :browser).stacked_bar_chart('/path/to/browser-share.png', 'Browser Share')
|
89
|
+
#
|
90
|
+
# Example 2:
|
91
|
+
#
|
92
|
+
# Graphene.percentages(logs, :browser).stacked_bar_chart('/path/to/browser-share.png') do |chart|
|
93
|
+
# chart.title = 'Browser Share'
|
94
|
+
# chart.font = '/path/to/font.ttf'
|
95
|
+
# chart.theme = chart.theme_37signals
|
96
|
+
# end
|
97
|
+
#
|
98
|
+
# Example 3:
|
99
|
+
#
|
100
|
+
# blog = Graphene.subtotals(logs, :browser).stacked_bar_chart.to_blob
|
101
|
+
#
|
102
|
+
def stacked_bar_chart(path=nil, title=nil, &block)
|
103
|
+
Graphene.gruff do
|
104
|
+
chart(Gruff::StackedBar.new, path, title, true, &block)
|
105
|
+
end
|
106
|
+
end
|
107
|
+
alias_method :stacked_bar_graph, :stacked_bar_chart
|
108
|
+
|
109
|
+
# Returns a Gruff::SideBar object with the stats set.
|
110
|
+
#
|
111
|
+
# Optionally you may pass a file path and chart title. If you pass a file path, the chart will
|
112
|
+
# be written to file automatically. Otherwise, you would call "write('/path/to/graph.png')" on the
|
113
|
+
# returned chart object.
|
114
|
+
#
|
115
|
+
# If you pass a block, it will be called, giving you access to the Gruff::SideBar object before it is
|
116
|
+
# written to file (that is, if you also passed a file path).
|
117
|
+
#
|
118
|
+
# Example 1:
|
119
|
+
#
|
120
|
+
# Graphene.percentages(logs, :browser).side_bar_chart('/path/to/browser-share.png', 'Browser Share')
|
121
|
+
#
|
122
|
+
# Example 2:
|
123
|
+
#
|
124
|
+
# Graphene.percentages(logs, :browser).side_bar_chart('/path/to/browser-share.png') do |chart|
|
125
|
+
# chart.title = 'Browser Share'
|
126
|
+
# chart.font = '/path/to/font.ttf'
|
127
|
+
# chart.theme = chart.theme_37signals
|
128
|
+
# end
|
129
|
+
#
|
130
|
+
# Example 3:
|
131
|
+
#
|
132
|
+
# blog = Graphene.subtotals(logs, :browser).side_bar_chart.to_blob
|
133
|
+
#
|
134
|
+
def side_bar_chart(path=nil, title=nil, &block)
|
135
|
+
Graphene.gruff do
|
136
|
+
chart(Gruff::SideBar.new, path, title, true, &block)
|
137
|
+
end
|
138
|
+
end
|
139
|
+
alias_method :side_bar_graph, :side_bar_chart
|
140
|
+
|
141
|
+
# Returns a Gruff::StackedSideBar object with the stats set.
|
142
|
+
#
|
143
|
+
# Optionally you may pass a file path and chart title. If you pass a file path, the chart will
|
144
|
+
# be written to file automatically. Otherwise, you would call "write('/path/to/graph.png')" on the
|
145
|
+
# returned chart object.
|
146
|
+
#
|
147
|
+
# If you pass a block, it will be called, giving you access to the Gruff::StackedSideBar object before it is
|
148
|
+
# written to file (that is, if you also passed a file path).
|
149
|
+
#
|
150
|
+
# Example 1:
|
151
|
+
#
|
152
|
+
# Graphene.percentages(logs, :browser).side_stacked_bar_chart('/path/to/browser-share.png', 'Browser Share')
|
153
|
+
#
|
154
|
+
# Example 2:
|
155
|
+
#
|
156
|
+
# Graphene.percentages(logs, :browser).side_stacked_bar_chart('/path/to/browser-share.png') do |chart|
|
157
|
+
# chart.title = 'Browser Share'
|
158
|
+
# chart.font = '/path/to/font.ttf'
|
159
|
+
# chart.theme = chart.theme_37signals
|
160
|
+
# end
|
161
|
+
#
|
162
|
+
# Example 3:
|
163
|
+
#
|
164
|
+
# blog = Graphene.subtotals(logs, :browser).side_stacked_bar_chart.to_blob
|
165
|
+
#
|
166
|
+
def side_stacked_bar_chart(path=nil, title=nil, &block)
|
167
|
+
Graphene.gruff do
|
168
|
+
chart(Gruff::SideStackedBar.new, path, title, true, &block)
|
169
|
+
end
|
170
|
+
end
|
171
|
+
alias_method :side_stacked_bar_graph, :side_stacked_bar_chart
|
172
|
+
|
173
|
+
# Returns a Gruff::Spider object with the stats set.
|
174
|
+
#
|
175
|
+
# Optionally you may pass a file path and chart title. If you pass a file path, the chart will
|
176
|
+
# be written to file automatically. Otherwise, you would call "write('/path/to/graph.png')" on the
|
177
|
+
# returned chart object.
|
178
|
+
#
|
179
|
+
# If you pass a block, it will be called, giving you access to the Gruff::Spider object before it is
|
180
|
+
# written to file (that is, if you also passed a file path).
|
181
|
+
#
|
182
|
+
# Example 1:
|
183
|
+
#
|
184
|
+
# Graphene.percentages(logs, :browser).spider_chart('/path/to/browser-share.png', 'Browser Share')
|
185
|
+
#
|
186
|
+
# Example 2:
|
187
|
+
#
|
188
|
+
# Graphene.percentages(logs, :browser).spider_chart('/path/to/browser-share.png') do |chart|
|
189
|
+
# chart.title = 'Browser Share'
|
190
|
+
# chart.font = '/path/to/font.ttf'
|
191
|
+
# chart.theme = chart.theme_37signals
|
192
|
+
# end
|
193
|
+
#
|
194
|
+
# Example 3:
|
195
|
+
#
|
196
|
+
# blog = Graphene.subtotals(logs, :browser).spider_chart.to_blob
|
197
|
+
#
|
198
|
+
def spider_chart(path=nil, title=nil, &block)
|
199
|
+
Graphene.gruff do
|
200
|
+
chart(Gruff::Spider.new(max_result), path, title, false, &block)
|
201
|
+
end
|
202
|
+
end
|
203
|
+
alias_method :spider_graph, :spider_chart
|
204
|
+
|
205
|
+
private
|
206
|
+
|
207
|
+
# Builds a chart
|
208
|
+
def chart(chart, path=nil, title=nil, hack=false, &block)
|
209
|
+
chart.title = title unless title.nil?
|
210
|
+
block.call(chart) if block
|
211
|
+
|
212
|
+
each do |result|
|
213
|
+
name = result[0..attributes.size-1].join(' / ')
|
214
|
+
n = result[attributes.size]
|
215
|
+
chart.data name, n
|
216
|
+
end
|
217
|
+
# XXX Required by SideBar and SideStackedBar. Probably a bug.
|
218
|
+
chart.labels = {0 => ' '} if hack
|
219
|
+
|
220
|
+
chart.write(path) unless path.nil?
|
221
|
+
chart
|
222
|
+
end
|
223
|
+
end
|
224
|
+
|
225
|
+
# Extends calculators with two-dimensional graphs, like line graphs.
|
226
|
+
module TwoDGraphs
|
227
|
+
# Returns a Gruff::Line object with the stats set.
|
228
|
+
#
|
229
|
+
# Optionally you may pass a file path and graph title. If you pass a file path, the graph will
|
230
|
+
# be written to file automatically. Otherwise, you would call "write('/path/to/graph.png')" on the
|
231
|
+
# returned graph object.
|
232
|
+
#
|
233
|
+
# If you pass a block, it will be called, giving you access to the Gruff::Line object before it is
|
234
|
+
# written to file (that is, if you also passed a file path). It will also give you access to a Proc
|
235
|
+
# for labeling the X axis.
|
236
|
+
#
|
237
|
+
# Example 1:
|
238
|
+
#
|
239
|
+
# Graphene.percentages(logs, :browser).over(:date).line_graph('/path/to/browser-share.png', 'Browser Share')
|
240
|
+
#
|
241
|
+
# Example 2:
|
242
|
+
#
|
243
|
+
# Graphene.subtotals(logs, :browser).over(:date).line_graph('/path/to/browser-share.png') do |chart, labeler|
|
244
|
+
# chart.title = 'Browser Share'
|
245
|
+
# chart.font = '/path/to/font.ttf'
|
246
|
+
# chart.theme = pie.theme_37signals
|
247
|
+
# end
|
248
|
+
#
|
249
|
+
# Example 3:
|
250
|
+
#
|
251
|
+
# Graphene.subtotals(logs, :browser).over(:date).line_graph('/path/to/browser-share.png') do |chart, labeler|
|
252
|
+
# chart.title = 'Browser Share'
|
253
|
+
#
|
254
|
+
# # Both the 10 and the block are optional.
|
255
|
+
# # - "10" means that only every 10'th label will be printed. Otherwise, each would be.
|
256
|
+
# # - The block is passed each label (the return value of the "over attribute") and may return a formatted version.
|
257
|
+
# labeler.call(10) do |date|
|
258
|
+
# date.strftime('%m/%d/%Y')
|
259
|
+
# end
|
260
|
+
# end
|
261
|
+
#
|
262
|
+
# Example 4:
|
263
|
+
#
|
264
|
+
# Graphene.percentages(logs, :platform, :browser).over(->(l) { l.date.strftime('%m/%Y') }).line_graph('/path/to/os-browser-share.png', 'OS / Browser Share by Month')
|
265
|
+
#
|
266
|
+
def line_graph(path=nil, title=nil, &block)
|
267
|
+
Graphene.gruff do
|
268
|
+
graph(Gruff::Line.new, path, title, &block)
|
269
|
+
end
|
270
|
+
end
|
271
|
+
alias_method :line_chart, :line_graph
|
272
|
+
|
273
|
+
# Returns a Gruff::Net object with the stats set.
|
274
|
+
#
|
275
|
+
# Optionally you may pass a file path and graph title. If you pass a file path, the graph will
|
276
|
+
# be written to file automatically. Otherwise, you would call "write('/path/to/graph.png')" on the
|
277
|
+
# returned graph object.
|
278
|
+
#
|
279
|
+
# If you pass a block, it will be called, giving you access to the Gruff::Net object before it is
|
280
|
+
# written to file (that is, if you also passed a file path). It will also give you access to a Proc
|
281
|
+
# for labeling the X axis.
|
282
|
+
#
|
283
|
+
# Example 1:
|
284
|
+
#
|
285
|
+
# Graphene.percentages(logs, :browser).over(:date).net_graph('/path/to/browser-share.png', 'Browser Share')
|
286
|
+
#
|
287
|
+
# Example 2:
|
288
|
+
#
|
289
|
+
# Graphene.subtotals(logs, :browser).over(:date).net_graph('/path/to/browser-share.png') do |chart, labeler|
|
290
|
+
# chart.title = 'Browser Share'
|
291
|
+
# chart.font = '/path/to/font.ttf'
|
292
|
+
# chart.theme = pie.theme_37signals
|
293
|
+
# end
|
294
|
+
#
|
295
|
+
# Example 3:
|
296
|
+
#
|
297
|
+
# Graphene.subtotals(logs, :browser).over(:date).net_graph('/path/to/browser-share.png') do |chart, labeler|
|
298
|
+
# chart.title = 'Browser Share'
|
299
|
+
#
|
300
|
+
# # Both the 10 and the block are optional.
|
301
|
+
# # - "10" means that only every 10'th label will be printed. Otherwise, each would be.
|
302
|
+
# # - The block is passed each label (the return value of the "over attribute") and may return a formatted version.
|
303
|
+
# labeler.call(10) do |date|
|
304
|
+
# date.strftime('%m/%d/%Y')
|
305
|
+
# end
|
306
|
+
# end
|
307
|
+
#
|
308
|
+
# Example 4:
|
309
|
+
#
|
310
|
+
# Graphene.percentages(logs, :platform, :browser).over(->(l) { l.date.strftime('%m/%Y') }).net_graph('/path/to/os-browser-share.png', 'OS / Browser Share by Month')
|
311
|
+
#
|
312
|
+
def net_graph(path=nil, title=nil, &block)
|
313
|
+
Graphene.gruff do
|
314
|
+
graph(Gruff::Net.new, path, title, &block)
|
315
|
+
end
|
316
|
+
end
|
317
|
+
alias_method :net_chart, :net_graph
|
318
|
+
|
319
|
+
private
|
320
|
+
|
321
|
+
# Builds a graph
|
322
|
+
def graph(graph, path=nil, title=nil, &block)
|
323
|
+
graph.title = title unless title.nil?
|
324
|
+
|
325
|
+
# Create an empty array for each group (e.g. {["Firefox"] => [], ["Safari"] => []}), even if it's empty.
|
326
|
+
data = inject({}) do |dat, (label, rows)|
|
327
|
+
for row in rows
|
328
|
+
attrs = row[0..-2]
|
329
|
+
dat[attrs] ||= []
|
330
|
+
end
|
331
|
+
dat
|
332
|
+
end
|
333
|
+
|
334
|
+
# Group the data on the x axis
|
335
|
+
to_a.each do |x_attr, rows|
|
336
|
+
groups = rows.group_by { |row| row[0..-2] }
|
337
|
+
for attrs, dat in data
|
338
|
+
dat << (groups[attrs] ? groups[attrs].last.last : 0)
|
339
|
+
end
|
340
|
+
end
|
341
|
+
|
342
|
+
# Build the labeling proc
|
343
|
+
label_every_n, labeler = 1, :to_s.to_proc
|
344
|
+
get_labeler = proc do |n=1, &block|
|
345
|
+
label_every_n = n
|
346
|
+
labeler = block if block
|
347
|
+
end
|
348
|
+
yield(graph, get_labeler) if block_given?
|
349
|
+
# Build labels and add them to graph
|
350
|
+
labels = @results.keys
|
351
|
+
graph.labels = Hash[*labels.select { |x| labels.index(x) % label_every_n == 0 }.map { |x| [*labels.index(x), labeler[x]] }.flatten]
|
352
|
+
|
353
|
+
# Add data to the graph
|
354
|
+
data.each { |attrs, dat| graph.data(attrs.join(' / '), dat) }
|
355
|
+
|
356
|
+
graph.write(path) unless path.nil?
|
357
|
+
graph
|
358
|
+
end
|
359
|
+
end
|
360
|
+
end
|
File without changes
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module Graphene
|
2
|
+
# Includes Enumerable and alculates @results lazily. An enumerate! method must be implemented.
|
3
|
+
module LazyEnumerable
|
4
|
+
def self.included(base) # :nodoc:
|
5
|
+
base.send(:include, Enumerable)
|
6
|
+
end
|
7
|
+
|
8
|
+
# Implements the "each" method required by Enumerable.
|
9
|
+
def each(&block)
|
10
|
+
lazily_enumerate!
|
11
|
+
@results.each &block
|
12
|
+
end
|
13
|
+
|
14
|
+
# Tests equality between this and another set
|
15
|
+
def ==(other)
|
16
|
+
lazily_enumerate!
|
17
|
+
to_a == other.to_a
|
18
|
+
end
|
19
|
+
|
20
|
+
# Tests equality between this and another set
|
21
|
+
def ===(other)
|
22
|
+
lazily_enumerate!
|
23
|
+
to_a === other.to_a
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
# Run the calculation if it hasn't already been
|
29
|
+
def lazily_enumerate!
|
30
|
+
enumerate! if @results.empty?
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
module Graphene
|
2
|
+
# Groups the stats of resources by the given method symbol or lambda.
|
3
|
+
#
|
4
|
+
# Example by date
|
5
|
+
#
|
6
|
+
# Graphene.percentages(logs, :browser).over(:date)
|
7
|
+
# => {#<Date: 2012-07-22> => [["Firefox", 45], ["Chrome", 40], ["Internet Explorer", 15]],
|
8
|
+
# #<Date: 2012-07-23> => [["Firefox", 41], ["Chrome", 40], ["Internet Explorer", 19]],
|
9
|
+
# #<Date: 2012-07-24> => [["Chrome", 50], ["Firefox", 40], ["Internet Explorer", 10]]}
|
10
|
+
#
|
11
|
+
# See Graphene::LazyEnumerable, Graphene::Tablize and Graphene::TwoDGraphs for more documentation.
|
12
|
+
class OverX
|
13
|
+
include LazyEnumerable
|
14
|
+
include TwoDGraphs
|
15
|
+
|
16
|
+
# The attribute that are being statted, passed in the constructor
|
17
|
+
attr_reader :result_set
|
18
|
+
|
19
|
+
# Accepts a ResultSet object (i.e. Graphene::Subtotals or Graphene::Percentages), and a
|
20
|
+
# method symbol or proc/lambda to build the X axis
|
21
|
+
def initialize(result_set, attr_or_lambda)
|
22
|
+
@result_set = result_set
|
23
|
+
@attribute = attr_or_lambda
|
24
|
+
@results = {}
|
25
|
+
end
|
26
|
+
|
27
|
+
# Returns a string representation of the results
|
28
|
+
def to_s
|
29
|
+
to_hash.to_s
|
30
|
+
end
|
31
|
+
|
32
|
+
# Returns a Hash representation of the results
|
33
|
+
def to_hash
|
34
|
+
@results.clone
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
# Run the calculation
|
40
|
+
def enumerate!
|
41
|
+
resources_by_x = result_set.resources.group_by(&@attribute).sort_by(&:first)
|
42
|
+
@results = resources_by_x.inject({}) do |results, (x, group)|
|
43
|
+
results[x] ||= []
|
44
|
+
results[x] += result_set.class.new(group, *result_set.attributes).to_a
|
45
|
+
results
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
require 'graphene/subtotals'
|
2
|
+
|
3
|
+
module Graphene
|
4
|
+
# Calculates and contains the percent subtotals of each attr (or attr group). Inherits from Graphene::ResultSet.
|
5
|
+
# See Graphene::LazyEnumerable, Graphene::Tablize and Graphene::OneDGraphs for more documentation.
|
6
|
+
#
|
7
|
+
# If you passed an options Hash containing :threshold to the constructor,
|
8
|
+
# any results falling below it will be excluded.
|
9
|
+
#
|
10
|
+
# Don't create instance manually. Instead, use the Graphene.percentages method, which will return
|
11
|
+
# a properly instantiated object.
|
12
|
+
class Percentages < ResultSet
|
13
|
+
# Convert the percentages to subtotals
|
14
|
+
def subtotals(opts=nil)
|
15
|
+
@subtotals ||= transmogrify(Subtotals, opts)
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
# Perform the calculations
|
21
|
+
def enumerate!
|
22
|
+
# Now replace them with the percentages
|
23
|
+
total = resources.size.to_f
|
24
|
+
# Replace the subtotal with the percent
|
25
|
+
@results = subtotals.map do |*args, count|
|
26
|
+
percent = ((count * 100) / total).round(2)
|
27
|
+
[*args, percent]
|
28
|
+
end
|
29
|
+
|
30
|
+
# Drop results that are too small
|
31
|
+
if options.has_key? :threshold
|
32
|
+
@results.reject! { |result| result[-1] < options[:threshold] }
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
module Graphene
|
2
|
+
# A generic class for Graphene stat result sets, which implements Enumerable and Graphene::LazyEnumerable.
|
3
|
+
# Calculations are performed lazily, so ResultSet objects are *cheap to create, but not necessarily cheap to use*.
|
4
|
+
class ResultSet
|
5
|
+
include LazyEnumerable
|
6
|
+
include Tablize
|
7
|
+
include OneDGraphs
|
8
|
+
|
9
|
+
# The options Hash passed in the constructor
|
10
|
+
attr_reader :options
|
11
|
+
# The attribute(s) that are being statted, passed in the constructor
|
12
|
+
attr_reader :attributes
|
13
|
+
# The original array of objects from which the stats were generated
|
14
|
+
attr_reader :resources
|
15
|
+
|
16
|
+
# Accepts an array of objects, and an unlimited number of method symbols (which sould be methods in the objects).
|
17
|
+
# Optionally accepts an options hash as a last argument.
|
18
|
+
#
|
19
|
+
# Implements Enumerable, so the results can be accessed by any of those methods, including each and to_a.
|
20
|
+
def initialize(resources, *args)
|
21
|
+
@options = args.last.is_a?(Hash) ? args.pop : {}
|
22
|
+
@attributes = args
|
23
|
+
@resources = resources
|
24
|
+
@results = []
|
25
|
+
end
|
26
|
+
|
27
|
+
# Calculates the resources, gropuing them by "over_x", which can be a method symbol or lambda
|
28
|
+
#
|
29
|
+
# Example by date
|
30
|
+
#
|
31
|
+
# Graphene.percentages(logs, :browser).over(:date)
|
32
|
+
# => {#<Date: 2012-07-22> => [["Firefox", 45], ["Chrome", 40], ["Internet Explorer", 15]],
|
33
|
+
# #<Date: 2012-07-23> => [["Firefox", 41], ["Chrome", 40], ["Internet Explorer", 19]],
|
34
|
+
# #<Date: 2012-07-24> => [["Chrome", 50], ["Firefox", 40], ["Internet Explorer", 10]]}
|
35
|
+
#
|
36
|
+
def over(over_x)
|
37
|
+
OverX.new(self, over_x)
|
38
|
+
end
|
39
|
+
|
40
|
+
# Returns a string representation of the results
|
41
|
+
def to_s
|
42
|
+
to_a.to_s
|
43
|
+
end
|
44
|
+
|
45
|
+
# Returns the maximum result
|
46
|
+
def max_result
|
47
|
+
x = attributes.size
|
48
|
+
sort_by { |result| result[x] }.last[x]
|
49
|
+
end
|
50
|
+
|
51
|
+
private
|
52
|
+
|
53
|
+
# Change it to another ResultSet subclass
|
54
|
+
def transmogrify(klass, opts=nil)
|
55
|
+
opts ||= self.options
|
56
|
+
klass.new(resources, *attributes, opts)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
require 'graphene/result_set'
|
2
|
+
|
3
|
+
module Graphene
|
4
|
+
# Calculates and contains the subtotals of each attr (or attr group). Inherits from Graphene::ResultSet.
|
5
|
+
# See Graphene::LazyEnumerable, Graphene::Tablize and Graphene::OneDGraphs for more documentation.
|
6
|
+
#
|
7
|
+
# Don't create instance manually. Instead, use the Graphene.subtotals method, which will return
|
8
|
+
# a properly instantiated object.
|
9
|
+
class Subtotals < ResultSet
|
10
|
+
# Convert the percentages to subtotals
|
11
|
+
def percentages(opts=nil)
|
12
|
+
transmogrify(Percentages, opts)
|
13
|
+
end
|
14
|
+
|
15
|
+
private
|
16
|
+
|
17
|
+
# Calculates the subtotals
|
18
|
+
def enumerate!
|
19
|
+
# Count the occurrence of each
|
20
|
+
results = resources.inject({}) do |res, resource|
|
21
|
+
attrs = attributes.map { |attr| attr.respond_to?(:call) ? attr.call(resource) : resource.send(attr) }
|
22
|
+
res[attrs] ||= 0
|
23
|
+
res[attrs] += 1
|
24
|
+
res
|
25
|
+
end.to_a
|
26
|
+
results.each(&:flatten!)
|
27
|
+
# Sort in ascending order
|
28
|
+
results.sort! { |a,b| b.last <=> a.last }
|
29
|
+
@results = results
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module Graphene
|
2
|
+
module Tablize
|
3
|
+
# Return the results formatted by the tablizer gem. Do disable headers, pass :header => false. Set alignment with :align.
|
4
|
+
def tablize(options={})
|
5
|
+
rows = to_a
|
6
|
+
# Default to including headers
|
7
|
+
unless options[:header] == false
|
8
|
+
headers = attributes.map(&:to_s) << self.class.name.scan(/\w+$/).last.to_s.gsub(/s$/, '').capitalize
|
9
|
+
rows.unshift(headers)
|
10
|
+
options[:header] = true
|
11
|
+
end
|
12
|
+
Tablizer::Table.new(rows, options)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
data/lib/graphene.rb
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
# Required gems
|
2
|
+
require 'tablizer'
|
3
|
+
begin
|
4
|
+
require 'gruff'
|
5
|
+
rescue LoadError => e
|
6
|
+
$stderr.puts "NOTICE Graphene cannot find Gruff; graphing will not be not available. Install the \"gruff\" gem in enable it."
|
7
|
+
end
|
8
|
+
|
9
|
+
require 'graphene/version'
|
10
|
+
require 'graphene/graphene'
|
11
|
+
|
12
|
+
# Formatters and helpers
|
13
|
+
require 'graphene/tablizer'
|
14
|
+
require 'graphene/gruff'
|
15
|
+
|
16
|
+
# Calculators
|
17
|
+
require 'graphene/lazy_enumerable'
|
18
|
+
require 'graphene/result_set'
|
19
|
+
require 'graphene/subtotals'
|
20
|
+
require 'graphene/percentages'
|
21
|
+
require 'graphene/over_x'
|
metadata
ADDED
@@ -0,0 +1,89 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: graphene
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
prerelease: false
|
5
|
+
segments:
|
6
|
+
- 0
|
7
|
+
- 0
|
8
|
+
- 1
|
9
|
+
version: 0.0.1
|
10
|
+
platform: ruby
|
11
|
+
authors:
|
12
|
+
- Jordan Hollinger
|
13
|
+
autorequire:
|
14
|
+
bindir: bin
|
15
|
+
cert_chain: []
|
16
|
+
|
17
|
+
date: 2012-07-22 00:00:00 -04:00
|
18
|
+
default_executable:
|
19
|
+
dependencies:
|
20
|
+
- !ruby/object:Gem::Dependency
|
21
|
+
name: tablizer
|
22
|
+
prerelease: false
|
23
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
24
|
+
none: false
|
25
|
+
requirements:
|
26
|
+
- - ~>
|
27
|
+
- !ruby/object:Gem::Version
|
28
|
+
segments:
|
29
|
+
- 1
|
30
|
+
- 0
|
31
|
+
version: "1.0"
|
32
|
+
type: :runtime
|
33
|
+
version_requirements: *id001
|
34
|
+
description: Library for calculating subtotals, percentages, tables and graphs from collections of Ruby objects
|
35
|
+
email: jordan@jordanhollinger.com
|
36
|
+
executables: []
|
37
|
+
|
38
|
+
extensions: []
|
39
|
+
|
40
|
+
extra_rdoc_files: []
|
41
|
+
|
42
|
+
files:
|
43
|
+
- lib/graphene.rb
|
44
|
+
- lib/graphene/graphene.rb
|
45
|
+
- lib/graphene/gruff.rb
|
46
|
+
- lib/graphene/gruff_helpers.rb
|
47
|
+
- lib/graphene/percentages.rb
|
48
|
+
- lib/graphene/result_set.rb
|
49
|
+
- lib/graphene/subtotals.rb
|
50
|
+
- lib/graphene/tablizer.rb
|
51
|
+
- lib/graphene/version.rb
|
52
|
+
- lib/graphene/over_x.rb
|
53
|
+
- lib/graphene/lazy_enumerable.rb
|
54
|
+
- README.rdoc
|
55
|
+
- LICENSE
|
56
|
+
has_rdoc: true
|
57
|
+
homepage: http://github.com/jhollinger/graphene
|
58
|
+
licenses: []
|
59
|
+
|
60
|
+
post_install_message:
|
61
|
+
rdoc_options: []
|
62
|
+
|
63
|
+
require_paths:
|
64
|
+
- lib
|
65
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
66
|
+
none: false
|
67
|
+
requirements:
|
68
|
+
- - ">="
|
69
|
+
- !ruby/object:Gem::Version
|
70
|
+
segments:
|
71
|
+
- 0
|
72
|
+
version: "0"
|
73
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
74
|
+
none: false
|
75
|
+
requirements:
|
76
|
+
- - ">="
|
77
|
+
- !ruby/object:Gem::Version
|
78
|
+
segments:
|
79
|
+
- 0
|
80
|
+
version: "0"
|
81
|
+
requirements: []
|
82
|
+
|
83
|
+
rubyforge_project:
|
84
|
+
rubygems_version: 1.3.7
|
85
|
+
signing_key:
|
86
|
+
specification_version: 3
|
87
|
+
summary: Easily create stats and graphs from collections of Ruby objects
|
88
|
+
test_files: []
|
89
|
+
|