visage-app 0.1.8 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore CHANGED
@@ -1,4 +1,3 @@
1
- config/profiles.yaml
2
1
  .DS_Store
3
2
  log/*
4
3
  tmp/*
@@ -7,3 +6,4 @@ pkg/*
7
6
  .#*
8
7
  webrat*
9
8
  visage-app.gemspec
9
+ lib/visage/config/profiles.yaml
data/README.md CHANGED
@@ -45,7 +45,6 @@ You can try out the application quickly with:
45
45
 
46
46
  $ visage start
47
47
 
48
-
49
48
  Configuring
50
49
  -----------
51
50
 
@@ -91,7 +90,6 @@ Visage can attempt to generate an Apache vhost config for use with Passenger:
91
90
  Order allow,deny
92
91
  Allow from all
93
92
  </Directory>
94
-
95
93
  </VirtualHost>
96
94
 
97
95
  Copypasta this into your system's Apache config structure and tune to taste.
@@ -123,16 +121,11 @@ Run all cucumber features:
123
121
 
124
122
  $ rake cucumber
125
123
 
126
- Specific features:
127
-
128
- $ bin/cucumber --require features/ features/something.feature
129
-
130
124
  TODO
131
125
  ----
132
126
 
133
- * refactor tests to work on hosts other than my laptop
134
- * interface to build custom graph profiles
135
- * combine graphs from different hosts
136
- * detailed point-in-time data on hover
137
- * comment on time periods
138
- * view list of comments
127
+ * detailed point-in-time data on hover (timestamp, value)
128
+ * give graph profile an alternate private url
129
+ * make notes/annotations on private url
130
+ * include table of axis mappings + default y-axis heights for rendering
131
+ * view metrics from multiple hosts on the same graph
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.1.8
1
+ 0.2.0
data/bin/visage CHANGED
@@ -10,7 +10,8 @@ when "start"
10
10
  require 'rubygems'
11
11
  require 'rack'
12
12
  config = @root.join('lib/visage/config.ru').to_s
13
- Rack::Server.start(:config => config, :Port => 9292)
13
+ server = Rack::Server.new(:config => config, :Port => 9292)
14
+ server.start
14
15
  when "genapache"
15
16
  require 'socket'
16
17
  fqdn = Socket.gethostbyname(Socket.gethostname).first
@@ -4,63 +4,74 @@ Feature: Export data
4
4
  Must be able to extract data
5
5
  From the application
6
6
 
7
+ Scenario: Retreive a list of hosts
8
+ When I go to /data
9
+ Then the request should succeed
10
+ Then I should receive valid JSON
11
+ And the JSON should have a list of hosts
12
+
13
+ Scenario: Retreive a list of hosts
14
+ When I visit the first available host
15
+ Then the request should succeed
16
+ Then I should receive valid JSON
17
+ And the JSON should have a list of plugins
18
+
7
19
  Scenario: Retrieve single plugin instance
8
- When I go to /data/theodor/memory/memory-free
20
+ Given a list of hosts exist
21
+ When I visit "memory/memory-free" on the first available host
9
22
  Then the request should succeed
10
23
  Then I should receive valid JSON
11
24
  And the JSON should have a plugin instance named "memory-free"
12
25
 
13
26
  Scenario: Retrieve multiple plugin instances
14
- When I go to /data/theodor/memory
27
+ Given a list of hosts exist
28
+ When I visit "memory" on the first available host
15
29
  Then the request should succeed
16
30
  Then I should receive valid JSON
17
31
  And the JSON should have a plugin named "memory"
18
32
  And the JSON should have multiple plugin instances under the "memory" plugin
19
33
 
20
34
  Scenario: Make cross-domain requests
21
- When I go to /data/theodor/cpu-0?callback=foobar
35
+ Given a list of hosts exist
36
+ When I visit "cpu-0?callback=foobar" on the first available host
22
37
  Then I should receive JSON wrapped in a callback named "foobar"
23
38
 
24
39
  Scenario: Retrieve multiple plugin instances without color definition
25
- When I go to /data/theodor/memory
40
+ Given a list of hosts exist
41
+ When I visit "memory" on the first available host
26
42
  Then the request should succeed
27
43
  Then I should receive valid JSON
28
44
  And each plugin instance should have a different color
29
-
45
+
30
46
  Scenario Outline: Return only one colour per metric
31
- When I go to /data/theodor/<path>
47
+ Given a list of hosts exist
48
+ When I visit "<path>" on the first available host
32
49
  Then the request should succeed
33
50
  Then I should receive valid JSON
34
51
  And each plugin instance should have a different color
35
52
 
36
- Examples:
53
+ Examples:
37
54
  | path |
38
55
  | cpu-0/cpu-user |
39
56
  | df/df-root |
40
57
 
41
58
  Scenario: Retrieve single plugin instance with a color definition
42
- When I go to /data/theodor/tcpconns-80-local/tcp_connections-LISTEN
59
+ Given a list of hosts exist
60
+ When I visit "swap/swap-used" on the first available host
43
61
  Then the request should succeed
44
62
  Then I should receive valid JSON
45
63
  And the plugin instance should have a color
46
64
 
47
65
  Scenario: Retrieve multiple plugins through a glob
48
- Given I have the "tcpconns" plugin collecting data on multiple ports
49
- When I go to /data/theodor/tcpconns-*-local/tcp_connections-LISTEN
66
+ Given a list of hosts exist
67
+ When I visit "disk*/disk_ops" on the first available host
50
68
  Then the request should succeed
51
69
  Then I should receive valid JSON
52
70
  And I should see multiple plugins
53
71
 
54
- Scenario Outline: Retrieve multple hosts through a glob
55
- Given I have the "memory" plugin collecting data on multiple ports
56
- When I go to /data/<host>/libvirt/virt_cpu_total
72
+ Scenario: Retrieve multple hosts through a glob
73
+ When I go to /data/*/memory
57
74
  Then the request should succeed
58
75
  Then I should receive valid JSON
59
76
  And I should see multiple hosts
60
77
 
61
- Examples:
62
- | host |
63
- | * |
64
- | %7Brgh,flapjack-test%7D |
65
-
66
-
@@ -4,6 +4,8 @@ Feature: Visit site
4
4
  Must be able to visualise the data
5
5
 
6
6
  Scenario: Show available hosts
7
- When I go to /
8
- Then I should see a list of available hosts
9
- When I follow the first host
7
+ When I go to /profiles
8
+ And I visit the first profile
9
+ Then I should see a list of graphs
10
+
11
+
@@ -3,17 +3,26 @@ Then /^I should receive valid JSON$/ do
3
3
  lambda {
4
4
  @response = yajl.parse(response_body)
5
5
  }.should_not raise_error
6
-
7
- host = @response.keys.first
8
- plugin = @response[host].keys.first
9
- metric = @response[host][plugin].keys.first
10
-
11
- host.should_not be_nil
12
- plugin.should_not be_nil
13
- metric.should_not be_nil
14
-
15
- data = @response[host][plugin][metric]["data"]
16
- #.values.first.size.should > 0
6
+
7
+ case
8
+ when @response.keys.first == "hosts"
9
+ @response["hosts"].should respond_to(:size)
10
+ when @response[@response.keys.first].respond_to?(:size)
11
+ host = @response.keys.first
12
+ plugins = @response[host]
13
+ plugins.size.should > 0
14
+ else
15
+ host = @response.keys.first
16
+ plugin = @response[host].keys.first
17
+ metric = @response[host][plugin].keys.first
18
+
19
+ host.should_not be_nil
20
+ plugin.should_not be_nil
21
+ metric.should_not be_nil
22
+
23
+ data = @response[host][plugin][metric]["data"]
24
+ end
25
+
17
26
  end
18
27
 
19
28
  Then /^I should receive JSON wrapped in a callback named "([^\"]*)"$/ do |callback|
@@ -77,3 +86,38 @@ Then /^I should see multiple hosts$/ do
77
86
  @response.should_not be_nil
78
87
  @response.keys.size.should > 1
79
88
  end
89
+
90
+ Then /^the JSON should have a list of hosts$/ do
91
+ @response["hosts"].size.should > 0
92
+ end
93
+
94
+ Given /^a list of hosts exist$/ do
95
+ When 'I go to /data'
96
+ Then 'the request should succeed'
97
+ Then 'I should receive valid JSON'
98
+ Then 'the JSON should have a list of hosts'
99
+ end
100
+
101
+ When /^I visit "([^"]*)" on the first available host$/ do |glob|
102
+ host = @response["hosts"].first
103
+ url = "/data/#{host}/#{glob}"
104
+ When "I go to #{url}"
105
+ end
106
+
107
+
108
+ When /^I visit the first available host$/ do
109
+ When 'I go to /data'
110
+ Then 'the request should succeed'
111
+ Then 'I should receive valid JSON'
112
+ Then 'the JSON should have a list of hosts'
113
+
114
+ host = @response["hosts"].first
115
+ url = "/data/#{host}"
116
+ When "I go to #{url}"
117
+ end
118
+
119
+ Then /^the JSON should have a list of plugins$/ do
120
+ host = @response.keys.first
121
+ plugins = @response[host]
122
+ plugins.size.should > 0
123
+ end
@@ -1,4 +1,11 @@
1
- Then /^I should see a list of available hosts$/ do
1
+ When /^I visit the first profile$/ do
2
2
  doc = Nokogiri::HTML(response_body)
3
- doc.search('div#hosts li').size.should > 0
3
+ link_text = doc.search('div#profiles ul a').first['href']
4
+
5
+ visit(link_text)
6
+ end
7
+
8
+ Then /^I should see a list of graphs$/ do
9
+ doc = Nokogiri::HTML(response_body)
10
+ doc.search('div#profile div.graph').size.should > 1
4
11
  end
@@ -23,10 +23,10 @@ class SinatraWorld
23
23
  include Webrat::Methods
24
24
  include Webrat::Matchers
25
25
 
26
- Webrat::Methods.delegate_to_session :response_code, :response_body
26
+ Webrat::Methods.delegate_to_session :response_code, :response_body, :response_headers, :response
27
27
 
28
28
  def app
29
- Sinatra::Application
29
+ Visage::JSON
30
30
  end
31
31
  end
32
32
 
@@ -1,11 +1,10 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
- $: << File.expand_path(File.dirname(__FILE__))
4
- $: << File.expand_path(File.join(File.dirname(__FILE__), '..'))
5
-
3
+ @root = Pathname.new(File.dirname(__FILE__)).parent.parent.parent.expand_path
4
+ $: << @root.to_s
5
+ require 'lib/visage/patches'
6
6
  require 'errand'
7
7
  require 'yajl'
8
- require 'patches'
9
8
 
10
9
  # Exposes RRDs as JSON.
11
10
  #
@@ -0,0 +1,53 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ $: << File.expand_path(File.join(File.dirname(__FILE__), '..', '..', '..'))
4
+
5
+ require 'lib/visage/patches'
6
+
7
+ module Visage
8
+ module Collectd
9
+ class RRDs
10
+
11
+ class << self
12
+ def rrddir
13
+ @rrddir = "/var/lib/collectd/rrd"
14
+ end
15
+
16
+ def hosts(opts={})
17
+ case
18
+ when opts[:hosts].blank?
19
+ glob = "*"
20
+ when opts[:hosts] =~ /,/
21
+ glob = "{#{opts[:hosts].strip.gsub(/\s*/, '').gsub(/,$/, '')}}"
22
+ else
23
+ glob = opts[:hosts]
24
+ end
25
+
26
+ Dir.glob("#{rrddir}/#{glob}").map {|e| e.split('/').last }.sort.uniq
27
+ end
28
+
29
+ def metrics(opts={})
30
+ case
31
+ when opts[:metrics].blank?
32
+ glob = "*/*"
33
+ when opts[:metrics] =~ /,/
34
+ puts "\n" * 4
35
+ glob = "{" + opts[:metrics].split(/\s*,\s*/).map { |m|
36
+ m =~ /\// ? m : ["*/#{m}", "#{m}/*"]
37
+ }.join(',').gsub(/,$/, '') + "}"
38
+ when opts[:metrics] !~ /\//
39
+ glob = "#{opts[:metrics]}/#{opts[:metrics]}"
40
+ else
41
+ glob = opts[:metrics]
42
+ end
43
+
44
+ host_glob = (opts[:host] || "*")
45
+
46
+ Dir.glob("#{rrddir}/#{host_glob}/#{glob}.rrd").map {|e| e.split('/')[-2..-1].join('/').gsub(/\.rrd$/, '')}.sort.uniq
47
+ end
48
+
49
+ end
50
+
51
+ end
52
+ end
53
+ end
@@ -1,30 +1,34 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
- __DIR__ = File.expand_path(File.dirname(__FILE__))
4
- require File.join(__DIR__, '..', 'config')
3
+ @root = Pathname.new(File.dirname(__FILE__)).parent.parent.parent.expand_path
4
+ @config_directory = Pathname.new(File.dirname(__FILE__)).expand_path
5
+ require @root.join('lib/visage/config')
5
6
  require 'yaml'
6
7
 
7
8
  Visage::Config.use do |c|
8
- c['fallback_colors'] = YAML::load(File.read(File.join(__DIR__, 'fallback-colors.yaml')))
9
-
10
- profile_filename = File.join(__DIR__, 'profiles.yaml')
9
+ # setup profiles file
10
+ profile_filename = @config_directory.join('profiles.yaml')
11
11
  unless File.exists?(profile_filename)
12
- puts "You need to specify a list of profiles in config/profile.yaml!"
13
- puts "Check out config/profiles.yaml.sample for an example."
14
- exit 1
15
- end
16
- YAML::load(File.read(profile_filename)).each_pair do |key, value|
17
- c[key] = value
12
+ FileUtils.touch(profile_filename)
18
13
  end
19
14
 
20
- plugin_colors_filename = File.join(__DIR__, 'plugin-colors.yaml')
15
+ # setup plugin colors file
16
+ plugin_colors_filename = @config_directory.join('plugin-colors.yaml')
21
17
  unless File.exists?(plugin_colors_filename)
22
18
  puts "It's highly recommended you specify graph line colors in config/plugin-colors.yaml!"
23
19
  end
24
- YAML::load(File.read(plugin_colors_filename)).each_pair do |key, value|
25
- c[key] = value
20
+
21
+ # load config from profiles + plugin colors file
22
+ [profile_filename, plugin_colors_filename].each do |filename|
23
+ if File.exists?(filename)
24
+ config = YAML::load_file(filename) || {}
25
+ config.each_pair {|key, value| c[key] = value}
26
+ end
26
27
  end
27
28
 
29
+ # load fallback colors
30
+ c['fallback_colors'] = YAML::load(File.read(@config_directory.join('fallback-colors.yaml')))
31
+
28
32
  # Location of collectd's RRD - you may want to edit this!
29
33
  c['rrddir'] = "/var/lib/collectd/rrd"
30
34
 
data/lib/visage/config.ru CHANGED
@@ -7,5 +7,7 @@ $: << @root.to_s
7
7
  $0 = "visage"
8
8
 
9
9
  require 'lib/visage-app'
10
- run Sinatra::Application
11
-
10
+ use Visage::Profiles
11
+ use Visage::Builder
12
+ use Visage::JSON
13
+ run Sinatra::Base
@@ -0,0 +1,21 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'digest/md5'
4
+
5
+ module Visage
6
+ class Graph
7
+
8
+ attr_accessor :host, :plugin, :instances
9
+
10
+ def initialize(opts={})
11
+ @host = opts[:host]
12
+ @plugin = opts[:plugin]
13
+ @instances = opts[:instances]
14
+ end
15
+
16
+ def id
17
+ Digest::MD5.hexdigest("#{@host}-#{@plugin}-#{@instances}\n")
18
+ end
19
+
20
+ end
21
+ end
@@ -24,5 +24,13 @@ module Sinatra
24
24
  end
25
25
  end
26
26
 
27
- helpers LinkToHelper
27
+ module PageTitleHelper
28
+ def page_title(string)
29
+ @page_title = string
30
+ end
31
+
32
+ def include_page_title
33
+ @page_title ? "#{@page_title} | Visage" : "Visage"
34
+ end
35
+ end
28
36
  end
@@ -0,0 +1,126 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ root = Pathname.new(File.dirname(__FILE__)).parent.parent
4
+ $: << root.join('lib')
5
+ require 'lib/visage/graph'
6
+ require 'lib/visage/patches'
7
+ require 'digest/md5'
8
+
9
+ module Visage
10
+ class Profile
11
+ attr_reader :options, :selected_hosts, :hosts, :selected_metrics, :metrics,
12
+ :name, :errors
13
+
14
+ @@root = Pathname.new(File.dirname(__FILE__)).parent.parent.expand_path
15
+ @@profiles_filename = @@root.join('lib/visage/config/profiles.yaml')
16
+
17
+ def self.get(id)
18
+ url = id.downcase.gsub(/[^\w]+/, "+")
19
+ profiles = YAML::load_file(@@profiles_filename) || {}
20
+ profiles[url] ? self.new(profiles[url]) : nil
21
+ end
22
+
23
+ def self.all
24
+ profiles = YAML::load_file(@@profiles_filename) || {}
25
+ profiles.values.map { |prof| self.new(prof) }
26
+ end
27
+
28
+ def initialize(opts={})
29
+ @options = opts
30
+ @options[:url] = @options[:profile_name] ? @options[:profile_name].downcase.gsub(/[^\w]+/, "+") : nil
31
+ @errors = {}
32
+
33
+ # FIXME: this is nasty
34
+ # FIXME: doesn't work if there's only one host
35
+ # FIXME: add regex matching option
36
+ if @options[:hosts].blank?
37
+ @selected_hosts = []
38
+ @hosts = Visage::Collectd::RRDs.hosts
39
+ else
40
+ @selected_hosts = Visage::Collectd::RRDs.hosts(:hosts => @options[:hosts])
41
+ @hosts = Visage::Collectd::RRDs.hosts - @selected_hosts
42
+ end
43
+
44
+ if @options[:metrics].blank?
45
+ @selected_metrics = []
46
+ @metrics = Visage::Collectd::RRDs.metrics
47
+ else
48
+ @selected_metrics = Visage::Collectd::RRDs.metrics(:metrics => @options[:metrics])
49
+ @metrics = Visage::Collectd::RRDs.metrics - @selected_metrics
50
+ end
51
+ end
52
+
53
+ # Hashed based access to @options.
54
+ def method_missing(method)
55
+ @options[method]
56
+ end
57
+
58
+ def save
59
+ if valid?
60
+ # Construct record.
61
+ attrs = { :hosts => @options[:hosts],
62
+ :metrics => @options[:metrics],
63
+ :profile_name => @options[:profile_name],
64
+ :url => @options[:profile_name].downcase.gsub(/[^\w]+/, "+") }
65
+
66
+ # Save it.
67
+ profiles = YAML::load_file(@@profiles_filename) || {}
68
+ profiles[attrs[:url]] = attrs
69
+
70
+ File.open(@@profiles_filename, 'w') do |file|
71
+ file << profiles.to_yaml
72
+ end
73
+
74
+ true
75
+ else
76
+ false
77
+ end
78
+ end
79
+
80
+ def valid?
81
+ valid_profile_name?
82
+ end
83
+
84
+ def graphs
85
+ graphs = []
86
+
87
+ hosts = Visage::Collectd::RRDs.hosts(:hosts => @options[:hosts])
88
+ metrics = @options[:metrics]
89
+ hosts.each do |host|
90
+ attrs = {}
91
+ globs = Visage::Collectd::RRDs.metrics(:host => host, :metrics => metrics)
92
+ globs.each do |n|
93
+ parts = n.split('/')
94
+ plugin = parts[0]
95
+ instance = parts[1]
96
+ attrs[plugin] ||= []
97
+ attrs[plugin] << instance
98
+ end
99
+
100
+ attrs.each_pair do |plugin, instances|
101
+ graphs << Visage::Graph.new(:host => host,
102
+ :plugin => plugin,
103
+ :instances => instances)
104
+ end
105
+ end
106
+
107
+ graphs
108
+ end
109
+
110
+ def private_id
111
+ Digest::MD5.hexdigest("#{@options[:url]}\n")
112
+ end
113
+
114
+ private
115
+
116
+ def valid_profile_name?
117
+ if @options[:profile_name].blank?
118
+ @errors[:profile_name] = "Profile name must not be blank."
119
+ false
120
+ else
121
+ true
122
+ end
123
+ end
124
+
125
+ end
126
+ end
Binary file
Binary file
Binary file
@@ -0,0 +1,7 @@
1
+ /*
2
+ * g.Raphael 0.4.1 - Charting library, based on Raphaël
3
+ *
4
+ * Copyright (c) 2009 Dmitry Baranovskiy (http://g.raphaeljs.com)
5
+ * Licensed under the MIT (http://www.opensource.org/licenses/mit-license.php) license.
6
+ */
7
+ Raphael.fn.g.linechart=function(J,I,a,c,s,r,C){function B(y,Y){var x=y.length/Y,V=0,i=x,X=0,W=[];while(V<y.length){i--;if(i<0){X+=y[V]*(1+i);W.push(X/x);X=y[V++]*-i;i+=x;}else{X+=y[V++];}}return W;}C=C||{};if(!this.raphael.is(s[0],"array")){s=[s];}if(!this.raphael.is(r[0],"array")){r=[r];}var O=Array.prototype.concat.apply([],s),M=Array.prototype.concat.apply([],r),o=this.g.snapEnds(Math.min.apply(Math,O),Math.max.apply(Math,O),s[0].length-1),v=o.from,h=o.to,l=C.gutter||10,P=(a-l*2)/(h-v),G=this.g.snapEnds(Math.min.apply(Math,M),Math.max.apply(Math,M),r[0].length-1),u=G.from,g=G.to,N=(c-l*2)/(g-u),t=Math.max(s[0].length,r[0].length),n=C.symbol||"",K=C.colors||Raphael.fn.g.colors,H=this,p=null,k=null,T=this.set(),L=[];for(var S=0,E=r.length;S<E;S++){t=Math.max(t,r[S].length);}var U=this.set();for(var S=0,E=r.length;S<E;S++){if(C.shade){U.push(this.path().attr({stroke:"none",fill:K[S],opacity:C.nostroke?1:0.3}));}if(r[S].length>a-2*l){r[S]=B(r[S],a-2*l);t=a-2*l;}if(s[S]&&s[S].length>a-2*l){s[S]=B(s[S],a-2*l);}}var w=this.set();if(C.axis){var f=(C.axis+"").split(/[,\s]+/);+f[0]&&w.push(this.g.axis(J+l,I+l,a-2*l,v,h,C.axisxstep||Math.floor((a-2*l)/20),2));+f[1]&&w.push(this.g.axis(J+a-l,I+c-l,c-2*l,u,g,C.axisystep||Math.floor((c-2*l)/20),3));+f[2]&&w.push(this.g.axis(J+l,I+c-l,a-2*l,v,h,C.axisxstep||Math.floor((a-2*l)/20),0));+f[3]&&w.push(this.g.axis(J+l,I+c-l,c-2*l,u,g,C.axisystep||Math.floor((c-2*l)/20),1));}var F=this.set(),Q=this.set(),m;for(var S=0,E=r.length;S<E;S++){if(!C.nostroke){F.push(m=this.path().attr({stroke:K[S],"stroke-width":C.width||2,"stroke-linejoin":"round","stroke-linecap":"round","stroke-dasharray":C.dash||""}));}var b=this.raphael.is(n,"array")?n[S]:n,z=this.set();L=[];for(var R=0,q=r[S].length;R<q;R++){var e=J+l+((s[S]||s[0])[R]-v)*P;var d=I+c-l-(r[S][R]-u)*N;(Raphael.is(b,"array")?b[R]:b)&&z.push(this.g[Raphael.fn.g.markers[this.raphael.is(b,"array")?b[R]:b]](e,d,(C.width||2)*3).attr({fill:K[S],stroke:"none"}));L=L.concat([R?"L":"M",e,d]);}Q.push(z);if(C.shade){U[S].attr({path:L.concat(["L",e,I+c-l,"L",J+l+((s[S]||s[0])[0]-v)*P,I+c-l,"z"]).join(",")});}!C.nostroke&&m.attr({path:L.join(",")});}function D(ae){var ab=[];for(var ac=0,ag=s.length;ac<ag;ac++){ab=ab.concat(s[ac]);}ab.sort();var ah=[],Y=[];for(var ac=0,ag=ab.length;ac<ag;ac++){ab[ac]!=ab[ac-1]&&ah.push(ab[ac])&&Y.push(J+l+(ab[ac]-v)*P);}ab=ah;ag=ab.length;var W=ae||H.set();for(var ac=0;ac<ag;ac++){var V=Y[ac]-(Y[ac]-(Y[ac-1]||J))/2,af=((Y[ac+1]||J+a)-Y[ac])/2+(Y[ac]-(Y[ac-1]||J))/2,x;ae?(x={}):W.push(x=H.rect(V-1,I,Math.max(af+1,1),c).attr({stroke:"none",fill:"#000",opacity:0}));x.values=[];x.symbols=H.set();x.y=[];x.x=Y[ac];x.axis=ab[ac];for(var aa=0,ad=r.length;aa<ad;aa++){ah=s[aa]||s[0];for(var Z=0,y=ah.length;Z<y;Z++){if(ah[Z]==ab[ac]){x.values.push(r[aa][Z]);x.y.push(I+c-l-(r[aa][Z]-u)*N);x.symbols.push(T.symbols[aa][Z]);}}}ae&&ae.call(x);}!ae&&(p=W);}function A(ac){var W=ac||H.set(),x;for(var aa=0,ae=r.length;aa<ae;aa++){for(var Z=0,ab=r[aa].length;Z<ab;Z++){var V=J+l+((s[aa]||s[0])[Z]-v)*P,ad=J+l+((s[aa]||s[0])[Z?Z-1:1]-v)*P,y=I+c-l-(r[aa][Z]-u)*N;ac?(x={}):W.push(x=H.circle(V,y,Math.abs(ad-V)/2).attr({stroke:"none",fill:"#000",opacity:0}));x.x=V;x.y=y;x.value=r[aa][Z];x.line=T.lines[aa];x.shade=T.shades[aa];x.symbol=T.symbols[aa][Z];x.symbols=T.symbols[aa];x.axis=(s[aa]||s[0])[Z];ac&&ac.call(x);}}!ac&&(k=W);}T.push(F,U,Q,w,p,k);T.lines=F;T.shades=U;T.symbols=Q;T.axis=w;T.hoverColumn=function(j,i){!p&&D();p.mouseover(j).mouseout(i);return this;};T.clickColumn=function(i){!p&&D();p.click(i);return this;};T.hrefColumn=function(W){var X=H.raphael.is(arguments[0],"array")?arguments[0]:arguments;if(!(arguments.length-1)&&typeof W=="object"){for(var j in W){for(var y=0,V=p.length;y<V;y++){if(p[y].axis==j){p[y].attr("href",W[j]);}}}}!p&&D();for(var y=0,V=X.length;y<V;y++){p[y]&&p[y].attr("href",X[y]);}return this;};T.hover=function(j,i){!k&&A();k.mouseover(j).mouseout(i);return this;};T.click=function(i){!k&&A();k.click(i);return this;};T.each=function(i){A(i);return this;};T.eachColumn=function(i){D(i);return this;};return T;};
@@ -1,5 +1,5 @@
1
- /*
2
- * g.Raphael 0.4 - Charting library, based on Raphaël
1
+ /*!
2
+ * g.Raphael 0.4.1 - Charting library, based on Raphaël
3
3
  *
4
4
  * Copyright (c) 2009 Dmitry Baranovskiy (http://g.raphaeljs.com)
5
5
  * Licensed under the MIT (http://www.opensource.org/licenses/mit-license.php) license.
@@ -0,0 +1,7 @@
1
+ /*
2
+ * g.Raphael 0.4.1 - Charting library, based on Raphaël
3
+ *
4
+ * Copyright (c) 2009 Dmitry Baranovskiy (http://g.raphaeljs.com)
5
+ * Licensed under the MIT (http://www.opensource.org/licenses/mit-license.php) license.
6
+ */
7
+ (function(){var a=Math.max,c=Math.min;Raphael.fn.g=Raphael.fn.g||{};Raphael.fn.g.markers={disc:"disc",o:"disc",flower:"flower",f:"flower",diamond:"diamond",d:"diamond",square:"square",s:"square",triangle:"triangle",t:"triangle",star:"star","*":"star",cross:"cross",x:"cross",plus:"plus","+":"plus",arrow:"arrow","->":"arrow"};Raphael.fn.g.shim={stroke:"none",fill:"#000","fill-opacity":0};Raphael.fn.g.txtattr={font:"12px Arial, sans-serif"};Raphael.fn.g.colors=[];var e=[0.6,0.2,0.05,0.1333,0.75,0];for(var b=0;b<10;b++){if(b<e.length){Raphael.fn.g.colors.push("hsb("+e[b]+", .75, .75)");}else{Raphael.fn.g.colors.push("hsb("+e[b-e.length]+", 1, .5)");}}Raphael.fn.g.text=function(f,h,g){return this.text(f,h,g).attr(this.g.txtattr);};Raphael.fn.g.labelise=function(f,h,g){if(f){return(f+"").replace(/(##+(?:\.#+)?)|(%%+(?:\.%+)?)/g,function(i,k,j){if(k){return(+h).toFixed(k.replace(/^#+\.?/g,"").length);}if(j){return(h*100/g).toFixed(j.replace(/^%+\.?/g,"").length)+"%";}});}else{return(+h).toFixed(0);}};Raphael.fn.g.finger=function(l,k,g,m,h,i,j){if((h&&!m)||(!h&&!g)){return j?"":this.path();}i={square:"square",sharp:"sharp",soft:"soft"}[i]||"round";var o;m=Math.round(m);g=Math.round(g);l=Math.round(l);k=Math.round(k);switch(i){case"round":if(!h){var f=~~(m/2);if(g<f){f=g;o=["M",l+0.5,k+0.5-~~(m/2),"l",0,0,"a",f,~~(m/2),0,0,1,0,m,"l",0,0,"z"];}else{o=["M",l+0.5,k+0.5-f,"l",g-f,0,"a",f,f,0,1,1,0,m,"l",f-g,0,"z"];}}else{f=~~(g/2);if(m<f){f=m;o=["M",l-~~(g/2),k,"l",0,0,"a",~~(g/2),f,0,0,1,g,0,"l",0,0,"z"];}else{o=["M",l-f,k,"l",0,f-m,"a",f,f,0,1,1,g,0,"l",0,m-f,"z"];}}break;case"sharp":if(!h){var n=~~(m/2);o=["M",l,k+n,"l",0,-m,a(g-n,0),0,c(n,g),n,-c(n,g),n+(n*2<m),"z"];}else{n=~~(g/2);o=["M",l+n,k,"l",-g,0,0,-a(m-n,0),n,-c(n,m),n,c(n,m),n,"z"];}break;case"square":if(!h){o=["M",l,k+~~(m/2),"l",0,-m,g,0,0,m,"z"];}else{o=["M",l+~~(g/2),k,"l",1-g,0,0,-m,g-1,0,"z"];}break;case"soft":if(!h){f=c(g,Math.round(m/5));o=["M",l+0.5,k+0.5-~~(m/2),"l",g-f,0,"a",f,f,0,0,1,f,f,"l",0,m-f*2,"a",f,f,0,0,1,-f,f,"l",f-g,0,"z"];}else{f=c(Math.round(g/5),m);o=["M",l-~~(g/2),k,"l",0,f-m,"a",f,f,0,0,1,f,-f,"l",g-2*f,0,"a",f,f,0,0,1,f,f,"l",0,m-f,"z"];}}if(j){return o.join(",");}else{return this.path(o);}};Raphael.fn.g.disc=function(f,h,g){return this.circle(f,h,g);};Raphael.fn.g.line=function(f,h,g){return this.rect(f-g,h-g/5,2*g,2*g/5);};Raphael.fn.g.square=function(f,h,g){g=g*0.7;return this.rect(f-g,h-g,2*g,2*g);};Raphael.fn.g.triangle=function(f,h,g){g*=1.75;return this.path("M".concat(f,",",h,"m0-",g*0.58,"l",g*0.5,",",g*0.87,"-",g,",0z"));};Raphael.fn.g.diamond=function(f,h,g){return this.path(["M",f,h-g,"l",g,g,-g,g,-g,-g,g,-g,"z"]);};Raphael.fn.g.flower=function(j,h,f,g){f=f*1.25;var o=f,m=o*0.5;g=+g<3||!g?5:g;var p=["M",j,h+m,"Q"],l;for(var k=1;k<g*2+1;k++){l=k%2?o:m;p=p.concat([+(j+l*Math.sin(k*Math.PI/g)).toFixed(3),+(h+l*Math.cos(k*Math.PI/g)).toFixed(3)]);}p.push("z");return this.path(p.join(","));};Raphael.fn.g.star=function(f,n,m,h,g){h=h||m*0.382;g=g||5;var l=["M",f,n+h,"L"],k;for(var j=1;j<g*2;j++){k=j%2?m:h;l=l.concat([(f+k*Math.sin(j*Math.PI/g)),(n+k*Math.cos(j*Math.PI/g))]);}l.push("z");return this.path(l.join(","));};Raphael.fn.g.cross=function(f,h,g){g=g/2.5;return this.path("M".concat(f-g,",",h,"l",[-g,-g,g,-g,g,g,g,-g,g,g,-g,g,g,g,-g,g,-g,-g,-g,g,-g,-g,"z"]));};Raphael.fn.g.plus=function(f,h,g){g=g/2;return this.path("M".concat(f-g/2,",",h-g/2,"l",[0,-g,g,0,0,g,g,0,0,g,-g,0,0,g,-g,0,0,-g,-g,0,0,-g,"z"]));};Raphael.fn.g.arrow=function(f,h,g){return this.path("M".concat(f-g*0.7,",",h-g*0.4,"l",[g*0.6,0,0,-g*0.4,g,g*0.8,-g,g*0.8,0,-g*0.4,-g*0.6,0],"z"));};Raphael.fn.g.tag=function(f,m,l,k,i){k=k||0;i=i==null?5:i;l=l==null?"$9.99":l;var h=0.5522*i,g=this.set(),j=3;g.push(this.path().attr({fill:"#000",stroke:"#000"}));g.push(this.text(f,m,l).attr(this.g.txtattr).attr({fill:"#fff","font-family":"Helvetica, Arial"}));g.update=function(){this.rotate(0,f,m);var o=this[1].getBBox();if(o.height>=i*2){this[0].attr({path:["M",f,m+i,"a",i,i,0,1,1,0,-i*2,i,i,0,1,1,0,i*2,"m",0,-i*2-j,"a",i+j,i+j,0,1,0,0,(i+j)*2,"L",f+i+j,m+o.height/2+j,"l",o.width+2*j,0,0,-o.height-2*j,-o.width-2*j,0,"L",f,m-i-j].join(",")});}else{var n=Math.sqrt(Math.pow(i+j,2)-Math.pow(o.height/2+j,2));this[0].attr({path:["M",f,m+i,"c",-h,0,-i,h-i,-i,-i,0,-h,i-h,-i,i,-i,h,0,i,i-h,i,i,0,h,h-i,i,-i,i,"M",f+n,m-o.height/2-j,"a",i+j,i+j,0,1,0,0,o.height+2*j,"l",i+j-n+o.width+2*j,0,0,-o.height-2*j,"L",f+n,m-o.height/2-j].join(",")});}this[1].attr({x:f+i+j+o.width/2,y:m});k=(360-k)%360;this.rotate(k,f,m);k>90&&k<270&&this[1].attr({x:f-i-j-o.width/2,y:m,rotation:[180+k,f,m]});return this;};g.update();return g;};Raphael.fn.g.popupit=function(l,k,m,g,t){g=g==null?2:g;t=t||5;l=Math.round(l);k=Math.round(k);var j=m.getBBox(),n=Math.round(j.width/2),i=Math.round(j.height/2),s=[0,n+t*2,0,-n-t*2],o=[-i*2-t*3,-i-t,0,-i-t],f=["M",l-s[g],k-o[g],"l",-t,(g==2)*-t,-a(n-t,0),0,"a",t,t,0,0,1,-t,-t,"l",0,-a(i-t,0),(g==3)*-t,-t,(g==3)*t,-t,0,-a(i-t,0),"a",t,t,0,0,1,t,-t,"l",a(n-t,0),0,t,!g*-t,t,!g*t,a(n-t,0),0,"a",t,t,0,0,1,t,t,"l",0,a(i-t,0),(g==1)*t,t,(g==1)*-t,t,0,a(i-t,0),"a",t,t,0,0,1,-t,t,"l",-a(n-t,0),0,"z"].join(","),q=[{x:l,y:k+t*2+i},{x:l-t*2-n,y:k},{x:l,y:k-t*2-i},{x:l+t*2+n,y:k}][g];m.translate(q.x-n-j.x,q.y-i-j.y);return this.path(f).attr({fill:"#000",stroke:"none"}).insertBefore(m.node?m:m[0]);};Raphael.fn.g.popup=function(f,l,k,g,i){g=g==null?2:g>3?3:g;i=i||5;k=k||"$9.99";var h=this.set(),j=3;h.push(this.path().attr({fill:"#000",stroke:"#000"}));h.push(this.text(f,l,k).attr(this.g.txtattr).attr({fill:"#fff","font-family":"Helvetica, Arial"}));h.update=function(o,n,q){o=o||f;n=n||l;var t=this[1].getBBox(),u=t.width/2,s=t.height/2,y=[0,u+i*2,0,-u-i*2],v=[-s*2-i*3,-s-i,0,-s-i],m=["M",o-y[g],n-v[g],"l",-i,(g==2)*-i,-a(u-i,0),0,"a",i,i,0,0,1,-i,-i,"l",0,-a(s-i,0),(g==3)*-i,-i,(g==3)*i,-i,0,-a(s-i,0),"a",i,i,0,0,1,i,-i,"l",a(u-i,0),0,i,!g*-i,i,!g*i,a(u-i,0),0,"a",i,i,0,0,1,i,i,"l",0,a(s-i,0),(g==1)*i,i,(g==1)*-i,i,0,a(s-i,0),"a",i,i,0,0,1,-i,i,"l",-a(u-i,0),0,"z"].join(","),x=[{x:o,y:n+i*2+s},{x:o-i*2-u,y:n},{x:o,y:n-i*2-s},{x:o+i*2+u,y:n}][g];x.path=m;if(q){this.animate(x,500,">");}else{this.attr(x);}return this;};return h.update(f,l);};Raphael.fn.g.flag=function(f,k,j,i){i=i||0;j=j||"$9.99";var g=this.set(),h=3;g.push(this.path().attr({fill:"#000",stroke:"#000"}));g.push(this.text(f,k,j).attr(this.g.txtattr).attr({fill:"#fff","font-family":"Helvetica, Arial"}));g.update=function(l,o){this.rotate(0,l,o);var n=this[1].getBBox(),m=n.height/2;this[0].attr({path:["M",l,o,"l",m+h,-m-h,n.width+2*h,0,0,n.height+2*h,-n.width-2*h,0,"z"].join(",")});this[1].attr({x:l+m+h+n.width/2,y:o});i=360-i;this.rotate(i,l,o);i>90&&i<270&&this[1].attr({x:l-r-h-n.width/2,y:o,rotation:[180+i,l,o]});return this;};return g.update(f,k);};Raphael.fn.g.label=function(f,i,h){var g=this.set();g.push(this.rect(f,i,10,10).attr({stroke:"none",fill:"#000"}));g.push(this.text(f,i,h).attr(this.g.txtattr).attr({fill:"#fff"}));g.update=function(){var k=this[1].getBBox(),j=c(k.width+10,k.height+10)/2;this[0].attr({x:k.x-j/2,y:k.y-j/2,width:k.width+j,height:k.height+j,r:j});};g.update();return g;};Raphael.fn.g.labelit=function(h){var g=h.getBBox(),f=c(20,g.width+10,g.height+10)/2;return this.rect(g.x-f/2,g.y-f/2,g.width+f,g.height+f,f).attr({stroke:"none",fill:"#000"}).insertBefore(h.node?h:h[0]);};Raphael.fn.g.drop=function(f,k,j,h,i){h=h||30;i=i||0;var g=this.set();g.push(this.path(["M",f,k,"l",h,0,"A",h*0.4,h*0.4,0,1,0,f+h*0.7,k-h*0.7,"z"]).attr({fill:"#000",stroke:"none",rotation:[22.5-i,f,k]}));i=(i+90)*Math.PI/180;g.push(this.text(f+h*Math.sin(i),k+h*Math.cos(i),j).attr(this.g.txtattr).attr({"font-size":h*12/30,fill:"#fff"}));g.drop=g[0];g.text=g[1];return g;};Raphael.fn.g.blob=function(g,m,l,k,i){k=(+k+1?k:45)+90;i=i||12;var f=Math.PI/180,j=i*12/12;var h=this.set();h.push(this.path().attr({fill:"#000",stroke:"none"}));h.push(this.text(g+i*Math.sin((k)*f),m+i*Math.cos((k)*f)-j/2,l).attr(this.g.txtattr).attr({"font-size":j,fill:"#fff"}));h.update=function(t,s,y){t=t||g;s=s||m;var A=this[1].getBBox(),D=a(A.width+j,i*25/12),z=a(A.height+j,i*25/12),o=t+i*Math.sin((k-22.5)*f),B=s+i*Math.cos((k-22.5)*f),q=t+i*Math.sin((k+22.5)*f),C=s+i*Math.cos((k+22.5)*f),F=(q-o)/2,E=(C-B)/2,p=D/2,n=z/2,x=-Math.sqrt(Math.abs(p*p*n*n-p*p*E*E-n*n*F*F)/(p*p*E*E+n*n*F*F)),v=x*p*E/n+(q+o)/2,u=x*-n*F/p+(C+B)/2;if(y){this.animate({x:v,y:u,path:["M",g,m,"L",q,C,"A",p,n,0,1,1,o,B,"z"].join(",")},500,">");}else{this.attr({x:v,y:u,path:["M",g,m,"L",q,C,"A",p,n,0,1,1,o,B,"z"].join(",")});}return this;};h.update(g,m);return h;};Raphael.fn.g.colorValue=function(i,h,g,f){return"hsb("+[c((1-i/h)*0.4,1),g||0.75,f||0.75]+")";};Raphael.fn.g.snapEnds=function(n,o,m){var k=n,p=o;if(k==p){return{from:k,to:p,power:0};}function q(f){return Math.abs(f-0.5)<0.25?~~(f)+0.5:Math.round(f);}var l=(p-k)/m,g=~~(l),j=g,h=0;if(g){while(j){h--;j=~~(l*Math.pow(10,h))/Math.pow(10,h);}h++;}else{while(!g){h=h||1;g=~~(l*Math.pow(10,h))/Math.pow(10,h);h++;}h&&h--;}p=q(o*Math.pow(10,h))/Math.pow(10,h);if(p<o){p=q((o+0.5)*Math.pow(10,h))/Math.pow(10,h);}k=q((n-(h>0?0:0.5))*Math.pow(10,h))/Math.pow(10,h);return{from:k,to:p,power:h};};Raphael.fn.g.axis=function(v,u,o,G,l,J,m,L,n,g){g=g==null?2:g;n=n||"t";J=J||10;var F=n=="|"||n==" "?["M",v+0.5,u,"l",0,0.001]:m==1||m==3?["M",v+0.5,u,"l",0,-o]:["M",v,u+0.5,"l",o,0],z=this.g.snapEnds(G,l,J),K=z.from,B=z.to,I=z.power,H=0,C=this.set();d=(B-K)/J;var s=K,q=I>0?I:0;w=o/J;if(+m==1||+m==3){var h=u,A=(m-1?1:-1)*(g+3+!!(m-1));while(h>=u-o){n!="-"&&n!=" "&&(F=F.concat(["M",v-(n=="+"||n=="|"?g:!(m-1)*g*2),h+0.5,"l",g*2+1,0]));C.push(this.text(v+A,h,(L&&L[H++])||(Math.round(s)==s?s:+s.toFixed(q))).attr(this.g.txtattr).attr({"text-anchor":m-1?"start":"end"}));s+=d;h-=w;}if(Math.round(h+w-(u-o))){n!="-"&&n!=" "&&(F=F.concat(["M",v-(n=="+"||n=="|"?g:!(m-1)*g*2),u-o+0.5,"l",g*2+1,0]));C.push(this.text(v+A,u-o,(L&&L[H])||(Math.round(s)==s?s:+s.toFixed(q))).attr(this.g.txtattr).attr({"text-anchor":m-1?"start":"end"}));}}else{s=K;q=(I>0)*I;A=(m?-1:1)*(g+9+!m);var k=v,w=o/J,D=0,E=0;while(k<=v+o){n!="-"&&n!=" "&&(F=F.concat(["M",k+0.5,u-(n=="+"?g:!!m*g*2),"l",0,g*2+1]));C.push(D=this.text(k,u+A,(L&&L[H++])||(Math.round(s)==s?s:+s.toFixed(q))).attr(this.g.txtattr));var p=D.getBBox();if(E>=p.x-5){C.pop(C.length-1).remove();}else{E=p.x+p.width;}s+=d;k+=w;}if(Math.round(k-w-v-o)){n!="-"&&n!=" "&&(F=F.concat(["M",v+o+0.5,u-(n=="+"?g:!!m*g*2),"l",0,g*2+1]));C.push(this.text(v+o,u+A,(L&&L[H])||(Math.round(s)==s?s:+s.toFixed(q))).attr(this.g.txtattr));}}var M=this.path(F);M.text=C;M.all=this.set([M,C]);M.remove=function(){this.text.remove();this.constructor.prototype.remove.call(this);};return M;};Raphael.el.lighter=function(g){g=g||2;var f=[this.attrs.fill,this.attrs.stroke];this.fs=this.fs||[f[0],f[1]];f[0]=Raphael.rgb2hsb(Raphael.getRGB(f[0]).hex);f[1]=Raphael.rgb2hsb(Raphael.getRGB(f[1]).hex);f[0].b=c(f[0].b*g,1);f[0].s=f[0].s/g;f[1].b=c(f[1].b*g,1);f[1].s=f[1].s/g;this.attr({fill:"hsb("+[f[0].h,f[0].s,f[0].b]+")",stroke:"hsb("+[f[1].h,f[1].s,f[1].b]+")"});};Raphael.el.darker=function(g){g=g||2;var f=[this.attrs.fill,this.attrs.stroke];this.fs=this.fs||[f[0],f[1]];f[0]=Raphael.rgb2hsb(Raphael.getRGB(f[0]).hex);f[1]=Raphael.rgb2hsb(Raphael.getRGB(f[1]).hex);f[0].s=c(f[0].s*g,1);f[0].b=f[0].b/g;f[1].s=c(f[1].s*g,1);f[1].b=f[1].b/g;this.attr({fill:"hsb("+[f[0].h,f[0].s,f[0].b]+")",stroke:"hsb("+[f[1].h,f[1].s,f[1].b]+")"});};Raphael.el.original=function(){if(this.fs){this.attr({fill:this.fs[0],stroke:this.fs[1]});delete this.fs;}};})();