blacklight_range_limit 1.2.0 → 1.2.1

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore CHANGED
@@ -17,5 +17,6 @@ tmtags
17
17
  coverage
18
18
  rdoc
19
19
  pkg
20
+ Gemfile.lock
20
21
 
21
22
  ## PROJECT::SPECIFIC
data/Gemfile ADDED
@@ -0,0 +1,5 @@
1
+ source 'http://rubygems.org'
2
+
3
+ gemspec
4
+
5
+ gem 'combustion'
@@ -23,13 +23,19 @@ The pre Solr 1.4 now deprecated sint or slong types should work fine too.
23
23
 
24
24
  = Installation
25
25
 
26
- == Blacklight 3.x/Rails 3.x
26
+ Recent versions of this plugin require Blacklight versions 3.2 or later, which requires Rails 3.1 or later and the Rails asset pipeline.
27
27
 
28
28
  Add
29
+
29
30
  gem "blacklight_range_limit"
31
+
30
32
  to your Gemfile. Run "bundle install".
31
33
 
32
- Then run "rails generate blacklight_range_limit". This will install some asset references in your application.js and application.css
34
+ Then run
35
+
36
+ rails generate blacklight_range_limit
37
+
38
+ This will install some asset references in your application.js and application.css.
33
39
 
34
40
  = Configuration
35
41
 
@@ -39,7 +45,6 @@ You have at least one solr field you want to display as a range limit, that's wh
39
45
 
40
46
  You should now get range limit display. More complicated configuration is available if desired, see Range Facet Configuration below.
41
47
 
42
- Note that the plugin will be default inject links to the Flot JQuery plugin, and a couple dependencies. The weird way it has to do this may fail in weird configurations. You can turn this off and instead include Flot and its dependencies manually in your application, see Injection below.
43
48
 
44
49
  You can also configure the look and feel of the Flot chart using the jQuery .data() method. On the `.facet_limit` container you want to configure, add a Flot options associative array (documented at http://people.iola.dk/olau/flot/API.txt) as the `plot-config` key. The `plot-config` key to set the `plot-config` key on the appropriate `.facet_limit` container. In order to customize the plot colors, for example, you could use this code:
45
50
 
@@ -52,6 +57,7 @@ You can also configure the look and feel of the Flot chart using the jQuery .dat
52
57
 
53
58
  You can add this configuration in app/assets/javascript/application.js, or anywhere else loaded before the blacklight range limit javascript.
54
59
 
60
+
55
61
  == A note on AJAX use
56
62
 
57
63
  In order to calculate distribution segment ranges, we need to first know the min and max boundaries. But we don't really know that until we've fetched the result set (we use the Solr Stats component to get min and max with a result set).
@@ -69,24 +75,33 @@ Note that a drill-down will never require the second request, because boundaries
69
75
 
70
76
  Instead of simply passing "true", you can pass a hash with additional configuration. Here's an example with all the available keys, you don't need to use them all, just the ones you want to set to non-default values.
71
77
 
72
- config.add_facet_field 'pub_date', :label => 'Publication Year', :range => {
73
- :num_segments => 6,
74
- :assumed_boundaries => [1100, Time.now.year + 2],
75
- :slider_js => false,
76
- :chart_js => false,
77
- :segments => false
78
- }
78
+ config.add_facet_field 'pub_date', :label => 'Publication Year', :range => {
79
+ :num_segments => 6,
80
+ :assumed_boundaries => [1100, Time.now.year + 2],
81
+ :segments => false
82
+ }
79
83
 
80
84
  [num_segments]
81
85
  Default 10. Approximately how many segments to divide the range into for segment facets, which become segments on the chart. Actual segments are calculated to be 'nice' values, so may not exactly match your setting.
82
86
  [assumed_boundaries]
83
87
  Default null. For a result set that has not yet been limited, instead of taking boundaries from results and making a second AJAX request to fetch segments, just assume these given boundaries. If you'd like to avoid this second AJAX Solr call, you can set :assumed_boundaries to a two-element array of integers instead, and the assumed boundaries will always be used. Note this is live ruby code, you can put calculations in there like Time.now.year + 2.
84
- [:slider_js]
85
- Default true. If set to false, then the slider javascript behavior will not be loaded. Without this behavior, you will see a div with textual min and max values. You can hide that with CSS if you like.
86
- [:chart_js]
87
- Default true. If set to false, then the Flot area-chart is not loaded. You will instead get textual facet values for each of the segment ranges. Note that this still often will result in a second AJAX request to fetch ranges, you haven't disabled AJAX by setting :chart_js=>true. If you'd like to turn off segment ranges altogether, see :segments. If you'd like to prevent the ajax but keep segments, see :assumed_boundaries.
88
88
  [:segments]
89
89
  Default true. If set to false, then distribution segment facets will not be loaded at all.
90
+
91
+ == Javascript dependencies
92
+
93
+ The selectable histograms/barcharts are done with Javascript, using Flot[http://code.google.com/p/flot/]. Flot requires JQuery, as well as support for the HTML5 canvas element. In IE previous to IE9, canvas element support is added with excanvas[http://excanvas.sourceforge.net/].
94
+
95
+ A `require 'blacklight_range_limit'` in a Rails asset pipeline manifest file will automatically include all of these things. The blacklight_range_limit adds just this line to your `app/assets/application.js`.
96
+
97
+ There is a copy of flot vendored in this gem for this purpose. jquery is obtained from the jquery-rails gem, which this gem depends on.
98
+
99
+ Note this means a copy of jquery, from the jquery-rails gem, will be included in your assets by blacklight_range_limit even if you didn't include it yourself explicitly in application.js. Flot will also be included.
100
+
101
+ If you don't want any of this gem's JS, you can simply remove the `require 'blacklight_range_limit'` line from your application.js, and hack something else together yourself.
102
+
103
+ The excanvas inclusion for IE is handled a bit differently. Coudln't get conditional inclusion of excanvas in pure JS to work, it does need an actual seperate script line in the HTML document surrounded by IE conditional comments; this gem adds that line using Blacklight's `extra_head_content` feature allowing dependencies to inject content in HTML head; requires your layout to follow BL conventions, or just add excanvas yourself manually. This gem's attempt to inject the excanvas script line can be turned off in configuration, see Injection below.
104
+
90
105
 
91
106
  == Injection
92
107
 
@@ -103,20 +118,29 @@ to turn off all injection. The plugin will be completely non-functional if you d
103
118
  You can also turn off injection of individual components, which could be more useful:
104
119
 
105
120
  BlacklightRangeLimit.omit_inject = {
106
- :excanvas => false,
107
121
  :view_helpers => false,
108
- :controller_mixin => false
122
+ :controller_mixin => false,
123
+ :routes => false,
124
+ :excanvas => false
109
125
  }
110
- [:excanvas]
111
- Set to false to disable loading the IE 'excanvas' compatibility layer
112
126
  [:view_helpers]
113
127
  Set to false and the plugin will not insert it's own rails view helpers into the app. It will raise lots of errors if you do this, you probably don't want to.
114
128
  [:controller_mixin]
115
- The plugin mixes some methods into CatalogController, both over-riding Blacklight methods, and providing a new action of it's own. Set to false, and the plugin won't. You've basically disabled the plugin if you do this.
129
+ The plugin mixes some methods into CatalogController, both over-riding Blacklight methods, and providing a new action of it's own. Set to false, and the plugin won't. You've basically disabled the plugin if you do this.
130
+ [:routes]
131
+ Disable automatic routes loading
132
+ [:excanvas]
133
+ Disables injection of a conditionally-commented script tag to load the excanvas library for supporting 'canvas' on IE. blacklight_range_limit does this in the controller mixin, using Blacklight's "extra_head_content" feature to add actual conditional script tag for IE in html <head>.
134
+
135
+ See Javascript Dependencies above for disabling injection of gem's js.
116
136
 
117
137
  = Tests
118
138
 
119
- There are none. This is bad I know, sorry.
139
+ Start the Blacklight demo jetty server (on port 8983)
140
+
141
+ Run the rspec tests by running:
142
+
143
+ rspec
120
144
 
121
145
  = Possible future To Do
122
146
  * StatsComponent replacement. We use StatsComponent to get min/max of result set, as well as missing count. StatsComponent is included on every non-drilldown request, so ranges and slider can be displayed. However, StatsComponent really can slow down the solr response with a large result set. So replace StatsComponent with other strategies. No ideal ones, we can use facet.missing to get missing count instead, but RSolr makes it harder than it should be to grab this info. We can use seperate solr queries to get min/max (sort on our field, asc and desc), but this is more complicated, more solr queries, and possibly requires redesign of AJAXy stuff, so even a lone slider can have min/max.
data/VERSION CHANGED
@@ -1 +1 @@
1
- 1.2.0
1
+ 1.2.1
@@ -3,7 +3,6 @@
3
3
  // require does not need to change if we change file list.
4
4
 
5
5
  //= require 'jquery'
6
- //= require 'jquery-ui'
7
6
  //= require 'flot/jquery.flot.js'
8
7
  //= require 'flot/jquery.flot.selection.js'
9
8
  //= require_tree './blacklight_range_limit'
@@ -19,5 +19,12 @@ Gem::Specification.new do |s|
19
19
 
20
20
 
21
21
  s.add_dependency "rails", "~> 3.0"
22
+ s.add_dependency "jquery-rails" # our JS needs jquery_rails
22
23
  s.add_dependency "blacklight", "~> 3.2"
24
+
25
+ s.add_development_dependency "rspec"
26
+ s.add_development_dependency "rspec-rails"
27
+ s.add_development_dependency "capybara"
28
+ s.add_development_dependency "sqlite3"
29
+ s.add_development_dependency 'launchy'
23
30
  end
@@ -24,7 +24,7 @@ module BlacklightRangeLimit
24
24
  # canvas for IE. Need to inject it like this even with asset pipeline
25
25
  # cause it needs IE conditional include. view_context hacky way
26
26
  # to get asset url helpers.
27
- controller.extra_head_content << ('<!--[if IE]>' + view_context.javascript_include_tag("flot/excanvas.min.js") + '<![endif]-->').html_safe
27
+ controller.extra_head_content << ('<!--[if lt IE 9]>' + view_context.javascript_include_tag("flot/excanvas.min.js") + ' <![endif]-->').html_safe
28
28
  end
29
29
  end
30
30
  end
@@ -5,6 +5,12 @@ require 'rails'
5
5
  module BlacklightRangeLimit
6
6
  class Engine < Rails::Engine
7
7
 
8
+ # Need to tell asset pipeline to precompile the excanvas
9
+ # we use for IE.
10
+ initializer "blacklight_range_limit.assets", :after => "assets" do
11
+ Rails.application.config.assets.precompile += %w( flot/excanvas.min.js )
12
+ end
13
+
8
14
  # Do these things in a to_prepare block, to try and make them work
9
15
  # in development mode with class-reloading. The trick is we can't
10
16
  # be sure if the controllers we're modifying are being reloaded in
@@ -25,10 +25,11 @@ module BlacklightRangeLimit
25
25
  }
26
26
  end
27
27
 
28
- insert_into_file "app/assets/javascripts/application.js", :after => "//= require jquery$" do
28
+ insert_into_file "app/assets/javascripts/application.js", :after => "//= require jquery\n" do
29
29
  %q{
30
30
 
31
- // You can elmiminate one or both of these if you don't want their functionality
31
+ // For blacklight_range_limit built-in JS, if you don't want it you don't need
32
+ // this:
32
33
  //= require 'blacklight_range_limit'
33
34
 
34
35
  }
@@ -0,0 +1,36 @@
1
+ require 'spec_helper'
2
+
3
+ describe "Blacklight Range Limit" do
4
+ before do
5
+ CatalogController.blacklight_config = Blacklight::Configuration.new
6
+ CatalogController.configure_blacklight do |config|
7
+ config.add_facet_field 'pub_date_sort', :range => true
8
+ config.default_solr_params[:'facet.field'] = config.facet_fields.keys
9
+ end
10
+
11
+ end
12
+
13
+ it "should show the range limit facet" do
14
+ visit '/catalog?q='
15
+ page.should have_selector 'input.range_begin'
16
+ page.should have_selector 'input.range_end'
17
+ end
18
+
19
+ it "should provide distribution information" do
20
+ visit '/catalog?q='
21
+ click_link 'View distribution'
22
+
23
+ page.should have_content("1941 to 1944 (1)")
24
+ page.should have_content("2005 to 2008 (7)")
25
+ end
26
+
27
+ it "should limit appropriately" do
28
+ visit '/catalog?q='
29
+ click_link 'View distribution'
30
+ click_link '1941 to 1944'
31
+
32
+ save_and_open_page
33
+
34
+ page.should have_content "1941 to 1944 (1) [remove]"
35
+ end
36
+ end
@@ -0,0 +1,10 @@
1
+ require 'spec_helper'
2
+
3
+ describe 'Blacklight Test Application' do
4
+ it "should have a Blacklight module" do
5
+ Blacklight.should be_a_kind_of(Module)
6
+ end
7
+ it "should have a Catalog controller" do
8
+ CatalogController.blacklight_config.should be_a_kind_of(Blacklight::Configuration)
9
+ end
10
+ end
@@ -0,0 +1,4 @@
1
+ class ApplicationController < ActionController::Base
2
+ include Blacklight::Controller
3
+
4
+ end
@@ -0,0 +1,3 @@
1
+ class SolrDocument
2
+ include Blacklight::Solr::Document
3
+ end
@@ -0,0 +1,3 @@
1
+ test:
2
+ adapter: sqlite3
3
+ database: db/combustion_test.sqlite
@@ -0,0 +1,6 @@
1
+ Rails.application.routes.draw do
2
+ Blacklight.add_routes(self)
3
+
4
+ root :to => "catalog#index"
5
+ #
6
+ end
@@ -0,0 +1,18 @@
1
+ # = jetty_path key
2
+ # each environment can have a jetty_path with absolute or relative
3
+ # (to app root) path to a jetty/solr install. This is used
4
+ # by the rake tasks that start up solr automatically for testing
5
+ # and by rake solr:marc:index.
6
+ #
7
+ # jetty_path is not used by a running Blacklight application
8
+ # at all. In general you do NOT need to deploy solr in Jetty, you can deploy it
9
+ # however you want.
10
+ # jetty_path is only required for rake tasks that need to know
11
+ # how to start up solr, generally for automated testing.
12
+
13
+ development:
14
+ url: http://127.0.0.1:8983/solr
15
+ test: &test
16
+ url: http://127.0.0.1:8983/solr
17
+ cucumber:
18
+ <<: *test
@@ -0,0 +1,53 @@
1
+ # encoding: UTF-8
2
+ # This file is auto-generated from the current state of the database. Instead
3
+ # of editing this file, please use the migrations feature of Active Record to
4
+ # incrementally modify your database, and then regenerate this schema definition.
5
+ #
6
+ # Note that this schema.rb definition is the authoritative source for your
7
+ # database schema. If you need to create the application database on another
8
+ # system, you should be using db:schema:load, not running all the migrations
9
+ # from scratch. The latter is a flawed and unsustainable approach (the more migrations
10
+ # you'll amass, the slower it'll run and the greater likelihood for issues).
11
+ #
12
+ # It's strongly recommended to check this file into your version control system.
13
+
14
+ ActiveRecord::Schema.define(:version => 20111123152341) do
15
+
16
+ create_table "bookmarks", :force => true do |t|
17
+ t.integer "user_id", :null => false
18
+ t.string "document_id"
19
+ t.string "title"
20
+ t.datetime "created_at"
21
+ t.datetime "updated_at"
22
+ t.string "user_type"
23
+ end
24
+
25
+ create_table "searches", :force => true do |t|
26
+ t.text "query_params"
27
+ t.integer "user_id"
28
+ t.datetime "created_at"
29
+ t.datetime "updated_at"
30
+ t.string "user_type"
31
+ end
32
+
33
+ add_index "searches", ["user_id"], :name => "index_searches_on_user_id"
34
+
35
+ create_table "users", :force => true do |t|
36
+ t.string "email", :default => "", :null => false
37
+ t.string "encrypted_password", :limit => 128, :default => "", :null => false
38
+ t.string "reset_password_token"
39
+ t.datetime "reset_password_sent_at"
40
+ t.datetime "remember_created_at"
41
+ t.integer "sign_in_count", :default => 0
42
+ t.datetime "current_sign_in_at"
43
+ t.datetime "last_sign_in_at"
44
+ t.string "current_sign_in_ip"
45
+ t.string "last_sign_in_ip"
46
+ t.datetime "created_at"
47
+ t.datetime "updated_at"
48
+ end
49
+
50
+ add_index "users", ["email"], :name => "index_users_on_email", :unique => true
51
+ add_index "users", ["reset_password_token"], :name => "index_users_on_reset_password_token", :unique => true
52
+
53
+ end
@@ -0,0 +1 @@
1
+ *.log
File without changes
@@ -0,0 +1,25 @@
1
+ require 'rubygems'
2
+ require 'bundler'
3
+
4
+ Bundler.require :default, :development
5
+
6
+ require 'blacklight/engine'
7
+ require 'rsolr'
8
+ require 'rsolr-ext'
9
+ require 'capybara/rspec'
10
+ Combustion.initialize!
11
+
12
+ Blacklight.solr_config = { :url => 'http://127.0.0.1:8983/solr' }
13
+
14
+ class SolrDocument
15
+ include Blacklight::Solr::Document
16
+ end
17
+
18
+ require 'rspec/rails'
19
+ require 'capybara/rails'
20
+
21
+
22
+ RSpec.configure do |config|
23
+
24
+ end
25
+
@@ -1,4 +1,4 @@
1
- /* Javascript plotting library for jQuery, v. 0.6.
1
+ /*! Javascript plotting library for jQuery, v. 0.7.
2
2
  *
3
3
  * Released under the MIT license by IOLA, December 2007.
4
4
  *
@@ -9,7 +9,7 @@
9
9
 
10
10
  /* Plugin for jQuery for working with colors.
11
11
  *
12
- * Version 1.0.
12
+ * Version 1.1.
13
13
  *
14
14
  * Inspiration from jQuery color animation plugin by John Resig.
15
15
  *
@@ -22,10 +22,13 @@
22
22
  * console.log(c.r, c.g, c.b, c.a);
23
23
  * $.color.make(100, 50, 25, 0.4).toString() // returns "rgba(100,50,25,0.4)"
24
24
  *
25
- * Note that .scale() and .add() work in-place instead of returning
26
- * new objects.
25
+ * Note that .scale() and .add() return the same modified object
26
+ * instead of making a new one.
27
+ *
28
+ * V. 1.1: Fix error handling so e.g. parsing an empty string does
29
+ * produce a color rather than just crashing.
27
30
  */
28
- (function(){jQuery.color={};jQuery.color.make=function(E,D,B,C){var F={};F.r=E||0;F.g=D||0;F.b=B||0;F.a=C!=null?C:1;F.add=function(I,H){for(var G=0;G<I.length;++G){F[I.charAt(G)]+=H}return F.normalize()};F.scale=function(I,H){for(var G=0;G<I.length;++G){F[I.charAt(G)]*=H}return F.normalize()};F.toString=function(){if(F.a>=1){return"rgb("+[F.r,F.g,F.b].join(",")+")"}else{return"rgba("+[F.r,F.g,F.b,F.a].join(",")+")"}};F.normalize=function(){function G(I,J,H){return J<I?I:(J>H?H:J)}F.r=G(0,parseInt(F.r),255);F.g=G(0,parseInt(F.g),255);F.b=G(0,parseInt(F.b),255);F.a=G(0,F.a,1);return F};F.clone=function(){return jQuery.color.make(F.r,F.b,F.g,F.a)};return F.normalize()};jQuery.color.extract=function(C,B){var D;do{D=C.css(B).toLowerCase();if(D!=""&&D!="transparent"){break}C=C.parent()}while(!jQuery.nodeName(C.get(0),"body"));if(D=="rgba(0, 0, 0, 0)"){D="transparent"}return jQuery.color.parse(D)};jQuery.color.parse=function(E){var D,B=jQuery.color.make;if(D=/rgb\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*\)/.exec(E)){return B(parseInt(D[1],10),parseInt(D[2],10),parseInt(D[3],10))}if(D=/rgba\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]+(?:\.[0-9]+)?)\s*\)/.exec(E)){return B(parseInt(D[1],10),parseInt(D[2],10),parseInt(D[3],10),parseFloat(D[4]))}if(D=/rgb\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*\)/.exec(E)){return B(parseFloat(D[1])*2.55,parseFloat(D[2])*2.55,parseFloat(D[3])*2.55)}if(D=/rgba\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\s*\)/.exec(E)){return B(parseFloat(D[1])*2.55,parseFloat(D[2])*2.55,parseFloat(D[3])*2.55,parseFloat(D[4]))}if(D=/#([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})/.exec(E)){return B(parseInt(D[1],16),parseInt(D[2],16),parseInt(D[3],16))}if(D=/#([a-fA-F0-9])([a-fA-F0-9])([a-fA-F0-9])/.exec(E)){return B(parseInt(D[1]+D[1],16),parseInt(D[2]+D[2],16),parseInt(D[3]+D[3],16))}var C=jQuery.trim(E).toLowerCase();if(C=="transparent"){return B(255,255,255,0)}else{D=A[C];return B(D[0],D[1],D[2])}};var A={aqua:[0,255,255],azure:[240,255,255],beige:[245,245,220],black:[0,0,0],blue:[0,0,255],brown:[165,42,42],cyan:[0,255,255],darkblue:[0,0,139],darkcyan:[0,139,139],darkgrey:[169,169,169],darkgreen:[0,100,0],darkkhaki:[189,183,107],darkmagenta:[139,0,139],darkolivegreen:[85,107,47],darkorange:[255,140,0],darkorchid:[153,50,204],darkred:[139,0,0],darksalmon:[233,150,122],darkviolet:[148,0,211],fuchsia:[255,0,255],gold:[255,215,0],green:[0,128,0],indigo:[75,0,130],khaki:[240,230,140],lightblue:[173,216,230],lightcyan:[224,255,255],lightgreen:[144,238,144],lightgrey:[211,211,211],lightpink:[255,182,193],lightyellow:[255,255,224],lime:[0,255,0],magenta:[255,0,255],maroon:[128,0,0],navy:[0,0,128],olive:[128,128,0],orange:[255,165,0],pink:[255,192,203],purple:[128,0,128],violet:[128,0,128],red:[255,0,0],silver:[192,192,192],white:[255,255,255],yellow:[255,255,0]}})();
31
+ (function(B){B.color={};B.color.make=function(F,E,C,D){var G={};G.r=F||0;G.g=E||0;G.b=C||0;G.a=D!=null?D:1;G.add=function(J,I){for(var H=0;H<J.length;++H){G[J.charAt(H)]+=I}return G.normalize()};G.scale=function(J,I){for(var H=0;H<J.length;++H){G[J.charAt(H)]*=I}return G.normalize()};G.toString=function(){if(G.a>=1){return"rgb("+[G.r,G.g,G.b].join(",")+")"}else{return"rgba("+[G.r,G.g,G.b,G.a].join(",")+")"}};G.normalize=function(){function H(J,K,I){return K<J?J:(K>I?I:K)}G.r=H(0,parseInt(G.r),255);G.g=H(0,parseInt(G.g),255);G.b=H(0,parseInt(G.b),255);G.a=H(0,G.a,1);return G};G.clone=function(){return B.color.make(G.r,G.b,G.g,G.a)};return G.normalize()};B.color.extract=function(D,C){var E;do{E=D.css(C).toLowerCase();if(E!=""&&E!="transparent"){break}D=D.parent()}while(!B.nodeName(D.get(0),"body"));if(E=="rgba(0, 0, 0, 0)"){E="transparent"}return B.color.parse(E)};B.color.parse=function(F){var E,C=B.color.make;if(E=/rgb\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*\)/.exec(F)){return C(parseInt(E[1],10),parseInt(E[2],10),parseInt(E[3],10))}if(E=/rgba\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]+(?:\.[0-9]+)?)\s*\)/.exec(F)){return C(parseInt(E[1],10),parseInt(E[2],10),parseInt(E[3],10),parseFloat(E[4]))}if(E=/rgb\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*\)/.exec(F)){return C(parseFloat(E[1])*2.55,parseFloat(E[2])*2.55,parseFloat(E[3])*2.55)}if(E=/rgba\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\s*\)/.exec(F)){return C(parseFloat(E[1])*2.55,parseFloat(E[2])*2.55,parseFloat(E[3])*2.55,parseFloat(E[4]))}if(E=/#([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})/.exec(F)){return C(parseInt(E[1],16),parseInt(E[2],16),parseInt(E[3],16))}if(E=/#([a-fA-F0-9])([a-fA-F0-9])([a-fA-F0-9])/.exec(F)){return C(parseInt(E[1]+E[1],16),parseInt(E[2]+E[2],16),parseInt(E[3]+E[3],16))}var D=B.trim(F).toLowerCase();if(D=="transparent"){return C(255,255,255,0)}else{E=A[D]||[0,0,0];return C(E[0],E[1],E[2])}};var A={aqua:[0,255,255],azure:[240,255,255],beige:[245,245,220],black:[0,0,0],blue:[0,0,255],brown:[165,42,42],cyan:[0,255,255],darkblue:[0,0,139],darkcyan:[0,139,139],darkgrey:[169,169,169],darkgreen:[0,100,0],darkkhaki:[189,183,107],darkmagenta:[139,0,139],darkolivegreen:[85,107,47],darkorange:[255,140,0],darkorchid:[153,50,204],darkred:[139,0,0],darksalmon:[233,150,122],darkviolet:[148,0,211],fuchsia:[255,0,255],gold:[255,215,0],green:[0,128,0],indigo:[75,0,130],khaki:[240,230,140],lightblue:[173,216,230],lightcyan:[224,255,255],lightgreen:[144,238,144],lightgrey:[211,211,211],lightpink:[255,182,193],lightyellow:[255,255,224],lime:[0,255,0],magenta:[255,0,255],maroon:[128,0,0],navy:[0,0,128],olive:[128,128,0],orange:[255,165,0],pink:[255,192,203],purple:[128,0,128],violet:[128,0,128],red:[255,0,0],silver:[192,192,192],white:[255,255,255],yellow:[255,255,0]}})(jQuery);
29
32
 
30
33
  // the actual Flot code
31
34
  (function($) {
@@ -51,6 +54,7 @@
51
54
  backgroundOpacity: 0.85 // set to 0 to avoid background
52
55
  },
53
56
  xaxis: {
57
+ show: null, // null = auto-detect, true = always, false = never
54
58
  position: "bottom", // or "top"
55
59
  mode: null, // null or "time"
56
60
  color: null, // base color, labels, ticks
@@ -64,6 +68,7 @@
64
68
  tickFormatter: null, // fn: number -> string
65
69
  labelWidth: null, // size of tick labels in pixels
66
70
  labelHeight: null,
71
+ reserveSpace: null, // whether to reserve space even if axis isn't shown
67
72
  tickLength: null, // size in pixels of ticks, or "full" for whole line
68
73
  alignTicksWithAxis: null, // axis number or null for no sync
69
74
 
@@ -119,6 +124,7 @@
119
124
  labelMargin: 5, // in pixels
120
125
  axisMargin: 8, // in pixels
121
126
  borderWidth: 2, // in pixels
127
+ minBorderMargin: null, // in pixels, null means taken from points radius
122
128
  markings: null, // array of ranges or fn: axes -> array of ranges
123
129
  markingsColor: "#f4f4f4",
124
130
  markingsLineWidth: 2,
@@ -145,7 +151,8 @@
145
151
  drawSeries: [],
146
152
  draw: [],
147
153
  bindEvents: [],
148
- drawOverlay: []
154
+ drawOverlay: [],
155
+ shutdown: []
149
156
  },
150
157
  plot = this;
151
158
 
@@ -165,30 +172,16 @@
165
172
  return o;
166
173
  };
167
174
  plot.getData = function () { return series; };
168
- plot.getAxis = function (dir, number) {
169
- var a = (dir == x ? xaxes : yaxes)[number - 1];
170
- if (a && !a.used)
171
- a = null;
172
- return a;
173
- };
174
175
  plot.getAxes = function () {
175
176
  var res = {}, i;
176
- for (i = 0; i < xaxes.length; ++i)
177
- res["x" + (i ? (i + 1) : "") + "axis"] = xaxes[i] || {};
178
- for (i = 0; i < yaxes.length; ++i)
179
- res["y" + (i ? (i + 1) : "") + "axis"] = yaxes[i] || {};
180
-
181
- // backwards compatibility - to be removed
182
- if (!res.x2axis)
183
- res.x2axis = { n: 2 };
184
- if (!res.y2axis)
185
- res.y2axis = { n: 2 };
186
-
177
+ $.each(xaxes.concat(yaxes), function (_, axis) {
178
+ if (axis)
179
+ res[axis.direction + (axis.n != 1 ? axis.n : "") + "axis"] = axis;
180
+ });
187
181
  return res;
188
182
  };
189
183
  plot.getXAxes = function () { return xaxes; };
190
184
  plot.getYAxes = function () { return yaxes; };
191
- plot.getUsedAxes = getUsedAxes; // return flat array with x and y axes that are in use
192
185
  plot.c2p = canvasToAxisCoords;
193
186
  plot.p2c = axisToCanvasCoords;
194
187
  plot.getOptions = function () { return options; };
@@ -201,6 +194,12 @@
201
194
  top: parseInt(yaxes[axisNumber(point, "y") - 1].p2c(+point.y) + plotOffset.top)
202
195
  };
203
196
  };
197
+ plot.shutdown = shutdown;
198
+ plot.resize = function () {
199
+ getCanvasDimensions();
200
+ resizeCanvas(canvas);
201
+ resizeCanvas(overlay);
202
+ };
204
203
 
205
204
  // public attributes
206
205
  plot.hooks = hooks;
@@ -208,7 +207,7 @@
208
207
  // initialize
209
208
  initPlugins(plot);
210
209
  parseOptions(options_);
211
- constructCanvas();
210
+ setupCanvases();
212
211
  setData(data_);
213
212
  setupGrid();
214
213
  draw();
@@ -256,20 +255,19 @@
256
255
  options.xaxes[i] = $.extend(true, {}, options.xaxis, options.xaxes[i]);
257
256
  for (i = 0; i < Math.max(1, options.yaxes.length); ++i)
258
257
  options.yaxes[i] = $.extend(true, {}, options.yaxis, options.yaxes[i]);
258
+
259
259
  // backwards compatibility, to be removed in future
260
260
  if (options.xaxis.noTicks && options.xaxis.ticks == null)
261
261
  options.xaxis.ticks = options.xaxis.noTicks;
262
262
  if (options.yaxis.noTicks && options.yaxis.ticks == null)
263
263
  options.yaxis.ticks = options.yaxis.noTicks;
264
264
  if (options.x2axis) {
265
- options.y2axis.position = "top";
266
- options.xaxes[1] = options.x2axis;
265
+ options.xaxes[1] = $.extend(true, {}, options.xaxis, options.x2axis);
266
+ options.xaxes[1].position = "top";
267
267
  }
268
268
  if (options.y2axis) {
269
- if (options.y2axis.autoscaleMargin === undefined)
270
- options.y2axis.autoscaleMargin = 0.02;
271
- options.y2axis.position = "right";
272
- options.yaxes[1] = options.y2axis;
269
+ options.yaxes[1] = $.extend(true, {}, options.yaxis, options.y2axis);
270
+ options.yaxes[1].position = "right";
273
271
  }
274
272
  if (options.grid.coloredAreas)
275
273
  options.grid.markings = options.grid.coloredAreas;
@@ -281,9 +279,10 @@
281
279
  $.extend(true, options.series.points, options.points);
282
280
  if (options.bars)
283
281
  $.extend(true, options.series.bars, options.bars);
284
- if (options.shadowSize)
282
+ if (options.shadowSize != null)
285
283
  options.series.shadowSize = options.shadowSize;
286
284
 
285
+ // save options on axes for future reference
287
286
  for (i = 0; i < options.xaxes.length; ++i)
288
287
  getOrCreateAxis(xaxes, i + 1).options = options.xaxes[i];
289
288
  for (i = 0; i < options.yaxes.length; ++i)
@@ -308,7 +307,7 @@
308
307
  for (var i = 0; i < d.length; ++i) {
309
308
  var s = $.extend(true, {}, options.series);
310
309
 
311
- if (d[i].data) {
310
+ if (d[i].data != null) {
312
311
  s.data = d[i].data; // move the data instead of deep-copy
313
312
  delete d[i].data;
314
313
 
@@ -333,6 +332,11 @@
333
332
  return a;
334
333
  }
335
334
 
335
+ function allAxes() {
336
+ // return flat array without annoying null entries
337
+ return $.grep(xaxes.concat(yaxes), function (a) { return a; });
338
+ }
339
+
336
340
  function canvasToAxisCoords(pos) {
337
341
  // return an object with x/y corresponding to all used axes
338
342
  var res = {}, i, axis;
@@ -367,7 +371,7 @@
367
371
  if (pos[key] == null && axis.n == 1)
368
372
  key = "x";
369
373
 
370
- if (pos[key]) {
374
+ if (pos[key] != null) {
371
375
  res.left = axis.p2c(pos[key]);
372
376
  break;
373
377
  }
@@ -381,7 +385,7 @@
381
385
  if (pos[key] == null && axis.n == 1)
382
386
  key = "y";
383
387
 
384
- if (pos[key]) {
388
+ if (pos[key] != null) {
385
389
  res.top = axis.p2c(pos[key]);
386
390
  break;
387
391
  }
@@ -391,21 +395,6 @@
391
395
  return res;
392
396
  }
393
397
 
394
- function getUsedAxes() {
395
- var res = [], i, axis;
396
- for (i = 0; i < xaxes.length; ++i) {
397
- axis = xaxes[i];
398
- if (axis && axis.used)
399
- res.push(axis);
400
- }
401
- for (i = 0; i < yaxes.length; ++i) {
402
- axis = yaxes[i];
403
- if (axis && axis.used)
404
- res.push(axis);
405
- }
406
- return res;
407
- }
408
-
409
398
  function getOrCreateAxis(axes, number) {
410
399
  if (!axes[number - 1])
411
400
  axes[number - 1] = {
@@ -500,29 +489,23 @@
500
489
  function processData() {
501
490
  var topSentry = Number.POSITIVE_INFINITY,
502
491
  bottomSentry = Number.NEGATIVE_INFINITY,
492
+ fakeInfinity = Number.MAX_VALUE,
503
493
  i, j, k, m, length,
504
494
  s, points, ps, x, y, axis, val, f, p;
505
495
 
506
- function initAxis(axis, number) {
507
- if (!axis)
508
- return;
509
-
510
- axis.datamin = topSentry;
511
- axis.datamax = bottomSentry;
512
- axis.used = false;
513
- }
514
-
515
496
  function updateAxis(axis, min, max) {
516
- if (min < axis.datamin)
497
+ if (min < axis.datamin && min != -fakeInfinity)
517
498
  axis.datamin = min;
518
- if (max > axis.datamax)
499
+ if (max > axis.datamax && max != fakeInfinity)
519
500
  axis.datamax = max;
520
501
  }
521
502
 
522
- for (i = 0; i < xaxes.length; ++i)
523
- initAxis(xaxes[i]);
524
- for (i = 0; i < yaxes.length; ++i)
525
- initAxis(yaxes[i]);
503
+ $.each(allAxes(), function (_, axis) {
504
+ // init axis
505
+ axis.datamin = topSentry;
506
+ axis.datamax = bottomSentry;
507
+ axis.used = false;
508
+ });
526
509
 
527
510
  for (i = 0; i < series.length; ++i) {
528
511
  s = series[i];
@@ -579,6 +562,10 @@
579
562
  val = +val; // convert to number
580
563
  if (isNaN(val))
581
564
  val = null;
565
+ else if (val == Infinity)
566
+ val = fakeInfinity;
567
+ else if (val == -Infinity)
568
+ val = -fakeInfinity;
582
569
  }
583
570
 
584
571
  if (val == null) {
@@ -653,7 +640,7 @@
653
640
  for (m = 0; m < ps; ++m) {
654
641
  val = points[j + m];
655
642
  f = format[m];
656
- if (!f)
643
+ if (!f || val == fakeInfinity || val == -fakeInfinity)
657
644
  continue;
658
645
 
659
646
  if (f.x) {
@@ -688,7 +675,7 @@
688
675
  updateAxis(s.yaxis, ymin, ymax);
689
676
  }
690
677
 
691
- $.each(getUsedAxes(), function (i, axis) {
678
+ $.each(allAxes(), function (_, axis) {
692
679
  if (axis.datamin == topSentry)
693
680
  axis.datamin = null;
694
681
  if (axis.datamax == bottomSentry)
@@ -696,46 +683,115 @@
696
683
  });
697
684
  }
698
685
 
699
- function constructCanvas() {
700
- function makeCanvas(width, height) {
701
- var c = document.createElement('canvas');
702
- c.width = width;
703
- c.height = height;
704
- if (!c.getContext) // excanvas hack
705
- c = window.G_vmlCanvasManager.initElement(c);
706
- return c;
707
- }
686
+ function makeCanvas(skipPositioning, cls) {
687
+ var c = document.createElement('canvas');
688
+ c.className = cls;
689
+ c.width = canvasWidth;
690
+ c.height = canvasHeight;
691
+
692
+ if (!skipPositioning)
693
+ $(c).css({ position: 'absolute', left: 0, top: 0 });
694
+
695
+ $(c).appendTo(placeholder);
696
+
697
+ if (!c.getContext) // excanvas hack
698
+ c = window.G_vmlCanvasManager.initElement(c);
699
+
700
+ // used for resetting in case we get replotted
701
+ c.getContext("2d").save();
708
702
 
703
+ return c;
704
+ }
705
+
706
+ function getCanvasDimensions() {
709
707
  canvasWidth = placeholder.width();
710
708
  canvasHeight = placeholder.height();
711
- placeholder.html(""); // clear placeholder
712
- if (placeholder.css("position") == 'static')
713
- placeholder.css("position", "relative"); // for positioning labels and overlay
714
-
709
+
715
710
  if (canvasWidth <= 0 || canvasHeight <= 0)
716
711
  throw "Invalid dimensions for plot, width = " + canvasWidth + ", height = " + canvasHeight;
712
+ }
713
+
714
+ function resizeCanvas(c) {
715
+ // resizing should reset the state (excanvas seems to be
716
+ // buggy though)
717
+ if (c.width != canvasWidth)
718
+ c.width = canvasWidth;
719
+
720
+ if (c.height != canvasHeight)
721
+ c.height = canvasHeight;
722
+
723
+ // so try to get back to the initial state (even if it's
724
+ // gone now, this should be safe according to the spec)
725
+ var cctx = c.getContext("2d");
726
+ cctx.restore();
717
727
 
718
- if (window.G_vmlCanvasManager) // excanvas hack
719
- window.G_vmlCanvasManager.init_(document); // make sure everything is setup
728
+ // and save again
729
+ cctx.save();
730
+ }
731
+
732
+ function setupCanvases() {
733
+ var reused,
734
+ existingCanvas = placeholder.children("canvas.base"),
735
+ existingOverlay = placeholder.children("canvas.overlay");
736
+
737
+ if (existingCanvas.length == 0 || existingOverlay == 0) {
738
+ // init everything
739
+
740
+ placeholder.html(""); // make sure placeholder is clear
720
741
 
721
- // the canvas
722
- canvas = $(makeCanvas(canvasWidth, canvasHeight)).appendTo(placeholder).get(0);
723
- ctx = canvas.getContext("2d");
742
+ placeholder.css({ padding: 0 }); // padding messes up the positioning
743
+
744
+ if (placeholder.css("position") == 'static')
745
+ placeholder.css("position", "relative"); // for positioning labels and overlay
746
+
747
+ getCanvasDimensions();
748
+
749
+ canvas = makeCanvas(true, "base");
750
+ overlay = makeCanvas(false, "overlay"); // overlay canvas for interactive features
724
751
 
725
- // overlay canvas for interactive features
726
- overlay = $(makeCanvas(canvasWidth, canvasHeight)).css({ position: 'absolute', left: 0, top: 0 }).appendTo(placeholder).get(0);
752
+ reused = false;
753
+ }
754
+ else {
755
+ // reuse existing elements
756
+
757
+ canvas = existingCanvas.get(0);
758
+ overlay = existingOverlay.get(0);
759
+
760
+ reused = true;
761
+ }
762
+
763
+ ctx = canvas.getContext("2d");
727
764
  octx = overlay.getContext("2d");
728
- octx.stroke();
729
- }
730
765
 
731
- function bindEvents() {
732
766
  // we include the canvas in the event holder too, because IE 7
733
767
  // sometimes has trouble with the stacking order
734
768
  eventHolder = $([overlay, canvas]);
735
769
 
770
+ if (reused) {
771
+ // run shutdown in the old plot object
772
+ placeholder.data("plot").shutdown();
773
+
774
+ // reset reused canvases
775
+ plot.resize();
776
+
777
+ // make sure overlay pixels are cleared (canvas is cleared when we redraw)
778
+ octx.clearRect(0, 0, canvasWidth, canvasHeight);
779
+
780
+ // then whack any remaining obvious garbage left
781
+ eventHolder.unbind();
782
+ placeholder.children().not([canvas, overlay]).remove();
783
+ }
784
+
785
+ // save in case we get replotted
786
+ placeholder.data("plot", plot);
787
+ }
788
+
789
+ function bindEvents() {
736
790
  // bind events
737
- if (options.grid.hoverable)
791
+ if (options.grid.hoverable) {
738
792
  eventHolder.mousemove(onMouseMove);
793
+ eventHolder.mouseleave(onMouseLeave);
794
+ }
739
795
 
740
796
  if (options.grid.clickable)
741
797
  eventHolder.click(onClick);
@@ -743,6 +799,17 @@
743
799
  executeHooks(hooks.bindEvents, [eventHolder]);
744
800
  }
745
801
 
802
+ function shutdown() {
803
+ if (redrawTimeout)
804
+ clearTimeout(redrawTimeout);
805
+
806
+ eventHolder.unbind("mousemove", onMouseMove);
807
+ eventHolder.unbind("mouseleave", onMouseLeave);
808
+ eventHolder.unbind("click", onClick);
809
+
810
+ executeHooks(hooks.shutdown, [eventHolder]);
811
+ }
812
+
746
813
  function setTransformationHelpers(axis) {
747
814
  // set helper functions on the axis, assumes plot area
748
815
  // has been computed already
@@ -752,42 +819,31 @@
752
819
  var s, m, t = axis.options.transform || identity,
753
820
  it = axis.options.inverseTransform;
754
821
 
822
+ // precompute how much the axis is scaling a point
823
+ // in canvas space
755
824
  if (axis.direction == "x") {
756
- // precompute how much the axis is scaling a point
757
- // in canvas space
758
- s = axis.scale = plotWidth / (t(axis.max) - t(axis.min));
759
- m = t(axis.min);
760
-
761
- // data point to canvas coordinate
762
- if (t == identity) // slight optimization
763
- axis.p2c = function (p) { return (p - m) * s; };
764
- else
765
- axis.p2c = function (p) { return (t(p) - m) * s; };
766
- // canvas coordinate to data point
767
- if (!it)
768
- axis.c2p = function (c) { return m + c / s; };
769
- else
770
- axis.c2p = function (c) { return it(m + c / s); };
825
+ s = axis.scale = plotWidth / Math.abs(t(axis.max) - t(axis.min));
826
+ m = Math.min(t(axis.max), t(axis.min));
771
827
  }
772
828
  else {
773
- s = axis.scale = plotHeight / (t(axis.max) - t(axis.min));
774
- m = t(axis.max);
775
-
776
- if (t == identity)
777
- axis.p2c = function (p) { return (m - p) * s; };
778
- else
779
- axis.p2c = function (p) { return (m - t(p)) * s; };
780
- if (!it)
781
- axis.c2p = function (c) { return m - c / s; };
782
- else
783
- axis.c2p = function (c) { return it(m - c / s); };
829
+ s = axis.scale = plotHeight / Math.abs(t(axis.max) - t(axis.min));
830
+ s = -s;
831
+ m = Math.max(t(axis.max), t(axis.min));
784
832
  }
833
+
834
+ // data point to canvas coordinate
835
+ if (t == identity) // slight optimization
836
+ axis.p2c = function (p) { return (p - m) * s; };
837
+ else
838
+ axis.p2c = function (p) { return (t(p) - m) * s; };
839
+ // canvas coordinate to data point
840
+ if (!it)
841
+ axis.c2p = function (c) { return m + c / s; };
842
+ else
843
+ axis.c2p = function (c) { return it(m + c / s); };
785
844
  }
786
845
 
787
846
  function measureTickLabels(axis) {
788
- if (!axis)
789
- return;
790
-
791
847
  var opts = axis.options, i, ticks = axis.ticks || [], labels = [],
792
848
  l, w = opts.labelWidth, h = opts.labelHeight, dummyDiv;
793
849
 
@@ -847,15 +903,12 @@
847
903
  w = 0;
848
904
  if (h == null)
849
905
  h = 0;
850
-
906
+
851
907
  axis.labelWidth = w;
852
908
  axis.labelHeight = h;
853
909
  }
854
910
 
855
- function computeAxisBox(axis) {
856
- if (!axis || (!axis.used && !(axis.labelWidth || axis.labelHeight)))
857
- return;
858
-
911
+ function allocateAxisBoxFirstPhase(axis) {
859
912
  // find the bounding box of the axis by looking at label
860
913
  // widths/heights and ticks, make room by diminishing the
861
914
  // plotOffset
@@ -871,7 +924,7 @@
871
924
 
872
925
  // determine axis margin
873
926
  var samePosition = $.grep(all, function (a) {
874
- return a && a.options.position == pos && (a.labelHeight || a.labelWidth);
927
+ return a && a.options.position == pos && a.reserveSpace;
875
928
  });
876
929
  if ($.inArray(axis, samePosition) == samePosition.length - 1)
877
930
  axismargin = 0; // outermost
@@ -881,7 +934,7 @@
881
934
  tickLength = "full";
882
935
 
883
936
  var sameDirection = $.grep(all, function (a) {
884
- return a && (a.labelHeight || a.labelWidth);
937
+ return a && a.reserveSpace;
885
938
  });
886
939
 
887
940
  var innermost = $.inArray(axis, sameDirection) == 0;
@@ -924,7 +977,7 @@
924
977
  axis.innermost = innermost;
925
978
  }
926
979
 
927
- function fixupAxisBox(axis) {
980
+ function allocateAxisBoxSecondPhase(axis) {
928
981
  // set remaining bounding box coordinates
929
982
  if (axis.direction == "x") {
930
983
  axis.box.left = plotOffset.left;
@@ -937,59 +990,67 @@
937
990
  }
938
991
 
939
992
  function setupGrid() {
940
- var axes = getUsedAxes(), j, k;
993
+ var i, axes = allAxes();
941
994
 
942
- // compute axis intervals
943
- for (k = 0; k < axes.length; ++k)
944
- setRange(axes[k]);
995
+ // first calculate the plot and axis box dimensions
996
+
997
+ $.each(axes, function (_, axis) {
998
+ axis.show = axis.options.show;
999
+ if (axis.show == null)
1000
+ axis.show = axis.used; // by default an axis is visible if it's got data
1001
+
1002
+ axis.reserveSpace = axis.show || axis.options.reserveSpace;
1003
+
1004
+ setRange(axis);
1005
+ });
1006
+
1007
+ allocatedAxes = $.grep(axes, function (axis) { return axis.reserveSpace; });
945
1008
 
946
-
947
1009
  plotOffset.left = plotOffset.right = plotOffset.top = plotOffset.bottom = 0;
948
1010
  if (options.grid.show) {
949
- // make the ticks
950
- for (k = 0; k < axes.length; ++k) {
951
- setupTickGeneration(axes[k]);
952
- setTicks(axes[k]);
953
- snapRangeToTicks(axes[k], axes[k].ticks);
954
- }
1011
+ $.each(allocatedAxes, function (_, axis) {
1012
+ // make the ticks
1013
+ setupTickGeneration(axis);
1014
+ setTicks(axis);
1015
+ snapRangeToTicks(axis, axis.ticks);
955
1016
 
956
- // find labelWidth/Height, do this on all, not just
957
- // used as we might need to reserve space for unused
958
- // too if their labelWidth/Height is set
959
- for (j = 0; j < xaxes.length; ++j)
960
- measureTickLabels(xaxes[j]);
961
- for (j = 0; j < yaxes.length; ++j)
962
- measureTickLabels(yaxes[j]);
963
-
964
- // compute the axis boxes, start from the outside (reverse order)
965
- for (j = xaxes.length - 1; j >= 0; --j)
966
- computeAxisBox(xaxes[j]);
967
- for (j = yaxes.length - 1; j >= 0; --j)
968
- computeAxisBox(yaxes[j]);
1017
+ // find labelWidth/Height for axis
1018
+ measureTickLabels(axis);
1019
+ });
1020
+
1021
+ // with all dimensions in house, we can compute the
1022
+ // axis boxes, start from the outside (reverse order)
1023
+ for (i = allocatedAxes.length - 1; i >= 0; --i)
1024
+ allocateAxisBoxFirstPhase(allocatedAxes[i]);
969
1025
 
970
1026
  // make sure we've got enough space for things that
971
1027
  // might stick out
972
- var maxOutset = 0;
973
- for (var i = 0; i < series.length; ++i)
974
- maxOutset = Math.max(maxOutset, 2 * (series[i].points.radius + series[i].points.lineWidth/2));
975
-
1028
+ var minMargin = options.grid.minBorderMargin;
1029
+ if (minMargin == null) {
1030
+ minMargin = 0;
1031
+ for (i = 0; i < series.length; ++i)
1032
+ minMargin = Math.max(minMargin, series[i].points.radius + series[i].points.lineWidth/2);
1033
+ }
1034
+
976
1035
  for (var a in plotOffset) {
977
1036
  plotOffset[a] += options.grid.borderWidth;
978
- plotOffset[a] = Math.max(maxOutset, plotOffset[a]);
1037
+ plotOffset[a] = Math.max(minMargin, plotOffset[a]);
979
1038
  }
980
1039
  }
981
1040
 
982
1041
  plotWidth = canvasWidth - plotOffset.left - plotOffset.right;
983
1042
  plotHeight = canvasHeight - plotOffset.bottom - plotOffset.top;
984
-
1043
+
985
1044
  // now we got the proper plotWidth/Height, we can compute the scaling
986
- for (k = 0; k < axes.length; ++k)
987
- setTransformationHelpers(axes[k]);
1045
+ $.each(axes, function (_, axis) {
1046
+ setTransformationHelpers(axis);
1047
+ });
988
1048
 
989
1049
  if (options.grid.show) {
990
- for (k = 0; k < axes.length; ++k)
991
- fixupAxisBox(axes[k]);
992
-
1050
+ $.each(allocatedAxes, function (_, axis) {
1051
+ allocateAxisBoxSecondPhase(axis);
1052
+ });
1053
+
993
1054
  insertAxisLabels();
994
1055
  }
995
1056
 
@@ -1008,7 +1069,7 @@
1008
1069
 
1009
1070
  if (opts.min == null)
1010
1071
  min -= widen;
1011
- // alway widen max if we couldn't widen min to ensure we
1072
+ // always widen max if we couldn't widen min to ensure we
1012
1073
  // don't fall into min == max which doesn't work
1013
1074
  if (opts.max == null || opts.min != null)
1014
1075
  max += widen;
@@ -1042,12 +1103,10 @@
1042
1103
  var noTicks;
1043
1104
  if (typeof opts.ticks == "number" && opts.ticks > 0)
1044
1105
  noTicks = opts.ticks;
1045
- else if (axis.direction == "x")
1046
- // heuristic based on the model a*sqrt(x) fitted to
1047
- // some reasonable data points
1048
- noTicks = 0.3 * Math.sqrt(canvasWidth);
1049
1106
  else
1050
- noTicks = 0.3 * Math.sqrt(canvasHeight);
1107
+ // heuristic based on the model a*sqrt(x) fitted to
1108
+ // some data points that seemed reasonable
1109
+ noTicks = 0.3 * Math.sqrt(axis.direction == "x" ? canvasWidth : canvasHeight);
1051
1110
 
1052
1111
  var delta = (axis.max - axis.min) / noTicks,
1053
1112
  size, generator, unit, formatter, i, magn, norm;
@@ -1310,9 +1369,7 @@
1310
1369
  }
1311
1370
 
1312
1371
  function setTicks(axis) {
1313
- axis.ticks = [];
1314
-
1315
- var oticks = axis.options.ticks, ticks = null;
1372
+ var oticks = axis.options.ticks, ticks = [];
1316
1373
  if (oticks == null || (typeof oticks == "number" && oticks > 0))
1317
1374
  ticks = axis.tickGenerator(axis);
1318
1375
  else if (oticks) {
@@ -1325,24 +1382,26 @@
1325
1382
 
1326
1383
  // clean up/labelify the supplied ticks, copy them over
1327
1384
  var i, v;
1385
+ axis.ticks = [];
1328
1386
  for (i = 0; i < ticks.length; ++i) {
1329
1387
  var label = null;
1330
1388
  var t = ticks[i];
1331
1389
  if (typeof t == "object") {
1332
- v = t[0];
1390
+ v = +t[0];
1333
1391
  if (t.length > 1)
1334
1392
  label = t[1];
1335
1393
  }
1336
1394
  else
1337
- v = t;
1395
+ v = +t;
1338
1396
  if (label == null)
1339
1397
  label = axis.tickFormatter(v, axis);
1340
- axis.ticks[i] = { v: v, label: label };
1398
+ if (!isNaN(v))
1399
+ axis.ticks.push({ v: v, label: label });
1341
1400
  }
1342
1401
  }
1343
1402
 
1344
1403
  function snapRangeToTicks(axis, ticks) {
1345
- if (axis.options.autoscaleMargin != null && ticks.length > 0) {
1404
+ if (axis.options.autoscaleMargin && ticks.length > 0) {
1346
1405
  // snap to ticks
1347
1406
  if (axis.options.min == null)
1348
1407
  axis.min = Math.min(axis.min, ticks[0].v);
@@ -1355,6 +1414,10 @@
1355
1414
  ctx.clearRect(0, 0, canvasWidth, canvasHeight);
1356
1415
 
1357
1416
  var grid = options.grid;
1417
+
1418
+ // draw background, if any
1419
+ if (grid.show && grid.backgroundColor)
1420
+ drawBackground();
1358
1421
 
1359
1422
  if (grid.show && !grid.aboveData)
1360
1423
  drawGrid();
@@ -1371,9 +1434,8 @@
1371
1434
  }
1372
1435
 
1373
1436
  function extractRange(ranges, coord) {
1374
- var axis, from, to, axes, key;
1437
+ var axis, from, to, key, axes = allAxes();
1375
1438
 
1376
- axes = getUsedAxes();
1377
1439
  for (i = 0; i < axes.length; ++i) {
1378
1440
  axis = axes[i];
1379
1441
  if (axis.direction == coord) {
@@ -1405,18 +1467,21 @@
1405
1467
  return { from: from, to: to, axis: axis };
1406
1468
  }
1407
1469
 
1470
+ function drawBackground() {
1471
+ ctx.save();
1472
+ ctx.translate(plotOffset.left, plotOffset.top);
1473
+
1474
+ ctx.fillStyle = getColorOrGradient(options.grid.backgroundColor, plotHeight, 0, "rgba(255, 255, 255, 0)");
1475
+ ctx.fillRect(0, 0, plotWidth, plotHeight);
1476
+ ctx.restore();
1477
+ }
1478
+
1408
1479
  function drawGrid() {
1409
1480
  var i;
1410
1481
 
1411
1482
  ctx.save();
1412
1483
  ctx.translate(plotOffset.left, plotOffset.top);
1413
1484
 
1414
- // draw background, if any
1415
- if (options.grid.backgroundColor) {
1416
- ctx.fillStyle = getColorOrGradient(options.grid.backgroundColor, plotHeight, 0, "rgba(255, 255, 255, 0)");
1417
- ctx.fillRect(0, 0, plotWidth, plotHeight);
1418
- }
1419
-
1420
1485
  // draw markings
1421
1486
  var markings = options.grid.markings;
1422
1487
  if (markings) {
@@ -1486,15 +1551,17 @@
1486
1551
  }
1487
1552
 
1488
1553
  // draw the ticks
1489
- var axes = getUsedAxes(), bw = options.grid.borderWidth;
1554
+ var axes = allAxes(), bw = options.grid.borderWidth;
1490
1555
 
1491
1556
  for (var j = 0; j < axes.length; ++j) {
1492
1557
  var axis = axes[j], box = axis.box,
1493
1558
  t = axis.tickLength, x, y, xoff, yoff;
1494
-
1559
+ if (!axis.show || axis.ticks.length == 0)
1560
+ continue
1561
+
1495
1562
  ctx.strokeStyle = axis.options.tickColor || $.color.parse(axis.options.color).scale('a', 0.22).toString();
1496
1563
  ctx.lineWidth = 1;
1497
-
1564
+
1498
1565
  // find the edges
1499
1566
  if (axis.direction == "x") {
1500
1567
  x = 0;
@@ -1588,9 +1655,11 @@
1588
1655
 
1589
1656
  var html = ['<div class="tickLabels" style="font-size:smaller">'];
1590
1657
 
1591
- var axes = getUsedAxes();
1658
+ var axes = allAxes();
1592
1659
  for (var j = 0; j < axes.length; ++j) {
1593
1660
  var axis = axes[j], box = axis.box;
1661
+ if (!axis.show)
1662
+ continue;
1594
1663
  //debug: html.push('<div style="position:absolute;opacity:0.10;background-color:red;left:' + box.left + 'px;top:' + box.top + 'px;width:' + box.width + 'px;height:' + box.height + 'px"></div>')
1595
1664
  html.push('<div class="' + axis.direction + 'Axis ' + axis.direction + axis.n + 'Axis" style="color:' + axis.options.color + '">');
1596
1665
  for (var i = 0; i < axis.ticks.length; ++i) {
@@ -2198,6 +2267,13 @@
2198
2267
  maxx = maxDistance / axisx.scale,
2199
2268
  maxy = maxDistance / axisy.scale;
2200
2269
 
2270
+ // with inverse transforms, we can't use the maxx/maxy
2271
+ // optimization, sadly
2272
+ if (axisx.options.inverseTransform)
2273
+ maxx = Number.MAX_VALUE;
2274
+ if (axisy.options.inverseTransform)
2275
+ maxy = Number.MAX_VALUE;
2276
+
2201
2277
  if (s.lines.show || s.points.show) {
2202
2278
  for (j = 0; j < points.length; j += ps) {
2203
2279
  var x = points[j], y = points[j + 1];
@@ -2264,7 +2340,13 @@
2264
2340
  triggerClickHoverEvent("plothover", e,
2265
2341
  function (s) { return s["hoverable"] != false; });
2266
2342
  }
2267
-
2343
+
2344
+ function onMouseLeave(e) {
2345
+ if (options.grid.hoverable)
2346
+ triggerClickHoverEvent("plothover", e,
2347
+ function (s) { return false; });
2348
+ }
2349
+
2268
2350
  function onClick(e) {
2269
2351
  triggerClickHoverEvent("plotclick", e,
2270
2352
  function (s) { return s["clickable"] != false; });
@@ -2294,7 +2376,9 @@
2294
2376
  for (var i = 0; i < highlights.length; ++i) {
2295
2377
  var h = highlights[i];
2296
2378
  if (h.auto == eventname &&
2297
- !(item && h.series == item.series && h.point == item.datapoint))
2379
+ !(item && h.series == item.series &&
2380
+ h.point[0] == item.datapoint[0] &&
2381
+ h.point[1] == item.datapoint[1]))
2298
2382
  unhighlight(h.series, h.point);
2299
2383
  }
2300
2384
 
@@ -2447,6 +2531,8 @@
2447
2531
  return plot;
2448
2532
  };
2449
2533
 
2534
+ $.plot.version = "0.7";
2535
+
2450
2536
  $.plot.plugins = [];
2451
2537
 
2452
2538
  // returns a string with the date d formatted according to fmt