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.
Files changed (62) hide show
  1. data/.gitignore +1 -0
  2. data/README.md +1 -1
  3. data/bin/omf_web_demo +3 -0
  4. data/bin/omf_web_demo.sh +0 -0
  5. data/doc/tutorial/tut01/hello_world.rb +27 -0
  6. data/doc/tutorial/tut02/hello_graph.rb +85 -0
  7. data/doc/tutorial/tut03/hello_database.rb +69 -0
  8. data/doc/tutorial/tut03/nmetric.sq3 +0 -0
  9. data/example/bridge/config.ru +2 -1
  10. data/example/demo/data_sources/downloads.csv +96 -1
  11. data/example/demo/demo_viz_server.rb +46 -28
  12. data/example/openflow-gec15/README.md +2 -0
  13. data/example/openflow-gec15/doc/gec15_topo.png +0 -0
  14. data/example/openflow-gec15/exp_source.rb +48 -9
  15. data/example/openflow-gec15/of_viz_server.rb +1 -1
  16. data/example/openflow-gec15/repository/of-exp.rb +117 -12
  17. data/example/openflow-gec15/repository/trema-ctl6.rb +2 -2
  18. data/example/simple/README.md +29 -1
  19. data/example/simple/data_sources/ping_source.rb +2 -2
  20. data/lib/irods4r/directory.rb +49 -0
  21. data/lib/irods4r/file.rb +53 -0
  22. data/lib/irods4r/icommands.rb +63 -0
  23. data/lib/irods4r.rb +34 -0
  24. data/lib/omf-web/content/content_proxy.rb +14 -5
  25. data/lib/omf-web/content/file_repository.rb +36 -75
  26. data/lib/omf-web/content/git_repository.rb +39 -35
  27. data/lib/omf-web/content/irods_repository.rb +191 -0
  28. data/lib/omf-web/content/repository.rb +84 -21
  29. data/lib/omf-web/content/static_repository.rb +61 -0
  30. data/lib/omf-web/data_source_proxy.rb +3 -3
  31. data/lib/omf-web/rack/session_authenticator.rb +67 -35
  32. data/lib/omf-web/rack/tab_mapper.rb +2 -1
  33. data/lib/omf-web/rack/websocket_handler.rb +49 -10
  34. data/lib/omf-web/session_store.rb +9 -8
  35. data/lib/omf-web/theme/bright/page.rb +1 -1
  36. data/lib/omf-web/thin/runner.rb +18 -5
  37. data/lib/omf-web/version.rb +1 -1
  38. data/lib/omf-web/widget/text/maruku/output/to_html.rb +8 -2
  39. data/lib/omf_web.rb +17 -2
  40. data/omf_web.gemspec +0 -1
  41. data/share/htdocs/graph/js/abstract_widget.js +3 -1
  42. data/share/htdocs/graph/js/barchart_brush.js +240 -0
  43. data/share/htdocs/js/data_source2.js +4 -1
  44. data/share/htdocs/js/mustache.js +17 -11
  45. data/share/htdocs/theme/bright/css/bright.css +1 -1
  46. data/share/htdocs/vendor/{bootstrap-2.1.1 → bootstrap-2.3.1}/css/bootstrap-responsive.css +56 -5
  47. data/share/htdocs/vendor/bootstrap-2.3.1/css/bootstrap-responsive.min.css +9 -0
  48. data/share/htdocs/vendor/{bootstrap-2.1.1 → bootstrap-2.3.1}/css/bootstrap.css +856 -472
  49. data/share/htdocs/vendor/bootstrap-2.3.1/css/bootstrap.min.css +9 -0
  50. data/share/htdocs/vendor/{bootstrap-2.1.1 → bootstrap-2.3.1}/img/glyphicons-halflings-white.png +0 -0
  51. data/share/htdocs/vendor/{bootstrap-2.1.1 → bootstrap-2.3.1}/img/glyphicons-halflings.png +0 -0
  52. data/share/htdocs/vendor/{bootstrap-2.1.1 → bootstrap-2.3.1}/js/bootstrap.js +427 -178
  53. data/share/htdocs/vendor/bootstrap-2.3.1/js/bootstrap.min.js +6 -0
  54. data/share/htdocs/vendor/jquery-tipsy/css/tipsy.css +25 -0
  55. data/share/htdocs/vendor/jquery-tipsy/js/jquery.tipsy.js +258 -0
  56. metadata +27 -14
  57. data/bin/omf-web-basic +0 -235
  58. data/share/htdocs/vendor/.DS_Store +0 -0
  59. data/share/htdocs/vendor/bootstrap-2.1.1/css/bootstrap-responsive.min.css +0 -9
  60. data/share/htdocs/vendor/bootstrap-2.1.1/css/bootstrap.min.css +0 -9
  61. data/share/htdocs/vendor/bootstrap-2.1.1/js/bootstrap.min.js +0 -6
  62. 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
- # :no_session - Array of regexp to ignore
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
- #puts "REQUEST: #{path_info}"
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
- sid = req.cookies['sid'] || "s#{(rand * 10000000).to_i}_#{(rand * 10000000).to_i}"
62
- debug "Setting session for '#{req.path_info}' to '#{sid}'"
63
- Thread.current["sessionID"] = sid
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
- if authenticated = self.class[:authenticated]
68
- # Check if it hasn't imed out
69
- if self.class[:valid_until] < Time.now
70
- debug "Session '#{sid}' expired"
71
- authenticated = false
72
- end
73
- end
74
- unless authenticated
75
- return [301, {'Location' => login_url, "Content-Type" => ""}, ['Login first']]
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
- headers['Set-Cookie'] = "sid=#{sid}" ##: name2=value2; Expires=Wed, 09-Jun-2021 ]
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.script_name}::#{req.inspect}"
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
- msg = {
58
- type: 'datasource_update',
59
- datasource: dsp.name,
60
- rows: rows,
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
- send_data({type: 'reply', status: 'ok'}.to_json)
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(sid = nil)
23
- unless sid
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
@@ -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
- error ex
106
- debug "#{ex.backtrace.join("\n")}"
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
 
@@ -1,7 +1,7 @@
1
1
 
2
2
  module OMF
3
3
  module Web
4
- VERSION = '0.9.7'
4
+ VERSION = '0.9.8'
5
5
  # Used for finding the example directory
6
6
  TOP_DIR = File.dirname(File.dirname(File.dirname(__FILE__)))
7
7
  end
@@ -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
- a.attributes['src'] = url.to_s
819
- a.attributes['alt'] = children_to_s
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
@@ -4,7 +4,6 @@ require "omf-web/version"
4
4
 
5
5
  Gem::Specification.new do |s|
6
6
  s.name = "omf_web"
7
- # s.version = OmfWeb::VERSION
8
7
  s.version = OMF::Web::VERSION
9
8
  s.authors = ["NICTA"]
10
9
  s.email = ["omf-user@lists.nicta.com.au"]
@@ -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
- this.mapping = this.process_single_mapping(null, this.opts.mapping, this.decl_properties);
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
+ });