Permalink
Browse files

Initial commit

  • Loading branch information...
0 parents commit be5cca48196883066b4f27128d3da7f4ad16afbd @jhollinger committed Jul 20, 2012
@@ -0,0 +1,13 @@
+ Copyright 2012 Jordan Hollinger
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
@@ -0,0 +1,115 @@
+= Graphene
+
+Description
+
+*NOTE* Ruby 1.8?
+
+== Installation
+
+ gem install graphene
+
+Or add "graphene" to your Gemfile
+
+== Loading
+
+ # Redundant if using Bundler
+ require 'graphene'
+
+ # Since Gruff's dependencies can a challenge to install, Graphene's Gruff module is not loaded by default.
+ # If you have Gruff installed, you may manually load graphene/gruff
+ require 'graphene/gruff'
+
+Read the full documentation at http://jordanhollinger.com/docs/graphene/
+
+= Calculations
+
+== Subtotals
+
+ # An array of objects with responds to methods like :browser, :platform, :date
+ logs = SomeLogParser.parse('/var/log/nginx/access.log.*')
+
+ subtotals = Graphene.subtotals(logs, :browser)
+
+ puts subtotals.to_a
+ => [["Firefox", 120], ["Chrome", 112], ["Internet Explorer", 98]]
+
+ subtotals.each do |browser, count|
+ puts "There were #{count} hits from #{browser}"
+ end
+
+ # You can also calculate by multiple criteria, and can use lambdas instead of symbols
+ subtotals = Graphene.subtotals(logs, ->(l) { l.browser }, :platform)
+
+ puts subtotals.to_a
+ => [["Chrome", "OS X", 102], ["Internet Explorer", 98], ["Firefox", "Windows", 90], ["Firefox", "GNU/Linux", 30], ["Chrome", "GNU/Linux", 10]]
+
+See Graphene::Subtotals for more info.
+
+== Percentages
+
+ # An array of objects with responds to methods like :browser, :platform, :date
+ logs = SomeLogParser.parse('/var/log/nginx/access.log.*')
+
+ percentages = Graphene.percentages(logs, :browser)
+
+ # Includes both the percent and the subtotal
+ puts percentages.to_a
+ => [["Firefox", 36.36, 120], ["Chrome", 33.93, 112], ["Internet Explorer", 29.69, 98]]
+
+ percentages.each do |browser, percentage, count|
+ puts "#{percentage}% of hits where from #{browser}"
+ end
+
+ # You can also calculate by multiple criteria, and can use lambdas instead of symbols
+ percentages = Graphene.percentages(logs, :browser, ->(l) { l.platform })
+
+See Graphene::Percentages for more info.
+
+= Formatting
+
+== Tablizer
+
+ puts Tablizer::Table.new(percentages.to_a)
+ => +-----------------+------------+----+---+
+ |Firefox |Windows |50.0|500|
+ |Internet Explorer|Windows |20.0|200|
+ |Safari |OS X |20.0|200|
+ |Firefox |GNU/Linux |10.0|100|
+ +-----------------+------------+----+---+
+
+== Gruff graphs
+
+Extends Graphene by adding several helpers for generating graphs. Requires that the Ruby "gruff" gem be installed.
+
+ require 'graphene/gruff'
+
+ # An array of objects with responds to methods like :browser, :platform, :date
+ logs = SomeLogParser.parse('/var/log/nginx/access.log.*')
+
+ # A pie chart of Firefox version shares
+ ff = logs.select { |e| e.browser_sym == :firefox }
+ Graphene.percentages(ff, :browser).pie_chart('/path/to/ff-version-share.png', 'Firefox Version Share')
+
+ # A line graph of daily browser family numbers over time, tricked out with lots of options
+ # See http://gruff.rubyforge.org/classes/Gruff/Base.html for more Gruff options
+ Graphene.subtotals(logs, :browser_family).line_graph(:date, '/path/to/browser-share.png') do |chart, labeler|
+ chart.title = 'Browser Share'
+ chart.font = '/path/to/awesome/font.ttf'
+ chart.theme = {
+ :colors => %w(orange purple green white red),
+ :marker_color => 'blue',
+ :background_colors => %w(black grey)
+ }
+
+ # Only show every 7th label, and make dates pretty
+ labeler.call(7) do |date|
+ date.strftime('%b %e')
+ end
+ end
+
+Look at Graphene::GruffHelpers for more graphs and examples.
+
+== License
+Copyright 2012 Jordan Hollinger
+
+Licensed under the Apache License
@@ -0,0 +1,20 @@
+# encoding: utf-8
+require File.join(File.dirname(__FILE__), 'lib', 'graphene', 'version')
+
+Gem::Specification.new do |spec|
+ spec.name = 'graphene'
+ spec.version = Graphene::VERSION
+ spec.summary = "Easily create stats and graphs from collections of Ruby objects"
+ spec.description = "Library for calculating subtotals, percentages, tables and graphs from collections of Ruby objects"
+ spec.authors = ['Jordan Hollinger']
+ spec.date = '2012-07-20'
+ spec.email = 'jordan@jordanhollinger.com'
+ spec.homepage = 'http://github.com/jhollinger/graphene'
+
+ spec.require_paths = ['lib']
+ spec.files = [Dir.glob('lib/**/*'), 'README.rdoc', 'LICENSE'].flatten
+
+ spec.add_dependency 'tablizer', '~> 1.0'
+
+ spec.specification_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION if spec.respond_to? :specification_version
+end
@@ -0,0 +1,13 @@
+require 'graphene/version'
+
+# Load calculators
+require 'graphene/result_set'
+require 'graphene/subtotals'
+require 'graphene/percentages'
+
+# Load formatters and helpers
+require 'graphene/tablizer'
+#require 'graphene/gruff'
+
+# Load main api
+require 'graphene/graphene'
@@ -0,0 +1,50 @@
+module Graphene
+ # For the given "resources", returns the share of the group that each attr(s) has.
+ #
+ # "resources" is an array of objects which responds to the "args" method(s).
+ #
+ # "args" is one or more method symbols which each object in "resources" responds to. Percentages
+ # will be calculated from the return values.
+ #
+ # "args" may have, as it's last member, :threshold => n, where n is the number of the lowest
+ # percentage you want returned.
+ #
+ # Returns an instance of Graphene::Percentages, which implements Enumerable. Each member is
+ # an array of [attribute(s), percentage, count]
+ #
+ # Example, Browser Family share:
+ #
+ # Graphene.percentages(logins, :browser_family).to_a
+ # [['Firefox', 50.4, 5040], ['Chrome', 19.6, 1960], ['Internet Explorer', 15, 1500], ['Safari', 10, 1000], ['Unknown', 5, 500]]
+ #
+ # Example, Browser/OS share, asking for symbols back:
+ #
+ # Graphene.percentages(server_log_entries, :browser_sym, :os_sym).to_a
+ # [[:firefox, :windows_7, 50.4, 5040], [:chrome, :osx, 19.6, 1960], [:msie, :windows_xp, 15, 1500], [:safari, :osx, 10, 1000], [:other, :other, 5, 100]]
+ def self.percentages(resources, *args)
+ Percentages.new(resources, *args)
+ end
+
+ # For the given "resources", returns the share of the count that each attr(s) has.
+ #
+ # "resources" is an array of objects which responds to the "args" method(s).
+ #
+ # "args" is one or more method symbols which each object in "resources" responds to. Subtotals
+ # will be calculated from the return values.
+ #
+ # Returns an instance of Graphene::Subtotals, which implements Enumerable. Each member is
+ # an array of [attribute(s), count]
+ #
+ # Example, Browser Family share:
+ #
+ # Graphene.subtotals(logins, :browser_family).to_a
+ # [['Firefox', 5040], ['Chrome', 1960], ['Internet Explorer', 1500], ['Safari', 1000], ['Unknown', 500]]
+ #
+ # Example, Browser/OS share, asking for symbols back:
+ #
+ # Graphene.subtotals(server_log_entries, :browser_sym, :os_sym).to_a
+ # [[:firefox, :windows_7, 50.4, 5040], [:chrome, :osx, 19.6, 1960], [:msie, :windows_xp, 15, 1500], [:safari, :osx, 10, 1000], [:other, :other, 5, 100]]
+ def self.subtotals(resources, *args)
+ Subtotals.new(resources, *args)
+ end
+end
@@ -0,0 +1,13 @@
+begin
+ require 'gruff'
+ require 'graphene/gruff_helpers'
+
+ module Graphene
+ class ResultSet
+ include GruffHelpers
+ end
+ end
+rescue LoadError => e
+ $stderr.puts "ERROR You must install \"gruff\" to use the gruff graphing helpers!"
+ raise e.class.name, e.message
+end
@@ -0,0 +1,162 @@
+module Graphene
+ # Extends the calculators with chart/graph generators. Requires the Ruby "gruff" gem.
+ module GruffHelpers
+ # Returns a Gruff::Pie object with the stats set.
+ #
+ # Optionally you may pass a file path and graph title. If you pass a file path, the graph will
+ # be written to file automatically. Otherwise, you would call "write('/path/to/graph.png')" on the
+ # returned graph object.
+ #
+ # If you pass a block, it will be called, giving you access to the Gruff::Pie object before it is
+ # written to file (that is, if you also passed a file path).
+ #
+ # Example 1:
+ #
+ # logs = SomeLogParser.parse("/var/log/nginx/my-site.access.log*")
+ # Graphene.percentages(logs, :browser).pie_chart('/path/to/browser-share.png', 'Browser Share')
+ #
+ # Example 2:
+ #
+ # Graphene.percentages(logs, :browser).pie_chart('/path/to/browser-share.png') do |pie|
+ # pie.title = 'Browser Share'
+ # pie.font = '/path/to/font.ttf'
+ # pie.theme = pie.theme_37signals
+ # end
+ #
+ # Example 3:
+ #
+ # blog = Graphene.percentages(logs, :browser).pie_chart.to_blob
+ #
+ def pie_chart(path=nil, title=nil, &block)
+ chart(Gruff::Pie.new, path, title, &block)
+ end
+
+ # Returns a Gruff::Bar object with the stats set.
+ #
+ # Optionally you may pass a file path and chart title. If you pass a file path, the chart will
+ # be written to file automatically. Otherwise, you would call "write('/path/to/graph.png')" on the
+ # returned chart object.
+ #
+ # If you pass a block, it will be called, giving you access to the Gruff::Pie object before it is
+ # written to file (that is, if you also passed a file path).
+ #
+ # Example 1:
+ #
+ # logs = SomeLogParser.parse("/var/log/nginx/my-site.access.log*")
+ # Graphene.percentages(logs, :browser).bar_chart('/path/to/browser-share.png', 'Browser Share')
+ #
+ # Example 2:
+ #
+ # Graphene.percentages(logs, :browser).bar_chart('/path/to/browser-share.png') do |chart|
+ # chart.title = 'Browser Share'
+ # chart.font = '/path/to/font.ttf'
+ # chart.theme = chart.theme_37signals
+ # end
+ #
+ # Example 3:
+ #
+ # blog = Graphene.counts(logs, :browser).bar_chart.to_blob
+ #
+ def bar_chart(path=nil, title=nil, &block)
+ chart(Gruff::Bar.new, path, title, &block)
+ end
+
+ # Returns a Gruff::Line object with the stats set.
+ #
+ # "x_method" should be a method on "resources" a lambda that accepts a resource and returns a
+ # value for the x axis.
+ #
+ # Optionally you may pass a file path and graph title. If you pass a file path, the graph will
+ # be written to file automatically. Otherwise, you would call "write('/path/to/graph.png')" on the
+ # returned graph object.
+ #
+ # If you pass a block, it will be called, giving you access to the Gruff::Line object before it is
+ # written to file (that is, if you also passed a file path). It will also give you access to a Proc
+ # for labeling the X axis.
+ #
+ # Example 1:
+ #
+ # logs = SomeLogParser.parse("/var/log/nginx/my-site.access.log*")
+ # Graphene.percentages(logs, :browser).line_graph(:date, '/path/to/browser-share.png', 'Browser Share')
+ #
+ # Example 2:
+ #
+ # Graphene.counts(logs, :browser).line_graph(:date, '/path/to/browser-share.png') do |chart, labeler|
+ # chart.title = 'Browser Share'
+ # chart.font = '/path/to/font.ttf'
+ # chart.theme = pie.theme_37signals
+ # end
+ #
+ # Example 3:
+ #
+ # Graphene.counts(logs, :browser).line_graph(:date, '/path/to/browser-share.png') do |chart, labeler|
+ # chart.title = 'Browser Share'
+ #
+ # # Both the 10 and the block are optional.
+ # # - "10" means that only every 10'th label will be printed. Otherwise, each would be.
+ # # - The block is passed each label (the return value of "x_method") and may return a formatted version.
+ # labeler.call(10) do |date|
+ # date.strftime('%m/%d/%Y')
+ # end
+ # end
+ #
+ # Example 4:
+ #
+ # Graphene.percentages(logs, :platform, :browser).line_graph(->(l) { l.date.strftime('%m/%Y') }, '/path/to/os-browser-share.png', 'OS / Browser Share by Month')
+ #
+ def line_graph(x_method, path=nil, title=nil)
+ chart = Gruff::Line.new
+ chart.title = title unless title.nil?
+
+ # Create an empty array for each group (e.g. {"Firefox" => [], "Safari" => []})
+ data = resources.map { |r| attributes.map { |attr| attr.respond_to?(:call) ? attr.call(r) : r.send(attr) } }.uniq.inject({}) do |dat, attrs|
+ dat[attrs] = []
+ dat
+ end
+
+ # Group the data on the x axis
+ resources_by_x = resources.group_by(&x_method)
+ for group in resources_by_x.sort_by(&:first).map(&:last)
+ stats = self.class.new(group, *attributes).group_by { |result| result[0..attributes.size-1] }
+ # Record how many from each group (e.g. Firefox, Safari) fall on this point of the x axis (e.g. date)
+ for attrs, dat in data
+ dat << (stats[attrs] ? stats[attrs][0][1] : 0)
+ end
+ end
+
+ # Build the labeling proc
+ label_every_n, labeler = 1, :to_s.to_proc
+ get_labeler = proc do |n=1, &block|
+ label_every_n = n
+ labeler = block if block
+ end
+ yield(chart, get_labeler) if block_given?
+ # Build labels and add them to chart
+ labels = resources_by_x.keys
+ chart.labels = Hash[*labels.select { |x| labels.index(x) % label_every_n == 0 }.map { |x| [*labels.index(x), labeler[x]] }.flatten]
+
+ # Add data to the chart
+ data.each { |attrs, dat| chart.data attrs.join(' / '), dat }
+
+ chart.write(path) unless path.nil?
+ chart
+ end
+
+ private
+
+ # Builds a chart
+ def chart(chart, path=nil, title=nil, &block)
+ chart.title = title unless title.nil?
+ block.call(chart) if block
+
+ each do |result|
+ name = result[0..attributes.size-1].join(' / ')
+ n = result[attributes.size]
+ chart.data name, n
+ end
+
+ chart.write(path) unless path.nil?
+ chart
+ end
+ end
+end
Oops, something went wrong.

0 comments on commit be5cca4

Please sign in to comment.