omf_web 0.9.7 → 0.9.8
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 +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
|
+
});
|