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 +1 -0
- data/Gemfile +5 -0
- data/README.rdoc +44 -20
- data/VERSION +1 -1
- data/app/assets/javascripts/blacklight_range_limit.js +0 -1
- data/blacklight_range_limit.gemspec +7 -0
- data/lib/blacklight_range_limit/controller_override.rb +1 -1
- data/lib/blacklight_range_limit/engine.rb +6 -0
- data/lib/generators/blacklight_range_limit/assets_generator.rb +3 -2
- data/spec/acceptance/blacklight_range_limit_spec.rb +36 -0
- data/spec/integration/blacklight_stub_spec.rb +10 -0
- data/spec/internal/app/controllers/application_controller.rb +4 -0
- data/spec/internal/app/models/solr_document.rb +3 -0
- data/spec/internal/config/database.yml +3 -0
- data/spec/internal/config/routes.rb +6 -0
- data/spec/internal/config/solr.yml +18 -0
- data/spec/internal/db/combustion_test.sqlite +0 -0
- data/spec/internal/db/schema.rb +53 -0
- data/spec/internal/log/.gitignore +1 -0
- data/spec/internal/public/favicon.ico +0 -0
- data/spec/spec_helper.rb +25 -0
- data/{app → vendor}/assets/javascripts/flot/excanvas.min.js +0 -0
- data/{app → vendor}/assets/javascripts/flot/jquery.flot.js +276 -190
- data/{app → vendor}/assets/javascripts/flot/jquery.flot.selection.js +31 -15
- metadata +102 -11
data/.gitignore
CHANGED
data/Gemfile
ADDED
data/README.rdoc
CHANGED
@@ -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
|
-
|
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
|
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
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
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
|
-
|
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.
|
1
|
+
1.2.1
|
@@ -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
|
28
|
+
insert_into_file "app/assets/javascripts/application.js", :after => "//= require jquery\n" do
|
29
29
|
%q{
|
30
30
|
|
31
|
-
//
|
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,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
|
Binary file
|
@@ -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
|
data/spec/spec_helper.rb
ADDED
@@ -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
|
+
|
File without changes
|
@@ -1,4 +1,4 @@
|
|
1
|
-
|
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.
|
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()
|
26
|
-
* new
|
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(){
|
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
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
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
|
-
|
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.
|
266
|
-
options.xaxes[1] =
|
265
|
+
options.xaxes[1] = $.extend(true, {}, options.xaxis, options.x2axis);
|
266
|
+
options.xaxes[1].position = "top";
|
267
267
|
}
|
268
268
|
if (options.y2axis) {
|
269
|
-
|
270
|
-
|
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
|
-
|
523
|
-
|
524
|
-
|
525
|
-
|
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(
|
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
|
700
|
-
|
701
|
-
|
702
|
-
|
703
|
-
|
704
|
-
|
705
|
-
|
706
|
-
|
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
|
-
|
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
|
-
|
719
|
-
|
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
|
-
|
722
|
-
|
723
|
-
|
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
|
-
|
726
|
-
|
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
|
-
|
757
|
-
|
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
|
-
|
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
|
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 &&
|
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 &&
|
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
|
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 =
|
993
|
+
var i, axes = allAxes();
|
941
994
|
|
942
|
-
//
|
943
|
-
|
944
|
-
|
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
|
-
|
950
|
-
|
951
|
-
setupTickGeneration(
|
952
|
-
setTicks(
|
953
|
-
snapRangeToTicks(
|
954
|
-
}
|
1011
|
+
$.each(allocatedAxes, function (_, axis) {
|
1012
|
+
// make the ticks
|
1013
|
+
setupTickGeneration(axis);
|
1014
|
+
setTicks(axis);
|
1015
|
+
snapRangeToTicks(axis, axis.ticks);
|
955
1016
|
|
956
|
-
|
957
|
-
|
958
|
-
|
959
|
-
|
960
|
-
|
961
|
-
|
962
|
-
|
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
|
973
|
-
|
974
|
-
|
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(
|
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
|
-
|
987
|
-
setTransformationHelpers(
|
1045
|
+
$.each(axes, function (_, axis) {
|
1046
|
+
setTransformationHelpers(axis);
|
1047
|
+
});
|
988
1048
|
|
989
1049
|
if (options.grid.show) {
|
990
|
-
|
991
|
-
|
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
|
-
//
|
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
|
-
|
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
|
-
|
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
|
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,
|
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 =
|
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 =
|
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 &&
|
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
|