omf_web 0.9.7 → 0.9.8
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +1 -0
- data/README.md +1 -1
- data/bin/omf_web_demo +3 -0
- data/bin/omf_web_demo.sh +0 -0
- data/doc/tutorial/tut01/hello_world.rb +27 -0
- data/doc/tutorial/tut02/hello_graph.rb +85 -0
- data/doc/tutorial/tut03/hello_database.rb +69 -0
- data/doc/tutorial/tut03/nmetric.sq3 +0 -0
- data/example/bridge/config.ru +2 -1
- data/example/demo/data_sources/downloads.csv +96 -1
- data/example/demo/demo_viz_server.rb +46 -28
- data/example/openflow-gec15/README.md +2 -0
- data/example/openflow-gec15/doc/gec15_topo.png +0 -0
- data/example/openflow-gec15/exp_source.rb +48 -9
- data/example/openflow-gec15/of_viz_server.rb +1 -1
- data/example/openflow-gec15/repository/of-exp.rb +117 -12
- data/example/openflow-gec15/repository/trema-ctl6.rb +2 -2
- data/example/simple/README.md +29 -1
- data/example/simple/data_sources/ping_source.rb +2 -2
- data/lib/irods4r/directory.rb +49 -0
- data/lib/irods4r/file.rb +53 -0
- data/lib/irods4r/icommands.rb +63 -0
- data/lib/irods4r.rb +34 -0
- data/lib/omf-web/content/content_proxy.rb +14 -5
- data/lib/omf-web/content/file_repository.rb +36 -75
- data/lib/omf-web/content/git_repository.rb +39 -35
- data/lib/omf-web/content/irods_repository.rb +191 -0
- data/lib/omf-web/content/repository.rb +84 -21
- data/lib/omf-web/content/static_repository.rb +61 -0
- data/lib/omf-web/data_source_proxy.rb +3 -3
- data/lib/omf-web/rack/session_authenticator.rb +67 -35
- data/lib/omf-web/rack/tab_mapper.rb +2 -1
- data/lib/omf-web/rack/websocket_handler.rb +49 -10
- data/lib/omf-web/session_store.rb +9 -8
- data/lib/omf-web/theme/bright/page.rb +1 -1
- data/lib/omf-web/thin/runner.rb +18 -5
- data/lib/omf-web/version.rb +1 -1
- data/lib/omf-web/widget/text/maruku/output/to_html.rb +8 -2
- data/lib/omf_web.rb +17 -2
- data/omf_web.gemspec +0 -1
- data/share/htdocs/graph/js/abstract_widget.js +3 -1
- data/share/htdocs/graph/js/barchart_brush.js +240 -0
- data/share/htdocs/js/data_source2.js +4 -1
- data/share/htdocs/js/mustache.js +17 -11
- data/share/htdocs/theme/bright/css/bright.css +1 -1
- data/share/htdocs/vendor/{bootstrap-2.1.1 → bootstrap-2.3.1}/css/bootstrap-responsive.css +56 -5
- data/share/htdocs/vendor/bootstrap-2.3.1/css/bootstrap-responsive.min.css +9 -0
- data/share/htdocs/vendor/{bootstrap-2.1.1 → bootstrap-2.3.1}/css/bootstrap.css +856 -472
- data/share/htdocs/vendor/bootstrap-2.3.1/css/bootstrap.min.css +9 -0
- data/share/htdocs/vendor/{bootstrap-2.1.1 → bootstrap-2.3.1}/img/glyphicons-halflings-white.png +0 -0
- data/share/htdocs/vendor/{bootstrap-2.1.1 → bootstrap-2.3.1}/img/glyphicons-halflings.png +0 -0
- data/share/htdocs/vendor/{bootstrap-2.1.1 → bootstrap-2.3.1}/js/bootstrap.js +427 -178
- data/share/htdocs/vendor/bootstrap-2.3.1/js/bootstrap.min.js +6 -0
- data/share/htdocs/vendor/jquery-tipsy/css/tipsy.css +25 -0
- data/share/htdocs/vendor/jquery-tipsy/js/jquery.tipsy.js +258 -0
- metadata +27 -14
- data/bin/omf-web-basic +0 -235
- data/share/htdocs/vendor/.DS_Store +0 -0
- data/share/htdocs/vendor/bootstrap-2.1.1/css/bootstrap-responsive.min.css +0 -9
- data/share/htdocs/vendor/bootstrap-2.1.1/css/bootstrap.min.css +0 -9
- data/share/htdocs/vendor/bootstrap-2.1.1/js/bootstrap.min.js +0 -6
- data/share/htdocs/vendor/jquery-ui-1.8.23/index.html +0 -383
@@ -3,31 +3,57 @@ require 'omf_common/lobject'
|
|
3
3
|
require 'rack'
|
4
4
|
require 'omf-web/session_store'
|
5
5
|
|
6
|
-
|
7
|
-
module OMF::Web::Rack
|
6
|
+
|
7
|
+
module OMF::Web::Rack
|
8
|
+
class AuthenticationFailedException < Exception
|
9
|
+
|
10
|
+
end
|
11
|
+
|
12
|
+
# This rack module maintains a session cookie and
|
13
|
+
# redirects any requests to protected pages to a
|
14
|
+
# 'login' page at the beginning of a session
|
15
|
+
#
|
16
|
+
# Calls to the class methods are resolved inthe context
|
17
|
+
# of a Session using 'OMF::Web::SessionStore'
|
18
|
+
#
|
8
19
|
class SessionAuthenticator < OMF::Common::LObject
|
9
|
-
|
20
|
+
|
21
|
+
# Returns true if this Rack module has been instantiated
|
22
|
+
# in the current Rack stack.
|
23
|
+
#
|
10
24
|
def self.active?
|
11
25
|
@@active
|
12
26
|
end
|
13
27
|
|
28
|
+
# Return true if the session is authenticated
|
29
|
+
#
|
14
30
|
def self.authenticated?
|
15
|
-
self[:authenticated]
|
31
|
+
debug "AUTH: #{self[:authenticated] == true}"
|
32
|
+
self[:authenticated] == true
|
16
33
|
end
|
17
34
|
|
35
|
+
# Calling this method will authenticate the current session
|
36
|
+
#
|
18
37
|
def self.authenticate
|
19
38
|
self[:authenticated] = true
|
20
39
|
self[:valid_until] = Time.now + @@expire_after
|
21
40
|
end
|
22
|
-
|
41
|
+
|
42
|
+
# Logging out will un-authenticate this session
|
43
|
+
#
|
23
44
|
def self.logout
|
45
|
+
debug "LOGOUT"
|
24
46
|
self[:authenticated] = false
|
25
47
|
end
|
26
48
|
|
49
|
+
# DO NOT CALL DIRECTLY
|
50
|
+
#
|
27
51
|
def self.[](key)
|
28
52
|
OMF::Web::SessionStore[key, :authenticator]
|
29
53
|
end
|
30
|
-
|
54
|
+
|
55
|
+
# DO NOT CALL DIRECTLY
|
56
|
+
#
|
31
57
|
def self.[]=(key, value)
|
32
58
|
OMF::Web::SessionStore[key, :authenticator] = value
|
33
59
|
end
|
@@ -35,10 +61,12 @@ module OMF::Web::Rack
|
|
35
61
|
@@active = false
|
36
62
|
# Expire authenticated session after being idle for that many seconds
|
37
63
|
@@expire_after = 2592000
|
38
|
-
|
64
|
+
|
39
65
|
#
|
40
66
|
# opts -
|
41
|
-
# :
|
67
|
+
# :login_url - URL to redirect if session is not authenticated
|
68
|
+
# :no_session - Array of regexp on 'path_info' which do not require an authenticated session
|
69
|
+
# :expire_after - Idle time in sec after which to expire a session
|
42
70
|
#
|
43
71
|
def initialize(app, opts = {})
|
44
72
|
@app = app
|
@@ -49,45 +77,49 @@ module OMF::Web::Rack
|
|
49
77
|
end
|
50
78
|
@@active = true
|
51
79
|
end
|
52
|
-
|
53
|
-
|
80
|
+
|
81
|
+
def check_authenticated
|
82
|
+
authenticated = self.class[:authenticated] == true
|
83
|
+
#puts "AUTHENTICATED: #{authenticated}"
|
84
|
+
raise AuthenticationFailedException.new unless authenticated
|
85
|
+
#self.class[:valid_until] = Time.now + @@expire_after
|
86
|
+
|
87
|
+
end
|
88
|
+
|
54
89
|
def call(env)
|
55
90
|
#puts env.keys.inspect
|
56
91
|
req = ::Rack::Request.new(env)
|
57
|
-
sid = nil
|
58
92
|
path_info = req.path_info
|
59
|
-
|
93
|
+
unless sid = req.cookies['sid']
|
94
|
+
sid = "s#{(rand * 10000000).to_i}_#{(rand * 10000000).to_i}"
|
95
|
+
end
|
96
|
+
Thread.current["sessionID"] = sid # needed for Session Store
|
60
97
|
unless @opts[:no_session].find {|rx| rx.match(path_info) }
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
# If 'login_url' is defined, check if this session is authenticated
|
65
|
-
login_url = @opts[:login_url]
|
98
|
+
|
99
|
+
# If 'login_page_url' is defined, check if this session is authenticated
|
100
|
+
login_url = @opts[:login_page_url]
|
66
101
|
if login_url && login_url != req.path_info
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
end
|
73
|
-
|
74
|
-
|
75
|
-
return [301,
|
102
|
+
begin
|
103
|
+
check_authenticated
|
104
|
+
rescue AuthenticationFailedException => ex
|
105
|
+
if err = self.class[:login_error]
|
106
|
+
login_url = login_url + "?msg=#{err}"
|
107
|
+
end
|
108
|
+
headers = {'Location' => login_url, "Content-Type" => ""}
|
109
|
+
Rack::Utils.set_cookie_header!(headers, 'sid', sid)
|
110
|
+
return [301, headers, ['Login first']]
|
76
111
|
end
|
77
112
|
end
|
78
|
-
self.class[:valid_until] = Time.now + @@expire_after
|
79
113
|
end
|
80
|
-
|
114
|
+
|
81
115
|
status, headers, body = @app.call(env)
|
82
|
-
if sid
|
83
|
-
|
84
|
-
end
|
85
|
-
[status, headers, body]
|
116
|
+
Rack::Utils.set_cookie_header!(headers, 'sid', sid) if sid
|
117
|
+
[status, headers, body]
|
86
118
|
end
|
87
119
|
end # class
|
88
|
-
|
120
|
+
|
89
121
|
end # module
|
90
122
|
|
91
123
|
|
92
|
-
|
93
|
-
|
124
|
+
|
125
|
+
|
@@ -57,11 +57,12 @@ module OMF::Web::Rack
|
|
57
57
|
comp_name = ((@opts[:tabs] || [])[0] || {})[:id]
|
58
58
|
end
|
59
59
|
comp_name = comp_name.to_sym if comp_name
|
60
|
+
#puts "PATH: #{path} - #{comp_name}"
|
60
61
|
comp_name
|
61
62
|
end
|
62
63
|
|
63
64
|
def render_card(req)
|
64
|
-
#puts ">>>> REQ: #{req.
|
65
|
+
#puts ">>>> REQ: #{req.path_info}::#{req.inspect}"
|
65
66
|
|
66
67
|
opts = @opts.dup
|
67
68
|
opts[:prefix] = req.script_name
|
@@ -2,6 +2,7 @@
|
|
2
2
|
require 'rack/websocket'
|
3
3
|
require 'omf_common/lobject'
|
4
4
|
require 'omf-web/session_store'
|
5
|
+
require 'thread'
|
5
6
|
|
6
7
|
module OMF::Web::Rack
|
7
8
|
|
@@ -9,6 +10,9 @@ module OMF::Web::Rack
|
|
9
10
|
include OMF::Common::Loggable
|
10
11
|
extend OMF::Common::Loggable
|
11
12
|
|
13
|
+
MESSAGE_DELAY = 0.5 # Delay in pushing action message to browser
|
14
|
+
# after receiving a 'on_change' from monitored data proxy
|
15
|
+
|
12
16
|
def on_open(env)
|
13
17
|
#puts ">>>>> OPEN"
|
14
18
|
end
|
@@ -36,7 +40,6 @@ module OMF::Web::Rack
|
|
36
40
|
debug "#{ex.backtrace.join("\n")}"
|
37
41
|
send_data({type: 'reply', status: 'exception', err_msg: ex.to_s}.to_json)
|
38
42
|
end
|
39
|
-
#puts "message processed"
|
40
43
|
end
|
41
44
|
|
42
45
|
def on_close(env)
|
@@ -53,17 +56,53 @@ module OMF::Web::Rack
|
|
53
56
|
dsp = find_data_source(args)
|
54
57
|
return unless dsp # should define appropriate exception
|
55
58
|
debug "Received registration for datasource proxy '#{dsp}'"
|
59
|
+
send_data({type: 'reply', status: 'ok'}.to_json)
|
60
|
+
|
61
|
+
mutex = Mutex.new
|
62
|
+
semaphore = ConditionVariable.new
|
63
|
+
action_queue = {}
|
64
|
+
|
56
65
|
dsp.on_changed(args['offset']) do |action, rows|
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
action: action
|
62
|
-
#offset: offset
|
63
|
-
}
|
64
|
-
send_data(msg.to_json)
|
66
|
+
mutex.synchronize do
|
67
|
+
(action_queue[action] ||= []).concat(rows)
|
68
|
+
semaphore.signal
|
69
|
+
end
|
65
70
|
end
|
66
|
-
|
71
|
+
|
72
|
+
# Send the rows in a separate thread, waiting a bit after the first one arriving
|
73
|
+
# to 'bunch' things into more manageable number of messages
|
74
|
+
Thread.new do
|
75
|
+
begin
|
76
|
+
loop do
|
77
|
+
# Now lets send them
|
78
|
+
mutex.synchronize do
|
79
|
+
action_queue.each do |action, rows|
|
80
|
+
next if rows.empty?
|
81
|
+
debug "Sending '#{action}' message with #{rows.length} rows"
|
82
|
+
msg = {
|
83
|
+
type: 'datasource_update',
|
84
|
+
datasource: dsp.name,
|
85
|
+
rows: rows,
|
86
|
+
action: action
|
87
|
+
#offset: offset
|
88
|
+
}
|
89
|
+
send_data(msg.to_json)
|
90
|
+
rows.clear
|
91
|
+
end
|
92
|
+
|
93
|
+
# wait until there is more to send
|
94
|
+
semaphore.wait(mutex)
|
95
|
+
end
|
96
|
+
|
97
|
+
# OK, there is something to do, but let's wait a bit, maybe there is more
|
98
|
+
sleep MESSAGE_DELAY
|
99
|
+
end
|
100
|
+
rescue Exception => ex
|
101
|
+
error "on_register_data_source - #{ex}"
|
102
|
+
debug "#{ex.backtrace.join("\n")}"
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
67
106
|
end
|
68
107
|
|
69
108
|
# args {"slice"=>{"col_name"=>"id", "col_value"=>"e8..."}, "ds_name"=>"individual_link"}}
|
@@ -19,20 +19,21 @@ module OMF::Web
|
|
19
19
|
self.session["#{domain}:#{key}"] = value
|
20
20
|
end
|
21
21
|
|
22
|
-
def self.session(
|
23
|
-
|
24
|
-
sid = Thread.current["sessionID"]
|
25
|
-
end
|
26
|
-
unless sid
|
27
|
-
raise "Missing session id 'sid'"
|
28
|
-
end
|
29
|
-
|
22
|
+
def self.session()
|
23
|
+
sid = session_id
|
30
24
|
session = @@sessions[sid] ||= {:content => {}}
|
31
25
|
#puts "STORE>> #{sid} = #{session[:content].keys.inspect}"
|
32
26
|
session[:ts] = Time.now
|
33
27
|
session[:content]
|
34
28
|
end
|
35
29
|
|
30
|
+
def self.session_id
|
31
|
+
sid = Thread.current["sessionID"]
|
32
|
+
unless sid
|
33
|
+
raise "Missing session id 'sid'"
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
36
37
|
def self.find_tab_from_path(comp_path)
|
37
38
|
sid = comp_path.shift
|
38
39
|
unless session = self.session(sid)
|
@@ -114,7 +114,7 @@ module OMF::Web::Theme
|
|
114
114
|
widget(Erector.inline(&@footer_right))
|
115
115
|
else
|
116
116
|
span :style => 'float:right;margin-right:10pt' do
|
117
|
-
text @footer_right || OMF::Web::VERSION
|
117
|
+
text @footer_right || "omf-web V#{OMF::Web::VERSION}"
|
118
118
|
end
|
119
119
|
end
|
120
120
|
if @footer_left.is_a? Proc
|
data/lib/omf-web/thin/runner.rb
CHANGED
@@ -73,9 +73,13 @@ module OMF::Web
|
|
73
73
|
if ph = @options[:handlers][:pre_parse]
|
74
74
|
ph.call(p)
|
75
75
|
end
|
76
|
-
|
77
|
-
parse!
|
78
76
|
|
77
|
+
parse!
|
78
|
+
# WHY IS THIS HERE
|
79
|
+
# unless life_cycle(:post_parse)
|
80
|
+
# puts p.to_s
|
81
|
+
# abort()
|
82
|
+
# end
|
79
83
|
if sopts
|
80
84
|
@options[:ssl] = true
|
81
85
|
@options[:ssl_key_file] ||= sopts[:key_file]
|
@@ -96,14 +100,23 @@ module OMF::Web
|
|
96
100
|
@@instance = self
|
97
101
|
end
|
98
102
|
|
99
|
-
def life_cycle(step)
|
103
|
+
def life_cycle(step, &exception_block)
|
100
104
|
begin
|
101
105
|
if (p = @options[:handlers][step])
|
102
106
|
p.call()
|
103
107
|
end
|
104
108
|
rescue => ex
|
105
|
-
|
106
|
-
|
109
|
+
if exception_block
|
110
|
+
begin
|
111
|
+
exception_block.call(ex)
|
112
|
+
rescue => ex2
|
113
|
+
error ex2
|
114
|
+
debug "#{ex2.backtrace.join("\n")}"
|
115
|
+
end
|
116
|
+
else
|
117
|
+
error ex
|
118
|
+
debug "#{ex.backtrace.join("\n")}"
|
119
|
+
end
|
107
120
|
end
|
108
121
|
end
|
109
122
|
|
data/lib/omf-web/version.rb
CHANGED
@@ -43,6 +43,7 @@ module MaRuKu; module Out; module HTML
|
|
43
43
|
|
44
44
|
# Render as an HTML fragment (no head, just the content of BODY). (returns a string)
|
45
45
|
def to_html(context={})
|
46
|
+
Thread.current['maruku_context'] = context
|
46
47
|
indent = context[:indent] || -1
|
47
48
|
ie_hack = context[:ie_hack] || true
|
48
49
|
|
@@ -803,6 +804,7 @@ of the form `#ff00ff`.
|
|
803
804
|
" Using SPAN element as replacement."
|
804
805
|
return wrap_as_element('span')
|
805
806
|
end
|
807
|
+
raise "IMAGE: #{url}"
|
806
808
|
return a
|
807
809
|
end
|
808
810
|
|
@@ -813,10 +815,14 @@ of the form `#ff00ff`.
|
|
813
815
|
" Using SPAN element as replacement."
|
814
816
|
return wrap_as_element('span')
|
815
817
|
end
|
818
|
+
if url_resolver = (Thread.current['maruku_context'] || {})[:img_url_resolver]
|
819
|
+
url = url_resolver.call(url)
|
820
|
+
end
|
821
|
+
|
816
822
|
title = self.title
|
817
823
|
a = create_html_element 'img'
|
818
|
-
|
819
|
-
|
824
|
+
a.attributes['src'] = url.to_s
|
825
|
+
a.attributes['alt'] = children_to_s
|
820
826
|
return a
|
821
827
|
end
|
822
828
|
|
data/lib/omf_web.rb
CHANGED
@@ -1,4 +1,5 @@
|
|
1
1
|
|
2
|
+
require 'omf-web/version'
|
2
3
|
|
3
4
|
module OMF
|
4
5
|
module Web
|
@@ -6,11 +7,15 @@ module OMF
|
|
6
7
|
module Rack; end
|
7
8
|
module Widget; end
|
8
9
|
|
9
|
-
VERSION = 'git:release-5.4'
|
10
|
+
#VERSION = 'git:release-5.4'
|
10
11
|
|
11
12
|
def self.start(opts, &block)
|
12
13
|
require 'omf-web/thin/runner'
|
13
14
|
|
15
|
+
if layout = opts.delete(:layout)
|
16
|
+
load_widget_from_file(layout)
|
17
|
+
end
|
18
|
+
|
14
19
|
#Thin::Logging.debug = true
|
15
20
|
runner = OMF::Web::Runner.new(ARGV, opts)
|
16
21
|
block.call if block
|
@@ -30,7 +35,17 @@ module OMF
|
|
30
35
|
wdescr = deep_symbolize_keys widget_descr
|
31
36
|
OMF::Web::Widget.register_widget(wdescr)
|
32
37
|
end
|
33
|
-
|
38
|
+
|
39
|
+
def self.load_widget_from_file(file_name)
|
40
|
+
require 'yaml'
|
41
|
+
y = YAML.load_file(file_name)
|
42
|
+
if w = y['widget']
|
43
|
+
OMF::Web.register_widget w
|
44
|
+
else
|
45
|
+
OMF::Common::LObject.error "Doesn't seem to be a widget definition. Expected 'widget' but found '#{y.keys.join(', ')}'"
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
34
49
|
def self.use_tab(tab_id)
|
35
50
|
OMF::Web::Tab.use_tab tab_id.to_sym
|
36
51
|
end
|
data/omf_web.gemspec
CHANGED
@@ -146,7 +146,9 @@ L.provide('OML.abstract_widget', ["vendor/d3/d3.js"], function () {
|
|
146
146
|
|
147
147
|
process_schema: function() {
|
148
148
|
this.schema = this.process_single_schema(this.data_source);
|
149
|
-
|
149
|
+
if (typeof(this.decl_properties) != "undefined") {
|
150
|
+
this.mapping = this.process_single_mapping(null, this.opts.mapping, this.decl_properties);
|
151
|
+
}
|
150
152
|
},
|
151
153
|
|
152
154
|
process_single_schema: function(data_source) {
|
@@ -0,0 +1,240 @@
|
|
1
|
+
/*
|
2
|
+
* Draws a simple barchart wiht a slection brush.
|
3
|
+
*
|
4
|
+
* Most code was copied from http://square.github.com/crossfilter/ and all that
|
5
|
+
* credit goes to Mike Bostok.
|
6
|
+
*/
|
7
|
+
|
8
|
+
|
9
|
+
L.provide('OML.barchart_brush', ["graph/js/abstract_chart", "#OML.abstract_chart"], function () {
|
10
|
+
|
11
|
+
OML.barchart_brush = OML.abstract_chart.extend({
|
12
|
+
decl_properties: [
|
13
|
+
['key', 'int', {property: 'key'}],
|
14
|
+
['value', 'float', {property: 'value'}],
|
15
|
+
// ['x_axis', 'key', {property: 'x'}],
|
16
|
+
// ['y_axis', 'key', {property: 'y'}],
|
17
|
+
// ['group_by', 'key', {property: 'id', optional: true}],
|
18
|
+
['stroke_width', 'int', 2],
|
19
|
+
['stroke_color', 'color', 'white'],
|
20
|
+
['fill_color', 'color', 'blue']
|
21
|
+
],
|
22
|
+
|
23
|
+
defaults: function() {
|
24
|
+
return this.deep_defaults({
|
25
|
+
relative: false, // If true, report percentage
|
26
|
+
axis: {
|
27
|
+
orientation: 'horizontal'
|
28
|
+
}
|
29
|
+
}, OML.barchart_brush.__super__.defaults.call(this));
|
30
|
+
},
|
31
|
+
|
32
|
+
configure_base_layer: function(vis) {
|
33
|
+
var base = this.base_layer = vis.append("svg:g")
|
34
|
+
.attr("class", "barchart")
|
35
|
+
;
|
36
|
+
var ca = this.chart_area;
|
37
|
+
this.legend_layer = base.append("svg:g");
|
38
|
+
this.chart_layer = base.append("svg:g");
|
39
|
+
this.axis_layer = base.append('g');
|
40
|
+
},
|
41
|
+
|
42
|
+
redraw: function(data) {
|
43
|
+
|
44
|
+
}
|
45
|
+
}) // end of barchart_brush
|
46
|
+
|
47
|
+
|
48
|
+
|
49
|
+
OML._barchart_brush = function barChart() {
|
50
|
+
if (!OML._barchart_brush.id) OML._barchart_brush.id = 0;
|
51
|
+
|
52
|
+
var margin = {top: 10, right: 10, bottom: 20, left: 10},
|
53
|
+
x,
|
54
|
+
y = d3.scale.linear().range([100, 0]),
|
55
|
+
id = OML._barchart_brush.id++,
|
56
|
+
axis = d3.svg.axis().orient("bottom"),
|
57
|
+
brush = d3.svg.brush(),
|
58
|
+
brushDirty,
|
59
|
+
dimension,
|
60
|
+
group,
|
61
|
+
round;
|
62
|
+
|
63
|
+
function chart(div) {
|
64
|
+
var width = x.range()[1],
|
65
|
+
height = y.range()[0];
|
66
|
+
|
67
|
+
y.domain([0, group.top(1)[0].value]);
|
68
|
+
|
69
|
+
div.each(function() {
|
70
|
+
var div = d3.select(this),
|
71
|
+
g = div.select("g");
|
72
|
+
|
73
|
+
// Create the skeletal chart.
|
74
|
+
if (g.empty()) {
|
75
|
+
div.select(".title").append("a")
|
76
|
+
.attr("href", "javascript:reset(" + id + ")")
|
77
|
+
.attr("class", "reset")
|
78
|
+
.text("reset")
|
79
|
+
.style("display", "none");
|
80
|
+
|
81
|
+
g = div.append("svg")
|
82
|
+
.attr("width", width + margin.left + margin.right)
|
83
|
+
.attr("height", height + margin.top + margin.bottom)
|
84
|
+
.append("g")
|
85
|
+
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
|
86
|
+
|
87
|
+
g.append("clipPath")
|
88
|
+
.attr("id", "clip-" + id)
|
89
|
+
.append("rect")
|
90
|
+
.attr("width", width)
|
91
|
+
.attr("height", height);
|
92
|
+
|
93
|
+
g.selectAll(".bar")
|
94
|
+
.data(["background", "foreground"])
|
95
|
+
.enter().append("path")
|
96
|
+
.attr("class", function(d) { return d + " bar"; })
|
97
|
+
.datum(group.all());
|
98
|
+
|
99
|
+
g.selectAll(".foreground.bar")
|
100
|
+
.attr("clip-path", "url(#clip-" + id + ")");
|
101
|
+
|
102
|
+
g.append("g")
|
103
|
+
.attr("class", "axis")
|
104
|
+
.attr("transform", "translate(0," + height + ")")
|
105
|
+
.call(axis);
|
106
|
+
|
107
|
+
// Initialize the brush component with pretty resize handles.
|
108
|
+
var gBrush = g.append("g").attr("class", "brush").call(brush);
|
109
|
+
gBrush.selectAll("rect").attr("height", height);
|
110
|
+
gBrush.selectAll(".resize").append("path").attr("d", resizePath);
|
111
|
+
}
|
112
|
+
|
113
|
+
// Only redraw the brush if set externally.
|
114
|
+
if (brushDirty) {
|
115
|
+
brushDirty = false;
|
116
|
+
g.selectAll(".brush").call(brush);
|
117
|
+
div.select(".title a").style("display", brush.empty() ? "none" : null);
|
118
|
+
if (brush.empty()) {
|
119
|
+
g.selectAll("#clip-" + id + " rect")
|
120
|
+
.attr("x", 0)
|
121
|
+
.attr("width", width);
|
122
|
+
} else {
|
123
|
+
var extent = brush.extent();
|
124
|
+
g.selectAll("#clip-" + id + " rect")
|
125
|
+
.attr("x", x(extent[0]))
|
126
|
+
.attr("width", x(extent[1]) - x(extent[0]));
|
127
|
+
}
|
128
|
+
}
|
129
|
+
|
130
|
+
g.selectAll(".bar").attr("d", barPath);
|
131
|
+
});
|
132
|
+
|
133
|
+
function barPath(groups) {
|
134
|
+
var path = [],
|
135
|
+
i = -1,
|
136
|
+
n = groups.length,
|
137
|
+
d;
|
138
|
+
while (++i < n) {
|
139
|
+
d = groups[i];
|
140
|
+
path.push("M", x(d.key), ",", height, "V", y(d.value), "h9V", height);
|
141
|
+
}
|
142
|
+
return path.join("");
|
143
|
+
}
|
144
|
+
|
145
|
+
function resizePath(d) {
|
146
|
+
var e = +(d == "e"),
|
147
|
+
x = e ? 1 : -1,
|
148
|
+
y = height / 3;
|
149
|
+
return "M" + (.5 * x) + "," + y
|
150
|
+
+ "A6,6 0 0 " + e + " " + (6.5 * x) + "," + (y + 6)
|
151
|
+
+ "V" + (2 * y - 6)
|
152
|
+
+ "A6,6 0 0 " + e + " " + (.5 * x) + "," + (2 * y)
|
153
|
+
+ "Z"
|
154
|
+
+ "M" + (2.5 * x) + "," + (y + 8)
|
155
|
+
+ "V" + (2 * y - 8)
|
156
|
+
+ "M" + (4.5 * x) + "," + (y + 8)
|
157
|
+
+ "V" + (2 * y - 8);
|
158
|
+
}
|
159
|
+
}
|
160
|
+
|
161
|
+
brush.on("brushstart.chart", function() {
|
162
|
+
var div = d3.select(this.parentNode.parentNode.parentNode);
|
163
|
+
div.select(".title a").style("display", null);
|
164
|
+
});
|
165
|
+
|
166
|
+
brush.on("brush.chart", function() {
|
167
|
+
var g = d3.select(this.parentNode),
|
168
|
+
extent = brush.extent();
|
169
|
+
if (round) g.select(".brush")
|
170
|
+
.call(brush.extent(extent = extent.map(round)))
|
171
|
+
.selectAll(".resize")
|
172
|
+
.style("display", null);
|
173
|
+
g.select("#clip-" + id + " rect")
|
174
|
+
.attr("x", x(extent[0]))
|
175
|
+
.attr("width", x(extent[1]) - x(extent[0]));
|
176
|
+
dimension.filterRange(extent);
|
177
|
+
});
|
178
|
+
|
179
|
+
brush.on("brushend.chart", function() {
|
180
|
+
if (brush.empty()) {
|
181
|
+
var div = d3.select(this.parentNode.parentNode.parentNode);
|
182
|
+
div.select(".title a").style("display", "none");
|
183
|
+
div.select("#clip-" + id + " rect").attr("x", null).attr("width", "100%");
|
184
|
+
dimension.filterAll();
|
185
|
+
}
|
186
|
+
});
|
187
|
+
|
188
|
+
chart.margin = function(_) {
|
189
|
+
if (!arguments.length) return margin;
|
190
|
+
margin = _;
|
191
|
+
return chart;
|
192
|
+
};
|
193
|
+
|
194
|
+
chart.x = function(_) {
|
195
|
+
if (!arguments.length) return x;
|
196
|
+
x = _;
|
197
|
+
axis.scale(x);
|
198
|
+
brush.x(x);
|
199
|
+
return chart;
|
200
|
+
};
|
201
|
+
|
202
|
+
chart.y = function(_) {
|
203
|
+
if (!arguments.length) return y;
|
204
|
+
y = _;
|
205
|
+
return chart;
|
206
|
+
};
|
207
|
+
|
208
|
+
chart.dimension = function(_) {
|
209
|
+
if (!arguments.length) return dimension;
|
210
|
+
dimension = _;
|
211
|
+
return chart;
|
212
|
+
};
|
213
|
+
|
214
|
+
chart.filter = function(_) {
|
215
|
+
if (_) {
|
216
|
+
brush.extent(_);
|
217
|
+
dimension.filterRange(_);
|
218
|
+
} else {
|
219
|
+
brush.clear();
|
220
|
+
dimension.filterAll();
|
221
|
+
}
|
222
|
+
brushDirty = true;
|
223
|
+
return chart;
|
224
|
+
};
|
225
|
+
|
226
|
+
chart.group = function(_) {
|
227
|
+
if (!arguments.length) return group;
|
228
|
+
group = _;
|
229
|
+
return chart;
|
230
|
+
};
|
231
|
+
|
232
|
+
chart.round = function(_) {
|
233
|
+
if (!arguments.length) return round;
|
234
|
+
round = _;
|
235
|
+
return chart;
|
236
|
+
};
|
237
|
+
|
238
|
+
return d3.rebind(chart, brush, "on");
|
239
|
+
}
|
240
|
+
});
|