add sparklines pie charts by the facet counts

git-svn-id: https://svn.apache.org/repos/asf/lucene/solr/trunk@504694 13f79535-47bb-0310-9956-ffa450edef68
This commit is contained in:
Erik Hatcher 2007-02-07 21:03:29 +00:00
parent e3991438b3
commit 667aba96f2
11 changed files with 779 additions and 0 deletions

View File

@ -0,0 +1,30 @@
require File.dirname(__FILE__) + '/../test_helper'
require 'sparklines_controller'
# Re-raise errors caught by the controller.
class SparklinesController; def rescue_action(e) raise e end; end
class SparklinesControllerTest < Test::Unit::TestCase
#fixtures :data
def setup
@controller = SparklinesController.new
@request = ActionController::TestRequest.new
@response = ActionController::TestResponse.new
end
def test_index
get :index, :results => "1,2,3,4,5", :type => 'bar', :line_color => 'black'
assert_response :success
assert_equal 'image/png', @response.headers['Content-Type']
end
# TODO Replace this with your actual tests
def test_show
get :show
assert_response :success
assert_equal 'image/png', @response.headers['Content-Type']
end
end

View File

@ -0,0 +1,20 @@
Copyright (c) 2006 Geoffrey Grosenbach
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:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
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.

View File

@ -0,0 +1,15 @@
== Sparklines Plugin
Make tiny graphs.
Also has a "sparklines" generator for copying a controller and functional test to your app.
./script/generate sparklines
See examples at http://nubyonrails.com/pages/sparklines
== Author
Geoffrey Grosenbach boss@topfunky.com http://nubyonrails.com

View File

@ -0,0 +1,23 @@
require 'rake'
require 'rake/testtask'
require 'rake/rdoctask'
desc 'Default: run unit tests.'
task :default => :test
desc 'Test the plugin.'
Rake::TestTask.new(:test) do |t|
t.libs << 'lib'
t.pattern = 'test/**/test_*.rb'
t.verbose = true
end
desc 'Generate documentation for the plugin.'
Rake::RDocTask.new(:rdoc) do |rdoc|
rdoc.rdoc_dir = 'rdoc'
rdoc.title = 'CalendarHelper'
rdoc.options << '--line-numbers' << '--inline-source'
rdoc.rdoc_files.include('README')
rdoc.rdoc_files.include('lib/**/*.rb')
end

View File

@ -0,0 +1,7 @@
author: topfunky
summary: A library for making small graphs, a la Edward Tufte. A generator and helper are also included.
homepage: http://nubyonrails.com/pages/sparklines
plugin: http://topfunky.net/svn/plugins/sparklines
license: MIT
version: 0.4.0
rails_version: 1.1+

View File

@ -0,0 +1,13 @@
class SparklinesGenerator < Rails::Generator::Base
def manifest
record do |m|
m.directory File.join("app/controllers")
m.directory File.join("test/functional")
m.file "controller.rb", File.join("app/controllers/sparklines_controller.rb")
m.file "functional_test.rb", File.join("test/functional/sparklines_controller_test.rb")
end
end
end

View File

@ -0,0 +1,40 @@
class SparklinesController < ApplicationController
# Handles requests for sparkline graphs from views.
#
# Params are generated by the sparkline_tag helper method.
#
def index
# Make array from comma-delimited list of data values
ary = []
if params.has_key?('results') && !params['results'].nil?
params['results'].split(',').each do |s|
ary << s.to_i
end
end
send_data( Sparklines.plot( ary, params ),
:disposition => 'inline',
:type => 'image/png',
:filename => "spark_#{params[:type]}.png" )
end
# Use this type of method for sparklines that can be cached. (Doesn't work with the helper.)
#
# To make caching easier, add a line like this to config/routes.rb:
# map.sparklines "sparklines/:action/:id/image.png", :controller => "sparklines"
#
# Then reference it with the named route:
# image_tag sparklines_url(:action => 'show', :id => 42)
def show
send_data(Sparklines.plot(
[42, 37, 89, 74, 70, 50, 40, 30, 40, 50],
:type => 'bar', :above_color => 'orange'
),
:disposition => 'inline',
:type => 'image/png',
:filename => "sparkline.png")
end
end

View File

@ -0,0 +1,30 @@
require File.dirname(__FILE__) + '/../test_helper'
require 'sparklines_controller'
# Re-raise errors caught by the controller.
class SparklinesController; def rescue_action(e) raise e end; end
class SparklinesControllerTest < Test::Unit::TestCase
#fixtures :data
def setup
@controller = SparklinesController.new
@request = ActionController::TestRequest.new
@response = ActionController::TestResponse.new
end
def test_index
get :index, :results => "1,2,3,4,5", :type => 'bar', :line_color => 'black'
assert_response :success
assert_equal 'image/png', @response.headers['Content-Type']
end
# TODO Replace this with your actual tests
def test_show
get :show
assert_response :success
assert_equal 'image/png', @response.headers['Content-Type']
end
end

View File

@ -0,0 +1 @@
ActionView::Base.send :include, SparklinesHelper

View File

@ -0,0 +1,582 @@
require 'rubygems'
require 'RMagick'
=begin rdoc
A library for generating small unmarked graphs (sparklines).
Can be used to write an image to a file or make a web service with Rails or other Ruby CGI apps.
Idea and much of the outline for the source lifted directly from {Joe Gregorio's Python Sparklines web service script}[http://bitworking.org/projects/sparklines].
Requires the RMagick image library.
==Authors
{Dan Nugent}[mailto:nugend@gmail.com] Original port from Python Sparklines library.
{Geoffrey Grosenbach}[mailto:boss@topfunky.com] -- http://nubyonrails.topfunky.com
-- Conversion to module and further maintenance.
==General Usage and Defaults
To use in a script:
require 'rubygems'
require 'sparklines'
Sparklines.plot([1,25,33,46,89,90,85,77,42],
:type => 'discrete',
:height => 20)
An image blob will be returned which you can print, write to STDOUT, etc.
For use with Ruby on Rails, see the sparklines plugin:
http://nubyonrails.com/pages/sparklines
In your view, call it like this:
<%= sparkline_tag [1,2,3,4,5,6] %>
Or specify details:
<%= sparkline_tag [1,2,3,4,5,6],
:type => 'discrete',
:height => 10,
:upper => 80,
:above_color => 'green',
:below_color => 'blue' %>
Graph types:
area
discrete
pie
smooth
bar
whisker
General Defaults:
:type => 'smooth'
:height => 14px
:upper => 50
:above_color => 'red'
:below_color => 'grey'
:background_color => 'white'
:line_color => 'lightgrey'
==License
Licensed under the MIT license.
=end
class Sparklines
VERSION = '0.4.1'
@@label_margin = 5.0
@@pointsize = 10.0
class << self
# Does the actual plotting of the graph.
# Calls the appropriate subclass based on the :type argument.
# Defaults to 'smooth'
def plot(data=[], options={})
defaults = {
:type => 'smooth',
:height => 14,
:upper => 50,
:diameter => 20,
:step => 2,
:line_color => 'lightgrey',
:above_color => 'red',
:below_color => 'grey',
:background_color => 'white',
:share_color => 'red',
:remain_color => 'lightgrey',
:min_color => 'blue',
:max_color => 'green',
:last_color => 'red',
:has_min => false,
:has_max => false,
:has_last => false,
:label => nil
}
# HACK for HashWithIndifferentAccess
options_sym = Hash.new
options.keys.each do |key|
options_sym[key.to_sym] = options[key]
end
options_sym = defaults.merge(options_sym)
# Call the appropriate method for actual plotting.
sparkline = self.new(data, options_sym)
if %w(area bar pie smooth discrete whisker).include? options_sym[:type]
sparkline.send options_sym[:type]
else
sparkline.plot_error options_sym
end
end
# Writes a graph to disk with the specified filename, or "sparklines.png"
def plot_to_file(filename="sparklines.png", data=[], options={})
File.open( filename, 'wb' ) do |png|
png << self.plot( data, options)
end
end
end # class methods
def initialize(data=[], options={})
@data = Array(data)
@options = options
normalize_data
end
##
# Creates a continuous area sparkline. Relevant options.
#
# :step - An integer that determines the distance between each point on the sparkline. Defaults to 2.
#
# :height - An integer that determines what the height of the sparkline will be. Defaults to 14
#
# :upper - An integer that determines the threshold for colorization purposes. Any value less than upper will be colored using below_color, anything above and equal to upper will use above_color. Defaults to 50.
#
# :has_min - Determines whether a dot will be drawn at the lowest value or not. Defaults to false.
#
# :has_max - Determines whether a dot will be drawn at the highest value or not. Defaults to false.
#
# :has_last - Determines whether a dot will be drawn at the last value or not. Defaults to false.
#
# :min_color - A string or color code representing the color that the dot drawn at the smallest value will be displayed as. Defaults to blue.
#
# :max_color - A string or color code representing the color that the dot drawn at the largest value will be displayed as. Defaults to green.
#
# :last_color - A string or color code representing the color that the dot drawn at the last value will be displayed as. Defaults to red.
#
# :above_color - A string or color code representing the color to draw values above or equal the upper value. Defaults to red.
#
# :below_color - A string or color code representing the color to draw values below the upper value. Defaults to gray.
def area
step = @options[:step].to_i
height = @options[:height].to_i
background_color = @options[:background_color]
create_canvas((@norm_data.size - 1) * step + 4, height, background_color)
upper = @options[:upper].to_i
has_min = @options[:has_min]
has_max = @options[:has_max]
has_last = @options[:has_last]
min_color = @options[:min_color]
max_color = @options[:max_color]
last_color = @options[:last_color]
below_color = @options[:below_color]
above_color = @options[:above_color]
coords = [[0,(height - 3 - upper/(101.0/(height-4)))]]
i=0
@norm_data.each do |r|
coords.push [(2 + i), (height - 3 - r/(101.0/(height-4)))]
i += step
end
coords.push [(@norm_data.size - 1) * step + 4, (height - 3 - upper/(101.0/(height-4)))]
# TODO Refactor! Should take a block and do both.
#
# Block off the bottom half of the image and draw the sparkline
@draw.fill(above_color)
@draw.define_clip_path('top') do
@draw.rectangle(0,0,(@norm_data.size - 1) * step + 4,(height - 3 - upper/(101.0/(height-4))))
end
@draw.clip_path('top')
@draw.polygon *coords.flatten
# Block off the top half of the image and draw the sparkline
@draw.fill(below_color)
@draw.define_clip_path('bottom') do
@draw.rectangle(0,(height - 3 - upper/(101.0/(height-4))),(@norm_data.size - 1) * step + 4,height)
end
@draw.clip_path('bottom')
@draw.polygon *coords.flatten
# The sparkline looks kinda nasty if either the above_color or below_color gets the center line
@draw.fill('black')
@draw.line(0,(height - 3 - upper/(101.0/(height-4))),(@norm_data.size - 1) * step + 4,(height - 3 - upper/(101.0/(height-4))))
# After the parts have been masked, we need to let the whole canvas be drawable again
# so a max dot can be displayed
@draw.define_clip_path('all') do
@draw.rectangle(0,0,@canvas.columns,@canvas.rows)
end
@draw.clip_path('all')
drawbox(coords[@norm_data.index(@norm_data.min)+1], 1, min_color) if has_min == true
drawbox(coords[@norm_data.index(@norm_data.max)+1], 1, max_color) if has_max == true
drawbox(coords[-2], 1, last_color) if has_last == true
@draw.draw(@canvas)
@canvas.to_blob
end
##
# A bar graph.
def bar
step = @options[:step].to_i
height = @options[:height].to_f
background_color = @options[:background_color]
create_canvas(@norm_data.length * step + 2, height, background_color)
upper = @options[:upper].to_i
below_color = @options[:below_color]
above_color = @options[:above_color]
i = 1
@norm_data.each_with_index do |r, index|
color = (r >= upper) ? above_color : below_color
@draw.stroke('transparent')
@draw.fill(color)
@draw.rectangle( i, @canvas.rows,
i + step - 2, @canvas.rows - ( (r / @maximum_value) * @canvas.rows) )
i += step
end
@draw.draw(@canvas)
@canvas.to_blob
end
##
# Creates a discretized sparkline
#
# :height - An integer that determines what the height of the sparkline will be. Defaults to 14
#
# :upper - An integer that determines the threshold for colorization purposes. Any value less than upper will be colored using below_color, anything above and equal to upper will use above_color. Defaults to 50.
#
# :above_color - A string or color code representing the color to draw values above or equal the upper value. Defaults to red.
#
# :below_color - A string or color code representing the color to draw values below the upper value. Defaults to gray.
def discrete
height = @options[:height].to_i
upper = @options[:upper].to_i
background_color = @options[:background_color]
step = @options[:step].to_i
create_canvas(@norm_data.size * step - 1, height, background_color)
below_color = @options[:below_color]
above_color = @options[:above_color]
i = 0
@norm_data.each do |r|
color = (r >= upper) ? above_color : below_color
@draw.stroke(color)
@draw.line(i, (@canvas.rows - r/(101.0/(height-4))-4).to_i,
i, (@canvas.rows - r/(101.0/(height-4))).to_i)
i += step
end
@draw.draw(@canvas)
@canvas.to_blob
end
##
# Creates a pie-chart sparkline
#
# :diameter - An integer that determines what the size of the sparkline will be. Defaults to 20
#
# :share_color - A string or color code representing the color to draw the share of the pie represented by percent. Defaults to red.
#
# :remain_color - A string or color code representing the color to draw the pie not taken by the share color. Defaults to lightgrey.
def pie
diameter = @options[:diameter].to_i
background_color = @options[:background_color]
create_canvas(diameter, diameter, background_color)
share_color = @options[:share_color]
remain_color = @options[:remain_color]
percent = @norm_data[0]
# Adjust the radius so there's some edge left in the pie
r = diameter/2.0 - 2
@draw.fill(remain_color)
@draw.ellipse(r + 2, r + 2, r , r , 0, 360)
@draw.fill(share_color)
# Special exceptions
if percent == 0
# For 0% return blank
@draw.draw(@canvas)
return @canvas.to_blob
elsif percent == 100
# For 100% just draw a full circle
@draw.ellipse(r + 2, r + 2, r , r , 0, 360)
@draw.draw(@canvas)
return @canvas.to_blob
end
# Okay, this part is as confusing as hell, so pay attention:
# This line determines the horizontal portion of the point on the circle where the X-Axis
# should end. It's caculated by taking the center of the on-image circle and adding that
# to the radius multiplied by the formula for determinig the point on a unit circle that a
# angle corresponds to. 3.6 * percent gives us that angle, but it's in degrees, so we need to
# convert, hence the muliplication by Pi over 180
arc_end_x = r + 2 + (r * Math.cos((3.6 * percent)*(Math::PI/180)))
# The same goes for here, except it's the vertical point instead of the horizontal one
arc_end_y = r + 2 + (r * Math.sin((3.6 * percent)*(Math::PI/180)))
# Because the SVG path format is seriously screwy, we need to set the large-arc-flag to 1
# if the angle of an arc is greater than 180 degrees. I have no idea why this is, but it is.
percent > 50? large_arc_flag = 1: large_arc_flag = 0
# This is also confusing
# M tells us to move to an absolute point on the image. We're moving to the center of the pie
# h tells us to move to a relative point. We're moving to the right edge of the circle.
# A tells us to start an absolute elliptical arc. The first two values are the radii of the ellipse
# the third value is the x-axis-rotation (how to rotate the ellipse if we wanted to [could have some fun
# with randomizing that maybe), the fourth value is our large-arc-flag, the fifth is the sweep-flag,
# (again, confusing), the sixth and seventh values are the end point of the arc which we calculated previously
# More info on the SVG path string format at: http://www.w3.org/TR/SVG/paths.html
path = "M#{r + 2},#{r + 2} h#{r} A#{r},#{r} 0 #{large_arc_flag},1 #{arc_end_x},#{arc_end_y} z"
@draw.path(path)
@draw.draw(@canvas)
@canvas.to_blob
end
##
# Creates a smooth sparkline.
#
# :step - An integer that determines the distance between each point on the sparkline. Defaults to 2.
#
# :height - An integer that determines what the height of the sparkline will be. Defaults to 14
#
# :has_min - Determines whether a dot will be drawn at the lowest value or not. Defaults to false.
#
# :has_max - Determines whether a dot will be drawn at the highest value or not. Defaults to false.
#
# :has_last - Determines whether a dot will be drawn at the last value or not. Defaults to false.
#
# :min_color - A string or color code representing the color that the dot drawn at the smallest value will be displayed as. Defaults to blue.
#
# :max_color - A string or color code representing the color that the dot drawn at the largest value will be displayed as. Defaults to green.
#
# :last_color - A string or color code representing the color that the dot drawn at the last value will be displayed as. Defaults to red.
def smooth
step = @options[:step].to_i
height = @options[:height].to_i
background_color = @options[:background_color]
create_canvas((@norm_data.size - 1) * step + 4, height, background_color)
min_color = @options[:min_color]
max_color = @options[:max_color]
last_color = @options[:last_color]
has_min = @options[:has_min]
has_max = @options[:has_max]
has_last = @options[:has_last]
line_color = @options[:line_color]
@draw.stroke(line_color)
coords = []
i=0
@norm_data.each do |r|
coords.push [ 2 + i, (height - 3 - r/(101.0/(height-4))) ]
i += step
end
open_ended_polyline(coords)
drawbox(coords[@norm_data.index(@norm_data.min)], 2, min_color) if has_min == true
drawbox(coords[@norm_data.index(@norm_data.max)], 2, max_color) if has_max == true
drawbox(coords[-1], 2, last_color) if has_last == true
@draw.draw(@canvas)
@canvas.to_blob
end
##
# Creates a whisker sparkline to track on/off type data. There are five states:
# on, off, no value, exceptional on, exceptional off. On values create an up
# whisker and off values create a down whisker. Exceptional values may be
# colored differently than regular values to indicate, for example, a shut out.
# No value produces an empty row to indicate a tie.
#
# * results - an array of integer values between -2 and 2. -2 is exceptional
# down, 1 is regular down, 0 is no value, 1 is up, and 2 is exceptional up.
# * options - a hash that takes parameters
#
# :height - height of the sparkline
#
# :whisker_color - the color of regular whiskers; defaults to black
#
# :exception_color - the color of exceptional whiskers; defaults to red
def whisker
# step = @options[:step].to_i
height = @options[:height].to_i
background_color = @options[:background_color]
create_canvas((@data.size - 1) * 2, height, background_color)
whisker_color = @options[:whisker_color] || 'black'
exception_color = @options[:exception_color] || 'red'
i = 0
@data.each do |r|
color = whisker_color
if ( (r == 2 || r == -2) && exception_color )
color = exception_color
end
y_mid_point = (r >= 1) ? (@canvas.rows/2.0 - 1).ceil : (@canvas.rows/2.0).floor
y_end_point = y_mid_point
if ( r > 0)
y_end_point = 0
end
if ( r < 0 )
y_end_point = @canvas.rows
end
@draw.stroke( color )
@draw.line( i, y_mid_point, i, y_end_point )
i += 2
end
@draw.draw(@canvas)
@canvas.to_blob
end
##
# Draw the error Sparkline.
def plot_error(options={})
create_canvas(40, 15, 'white')
@draw.fill('red')
@draw.line(0,0,40,15)
@draw.line(0,15,40,0)
@draw.draw(@canvas)
@canvas.to_blob
end
private
def normalize_data
@maximum_value = @data.max
if @options[:type].to_s == 'pie'
@norm_data = @data
else
@norm_data = @data.map { |value| value = (value.to_f / @maximum_value) * 100.0 }
end
end
##
# * :arr - an array of points (represented as two element arrays)
def open_ended_polyline(arr)
0.upto(arr.length - 2) { |i|
@draw.line(arr[i][0], arr[i][1], arr[i+1][0], arr[i+1][1])
}
end
##
# Create an image to draw on and a drawable to do the drawing with.
#
# TODO Refactor into smaller methods
def create_canvas(w, h, bkg_col)
@draw = Magick::Draw.new
@draw.pointsize = @@pointsize # TODO Use height
@canvas = Magick::Image.new(w , h) { self.background_color = bkg_col }
# Make room for label and last value
unless @options[:label].nil?
@options[:has_last] = true
@label_width = calculate_width(@options[:label])
@data_last_width = calculate_width(@data.last)
# HACK The 7.0 is a severe hack. Must figure out correct spacing
@label_and_data_last_width = @label_width + @data_last_width + @@label_margin * 7.0
w += @label_and_data_last_width
end
@canvas = Magick::Image.new(w , h) { self.background_color = bkg_col }
@canvas.format = "PNG"
# Draw label and last value
unless @options[:label].nil?
if ENV.has_key?('MAGICK_FONT_PATH')
vera_font_path = File.expand_path('Vera.ttf', ENV['MAGICK_FONT_PATH'])
@font = File.exists?(vera_font_path) ? vera_font_path : nil
else
@font = nil
end
@draw.fill = 'black'
@draw.font = @font if @font
@draw.gravity = Magick::WestGravity
@draw.annotate( @canvas,
@label_width, 1.0,
w - @label_and_data_last_width + @@label_margin, h - calculate_caps_height/2.0,
@options[:label])
@draw.fill = 'red'
@draw.annotate( @canvas,
@data_last_width, 1.0,
w - @data_last_width - @@label_margin * 2.0, h - calculate_caps_height/2.0,
@data.last.to_s)
end
end
##
# Utility to draw a coloured box
# Centred on pt, offset off in each direction, fill color is col
def drawbox(pt, offset, color)
@draw.stroke 'transparent'
@draw.fill(color)
@draw.rectangle(pt[0]-offset, pt[1]-offset, pt[0]+offset, pt[1]+offset)
end
def calculate_width(text)
@draw.get_type_metrics(@canvas, text.to_s).width
end
def calculate_caps_height
@draw.get_type_metrics(@canvas, 'X').height
end
end

View File

@ -0,0 +1,18 @@
# Provides a tag for embedding sparklines graphs into your Rails app.
#
module SparklinesHelper
# Call with an array of data and a hash of params for the Sparklines module.
#
# sparkline_tag [42, 37, 43, 182], :type => 'bar', :line_color => 'black'
#
# You can also pass :class => 'some_css_class' ('sparkline' by default).
def sparkline_tag(results=[], options={})
url = { :controller => 'sparklines',
:results => results.join(',') }
options = url.merge(options)
%(<img src="#{ url_for options }" class="#{options[:class] || 'sparkline'}" alt="Sparkline Graph" />)
end
end