scruffy 0.0.9
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/CHANGES +11 -0
- data/MIT-LICENSE +20 -0
- data/README +9 -0
- data/Rakefile +103 -0
- data/lib/scruffy.rb +23 -0
- data/lib/scruffy/graph.rb +182 -0
- data/lib/scruffy/graph_layers/all_smiles.rb +107 -0
- data/lib/scruffy/graph_layers/area.rb +32 -0
- data/lib/scruffy/graph_layers/average.rb +52 -0
- data/lib/scruffy/graph_layers/bar.rb +28 -0
- data/lib/scruffy/graph_layers/base.rb +120 -0
- data/lib/scruffy/graph_layers/blue_screen.rb +7 -0
- data/lib/scruffy/graph_layers/line.rb +11 -0
- data/lib/scruffy/renderers/standard.rb +132 -0
- data/lib/scruffy/resolver.rb +14 -0
- data/lib/scruffy/themes.rb +26 -0
- data/lib/scruffy/transformer.rb +73 -0
- data/lib/scruffy/version.rb +3 -0
- data/spec/graph_spec.rb +56 -0
- metadata +64 -0
data/CHANGES
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
= Scruffy Changelog
|
2
|
+
|
3
|
+
== Version 0.0.9
|
4
|
+
(August 10th, 2006)
|
5
|
+
|
6
|
+
* Initial release.
|
7
|
+
* Standard renderer.
|
8
|
+
* Marker transformers: currency, percentages.
|
9
|
+
* Basic Graphs: Area, Bar, Line.
|
10
|
+
* Advanced Graphs: Average, AllSmiles.
|
11
|
+
* Initial documentation.
|
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2006 Brasten Sager (brasten@nagilum.com)
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README
ADDED
@@ -0,0 +1,9 @@
|
|
1
|
+
==Scruffy Graphs
|
2
|
+
|
3
|
+
Author:: Brasten Sager (brasten@nagilum.com)
|
4
|
+
Date:: August 5th, 2006
|
5
|
+
|
6
|
+
Scruffy is a Ruby library for generating high quality, good looking graphs. It is designed
|
7
|
+
to be easy to use and highly customizable.
|
8
|
+
|
9
|
+
For basic usage instructions, refer to the documentation for Scruffy::Graph.
|
data/Rakefile
ADDED
@@ -0,0 +1,103 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rake'
|
3
|
+
require 'rake/testtask'
|
4
|
+
require 'rake/rdoctask'
|
5
|
+
require 'rake/packagetask'
|
6
|
+
require 'rake/gempackagetask'
|
7
|
+
require 'rake/contrib/rubyforgepublisher'
|
8
|
+
require 'lib/scruffy'
|
9
|
+
require 'spec'
|
10
|
+
require 'diff/lcs'
|
11
|
+
require 'spec/rake/spectask'
|
12
|
+
|
13
|
+
PKG_NAME = "scruffy"
|
14
|
+
PKG_VERSION = Scruffy::VERSION
|
15
|
+
PKG_FILE_NAME = "#{PKG_NAME}-#{PKG_VERSION}"
|
16
|
+
PKG_FILES = FileList[
|
17
|
+
'[A-Z]*',
|
18
|
+
'lib/**/*.rb',
|
19
|
+
'spec/**/*.rb'
|
20
|
+
]
|
21
|
+
|
22
|
+
desc 'Runs all RSpec specs - translated with test2spec from our own tests'
|
23
|
+
Spec::Rake::SpecTask.new do |t|
|
24
|
+
t.spec_files = FileList['spec/**/*_spec.rb']
|
25
|
+
end
|
26
|
+
|
27
|
+
desc "Deploy docs to RubyForge"
|
28
|
+
task :rdoc_deploy => [:rdoc] do
|
29
|
+
dirs = %w{doc}
|
30
|
+
onserver = "brasten@rubyforge.org:/var/www/gforge-projects/scruffy"
|
31
|
+
dirs.each do | dir|
|
32
|
+
`scp -r "#{`pwd`.chomp}/#{dir}" "#{onserver}"`
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
# Genereate the RDoc documentation
|
37
|
+
Rake::RDocTask.new { |rdoc|
|
38
|
+
rdoc.rdoc_dir = 'doc'
|
39
|
+
rdoc.title = "Scruffy - Graphing Library for Ruby"
|
40
|
+
# rdoc.options << '--line-numbers --inline-source --main README --accessor adv_attr_accessor=M'
|
41
|
+
# rdoc.template = "#{ENV['template']}.rb" if ENV['template']
|
42
|
+
rdoc.rdoc_files.include('README', 'CHANGELOG')
|
43
|
+
rdoc.rdoc_files.include('lib/scruffy.rb')
|
44
|
+
rdoc.rdoc_files.include('lib/scruffy/*.rb')
|
45
|
+
rdoc.rdoc_files.include('lib/scruffy/graph_layers/*.rb')
|
46
|
+
rdoc.rdoc_files.include('lib/scruffy/renderers/*.rb')
|
47
|
+
}
|
48
|
+
|
49
|
+
spec = Gem::Specification.new do |s|
|
50
|
+
s.name = PKG_NAME
|
51
|
+
s.version = PKG_VERSION
|
52
|
+
s.author = "Brasten Sager"
|
53
|
+
s.email = "brasten@nagilum.com"
|
54
|
+
s.homepage = "http://scruffy.rubyforge.org"
|
55
|
+
s.summary = "A powerful, clean graphing library for Ruby."
|
56
|
+
s.description = <<-EOF
|
57
|
+
Scruffy is a Ruby library for generating powerful graphs. It is based on
|
58
|
+
SVG, allowing for powerful, clean code, as well as a good foundation for
|
59
|
+
future features.
|
60
|
+
EOF
|
61
|
+
|
62
|
+
s.files = PKG_FILES.to_a
|
63
|
+
s.require_path = 'lib'
|
64
|
+
|
65
|
+
s.has_rdoc = true
|
66
|
+
|
67
|
+
#s.test_files = Dir.glob('test/*_test.rb')
|
68
|
+
s.require_path = 'lib'
|
69
|
+
s.autorequire = 'scruffy'
|
70
|
+
s.rubyforge_project = "scruffy"
|
71
|
+
end
|
72
|
+
|
73
|
+
Rake::GemPackageTask.new(spec) do |pkg|
|
74
|
+
pkg.need_zip = true
|
75
|
+
pkg.need_tar = true
|
76
|
+
end
|
77
|
+
|
78
|
+
task :verify_user do
|
79
|
+
raise "RUBYFORGE_USER environment variable not set!" unless ENV['RUBYFORGE_USER']
|
80
|
+
end
|
81
|
+
|
82
|
+
task :verify_password do
|
83
|
+
raise "RUBYFORGE_PASSWORD environment variable not set!" unless ENV['RUBYFORGE_PASSWORD']
|
84
|
+
end
|
85
|
+
|
86
|
+
desc "Publish gem+tgz+zip on RubyForge. You must make sure lib/version.rb is aligned with the CHANGELOG file"
|
87
|
+
task :publish_packages => [:verify_user, :verify_password, :package] do
|
88
|
+
require 'meta_project'
|
89
|
+
require 'rake/contrib/xforge'
|
90
|
+
release_files = FileList[
|
91
|
+
"pkg/#{PKG_FILE_NAME}.gem",
|
92
|
+
"pkg/#{PKG_FILE_NAME}.tgz",
|
93
|
+
"pkg/#{PKG_FILE_NAME}.zip"
|
94
|
+
]
|
95
|
+
|
96
|
+
Rake::XForge::Release.new(MetaProject::Project::XForge::RubyForge.new(PKG_NAME)) do |xf|
|
97
|
+
# Never hardcode user name and password in the Rakefile!
|
98
|
+
xf.user_name = ENV['RUBYFORGE_USER']
|
99
|
+
xf.password = ENV['RUBYFORGE_PASSWORD']
|
100
|
+
xf.files = release_files.to_a
|
101
|
+
xf.release_name = "Scruffy #{PKG_VERSION}"
|
102
|
+
end
|
103
|
+
end
|
data/lib/scruffy.rb
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
$:.unshift(File.dirname(__FILE__)) unless
|
2
|
+
$:.include?(File.dirname(__FILE__)) || $:.include?(File.expand_path(File.dirname(__FILE__)))
|
3
|
+
|
4
|
+
require 'rubygems'
|
5
|
+
require_gem 'builder', '>= 2.0'
|
6
|
+
|
7
|
+
# Base files
|
8
|
+
require 'scruffy/graph'
|
9
|
+
require 'scruffy/themes'
|
10
|
+
require 'scruffy/version'
|
11
|
+
require 'scruffy/transformer'
|
12
|
+
|
13
|
+
# Layer files
|
14
|
+
require 'scruffy/graph_layers/base'
|
15
|
+
require 'scruffy/graph_layers/area'
|
16
|
+
require 'scruffy/graph_layers/all_smiles'
|
17
|
+
require 'scruffy/graph_layers/bar'
|
18
|
+
require 'scruffy/graph_layers/blue_screen'
|
19
|
+
require 'scruffy/graph_layers/line'
|
20
|
+
require 'scruffy/graph_layers/average'
|
21
|
+
|
22
|
+
# Renderers
|
23
|
+
require 'scruffy/renderers/standard'
|
@@ -0,0 +1,182 @@
|
|
1
|
+
# ===Scruffy Graphing Library for Ruby
|
2
|
+
#
|
3
|
+
# Author:: Brasten Sager
|
4
|
+
# Date:: August 5th, 2006
|
5
|
+
#
|
6
|
+
# For information on generating graphs using Scruffy, see the
|
7
|
+
# documentation in Scruffy::Graph.
|
8
|
+
#
|
9
|
+
# For information on creating your own graph types, see the
|
10
|
+
# documentation in Scruffy::GraphLayer.
|
11
|
+
module Scruffy
|
12
|
+
|
13
|
+
# ==Scruffy Graphs
|
14
|
+
#
|
15
|
+
# Author:: Brasten Sager
|
16
|
+
# Date:: August 5th, 2006
|
17
|
+
#
|
18
|
+
#
|
19
|
+
# ====Graphs vs. Layers (Graph Types)
|
20
|
+
#
|
21
|
+
# Scruffy::Graph is the primary class you will use to generate your graphs. A Graph does not
|
22
|
+
# define a graph type nor does it directly hold any data. Instead, a Graph object can be thought
|
23
|
+
# of as a canvas on which other graphs are draw. (The actual graphs themselves are subclasses of Scruffy::GraphLayer)
|
24
|
+
# Despite the technical distinction, we will refer to Scruffy::Graph objects as 'graphs' and Scruffy::GraphLayers as
|
25
|
+
# 'layers' or 'graph types.'
|
26
|
+
#
|
27
|
+
#
|
28
|
+
# ==== Creating a Graph
|
29
|
+
#
|
30
|
+
# You can begin building a graph by instantiating a Graph object and optionally passing a hash
|
31
|
+
# of properties.
|
32
|
+
#
|
33
|
+
# graph = Scruffy::Graph.new
|
34
|
+
#
|
35
|
+
# OR
|
36
|
+
#
|
37
|
+
# graph = Scruffy::Graph.new(:title => "Monthly Profits", :theme => Scruffy::Theme::RUBY_BLOG)
|
38
|
+
#
|
39
|
+
# Once you have a Graph object, you can set any Graph-level properties (title, theme, etc), or begin adding
|
40
|
+
# graph layers. You can add a graph layer to a graph by using the Graph#add or Graph#<< methods. The two
|
41
|
+
# methods are identical and used to accommodate syntax preferences.
|
42
|
+
#
|
43
|
+
# graph.add(:line, 'John', [100, -20, 30, 60])
|
44
|
+
# graph.add(:line, 'Sara', [120, 50, -80, 20])
|
45
|
+
#
|
46
|
+
# OR
|
47
|
+
#
|
48
|
+
# graph << Scruffy::LineLayer.new(:title => 'John', :points => [100, -20, 30, 60])
|
49
|
+
# graph << Scruffy::LineLayer.new(:title => 'Sara', :points => [120, 50, -80, 20])
|
50
|
+
#
|
51
|
+
# Now that we've created our graph and added a layer to it, we're ready to render! You can render the graph
|
52
|
+
# directly to SVG with the Graph#render method:
|
53
|
+
#
|
54
|
+
# graph.render # Renders a 600x400 SVG graph
|
55
|
+
#
|
56
|
+
# OR
|
57
|
+
#
|
58
|
+
# graph.render(:size => [1500, 1000])
|
59
|
+
#
|
60
|
+
# And that's your basic Scruffy graph! Please check the documentation for the various methods and
|
61
|
+
# classes you'll be using, as there are a bunch of options not demonstrated here.
|
62
|
+
#
|
63
|
+
# A couple final things worth noting:
|
64
|
+
# * You can call Graph#render as often as you wish with different rendering options. In
|
65
|
+
# fact, you can modify the graph any way you wish between renders.
|
66
|
+
#
|
67
|
+
#
|
68
|
+
# * There are no restrictions to the combination of graph layers you can add. It is perfectly
|
69
|
+
# valid to do something like:
|
70
|
+
# graph.add(:line, [100, 200, 300])
|
71
|
+
# graph.add(:bar, [200, 150, 150])
|
72
|
+
#
|
73
|
+
# Of course, while you may be able to combine some things such as pie charts and line graphs, that
|
74
|
+
# doesn't necessarily mean they will make any logical sense together. We leave those decisions up to you. :)
|
75
|
+
class Graph
|
76
|
+
attr_accessor :title
|
77
|
+
attr_accessor :theme
|
78
|
+
attr_accessor :layers
|
79
|
+
attr_accessor :default_type
|
80
|
+
attr_accessor :point_markers
|
81
|
+
attr_accessor :renderer
|
82
|
+
attr_accessor :marker_transformer
|
83
|
+
|
84
|
+
# Returns a new Graph. You can optionally pass in a default graph type and an options hash.
|
85
|
+
#
|
86
|
+
# Graph.new # New graph
|
87
|
+
# Graph.new(:line) # New graph with default graph type of LineLayer
|
88
|
+
# Graph.new({...}) # New graph with options.
|
89
|
+
#
|
90
|
+
# Options:
|
91
|
+
#
|
92
|
+
# title:: Graph's title
|
93
|
+
# theme:: Theme to use when rendering graph
|
94
|
+
# renderer:: Sets the renderer to use when rendering graph
|
95
|
+
# marker_transformer:: Sets a transformer used to modify marker values prior to rendering
|
96
|
+
# point_markers:: Sets the x-axis marker values
|
97
|
+
def initialize(*args)
|
98
|
+
@default_type = args.shift if args.first.is_a?(Symbol)
|
99
|
+
options = args.shift if args.first.is_a?(Hash)
|
100
|
+
options ||= {}
|
101
|
+
|
102
|
+
@title = options[:title]
|
103
|
+
@theme = options[:theme] || Scruffy::Theme::KEYNOTE
|
104
|
+
@layers = []
|
105
|
+
@renderer = options[:renderer] || Scruffy::StandardRenderer.new
|
106
|
+
@marker_transformer = options[:marker_transformer]
|
107
|
+
@point_markers = options[:point_markers]
|
108
|
+
end
|
109
|
+
|
110
|
+
# Renders the graph in it's current state to an SVG object.
|
111
|
+
#
|
112
|
+
# Options:
|
113
|
+
# size:: An array indicating the size you wish to render the graph. ( [x, y] )
|
114
|
+
# width:: The width of the rendered graph. A height is calculated at 60% of the width.
|
115
|
+
# theme:: Theme used to render graph for this render only.
|
116
|
+
# min_value:: Overrides the calculated minimum value used for the graph.
|
117
|
+
# max_value:: Overrides the calculated maximum value used for the graph.
|
118
|
+
#
|
119
|
+
def render(options = {})
|
120
|
+
size = options.delete(:size) || (options[:width] ? [options[:width], (options.delete(:width) * 0.6).to_i] : [600, 400])
|
121
|
+
options[:theme] ||= @theme
|
122
|
+
options[:marker_transformer] ||= @marker_transformer
|
123
|
+
options[:point_markers] ||= @point_markers
|
124
|
+
|
125
|
+
@renderer.render( options.merge({:size => size, :title => title,
|
126
|
+
:layers => layers, :min_value => (options[:min_value] || bottom_value), :max_value => (options[:max_value] || top_value) } ) )
|
127
|
+
end
|
128
|
+
|
129
|
+
# Adds a GraphLayer to the Graph. Accepts either a list of arguments used to build a new layer, or
|
130
|
+
# a GraphLayer object. When passing a list of arguments, all arguments are optional, but the arguments
|
131
|
+
# specified must be provided in a particular order: type (Symbol), title (String), points (Array),
|
132
|
+
# options (Hash).
|
133
|
+
#
|
134
|
+
# Both #add and #<< can be used.
|
135
|
+
#
|
136
|
+
# graph.add(:line, [100, 200, 150]) # Create and add an untitled line graph
|
137
|
+
#
|
138
|
+
# graph << (:line, "John's Sales", [150, 100]) # Create and add a titled line graph
|
139
|
+
#
|
140
|
+
# graph << BarLayer.new({...}) # Adds BarLayer object to graph
|
141
|
+
#
|
142
|
+
def <<(*args)
|
143
|
+
if args[0].kind_of?(Scruffy::GraphLayer)
|
144
|
+
layers << args[0]
|
145
|
+
else
|
146
|
+
type = args.first.is_a?(Symbol) ? args.shift : default_type
|
147
|
+
title = args.shift if args.first.is_a?(String)
|
148
|
+
points = args.first.is_a?(Array) ? args.shift : []
|
149
|
+
options = args.first.is_a?(Hash) ? args.shift : {}
|
150
|
+
|
151
|
+
title ||= ''
|
152
|
+
|
153
|
+
raise ArgumentError,
|
154
|
+
'You must specify a graph type (:area, :bar, :line, etc) if you do not have a default type specified.' if type.nil?
|
155
|
+
|
156
|
+
layer = Kernel::module_eval("Scruffy::#{to_camelcase(type.to_s)}Layer").new(options.merge({:points => points, :title => title}))
|
157
|
+
layers << layer
|
158
|
+
end
|
159
|
+
layer
|
160
|
+
end
|
161
|
+
|
162
|
+
alias :add :<<
|
163
|
+
|
164
|
+
protected
|
165
|
+
def to_camelcase(type) # :nodoc:
|
166
|
+
type.split('_').map { |e| e.capitalize }.join('')
|
167
|
+
end
|
168
|
+
|
169
|
+
def top_value # :nodoc:
|
170
|
+
layers.inject(0) { |max, layer| (max = ((max < layer.highest_point) ? layer.highest_point : max)) unless layer.highest_point.nil?; max }
|
171
|
+
end
|
172
|
+
|
173
|
+
def bottom_value # :nodoc:
|
174
|
+
botval = layers.inject(top_value) { |min, layer| (min = ((min > layer.lowest_point) ? layer.lowest_point : min)) unless layer.lowest_point.nil?; min }
|
175
|
+
above_zero = (botval > 0)
|
176
|
+
|
177
|
+
botval = (botval - ((top_value - botval) * 0.15))
|
178
|
+
botval = 0 if (above_zero && botval < 0)
|
179
|
+
botval
|
180
|
+
end
|
181
|
+
end
|
182
|
+
end
|
@@ -0,0 +1,107 @@
|
|
1
|
+
module Scruffy
|
2
|
+
class AllSmilesLayer < GraphLayer
|
3
|
+
attr_accessor :standalone
|
4
|
+
|
5
|
+
def initialize(options = {})
|
6
|
+
super
|
7
|
+
@standalone = options[:standalone] || false
|
8
|
+
end
|
9
|
+
|
10
|
+
def draw(svg, coords, options={})
|
11
|
+
|
12
|
+
hero_smiley = nil
|
13
|
+
coords.each { |c| hero_smiley = c.last if (hero_smiley.nil? || c.last < hero_smiley) }
|
14
|
+
|
15
|
+
svg.defs {
|
16
|
+
svg.radialGradient(:id => 'SmileyGradient', :cx => '50%',
|
17
|
+
:cy => '50%', :r => '50%', :fx => '30%', :fy => '30%') {
|
18
|
+
|
19
|
+
svg.stop(:offset => '0%', 'stop-color' => '#FFF')
|
20
|
+
svg.stop(:offset => '20%', 'stop-color' => '#FFC')
|
21
|
+
svg.stop(:offset => '45%', 'stop-color' => '#FF3')
|
22
|
+
svg.stop(:offset => '60%', 'stop-color' => '#FF0')
|
23
|
+
svg.stop(:offset => '90%', 'stop-color' => '#990')
|
24
|
+
svg.stop(:offset => '100%', 'stop-color' => '#220')
|
25
|
+
}
|
26
|
+
svg.radialGradient(:id => 'HeroGradient', :cx => '50%',
|
27
|
+
:cy => '50%', :r => '50%', :fx => '30%', :fy => '30%') {
|
28
|
+
|
29
|
+
svg.stop(:offset => '0%', 'stop-color' => '#FEE')
|
30
|
+
svg.stop(:offset => '20%', 'stop-color' => '#F0E0C0')
|
31
|
+
svg.stop(:offset => '45%', 'stop-color' => '#8A2A1A')
|
32
|
+
svg.stop(:offset => '60%', 'stop-color' => '#821')
|
33
|
+
svg.stop(:offset => '90%', 'stop-color' => '#210')
|
34
|
+
}
|
35
|
+
svg.radialGradient(:id => 'StarGradient', :cx => '50%',
|
36
|
+
:cy => '50%', :r => '50%', :fx => '30%', :fy => '30%') {
|
37
|
+
|
38
|
+
svg.stop(:offset => '0%', 'stop-color' => '#FFF')
|
39
|
+
svg.stop(:offset => '20%', 'stop-color' => '#EFEFEF')
|
40
|
+
svg.stop(:offset => '45%', 'stop-color' => '#DDD')
|
41
|
+
svg.stop(:offset => '60%', 'stop-color' => '#BBB')
|
42
|
+
svg.stop(:offset => '90%', 'stop-color' => '#888')
|
43
|
+
}
|
44
|
+
}
|
45
|
+
|
46
|
+
unless standalone
|
47
|
+
svg.polyline( :points => stringify_coords(coords).join(' '), :fill => 'none',
|
48
|
+
:stroke => '#660', 'stroke-width' => scaled(10), 'stroke-dasharray' => "#{scaled(10)}, #{scaled(10)}" )
|
49
|
+
end
|
50
|
+
|
51
|
+
coords.each do |coord|
|
52
|
+
if standalone
|
53
|
+
svg.line( :x1 => coord.first, :y1 => coord.last, :x2 => coord.first, :y2 => height, :fill => 'none',
|
54
|
+
:stroke => '#660', 'stroke-width' => scaled(10), 'stroke-dasharray' => "#{scaled(10)}, #{scaled(10)}" )
|
55
|
+
end
|
56
|
+
svg.circle( :cx => coord.first + scaled(2), :cy => coord.last + scaled(2), :r => scaled(15),
|
57
|
+
:fill => 'black', :stroke => 'none', :opacity => 0.4)
|
58
|
+
svg.circle( :cx => coord.first, :cy => coord.last, :r => scaled(15),
|
59
|
+
:fill => (complexity == :minimal ? 'yellow' : 'url(#SmileyGradient)'), :stroke => 'black', 'stroke-width' => scaled(1) )
|
60
|
+
svg.line( :x1 => (coord.first - scaled(3)),
|
61
|
+
:x2 => (coord.first - scaled(3)),
|
62
|
+
:y1 => (coord.last),
|
63
|
+
:y2 => (coord.last - scaled(7)), :stroke => 'black', 'stroke-width' => scaled(1.4) )
|
64
|
+
svg.line( :x1 => (coord.first + scaled(3)),
|
65
|
+
:x2 => (coord.first + scaled(3)),
|
66
|
+
:y1 => (coord.last),
|
67
|
+
:y2 => (coord.last - scaled(7)), :stroke => 'black', 'stroke-width' => scaled(1.4) )
|
68
|
+
|
69
|
+
|
70
|
+
percent = 1.0 - (coord.last.to_f / height.to_f)
|
71
|
+
|
72
|
+
corners = scaled(8 - (5 * percent))
|
73
|
+
anchor = scaled((20 * percent) - 5)
|
74
|
+
svg.path( :d => "M#{coord.first - scaled(9)} #{coord.last + corners} Q#{coord.first} #{coord.last + anchor} #{coord.first + scaled(9)} #{coord.last + corners}",
|
75
|
+
:stroke => 'black', 'stroke-width' => scaled(1.4), :fill => 'none' )
|
76
|
+
|
77
|
+
|
78
|
+
# top hat
|
79
|
+
if coord.last == hero_smiley
|
80
|
+
svg.ellipse(:cx => coord.first, :cy => (coord.last - scaled(13)),
|
81
|
+
:rx => scaled(17), :ry => scaled(6.5), :fill => (complexity == :minimal ? 'purple' : 'url(#HeroGradient)'), :stroke => 'black', 'stroke-width' => scaled(1.4) )
|
82
|
+
|
83
|
+
svg.path(:d => "M#{coord.first} #{coord.last - scaled(60)} " +
|
84
|
+
"L#{coord.first + scaled(10)} #{coord.last - scaled(14)} " +
|
85
|
+
"C#{coord.first + scaled(10)},#{coord.last - scaled(9)} #{coord.first - scaled(10)},#{coord.last - scaled(9)} #{coord.first - scaled(10)},#{coord.last - scaled(14)}" +
|
86
|
+
"L#{coord.first} #{coord.last - scaled(60)}",
|
87
|
+
:stroke => 'black', 'stroke-width' => scaled(1.4), :fill => (complexity == :minimal ? 'purple' : 'url(#HeroGradient)'))
|
88
|
+
|
89
|
+
svg.path(:d => "M#{coord.first - scaled(4)} #{coord.last - scaled(23)}" +
|
90
|
+
"l-#{scaled(2.5)} #{scaled(10)} l#{scaled(7.5)} -#{scaled(5)} l-#{scaled(10)} 0 l#{scaled(7.5)} #{scaled(5)} l-#{scaled(2.5)} -#{scaled(10)}", :stroke => 'none', :fill => (complexity == :minimal ? 'white': 'url(#StarGradient)') )
|
91
|
+
svg.path(:d => "M#{coord.first + scaled(2)} #{coord.last - scaled(30)}" +
|
92
|
+
"l-#{scaled(2.5)} #{scaled(10)} l#{scaled(7.5)} -#{scaled(5)} l-#{scaled(10)} 0 l#{scaled(7.5)} #{scaled(5)} l-#{scaled(2.5)} -#{scaled(10)}", :stroke => 'none', :fill => (complexity == :minimal ? 'white': 'url(#StarGradient)') )
|
93
|
+
svg.path(:d => "M#{coord.first - scaled(2)} #{coord.last - scaled(33)}" +
|
94
|
+
"l-#{scaled(1.25)} #{scaled(5)} l#{scaled(3.75)} -#{scaled(2.5)} l-#{scaled(5)} 0 l#{scaled(3.75)} #{scaled(2.5)} l-#{scaled(1.25)} -#{scaled(5)}", :stroke => 'none', :fill => 'white' )
|
95
|
+
svg.path(:d => "M#{coord.first - scaled(2.2)} #{coord.last - scaled(32.7)}" +
|
96
|
+
"l-#{scaled(1.25)} #{scaled(5)} l#{scaled(3.75)} -#{scaled(2.5)} l-#{scaled(5)} 0 l#{scaled(3.75)} #{scaled(2.5)} l-#{scaled(1.25)} -#{scaled(5)}", :stroke => 'none', :fill => (complexity == :minimal ? 'white': 'url(#StarGradient)') )
|
97
|
+
svg.path(:d => "M#{coord.first + scaled(4.5)} #{coord.last - scaled(20)}" +
|
98
|
+
"l-#{scaled(1.25)} #{scaled(5)} l#{scaled(3.75)} -#{scaled(2.5)} l-#{scaled(5)} 0 l#{scaled(3.75)} #{scaled(2.5)} l-#{scaled(1.25)} -#{scaled(5)}", :stroke => 'none', :fill => (complexity == :minimal ? 'white': 'url(#StarGradient)') )
|
99
|
+
svg.path(:d => "M#{coord.first} #{coord.last - scaled(40)}" +
|
100
|
+
"l-#{scaled(1.25)} #{scaled(5)} l#{scaled(3.75)} -#{scaled(2.5)} l-#{scaled(5)} 0 l#{scaled(3.75)} #{scaled(2.5)} l-#{scaled(1.25)} -#{scaled(5)}", :stroke => 'none', :fill => (complexity == :minimal ? 'white': 'url(#StarGradient)') )
|
101
|
+
|
102
|
+
end
|
103
|
+
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module Scruffy
|
2
|
+
class AreaLayer < GraphLayer
|
3
|
+
def draw(svg, coords, options={})
|
4
|
+
points_value = "0,#{height} #{stringify_coords(coords).join(' ')} #{width},#{height}"
|
5
|
+
|
6
|
+
# Experimental, for later user.
|
7
|
+
#
|
8
|
+
# svg.defs {
|
9
|
+
# svg.filter(:id => 'MyFilter', :filterUnits => 'userSpaceOnUse', :x => 0, :y => 0, :width => 200, :height => '120') {
|
10
|
+
# svg.feGaussianBlur(:in => 'SourceAlpha', :stdDeviation => 4, :result => 'blur')
|
11
|
+
# svg.feOffset(:in => 'blur', :dx => 4, :dy => 4, :result => 'offsetBlur')
|
12
|
+
# svg.feSpecularLighting( :in => 'blur', :surfaceScale => 5, :specularConstant => '.75',
|
13
|
+
# :specularExponent => 20, 'lighting-color' => '#bbbbbb',
|
14
|
+
# :result => 'specOut') {
|
15
|
+
# svg.fePointLight(:x => '-5000', :y => '-10000', :z => '20000')
|
16
|
+
# }
|
17
|
+
#
|
18
|
+
# svg.feComposite(:in => 'specOut', :in2 => 'SourceAlpha', :operator => 'in', :result => 'specOut')
|
19
|
+
# svg.feComposite(:in => 'sourceGraphic', :in2 => 'specOut', :operator => 'arithmetic',
|
20
|
+
# :k1 => 0, :k2 => 1, :k3 => 1, :k4 => 0, :result => 'litPaint')
|
21
|
+
#
|
22
|
+
# svg.feMerge {
|
23
|
+
# svg.feMergeNode(:in => 'offsetBlur')
|
24
|
+
# svg.feMergeNode(:in => 'litPaint')
|
25
|
+
# }
|
26
|
+
# }
|
27
|
+
# }
|
28
|
+
|
29
|
+
svg.polygon(:points => points_value, :fill => color.to_s, :stroke => color.to_s, :opacity => options[:opacity])
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
module Scruffy
|
2
|
+
class AverageLayer < GraphLayer
|
3
|
+
def initialize(options = {})
|
4
|
+
super
|
5
|
+
|
6
|
+
@relevant_data = false
|
7
|
+
end
|
8
|
+
def draw(svg, coords, options = {})
|
9
|
+
svg.polyline( :points => coords.join(' '), :fill => 'none', :stroke => 'black',
|
10
|
+
'stroke-width' => scaled(25), :opacity => '0.4')
|
11
|
+
end
|
12
|
+
|
13
|
+
def highest_point
|
14
|
+
nil
|
15
|
+
end
|
16
|
+
|
17
|
+
def lowest_point
|
18
|
+
nil
|
19
|
+
end
|
20
|
+
|
21
|
+
protected
|
22
|
+
def generate_coordinates(options = {})
|
23
|
+
key_layer = points.find { |layer| layer.relevant_data? }
|
24
|
+
|
25
|
+
options[:point_distance] = width / (key_layer.points.size - 1).to_f
|
26
|
+
|
27
|
+
coords = []
|
28
|
+
|
29
|
+
key_layer.points.each_with_index do |layer, idx|
|
30
|
+
sum, objects = points.inject([0, 0]) do |arr, elem|
|
31
|
+
if elem.relevant_data?
|
32
|
+
arr[0] += elem.points[idx]
|
33
|
+
arr[1] += 1
|
34
|
+
end
|
35
|
+
arr
|
36
|
+
end
|
37
|
+
|
38
|
+
average = sum / objects.to_f
|
39
|
+
|
40
|
+
x_coord = options[:point_distance] * idx
|
41
|
+
|
42
|
+
relative_percent = ((average == min_value) ? 0 : ((average - min_value) / (max_value - min_value).to_f))
|
43
|
+
y_coord = (height - (height * relative_percent))
|
44
|
+
|
45
|
+
coords << [x_coord, y_coord].join(',')
|
46
|
+
end
|
47
|
+
|
48
|
+
return coords
|
49
|
+
end
|
50
|
+
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module Scruffy
|
2
|
+
class BarLayer < GraphLayer
|
3
|
+
|
4
|
+
def draw(svg, coords, options = {})
|
5
|
+
coords.each do |coord|
|
6
|
+
x, y, bar_height = (coord.first-(@bar_width * 0.5)), coord.last, (height - coord.last)
|
7
|
+
|
8
|
+
svg.rect( :x => x, :y => y, :width => @bar_width, :height => bar_height,
|
9
|
+
:fill => color.to_s, :stroke => color.to_s, :opacity => opacity )
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
protected
|
14
|
+
def generate_coordinates(options = {})
|
15
|
+
@bar_width = (width / points.size) * 0.9
|
16
|
+
options[:point_distance] = (width - (width / points.size)) / (points.size - 1).to_f
|
17
|
+
|
18
|
+
coords = (0...points.size).map do |idx|
|
19
|
+
x_coord = (options[:point_distance] * idx) + (width / points.size * 0.5)
|
20
|
+
|
21
|
+
relative_percent = ((points[idx] == min_value) ? 0 : ((points[idx] - min_value) / (max_value - min_value).to_f))
|
22
|
+
y_coord = (height - (height * relative_percent))
|
23
|
+
[x_coord, y_coord]
|
24
|
+
end
|
25
|
+
coords
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,120 @@
|
|
1
|
+
module Scruffy
|
2
|
+
# ==GraphLayer
|
3
|
+
#
|
4
|
+
# Author:: Brasten Sager
|
5
|
+
# Date:: August 5th, 2006
|
6
|
+
#
|
7
|
+
# GraphLayer contains the basic functionality needed by the various types of graphs. The GraphLayer
|
8
|
+
# class is responsible holding layer information such as the title and data points.
|
9
|
+
#
|
10
|
+
# When the graph is rendered, the graph renderer calls GraphLayer#render. GraphLayer#render sets up
|
11
|
+
# some standard information, and calculates the x,y coordinates of each data point. The draw() method,
|
12
|
+
# which should have been overridden by the current instance, is then called. The actual rendering of
|
13
|
+
# the graph takes place there.
|
14
|
+
#
|
15
|
+
# ====Create New Graph Types
|
16
|
+
#
|
17
|
+
# Assuming the information generated by GraphLayer is sufficient, you can create a new graph type
|
18
|
+
# simply by overriding the draw() method. See GraphLayer#draw for arguments.
|
19
|
+
#
|
20
|
+
class GraphLayer
|
21
|
+
attr_accessor :title
|
22
|
+
attr_accessor :points
|
23
|
+
attr_accessor :height, :width
|
24
|
+
attr_accessor :min_value, :max_value
|
25
|
+
attr_accessor :preferred_color, :color
|
26
|
+
attr_accessor :opacity
|
27
|
+
attr_accessor :resolver
|
28
|
+
attr_accessor :complexity
|
29
|
+
attr_accessor :relevant_data
|
30
|
+
|
31
|
+
# Returns a new GraphLayer.
|
32
|
+
#
|
33
|
+
# Options:
|
34
|
+
# title::
|
35
|
+
# points:: Array of data points
|
36
|
+
# color:: Color used to render this graph, overrides theme color.
|
37
|
+
# relevant_data:: Rarely used - indicates the data on this graph should not
|
38
|
+
# included in any graph data aggregations, such as averaging data points.
|
39
|
+
def initialize(options = {})
|
40
|
+
@title = options[:title] || ''
|
41
|
+
@points = options[:points] || []
|
42
|
+
@preferred_color = options[:color]
|
43
|
+
@relevant_data = options[:relevant_data] || true
|
44
|
+
end
|
45
|
+
|
46
|
+
# Builds SVG code for this graph using the provided Builder object.
|
47
|
+
# This method actually generates data needed by this graph, then passes the
|
48
|
+
# rendering responsibilities to GraphLayer#draw.
|
49
|
+
#
|
50
|
+
# svg:: a Builder object used to create SVG code.
|
51
|
+
def render(svg, options = {})
|
52
|
+
setup_variables(options)
|
53
|
+
coords = generate_coordinates(options)
|
54
|
+
|
55
|
+
draw(svg, coords, options)
|
56
|
+
end
|
57
|
+
|
58
|
+
# The method called by GraphLayer#draw to render the graph.
|
59
|
+
#
|
60
|
+
# svg:: a Builder object to use for creating SVG code.
|
61
|
+
# coords:: An array of coordinates relating to the graph's data points. ie: [[100, 120], [200, 140], [300, 40]]
|
62
|
+
# options:: Optional arguments.
|
63
|
+
def draw(svg, coords, options={})
|
64
|
+
# This is what the various graphs need to implement.
|
65
|
+
end
|
66
|
+
|
67
|
+
# Returns the value of relevant_data
|
68
|
+
def relevant_data?
|
69
|
+
@relevant_data
|
70
|
+
end
|
71
|
+
|
72
|
+
# The highest data point on this GraphLayer
|
73
|
+
def highest_point
|
74
|
+
points.sort.last
|
75
|
+
end
|
76
|
+
|
77
|
+
# The lowest data point on this GraphLayer
|
78
|
+
def lowest_point
|
79
|
+
points.sort.first
|
80
|
+
end
|
81
|
+
|
82
|
+
protected
|
83
|
+
def setup_variables(options = {}) # :nodoc:
|
84
|
+
@color = (preferred_color || options.delete(:color))
|
85
|
+
@resolver = options.delete(:resolver)
|
86
|
+
@width, @height = options.delete(:size)
|
87
|
+
@min_value, @max_value = options[:min_value], options[:max_value]
|
88
|
+
@opacity = options[:opacity]
|
89
|
+
@complexity = options[:complexity]
|
90
|
+
end
|
91
|
+
|
92
|
+
def generate_coordinates(options = {}) # :nodoc:
|
93
|
+
options[:point_distance] = width / (points.size - 1).to_f
|
94
|
+
|
95
|
+
coords = (0...points.size).map do |idx|
|
96
|
+
x_coord = options[:point_distance] * idx
|
97
|
+
|
98
|
+
relative_percent = ((points[idx] == min_value) ? 0 : ((points[idx] - min_value) / (max_value - min_value).to_f))
|
99
|
+
y_coord = (height - (height * relative_percent))
|
100
|
+
|
101
|
+
[x_coord, y_coord]
|
102
|
+
end
|
103
|
+
|
104
|
+
coords
|
105
|
+
end
|
106
|
+
|
107
|
+
# Returns a 'scaled' value of the given integer.
|
108
|
+
#
|
109
|
+
# Scaled adjusts the given point depending on the height of the graph, where 400px is considered 1:1.
|
110
|
+
#
|
111
|
+
# ie: On a graph that is 800px high, scaled(1) => 2. For 200px high, scaled(1) => 0.5, and so on...
|
112
|
+
def scaled(point)
|
113
|
+
resolver.to_point(point)
|
114
|
+
end
|
115
|
+
|
116
|
+
def stringify_coords(coords) # :nodoc:
|
117
|
+
coords.map { |c| c.join(',') }
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
module Scruffy
|
2
|
+
class LineLayer < GraphLayer
|
3
|
+
def draw(svg, coords, options={})
|
4
|
+
svg.polyline( :points => stringify_coords(coords).join(' '), :fill => 'none',
|
5
|
+
:stroke => color.to_s, 'stroke-width' => scaled(4) )
|
6
|
+
|
7
|
+
coords.each { |coord| svg.circle( :cx => coord.first, :cy => coord.last, :r => scaled(5),
|
8
|
+
:fill => color.to_s ) }
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
@@ -0,0 +1,132 @@
|
|
1
|
+
module Scruffy
|
2
|
+
class StandardRenderer
|
3
|
+
GRAPH_VIEWPORT = { :x => 0.20, :y => 0.30, :width => 0.70, :height => 0.55 }
|
4
|
+
MARKER_ALIGNMENT = { :x => 0.17, :y => 0.31}
|
5
|
+
MARKERS = 5
|
6
|
+
|
7
|
+
def render(options = {})
|
8
|
+
title = options[:title]
|
9
|
+
theme = options[:theme]
|
10
|
+
layers = options[:layers]
|
11
|
+
size = options[:size]
|
12
|
+
graph_id = options[:graph_id] || 'scruffy_graph'
|
13
|
+
|
14
|
+
|
15
|
+
options[:complexity] ||= (global_complexity || :normal)
|
16
|
+
options[:resolver] ||= Scruffy::Resolver.new(options[:size].last)
|
17
|
+
|
18
|
+
svg = Builder::XmlMarkup.new(:indent => 2)
|
19
|
+
svg.instruct!
|
20
|
+
svg.instruct! 'DOCTYPE', 'svg PUBLIC "-//W3C//DTD SVG 1.0//EN" "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd" type'
|
21
|
+
svg.svg(:xmlns => "http://www.w3.org/2000/svg", 'xmlns:xlink' => "http://www.w3.org/1999/xlink", :width => size[0], :height => size[1]) {
|
22
|
+
svg.g(:id => graph_id) {
|
23
|
+
render_background(svg, options)
|
24
|
+
render_markers(svg, options)
|
25
|
+
render_title(svg, options)
|
26
|
+
render_layers(svg, options)
|
27
|
+
|
28
|
+
}
|
29
|
+
}
|
30
|
+
svg.target!
|
31
|
+
end
|
32
|
+
|
33
|
+
protected
|
34
|
+
def render_background(svg, options)
|
35
|
+
fill = "#EEEEEE"
|
36
|
+
case options[:theme][:background]
|
37
|
+
when Symbol, String
|
38
|
+
fill = options[:theme][:background].to_s
|
39
|
+
when Array
|
40
|
+
fill = "url(#BackgroundGradient)"
|
41
|
+
svg.defs {
|
42
|
+
svg.linearGradient(:id=>'BackgroundGradient', :x1 => '0%', :y1 => '0%', :x2 => '0%', :y2 => '100%') {
|
43
|
+
svg.stop(:offset => '5%', 'stop-color' => options[:theme][:background][0])
|
44
|
+
svg.stop(:offset => '95%', 'stop-color' => options[:theme][:background][1])
|
45
|
+
}
|
46
|
+
}
|
47
|
+
when Proc
|
48
|
+
theme[:background].call(svg, options)
|
49
|
+
fill = nil
|
50
|
+
end
|
51
|
+
|
52
|
+
# Render background (maybe)
|
53
|
+
svg.rect(:width => "100%", :height => "100%", :x => "0", :y => "0", :fill => fill) unless fill.nil?
|
54
|
+
end
|
55
|
+
|
56
|
+
def render_layers(svg, options)
|
57
|
+
svg.g(:id => 'graphs', :transform => "translate(#{GRAPH_VIEWPORT[:x] * options[:size][0]},#{GRAPH_VIEWPORT[:y] * options[:size][1]})") {
|
58
|
+
options[:layers].each_with_index do |layer, idx|
|
59
|
+
|
60
|
+
layer_options = {}
|
61
|
+
layer_options[:index] = idx
|
62
|
+
layer_options[:color] = options[:theme][:colors].values[ (idx % options[:theme][:colors].size) ]
|
63
|
+
layer_options[:size] = [ GRAPH_VIEWPORT[:width] * options[:size][0], GRAPH_VIEWPORT[:height] * options[:size][1] ]
|
64
|
+
layer_options[:min_value] = options[:min_value]
|
65
|
+
layer_options[:max_value] = options[:max_value]
|
66
|
+
layer_options[:opacity] = (idx > 0) ? 0.6 : 1.0
|
67
|
+
layer_options[:resolver] = options[:resolver]
|
68
|
+
layer_options[:complexity] = options[:complexity]
|
69
|
+
layer_options[:theme] = options[:theme]
|
70
|
+
|
71
|
+
# puts options[:theme][:colors].values
|
72
|
+
# puts layer_options[:color]
|
73
|
+
# puts "#{idx} : #{options[:layers].size} == #{idx % options[:layers].size}"
|
74
|
+
|
75
|
+
svg.g {
|
76
|
+
layer.render(svg, layer_options)
|
77
|
+
}
|
78
|
+
end
|
79
|
+
}
|
80
|
+
end
|
81
|
+
|
82
|
+
def render_markers(svg, options)
|
83
|
+
svg.g(:id => 'markers', :transform => "translate(#{GRAPH_VIEWPORT[:x] * options[:size][0]},#{GRAPH_VIEWPORT[:y] * options[:size][1]})") {
|
84
|
+
(0...MARKERS).each do |idx|
|
85
|
+
marker = ((1 / (MARKERS - 1).to_f) * idx) * (options[:size].last * GRAPH_VIEWPORT[:height])
|
86
|
+
svg.line(:x1 => 0, :y1 => marker, :x2 => (GRAPH_VIEWPORT[:width] * options[:size].first), :y2 => marker, :style => "stroke: #{options[:theme][:marker].to_s}; stroke-width: #{options[:resolver].to_point(2)};")
|
87
|
+
end
|
88
|
+
}
|
89
|
+
|
90
|
+
svg.g(:id => 'marker-values', :transform => "translate(#{MARKER_ALIGNMENT[:x] * options[:size][0]},#{MARKER_ALIGNMENT[:y] * options[:size][1]})") {
|
91
|
+
(0...MARKERS).each do |idx|
|
92
|
+
marker = ((GRAPH_VIEWPORT[:height]) * options[:size].last) - ((1 / (MARKERS - 1).to_f) * idx) * (options[:size].last * GRAPH_VIEWPORT[:height])
|
93
|
+
marker_value = (options[:max_value] - options[:min_value]) * ((1 / (MARKERS - 1).to_f) * idx) + options[:min_value]
|
94
|
+
marker_value = options[:marker_transformer].route_transform(marker_value, idx, options) if options[:marker_transformer]
|
95
|
+
|
96
|
+
svg.text(marker_value.to_s, :x => 0, :y => marker, :style => "font-size: #{options[:resolver].to_point(18)}px; fill: #{((options.delete(:marker_color_override) || options[:theme][:marker]) || 'white').to_s}; text-anchor: end;")
|
97
|
+
end
|
98
|
+
}
|
99
|
+
|
100
|
+
unless options[:point_markers].nil?
|
101
|
+
svg.g(:id => 'point-markers', :transform => "translate(#{GRAPH_VIEWPORT[:x] * options[:size][0]},#{(GRAPH_VIEWPORT[:y] + GRAPH_VIEWPORT[:height]) * options[:size][1]})") {
|
102
|
+
point_distance = (options[:size].first * GRAPH_VIEWPORT[:width]) / (options[:point_markers].size - 1).to_f
|
103
|
+
|
104
|
+
(0...options[:point_markers].size).map do |idx|
|
105
|
+
x_coord = point_distance * idx
|
106
|
+
svg.text(options[:point_markers][idx], :x => x_coord, :y => options[:resolver].to_point(20),
|
107
|
+
'font-size' => options[:resolver].to_point(16),
|
108
|
+
:fill => (options[:theme][:marker] || 'white').to_s,
|
109
|
+
'text-anchor' => 'middle') unless options[:point_markers][idx].nil?
|
110
|
+
end
|
111
|
+
}
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
def render_title(svg, options)
|
116
|
+
svg.g(:transform => "translate(0, #{options[:size][1] * 0.09})") {
|
117
|
+
svg.text( options[:title], :x => options[:size][0] / 2, :y => 0,
|
118
|
+
'font-size' => options[:resolver].to_point(28), 'text-anchor' => 'middle',
|
119
|
+
:fill => 'white')
|
120
|
+
}
|
121
|
+
end
|
122
|
+
|
123
|
+
private
|
124
|
+
def global_complexity
|
125
|
+
if Kernel.const_defined? "SCRUFFY_COMPLEXITY"
|
126
|
+
SCRUFFY_COMPLEXITY
|
127
|
+
else
|
128
|
+
nil
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module Scruffy
|
2
|
+
module Theme
|
3
|
+
KEYNOTE = {
|
4
|
+
:background => [:black, '#4A465A'],
|
5
|
+
:marker => :white,
|
6
|
+
:colors => {:blue => '#6886B4',
|
7
|
+
:green => '#72AE6E', :red => '#D1695E',
|
8
|
+
:purple => '#8A6EAF', :orange => '#EFAA43'}
|
9
|
+
}
|
10
|
+
|
11
|
+
|
12
|
+
MEPHISTO = {
|
13
|
+
:background => ['#101010', '#999977'],
|
14
|
+
:marker => :white,
|
15
|
+
:colors => {:orange => '#DD3300', :blue => '#66AABB', :green => '#225533',
|
16
|
+
:red => '#992200'}
|
17
|
+
}
|
18
|
+
|
19
|
+
RUBY_BLOG = {
|
20
|
+
:background => ['#670A0A', '#831515'],
|
21
|
+
:marker => '#DBD1C1',
|
22
|
+
:colors => {:teal => '#007777', :purple => '#444477', :pink => '#994444',
|
23
|
+
:green => '#77FFBB', :orange => '#D75A20'}
|
24
|
+
}
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
module Scruffy
|
2
|
+
module Transformer
|
3
|
+
class Base
|
4
|
+
def route_transform(target, idx, options = {})
|
5
|
+
args = [target, idx, options]
|
6
|
+
if respond_to?(:transform)
|
7
|
+
send :transform, *args[0...self.method(:transform).arity]
|
8
|
+
elsif respond_to?(:transform!)
|
9
|
+
send :transform!, *args[0...self.method(:transform).arity]
|
10
|
+
target
|
11
|
+
else
|
12
|
+
raise NameError, "Transformer subclass must container either a transform() method or transform!() method."
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
protected
|
17
|
+
def number_with_precision(number, precision=3)
|
18
|
+
sprintf("%01.#{precision}f", number)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
class Currency < Base
|
23
|
+
def initialize(options = {})
|
24
|
+
@precision = options[:precision] || 2
|
25
|
+
@unit = options[:unit] || '$'
|
26
|
+
@separator = options[:separator] || '.'
|
27
|
+
@delimiter = options[:delimiter] || ','
|
28
|
+
@negative_color = options[:negative_color]
|
29
|
+
@special_negatives = options[:special_negatives] || false
|
30
|
+
end
|
31
|
+
|
32
|
+
def transform(target, idx, options)
|
33
|
+
@separator = "" unless @precision > 0
|
34
|
+
begin
|
35
|
+
parts = number_with_precision(target, @precision).split('.')
|
36
|
+
if @special_negatives && (target.to_f < 0)
|
37
|
+
number = "(" + @unit + parts[0].to_i.abs.to_s.gsub(/(\d)(?=(\d\d\d)+(?!\d))/, "\\1#{@delimiter}") + @separator + parts[1].to_s + ")"
|
38
|
+
else
|
39
|
+
number = @unit + parts[0].to_s.gsub(/(\d)(?=(\d\d\d)+(?!\d))/, "\\1#{@delimiter}") + @separator + parts[1].to_s
|
40
|
+
end
|
41
|
+
if (target.to_f < 0) && @negative_color
|
42
|
+
options[:marker_color_override] = @negative_color
|
43
|
+
end
|
44
|
+
number
|
45
|
+
rescue
|
46
|
+
target
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
class Percentage < Base
|
52
|
+
def initialize(options = {})
|
53
|
+
@precision = options[:precision] || 3
|
54
|
+
@separator = options[:separator] || '.'
|
55
|
+
end
|
56
|
+
|
57
|
+
def transform(target)
|
58
|
+
begin
|
59
|
+
number = number_with_precision(target, @precision)
|
60
|
+
parts = number.split('.')
|
61
|
+
if parts.at(1).nil?
|
62
|
+
parts[0] + "%"
|
63
|
+
else
|
64
|
+
parts[0] + @separator + parts[1].to_s + "%"
|
65
|
+
end
|
66
|
+
rescue
|
67
|
+
target
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
end
|
73
|
+
end
|
data/spec/graph_spec.rb
ADDED
@@ -0,0 +1,56 @@
|
|
1
|
+
require 'lib/scruffy'
|
2
|
+
|
3
|
+
context "A new Scruffy::Graph" do
|
4
|
+
setup do
|
5
|
+
@graph = Scruffy::Graph.new
|
6
|
+
end
|
7
|
+
|
8
|
+
specify "should have a blank title" do
|
9
|
+
@graph.title.should_be.nil
|
10
|
+
end
|
11
|
+
|
12
|
+
specify "should be set to the keynote theme" do
|
13
|
+
@graph.theme.should_equal Scruffy::Theme::KEYNOTE
|
14
|
+
end
|
15
|
+
|
16
|
+
specify "should have zero layers" do
|
17
|
+
@graph.should_have(0).layers
|
18
|
+
end
|
19
|
+
|
20
|
+
specify "should not have a default layer type" do
|
21
|
+
@graph.default_type.should_be.nil
|
22
|
+
end
|
23
|
+
|
24
|
+
specify "should not have any point markers (x-axis values)" do
|
25
|
+
@graph.point_markers.should_be.nil
|
26
|
+
end
|
27
|
+
|
28
|
+
specify "should not have a marker transformer" do
|
29
|
+
@graph.marker_transformer.should_be.nil
|
30
|
+
end
|
31
|
+
|
32
|
+
specify "should use a StandardRenderer" do
|
33
|
+
@graph.renderer.should_be_instance_of Scruffy::StandardRenderer
|
34
|
+
end
|
35
|
+
|
36
|
+
specify "should accept a new title" do
|
37
|
+
@graph.title = "New Title"
|
38
|
+
@graph.title.should_equal "New Title"
|
39
|
+
end
|
40
|
+
|
41
|
+
specify "should accept a new theme" do
|
42
|
+
@graph.theme = Scruffy::Theme::MEPHISTO
|
43
|
+
@graph.theme.should_equal Scruffy::Theme::MEPHISTO
|
44
|
+
end
|
45
|
+
|
46
|
+
specify "should accept a new default type" do
|
47
|
+
@graph.default_type = :line
|
48
|
+
@graph.default_type.should_equal :line
|
49
|
+
end
|
50
|
+
|
51
|
+
specify "should accept new point markers" do
|
52
|
+
markers = ['Jan', 'Feb', 'Mar']
|
53
|
+
@graph.point_markers = markers
|
54
|
+
@graph.point_markers.should_equal markers
|
55
|
+
end
|
56
|
+
end
|
metadata
ADDED
@@ -0,0 +1,64 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
rubygems_version: 0.9.0
|
3
|
+
specification_version: 1
|
4
|
+
name: scruffy
|
5
|
+
version: !ruby/object:Gem::Version
|
6
|
+
version: 0.0.9
|
7
|
+
date: 2006-08-10 00:00:00 -07:00
|
8
|
+
summary: A powerful, clean graphing library for Ruby.
|
9
|
+
require_paths:
|
10
|
+
- lib
|
11
|
+
email: brasten@nagilum.com
|
12
|
+
homepage: http://scruffy.rubyforge.org
|
13
|
+
rubyforge_project: scruffy
|
14
|
+
description: Scruffy is a Ruby library for generating powerful graphs. It is based on SVG, allowing for powerful, clean code, as well as a good foundation for future features.
|
15
|
+
autorequire: scruffy
|
16
|
+
default_executable:
|
17
|
+
bindir: bin
|
18
|
+
has_rdoc: true
|
19
|
+
required_ruby_version: !ruby/object:Gem::Version::Requirement
|
20
|
+
requirements:
|
21
|
+
- - ">"
|
22
|
+
- !ruby/object:Gem::Version
|
23
|
+
version: 0.0.0
|
24
|
+
version:
|
25
|
+
platform: ruby
|
26
|
+
signing_key:
|
27
|
+
cert_chain:
|
28
|
+
post_install_message:
|
29
|
+
authors:
|
30
|
+
- Brasten Sager
|
31
|
+
files:
|
32
|
+
- CHANGES
|
33
|
+
- MIT-LICENSE
|
34
|
+
- Rakefile
|
35
|
+
- README
|
36
|
+
- lib/scruffy.rb
|
37
|
+
- lib/scruffy/graph.rb
|
38
|
+
- lib/scruffy/resolver.rb
|
39
|
+
- lib/scruffy/themes.rb
|
40
|
+
- lib/scruffy/transformer.rb
|
41
|
+
- lib/scruffy/version.rb
|
42
|
+
- lib/scruffy/graph_layers/all_smiles.rb
|
43
|
+
- lib/scruffy/graph_layers/area.rb
|
44
|
+
- lib/scruffy/graph_layers/average.rb
|
45
|
+
- lib/scruffy/graph_layers/bar.rb
|
46
|
+
- lib/scruffy/graph_layers/base.rb
|
47
|
+
- lib/scruffy/graph_layers/blue_screen.rb
|
48
|
+
- lib/scruffy/graph_layers/line.rb
|
49
|
+
- lib/scruffy/renderers/standard.rb
|
50
|
+
- spec/graph_spec.rb
|
51
|
+
test_files: []
|
52
|
+
|
53
|
+
rdoc_options: []
|
54
|
+
|
55
|
+
extra_rdoc_files: []
|
56
|
+
|
57
|
+
executables: []
|
58
|
+
|
59
|
+
extensions: []
|
60
|
+
|
61
|
+
requirements: []
|
62
|
+
|
63
|
+
dependencies: []
|
64
|
+
|