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