graphene 0.0.1
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.
- 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
|
+
|