visage-app 0.1.8 → 0.2.0

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/.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;}};})();