blacklight_range_limit 1.0.0pre1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.gitignore +21 -0
- data/MIT-LICENSE +20 -0
- data/README.rdoc +141 -0
- data/Rakefile +5 -0
- data/VERSION +1 -0
- data/app/helpers/range_limit_helper.rb +89 -0
- data/app/views/blacklight_range_limit/_range_limit_panel.html.erb +74 -0
- data/app/views/blacklight_range_limit/_range_segments.html.erb +14 -0
- data/blacklight_range_limit.gemspec +23 -0
- data/config/routes.rb +6 -0
- data/lib/blacklight_range_limit.rb +55 -0
- data/lib/blacklight_range_limit/controller_override.rb +139 -0
- data/lib/blacklight_range_limit/engine.rb +17 -0
- data/lib/blacklight_range_limit/route_sets.rb +12 -0
- data/lib/blacklight_range_limit/segment_calculation.rb +103 -0
- data/lib/blacklight_range_limit/version.rb +10 -0
- data/lib/blacklight_range_limit/view_helper_override.rb +82 -0
- data/lib/generators/blacklight_range_limit/assets_generator.rb +25 -0
- data/lib/generators/blacklight_range_limit/blacklight_range_limit_generator.rb +11 -0
- data/lib/generators/blacklight_range_limit/templates/public/javascripts/flot/excanvas.min.js +1 -0
- data/lib/generators/blacklight_range_limit/templates/public/javascripts/flot/jquery.flot.js +2513 -0
- data/lib/generators/blacklight_range_limit/templates/public/javascripts/flot/jquery.flot.selection.js +328 -0
- data/lib/generators/blacklight_range_limit/templates/public/javascripts/range_limit_distro_facets.js +211 -0
- data/lib/generators/blacklight_range_limit/templates/public/javascripts/range_limit_slider.js +83 -0
- data/lib/generators/blacklight_range_limit/templates/public/stylesheets/blacklight_range_limit.css +13 -0
- metadata +122 -0
@@ -0,0 +1,139 @@
|
|
1
|
+
# Meant to be applied on top of a controller that implements
|
2
|
+
# Blacklight::SolrHelper. Will inject range limiting behaviors
|
3
|
+
# to solr parameters creation.
|
4
|
+
require 'blacklight_range_limit/segment_calculation'
|
5
|
+
module BlacklightRangeLimit
|
6
|
+
module ControllerOverride
|
7
|
+
include SegmentCalculation
|
8
|
+
extend ActiveSupport::Concern
|
9
|
+
|
10
|
+
included do
|
11
|
+
solr_search_params_logic << :add_range_limit_params
|
12
|
+
helper_method :range_config
|
13
|
+
|
14
|
+
|
15
|
+
|
16
|
+
unless BlacklightRangeLimit.omit_inject[:view_helpers]
|
17
|
+
helper BlacklightRangeLimit::ViewHelperOverride
|
18
|
+
helper RangeLimitHelper
|
19
|
+
end
|
20
|
+
|
21
|
+
before_filter do |controller|
|
22
|
+
unless BlacklightRangeLimit.omit_inject[:css]
|
23
|
+
controller.stylesheet_links << "blacklight_range_limit"
|
24
|
+
end
|
25
|
+
|
26
|
+
unless BlacklightRangeLimit.omit_inject[:flot]
|
27
|
+
# Replace with local version.
|
28
|
+
controller.javascript_includes << "flot/jquery.flot.js"
|
29
|
+
controller.javascript_includes << "flot/jquery.flot.selection.js"
|
30
|
+
# canvas for IE
|
31
|
+
|
32
|
+
# Hacky hack to insert URL to plugin asset when we don't have
|
33
|
+
# access to helper methods, bah, will break if you change plugin
|
34
|
+
# defaults. We need Rails 3.0 please.
|
35
|
+
controller.extra_head_content << ('<!--[if IE]>' + view_context.javascript_include_tag("flot/excanvas.min.js") + '<![endif]-->').html_safe
|
36
|
+
end
|
37
|
+
|
38
|
+
unless BlacklightRangeLimit.omit_inject[:js]
|
39
|
+
controller.javascript_includes << "range_limit_slider"
|
40
|
+
controller.javascript_includes << "range_limit_distro_facets"
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
# Action method of our own!
|
46
|
+
# Delivers a _partial_ that's a display of a single fields range facets.
|
47
|
+
# Used when we need a second Solr query to get range facets, after the
|
48
|
+
# first found min/max from result set.
|
49
|
+
def range_limit
|
50
|
+
solr_field = params[:range_field] # what field to fetch for
|
51
|
+
start = params[:range_start].to_i
|
52
|
+
finish = params[:range_end].to_i
|
53
|
+
|
54
|
+
solr_params = solr_search_params(params)
|
55
|
+
|
56
|
+
# Remove all field faceting for efficiency, we won't be using it.
|
57
|
+
solr_params.delete("facet.field")
|
58
|
+
solr_params.delete("facet.field".to_sym)
|
59
|
+
|
60
|
+
add_range_segments_to_solr!(solr_params, solr_field, start, finish )
|
61
|
+
# We don't need any actual rows or facets, we're just going to look
|
62
|
+
# at the facet.query's
|
63
|
+
solr_params[:rows] = 0
|
64
|
+
solr_params[:facets] = nil
|
65
|
+
# Not really any good way to turn off facet.field's from the solr default,
|
66
|
+
# no big deal it should be well-cached at this point.
|
67
|
+
|
68
|
+
@response = Blacklight.solr.find( solr_params )
|
69
|
+
|
70
|
+
if request.xhr?
|
71
|
+
render(:partial => 'blacklight_range_limit/range_segments', :locals => {:solr_field => solr_field})
|
72
|
+
else
|
73
|
+
render(:partial => 'blacklight_range_limit/range_segments', :layout => true, :locals => {:solr_field => solr_field})
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
# Method added to solr_search_params_logic to fetch
|
78
|
+
# proper things for date ranges.
|
79
|
+
def add_range_limit_params(solr_params, req_params)
|
80
|
+
all_range_config.each_pair do |solr_field, config|
|
81
|
+
config = {} if config == true
|
82
|
+
# If we have any range facets configured, we want to ask for
|
83
|
+
# the stats component to get min/max.
|
84
|
+
|
85
|
+
solr_params["stats"] = "true"
|
86
|
+
solr_params["stats.field"] ||= []
|
87
|
+
solr_params["stats.field"] << solr_field
|
88
|
+
|
89
|
+
hash = req_params["range"] && req_params["range"][solr_field] ?
|
90
|
+
req_params["range"][solr_field] :
|
91
|
+
{}
|
92
|
+
|
93
|
+
if !hash["missing"].blank?
|
94
|
+
# missing specified in request params
|
95
|
+
solr_params[:fq] ||= []
|
96
|
+
solr_params[:fq] << "-#{solr_field}:[* TO *]"
|
97
|
+
|
98
|
+
elsif !(hash["begin"].blank? && hash["end"].blank?)
|
99
|
+
# specified in request params, begin and/or end, might just have one
|
100
|
+
start = hash["begin"].blank? ? "*" : hash["begin"]
|
101
|
+
finish = hash["end"].blank? ? "*" : hash["end"]
|
102
|
+
|
103
|
+
solr_params[:fq] ||= []
|
104
|
+
solr_params[:fq] << "#{solr_field}: [#{start} TO #{finish}]"
|
105
|
+
|
106
|
+
if (config[:segments] != false && start != "*" && finish != "*")
|
107
|
+
# Add in our calculated segments, can only do with both boundaries.
|
108
|
+
add_range_segments_to_solr!(solr_params, solr_field, start.to_i, finish.to_i)
|
109
|
+
end
|
110
|
+
|
111
|
+
elsif (config[:segments] != false &&
|
112
|
+
boundaries = config[:assumed_boundaries])
|
113
|
+
# assumed_boundaries in config
|
114
|
+
add_range_segments_to_solr!(solr_params, solr_field, boundaries[0], boundaries[1])
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
return solr_params
|
119
|
+
end
|
120
|
+
|
121
|
+
# Returns range config hash for named solr field. Returns false
|
122
|
+
# if not configured. Returns hash even if configured to 'true'
|
123
|
+
# for consistency.
|
124
|
+
def range_config(solr_field)
|
125
|
+
config = all_range_config[solr_field] || false
|
126
|
+
config = {} if config == true # normalize bool true to hash
|
127
|
+
return config
|
128
|
+
end
|
129
|
+
# returns a hash of solr_field => config for all configured range
|
130
|
+
# facets, or empty hash.
|
131
|
+
# Uses Blacklight.config, needs to be modified when
|
132
|
+
# that changes to be controller-based. This is the only method
|
133
|
+
# in this plugin that accesses Blacklight.config, single point
|
134
|
+
# of contact.
|
135
|
+
def all_range_config
|
136
|
+
Blacklight.config[:facet][:range] || {}
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
require 'blacklight'
|
2
|
+
require 'blacklight_range_limit'
|
3
|
+
require 'rails'
|
4
|
+
|
5
|
+
module BlacklightRangeLimit
|
6
|
+
class Engine < Rails::Engine
|
7
|
+
|
8
|
+
# Do these things in a to_prepare block, to try and make them work
|
9
|
+
# in development mode with class-reloading. The trick is we can't
|
10
|
+
# be sure if the controllers we're modifying are being reloaded in
|
11
|
+
# dev mode, if they are in the BL plugin and haven't been copied to
|
12
|
+
# local, they won't be. But we do our best.
|
13
|
+
config.to_prepare do
|
14
|
+
BlacklightRangeLimit.inject!
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,103 @@
|
|
1
|
+
# Meant to be in a Controller, included in our ControllerOverride module.
|
2
|
+
module BlacklightRangeLimit
|
3
|
+
module SegmentCalculation
|
4
|
+
|
5
|
+
protected
|
6
|
+
|
7
|
+
# Calculates segment facets within a given start and end on a given
|
8
|
+
# field, returns request params to be added on to what's sent to
|
9
|
+
# solr to get the calculated facet segments.
|
10
|
+
# Assumes solr_field is an integer, as range endpoint will be found
|
11
|
+
# by subtracting one from subsequent boundary.
|
12
|
+
#
|
13
|
+
# Changes solr_params passed in.
|
14
|
+
def add_range_segments_to_solr!(solr_params, solr_field, min, max)
|
15
|
+
field_config = range_config(solr_field)
|
16
|
+
|
17
|
+
solr_params[:"facet.query"] = []
|
18
|
+
|
19
|
+
boundaries = boundaries_for_range_facets(min, max, (field_config[:num_segments] || 10) )
|
20
|
+
|
21
|
+
# Now make the boundaries into actual filter.queries.
|
22
|
+
0.upto(boundaries.length - 2) do |index|
|
23
|
+
first = boundaries[index]
|
24
|
+
last = boundaries[index+1].to_i - 1
|
25
|
+
|
26
|
+
solr_params[:"facet.query"] << "#{solr_field}:[#{first} TO #{last}]"
|
27
|
+
end
|
28
|
+
|
29
|
+
return solr_params
|
30
|
+
end
|
31
|
+
|
32
|
+
# returns an array of 'boundaries' for producing approx num_div
|
33
|
+
# segments between first and last. The boundaries are 'nicefied'
|
34
|
+
# to factors of 5 or 10, so exact number of segments may be more
|
35
|
+
# or less than num_div. Algorithm copied from Flot.
|
36
|
+
#
|
37
|
+
# Because of arithmetic issues with creating boundaries that will
|
38
|
+
# be turned into inclusive ranges, the FINAL boundary will be one
|
39
|
+
# unit more than the actual end of the last range later computed.
|
40
|
+
def boundaries_for_range_facets(first, last, num_div)
|
41
|
+
# arithmetic issues require last to be one more than the actual
|
42
|
+
# last value included in our inclusive range
|
43
|
+
last += 1
|
44
|
+
|
45
|
+
# code cribbed from Flot auto tick calculating, but leaving out
|
46
|
+
# some of Flot's options becuase it started to get confusing.
|
47
|
+
delta = (last - first).to_f / num_div
|
48
|
+
|
49
|
+
# Don't know what most of these variables mean, just copying
|
50
|
+
# from Flot.
|
51
|
+
dec = -1 * ( Math.log10(delta) ).floor
|
52
|
+
magn = (10 ** (-1 * dec)).to_f
|
53
|
+
norm = (magn == 0) ? delta : (delta / magn) # norm is between 1.0 and 10.0
|
54
|
+
|
55
|
+
size = 10
|
56
|
+
if (norm < 1.5)
|
57
|
+
size = 1
|
58
|
+
elsif (norm < 3)
|
59
|
+
size = 2;
|
60
|
+
# special case for 2.5, requires an extra decimal
|
61
|
+
if (norm > 2.25 )
|
62
|
+
size = 2.5;
|
63
|
+
dec = dec + 1
|
64
|
+
end
|
65
|
+
elsif (norm < 7.5)
|
66
|
+
size = 5
|
67
|
+
end
|
68
|
+
|
69
|
+
size = size * magn
|
70
|
+
|
71
|
+
boundaries = []
|
72
|
+
|
73
|
+
start = floorInBase(first, size)
|
74
|
+
i = 0
|
75
|
+
v = Float::MAX
|
76
|
+
prev = nil
|
77
|
+
begin
|
78
|
+
prev = v
|
79
|
+
v = start + i * size
|
80
|
+
boundaries.push(v.to_i)
|
81
|
+
i += 1
|
82
|
+
end while ( v < last && v != prev)
|
83
|
+
|
84
|
+
# Can create dups for small ranges, tighten up
|
85
|
+
boundaries.uniq!
|
86
|
+
|
87
|
+
# That algorithm i don't entirely understand will sometimes
|
88
|
+
# extend past our first and last, tighten it up and make sure
|
89
|
+
# first and last are endpoints.
|
90
|
+
boundaries.delete_if {|b| b <= first || b >= last}
|
91
|
+
boundaries.unshift(first)
|
92
|
+
boundaries.push(last)
|
93
|
+
|
94
|
+
return boundaries
|
95
|
+
end
|
96
|
+
|
97
|
+
# Cribbed from Flot. Round to nearby lower multiple of base
|
98
|
+
def floorInBase(n, base)
|
99
|
+
return base * (n / base).floor
|
100
|
+
end
|
101
|
+
|
102
|
+
end
|
103
|
+
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
# Meant to be applied on top of Blacklight helpers, to over-ride
|
2
|
+
# Will add rendering of limit itself in sidebar, and of constraings
|
3
|
+
# display.
|
4
|
+
module BlacklightRangeLimit::ViewHelperOverride
|
5
|
+
|
6
|
+
|
7
|
+
|
8
|
+
def render_facet_limit(solr_field)
|
9
|
+
if ( range_config(solr_field) )
|
10
|
+
if (should_show_limit(solr_field))
|
11
|
+
render(:partial => "blacklight_range_limit/range_limit_panel", :locals=> {:solr_field => solr_field })
|
12
|
+
end
|
13
|
+
else
|
14
|
+
super(solr_field)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def render_constraints_filters(my_params = params)
|
19
|
+
content = super(my_params)
|
20
|
+
# add a constraint for ranges?
|
21
|
+
unless my_params[:range].blank?
|
22
|
+
my_params[:range].each_pair do |solr_field, hash|
|
23
|
+
next unless hash["missing"] || (!hash["begin"].empty?) || (!hash["end"].empty?)
|
24
|
+
content << render_constraint_element(
|
25
|
+
facet_field_labels[solr_field],
|
26
|
+
range_display(solr_field, my_params),
|
27
|
+
:escape_value => false,
|
28
|
+
:remove => remove_range_param(solr_field, my_params)
|
29
|
+
)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
return content
|
33
|
+
end
|
34
|
+
|
35
|
+
def render_search_to_s_filters(my_params)
|
36
|
+
content = super(my_params)
|
37
|
+
# add a constraint for ranges?
|
38
|
+
unless my_params[:range].blank?
|
39
|
+
my_params[:range].each_pair do |solr_field, hash|
|
40
|
+
next unless hash["missing"] || hash["begin"] || hash["end"]
|
41
|
+
|
42
|
+
content << render_search_to_s_element(
|
43
|
+
facet_field_labels[solr_field],
|
44
|
+
range_display(solr_field, my_params),
|
45
|
+
:escape_value => false
|
46
|
+
)
|
47
|
+
|
48
|
+
end
|
49
|
+
end
|
50
|
+
return content
|
51
|
+
end
|
52
|
+
|
53
|
+
def remove_range_param(solr_field, my_params = params)
|
54
|
+
if ( my_params["range"] )
|
55
|
+
my_params = my_params.dup
|
56
|
+
my_params["range"] = my_params["range"].dup
|
57
|
+
my_params["range"].delete(solr_field)
|
58
|
+
end
|
59
|
+
return my_params
|
60
|
+
end
|
61
|
+
|
62
|
+
# Looks in the solr @response for ["facet_counts"]["facet_queries"][solr_field], for elements
|
63
|
+
# expressed as "solr_field:[X to Y]", turns them into
|
64
|
+
# a list of hashes with [:from, :to, :count], sorted by
|
65
|
+
# :from. Assumes integers for sorting purposes.
|
66
|
+
def solr_range_queries_to_a(solr_field)
|
67
|
+
return [] unless @response["facet_counts"] && @response["facet_counts"]["facet_queries"]
|
68
|
+
|
69
|
+
array = []
|
70
|
+
|
71
|
+
@response["facet_counts"]["facet_queries"].each_pair do |query, count|
|
72
|
+
if query =~ /#{solr_field}: *\[ *(\d+) *TO *(\d+) *\]/
|
73
|
+
array << {:from => $1, :to => $2, :count => count}
|
74
|
+
end
|
75
|
+
end
|
76
|
+
array = array.sort_by {|hash| hash[:from].to_i }
|
77
|
+
|
78
|
+
return array
|
79
|
+
end
|
80
|
+
|
81
|
+
end
|
82
|
+
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# Copy BlacklightRangeLimit assets to public folder in current app.
|
2
|
+
# If you want to do this on application startup, you can
|
3
|
+
# add this next line to your one of your environment files --
|
4
|
+
# generally you'd only want to do this in 'development', and can
|
5
|
+
# add it to environments/development.rb:
|
6
|
+
# require File.join(BlacklightRangeLimit.root, "lib", "generators", "blacklight", "assets_generator.rb")
|
7
|
+
# BlacklightRangeLimit::AssetsGenerator.start(["--force", "--quiet"])
|
8
|
+
|
9
|
+
|
10
|
+
# Need the requires here so we can call the generator from environment.rb
|
11
|
+
# as suggested above.
|
12
|
+
require 'rails/generators'
|
13
|
+
require 'rails/generators/base'
|
14
|
+
module BlacklightRangeLimit
|
15
|
+
class AssetsGenerator < Rails::Generators::Base
|
16
|
+
source_root File.expand_path('../templates', __FILE__)
|
17
|
+
|
18
|
+
def assets
|
19
|
+
directory("public/stylesheets")
|
20
|
+
directory("public/javascripts")
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
@@ -0,0 +1,11 @@
|
|
1
|
+
require 'rails/generators'
|
2
|
+
|
3
|
+
class BlacklightRangeLimitGenerator < Rails::Generators::Base
|
4
|
+
source_root File.expand_path('../templates', __FILE__)
|
5
|
+
|
6
|
+
require File.expand_path('../assets_generator.rb', __FILE__)
|
7
|
+
def copy_public_assets
|
8
|
+
BlacklightRangeLimit::AssetsGenerator.start
|
9
|
+
end
|
10
|
+
|
11
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
if(!document.createElement("canvas").getContext){(function(){var z=Math;var K=z.round;var J=z.sin;var U=z.cos;var b=z.abs;var k=z.sqrt;var D=10;var F=D/2;function T(){return this.context_||(this.context_=new W(this))}var O=Array.prototype.slice;function G(i,j,m){var Z=O.call(arguments,2);return function(){return i.apply(j,Z.concat(O.call(arguments)))}}function AD(Z){return String(Z).replace(/&/g,"&").replace(/"/g,""")}function r(i){if(!i.namespaces.g_vml_){i.namespaces.add("g_vml_","urn:schemas-microsoft-com:vml","#default#VML")}if(!i.namespaces.g_o_){i.namespaces.add("g_o_","urn:schemas-microsoft-com:office:office","#default#VML")}if(!i.styleSheets.ex_canvas_){var Z=i.createStyleSheet();Z.owningElement.id="ex_canvas_";Z.cssText="canvas{display:inline-block;overflow:hidden;text-align:left;width:300px;height:150px}"}}r(document);var E={init:function(Z){if(/MSIE/.test(navigator.userAgent)&&!window.opera){var i=Z||document;i.createElement("canvas");i.attachEvent("onreadystatechange",G(this.init_,this,i))}},init_:function(m){var j=m.getElementsByTagName("canvas");for(var Z=0;Z<j.length;Z++){this.initElement(j[Z])}},initElement:function(i){if(!i.getContext){i.getContext=T;r(i.ownerDocument);i.innerHTML="";i.attachEvent("onpropertychange",S);i.attachEvent("onresize",w);var Z=i.attributes;if(Z.width&&Z.width.specified){i.style.width=Z.width.nodeValue+"px"}else{i.width=i.clientWidth}if(Z.height&&Z.height.specified){i.style.height=Z.height.nodeValue+"px"}else{i.height=i.clientHeight}}return i}};function S(i){var Z=i.srcElement;switch(i.propertyName){case"width":Z.getContext().clearRect();Z.style.width=Z.attributes.width.nodeValue+"px";Z.firstChild.style.width=Z.clientWidth+"px";break;case"height":Z.getContext().clearRect();Z.style.height=Z.attributes.height.nodeValue+"px";Z.firstChild.style.height=Z.clientHeight+"px";break}}function w(i){var Z=i.srcElement;if(Z.firstChild){Z.firstChild.style.width=Z.clientWidth+"px";Z.firstChild.style.height=Z.clientHeight+"px"}}E.init();var I=[];for(var AC=0;AC<16;AC++){for(var AB=0;AB<16;AB++){I[AC*16+AB]=AC.toString(16)+AB.toString(16)}}function V(){return[[1,0,0],[0,1,0],[0,0,1]]}function d(m,j){var i=V();for(var Z=0;Z<3;Z++){for(var AF=0;AF<3;AF++){var p=0;for(var AE=0;AE<3;AE++){p+=m[Z][AE]*j[AE][AF]}i[Z][AF]=p}}return i}function Q(i,Z){Z.fillStyle=i.fillStyle;Z.lineCap=i.lineCap;Z.lineJoin=i.lineJoin;Z.lineWidth=i.lineWidth;Z.miterLimit=i.miterLimit;Z.shadowBlur=i.shadowBlur;Z.shadowColor=i.shadowColor;Z.shadowOffsetX=i.shadowOffsetX;Z.shadowOffsetY=i.shadowOffsetY;Z.strokeStyle=i.strokeStyle;Z.globalAlpha=i.globalAlpha;Z.font=i.font;Z.textAlign=i.textAlign;Z.textBaseline=i.textBaseline;Z.arcScaleX_=i.arcScaleX_;Z.arcScaleY_=i.arcScaleY_;Z.lineScale_=i.lineScale_}var B={aliceblue:"#F0F8FF",antiquewhite:"#FAEBD7",aquamarine:"#7FFFD4",azure:"#F0FFFF",beige:"#F5F5DC",bisque:"#FFE4C4",black:"#000000",blanchedalmond:"#FFEBCD",blueviolet:"#8A2BE2",brown:"#A52A2A",burlywood:"#DEB887",cadetblue:"#5F9EA0",chartreuse:"#7FFF00",chocolate:"#D2691E",coral:"#FF7F50",cornflowerblue:"#6495ED",cornsilk:"#FFF8DC",crimson:"#DC143C",cyan:"#00FFFF",darkblue:"#00008B",darkcyan:"#008B8B",darkgoldenrod:"#B8860B",darkgray:"#A9A9A9",darkgreen:"#006400",darkgrey:"#A9A9A9",darkkhaki:"#BDB76B",darkmagenta:"#8B008B",darkolivegreen:"#556B2F",darkorange:"#FF8C00",darkorchid:"#9932CC",darkred:"#8B0000",darksalmon:"#E9967A",darkseagreen:"#8FBC8F",darkslateblue:"#483D8B",darkslategray:"#2F4F4F",darkslategrey:"#2F4F4F",darkturquoise:"#00CED1",darkviolet:"#9400D3",deeppink:"#FF1493",deepskyblue:"#00BFFF",dimgray:"#696969",dimgrey:"#696969",dodgerblue:"#1E90FF",firebrick:"#B22222",floralwhite:"#FFFAF0",forestgreen:"#228B22",gainsboro:"#DCDCDC",ghostwhite:"#F8F8FF",gold:"#FFD700",goldenrod:"#DAA520",grey:"#808080",greenyellow:"#ADFF2F",honeydew:"#F0FFF0",hotpink:"#FF69B4",indianred:"#CD5C5C",indigo:"#4B0082",ivory:"#FFFFF0",khaki:"#F0E68C",lavender:"#E6E6FA",lavenderblush:"#FFF0F5",lawngreen:"#7CFC00",lemonchiffon:"#FFFACD",lightblue:"#ADD8E6",lightcoral:"#F08080",lightcyan:"#E0FFFF",lightgoldenrodyellow:"#FAFAD2",lightgreen:"#90EE90",lightgrey:"#D3D3D3",lightpink:"#FFB6C1",lightsalmon:"#FFA07A",lightseagreen:"#20B2AA",lightskyblue:"#87CEFA",lightslategray:"#778899",lightslategrey:"#778899",lightsteelblue:"#B0C4DE",lightyellow:"#FFFFE0",limegreen:"#32CD32",linen:"#FAF0E6",magenta:"#FF00FF",mediumaquamarine:"#66CDAA",mediumblue:"#0000CD",mediumorchid:"#BA55D3",mediumpurple:"#9370DB",mediumseagreen:"#3CB371",mediumslateblue:"#7B68EE",mediumspringgreen:"#00FA9A",mediumturquoise:"#48D1CC",mediumvioletred:"#C71585",midnightblue:"#191970",mintcream:"#F5FFFA",mistyrose:"#FFE4E1",moccasin:"#FFE4B5",navajowhite:"#FFDEAD",oldlace:"#FDF5E6",olivedrab:"#6B8E23",orange:"#FFA500",orangered:"#FF4500",orchid:"#DA70D6",palegoldenrod:"#EEE8AA",palegreen:"#98FB98",paleturquoise:"#AFEEEE",palevioletred:"#DB7093",papayawhip:"#FFEFD5",peachpuff:"#FFDAB9",peru:"#CD853F",pink:"#FFC0CB",plum:"#DDA0DD",powderblue:"#B0E0E6",rosybrown:"#BC8F8F",royalblue:"#4169E1",saddlebrown:"#8B4513",salmon:"#FA8072",sandybrown:"#F4A460",seagreen:"#2E8B57",seashell:"#FFF5EE",sienna:"#A0522D",skyblue:"#87CEEB",slateblue:"#6A5ACD",slategray:"#708090",slategrey:"#708090",snow:"#FFFAFA",springgreen:"#00FF7F",steelblue:"#4682B4",tan:"#D2B48C",thistle:"#D8BFD8",tomato:"#FF6347",turquoise:"#40E0D0",violet:"#EE82EE",wheat:"#F5DEB3",whitesmoke:"#F5F5F5",yellowgreen:"#9ACD32"};function g(i){var m=i.indexOf("(",3);var Z=i.indexOf(")",m+1);var j=i.substring(m+1,Z).split(",");if(j.length==4&&i.substr(3,1)=="a"){alpha=Number(j[3])}else{j[3]=1}return j}function C(Z){return parseFloat(Z)/100}function N(i,j,Z){return Math.min(Z,Math.max(j,i))}function c(AF){var j,i,Z;h=parseFloat(AF[0])/360%360;if(h<0){h++}s=N(C(AF[1]),0,1);l=N(C(AF[2]),0,1);if(s==0){j=i=Z=l}else{var m=l<0.5?l*(1+s):l+s-l*s;var AE=2*l-m;j=A(AE,m,h+1/3);i=A(AE,m,h);Z=A(AE,m,h-1/3)}return"#"+I[Math.floor(j*255)]+I[Math.floor(i*255)]+I[Math.floor(Z*255)]}function A(i,Z,j){if(j<0){j++}if(j>1){j--}if(6*j<1){return i+(Z-i)*6*j}else{if(2*j<1){return Z}else{if(3*j<2){return i+(Z-i)*(2/3-j)*6}else{return i}}}}function Y(Z){var AE,p=1;Z=String(Z);if(Z.charAt(0)=="#"){AE=Z}else{if(/^rgb/.test(Z)){var m=g(Z);var AE="#",AF;for(var j=0;j<3;j++){if(m[j].indexOf("%")!=-1){AF=Math.floor(C(m[j])*255)}else{AF=Number(m[j])}AE+=I[N(AF,0,255)]}p=m[3]}else{if(/^hsl/.test(Z)){var m=g(Z);AE=c(m);p=m[3]}else{AE=B[Z]||Z}}}return{color:AE,alpha:p}}var L={style:"normal",variant:"normal",weight:"normal",size:10,family:"sans-serif"};var f={};function X(Z){if(f[Z]){return f[Z]}var m=document.createElement("div");var j=m.style;try{j.font=Z}catch(i){}return f[Z]={style:j.fontStyle||L.style,variant:j.fontVariant||L.variant,weight:j.fontWeight||L.weight,size:j.fontSize||L.size,family:j.fontFamily||L.family}}function P(j,i){var Z={};for(var AF in j){Z[AF]=j[AF]}var AE=parseFloat(i.currentStyle.fontSize),m=parseFloat(j.size);if(typeof j.size=="number"){Z.size=j.size}else{if(j.size.indexOf("px")!=-1){Z.size=m}else{if(j.size.indexOf("em")!=-1){Z.size=AE*m}else{if(j.size.indexOf("%")!=-1){Z.size=(AE/100)*m}else{if(j.size.indexOf("pt")!=-1){Z.size=m/0.75}else{Z.size=AE}}}}}Z.size*=0.981;return Z}function AA(Z){return Z.style+" "+Z.variant+" "+Z.weight+" "+Z.size+"px "+Z.family}function t(Z){switch(Z){case"butt":return"flat";case"round":return"round";case"square":default:return"square"}}function W(i){this.m_=V();this.mStack_=[];this.aStack_=[];this.currentPath_=[];this.strokeStyle="#000";this.fillStyle="#000";this.lineWidth=1;this.lineJoin="miter";this.lineCap="butt";this.miterLimit=D*1;this.globalAlpha=1;this.font="10px sans-serif";this.textAlign="left";this.textBaseline="alphabetic";this.canvas=i;var Z=i.ownerDocument.createElement("div");Z.style.width=i.clientWidth+"px";Z.style.height=i.clientHeight+"px";Z.style.overflow="hidden";Z.style.position="absolute";i.appendChild(Z);this.element_=Z;this.arcScaleX_=1;this.arcScaleY_=1;this.lineScale_=1}var M=W.prototype;M.clearRect=function(){if(this.textMeasureEl_){this.textMeasureEl_.removeNode(true);this.textMeasureEl_=null}this.element_.innerHTML=""};M.beginPath=function(){this.currentPath_=[]};M.moveTo=function(i,Z){var j=this.getCoords_(i,Z);this.currentPath_.push({type:"moveTo",x:j.x,y:j.y});this.currentX_=j.x;this.currentY_=j.y};M.lineTo=function(i,Z){var j=this.getCoords_(i,Z);this.currentPath_.push({type:"lineTo",x:j.x,y:j.y});this.currentX_=j.x;this.currentY_=j.y};M.bezierCurveTo=function(j,i,AI,AH,AG,AE){var Z=this.getCoords_(AG,AE);var AF=this.getCoords_(j,i);var m=this.getCoords_(AI,AH);e(this,AF,m,Z)};function e(Z,m,j,i){Z.currentPath_.push({type:"bezierCurveTo",cp1x:m.x,cp1y:m.y,cp2x:j.x,cp2y:j.y,x:i.x,y:i.y});Z.currentX_=i.x;Z.currentY_=i.y}M.quadraticCurveTo=function(AG,j,i,Z){var AF=this.getCoords_(AG,j);var AE=this.getCoords_(i,Z);var AH={x:this.currentX_+2/3*(AF.x-this.currentX_),y:this.currentY_+2/3*(AF.y-this.currentY_)};var m={x:AH.x+(AE.x-this.currentX_)/3,y:AH.y+(AE.y-this.currentY_)/3};e(this,AH,m,AE)};M.arc=function(AJ,AH,AI,AE,i,j){AI*=D;var AN=j?"at":"wa";var AK=AJ+U(AE)*AI-F;var AM=AH+J(AE)*AI-F;var Z=AJ+U(i)*AI-F;var AL=AH+J(i)*AI-F;if(AK==Z&&!j){AK+=0.125}var m=this.getCoords_(AJ,AH);var AG=this.getCoords_(AK,AM);var AF=this.getCoords_(Z,AL);this.currentPath_.push({type:AN,x:m.x,y:m.y,radius:AI,xStart:AG.x,yStart:AG.y,xEnd:AF.x,yEnd:AF.y})};M.rect=function(j,i,Z,m){this.moveTo(j,i);this.lineTo(j+Z,i);this.lineTo(j+Z,i+m);this.lineTo(j,i+m);this.closePath()};M.strokeRect=function(j,i,Z,m){var p=this.currentPath_;this.beginPath();this.moveTo(j,i);this.lineTo(j+Z,i);this.lineTo(j+Z,i+m);this.lineTo(j,i+m);this.closePath();this.stroke();this.currentPath_=p};M.fillRect=function(j,i,Z,m){var p=this.currentPath_;this.beginPath();this.moveTo(j,i);this.lineTo(j+Z,i);this.lineTo(j+Z,i+m);this.lineTo(j,i+m);this.closePath();this.fill();this.currentPath_=p};M.createLinearGradient=function(i,m,Z,j){var p=new v("gradient");p.x0_=i;p.y0_=m;p.x1_=Z;p.y1_=j;return p};M.createRadialGradient=function(m,AE,j,i,p,Z){var AF=new v("gradientradial");AF.x0_=m;AF.y0_=AE;AF.r0_=j;AF.x1_=i;AF.y1_=p;AF.r1_=Z;return AF};M.drawImage=function(AO,j){var AH,AF,AJ,AV,AM,AK,AQ,AX;var AI=AO.runtimeStyle.width;var AN=AO.runtimeStyle.height;AO.runtimeStyle.width="auto";AO.runtimeStyle.height="auto";var AG=AO.width;var AT=AO.height;AO.runtimeStyle.width=AI;AO.runtimeStyle.height=AN;if(arguments.length==3){AH=arguments[1];AF=arguments[2];AM=AK=0;AQ=AJ=AG;AX=AV=AT}else{if(arguments.length==5){AH=arguments[1];AF=arguments[2];AJ=arguments[3];AV=arguments[4];AM=AK=0;AQ=AG;AX=AT}else{if(arguments.length==9){AM=arguments[1];AK=arguments[2];AQ=arguments[3];AX=arguments[4];AH=arguments[5];AF=arguments[6];AJ=arguments[7];AV=arguments[8]}else{throw Error("Invalid number of arguments")}}}var AW=this.getCoords_(AH,AF);var m=AQ/2;var i=AX/2;var AU=[];var Z=10;var AE=10;AU.push(" <g_vml_:group",' coordsize="',D*Z,",",D*AE,'"',' coordorigin="0,0"',' style="width:',Z,"px;height:",AE,"px;position:absolute;");if(this.m_[0][0]!=1||this.m_[0][1]||this.m_[1][1]!=1||this.m_[1][0]){var p=[];p.push("M11=",this.m_[0][0],",","M12=",this.m_[1][0],",","M21=",this.m_[0][1],",","M22=",this.m_[1][1],",","Dx=",K(AW.x/D),",","Dy=",K(AW.y/D),"");var AS=AW;var AR=this.getCoords_(AH+AJ,AF);var AP=this.getCoords_(AH,AF+AV);var AL=this.getCoords_(AH+AJ,AF+AV);AS.x=z.max(AS.x,AR.x,AP.x,AL.x);AS.y=z.max(AS.y,AR.y,AP.y,AL.y);AU.push("padding:0 ",K(AS.x/D),"px ",K(AS.y/D),"px 0;filter:progid:DXImageTransform.Microsoft.Matrix(",p.join(""),", sizingmethod='clip');")}else{AU.push("top:",K(AW.y/D),"px;left:",K(AW.x/D),"px;")}AU.push(' ">','<g_vml_:image src="',AO.src,'"',' style="width:',D*AJ,"px;"," height:",D*AV,'px"',' cropleft="',AM/AG,'"',' croptop="',AK/AT,'"',' cropright="',(AG-AM-AQ)/AG,'"',' cropbottom="',(AT-AK-AX)/AT,'"'," />","</g_vml_:group>");this.element_.insertAdjacentHTML("BeforeEnd",AU.join(""))};M.stroke=function(AM){var m=10;var AN=10;var AE=5000;var AG={x:null,y:null};var AL={x:null,y:null};for(var AH=0;AH<this.currentPath_.length;AH+=AE){var AK=[];var AF=false;AK.push("<g_vml_:shape",' filled="',!!AM,'"',' style="position:absolute;width:',m,"px;height:",AN,'px;"',' coordorigin="0,0"',' coordsize="',D*m,",",D*AN,'"',' stroked="',!AM,'"',' path="');var AO=false;for(var AI=AH;AI<Math.min(AH+AE,this.currentPath_.length);AI++){if(AI%AE==0&&AI>0){AK.push(" m ",K(this.currentPath_[AI-1].x),",",K(this.currentPath_[AI-1].y))}var Z=this.currentPath_[AI];var AJ;switch(Z.type){case"moveTo":AJ=Z;AK.push(" m ",K(Z.x),",",K(Z.y));break;case"lineTo":AK.push(" l ",K(Z.x),",",K(Z.y));break;case"close":AK.push(" x ");Z=null;break;case"bezierCurveTo":AK.push(" c ",K(Z.cp1x),",",K(Z.cp1y),",",K(Z.cp2x),",",K(Z.cp2y),",",K(Z.x),",",K(Z.y));break;case"at":case"wa":AK.push(" ",Z.type," ",K(Z.x-this.arcScaleX_*Z.radius),",",K(Z.y-this.arcScaleY_*Z.radius)," ",K(Z.x+this.arcScaleX_*Z.radius),",",K(Z.y+this.arcScaleY_*Z.radius)," ",K(Z.xStart),",",K(Z.yStart)," ",K(Z.xEnd),",",K(Z.yEnd));break}if(Z){if(AG.x==null||Z.x<AG.x){AG.x=Z.x}if(AL.x==null||Z.x>AL.x){AL.x=Z.x}if(AG.y==null||Z.y<AG.y){AG.y=Z.y}if(AL.y==null||Z.y>AL.y){AL.y=Z.y}}}AK.push(' ">');if(!AM){R(this,AK)}else{a(this,AK,AG,AL)}AK.push("</g_vml_:shape>");this.element_.insertAdjacentHTML("beforeEnd",AK.join(""))}};function R(j,AE){var i=Y(j.strokeStyle);var m=i.color;var p=i.alpha*j.globalAlpha;var Z=j.lineScale_*j.lineWidth;if(Z<1){p*=Z}AE.push("<g_vml_:stroke",' opacity="',p,'"',' joinstyle="',j.lineJoin,'"',' miterlimit="',j.miterLimit,'"',' endcap="',t(j.lineCap),'"',' weight="',Z,'px"',' color="',m,'" />')}function a(AO,AG,Ah,AP){var AH=AO.fillStyle;var AY=AO.arcScaleX_;var AX=AO.arcScaleY_;var Z=AP.x-Ah.x;var m=AP.y-Ah.y;if(AH instanceof v){var AL=0;var Ac={x:0,y:0};var AU=0;var AK=1;if(AH.type_=="gradient"){var AJ=AH.x0_/AY;var j=AH.y0_/AX;var AI=AH.x1_/AY;var Aj=AH.y1_/AX;var Ag=AO.getCoords_(AJ,j);var Af=AO.getCoords_(AI,Aj);var AE=Af.x-Ag.x;var p=Af.y-Ag.y;AL=Math.atan2(AE,p)*180/Math.PI;if(AL<0){AL+=360}if(AL<0.000001){AL=0}}else{var Ag=AO.getCoords_(AH.x0_,AH.y0_);Ac={x:(Ag.x-Ah.x)/Z,y:(Ag.y-Ah.y)/m};Z/=AY*D;m/=AX*D;var Aa=z.max(Z,m);AU=2*AH.r0_/Aa;AK=2*AH.r1_/Aa-AU}var AS=AH.colors_;AS.sort(function(Ak,i){return Ak.offset-i.offset});var AN=AS.length;var AR=AS[0].color;var AQ=AS[AN-1].color;var AW=AS[0].alpha*AO.globalAlpha;var AV=AS[AN-1].alpha*AO.globalAlpha;var Ab=[];for(var Ae=0;Ae<AN;Ae++){var AM=AS[Ae];Ab.push(AM.offset*AK+AU+" "+AM.color)}AG.push('<g_vml_:fill type="',AH.type_,'"',' method="none" focus="100%"',' color="',AR,'"',' color2="',AQ,'"',' colors="',Ab.join(","),'"',' opacity="',AV,'"',' g_o_:opacity2="',AW,'"',' angle="',AL,'"',' focusposition="',Ac.x,",",Ac.y,'" />')}else{if(AH instanceof u){if(Z&&m){var AF=-Ah.x;var AZ=-Ah.y;AG.push("<g_vml_:fill",' position="',AF/Z*AY*AY,",",AZ/m*AX*AX,'"',' type="tile"',' src="',AH.src_,'" />')}}else{var Ai=Y(AO.fillStyle);var AT=Ai.color;var Ad=Ai.alpha*AO.globalAlpha;AG.push('<g_vml_:fill color="',AT,'" opacity="',Ad,'" />')}}}M.fill=function(){this.stroke(true)};M.closePath=function(){this.currentPath_.push({type:"close"})};M.getCoords_=function(j,i){var Z=this.m_;return{x:D*(j*Z[0][0]+i*Z[1][0]+Z[2][0])-F,y:D*(j*Z[0][1]+i*Z[1][1]+Z[2][1])-F}};M.save=function(){var Z={};Q(this,Z);this.aStack_.push(Z);this.mStack_.push(this.m_);this.m_=d(V(),this.m_)};M.restore=function(){if(this.aStack_.length){Q(this.aStack_.pop(),this);this.m_=this.mStack_.pop()}};function H(Z){return isFinite(Z[0][0])&&isFinite(Z[0][1])&&isFinite(Z[1][0])&&isFinite(Z[1][1])&&isFinite(Z[2][0])&&isFinite(Z[2][1])}function y(i,Z,j){if(!H(Z)){return }i.m_=Z;if(j){var p=Z[0][0]*Z[1][1]-Z[0][1]*Z[1][0];i.lineScale_=k(b(p))}}M.translate=function(j,i){var Z=[[1,0,0],[0,1,0],[j,i,1]];y(this,d(Z,this.m_),false)};M.rotate=function(i){var m=U(i);var j=J(i);var Z=[[m,j,0],[-j,m,0],[0,0,1]];y(this,d(Z,this.m_),false)};M.scale=function(j,i){this.arcScaleX_*=j;this.arcScaleY_*=i;var Z=[[j,0,0],[0,i,0],[0,0,1]];y(this,d(Z,this.m_),true)};M.transform=function(p,m,AF,AE,i,Z){var j=[[p,m,0],[AF,AE,0],[i,Z,1]];y(this,d(j,this.m_),true)};M.setTransform=function(AE,p,AG,AF,j,i){var Z=[[AE,p,0],[AG,AF,0],[j,i,1]];y(this,Z,true)};M.drawText_=function(AK,AI,AH,AN,AG){var AM=this.m_,AQ=1000,i=0,AP=AQ,AF={x:0,y:0},AE=[];var Z=P(X(this.font),this.element_);var j=AA(Z);var AR=this.element_.currentStyle;var p=this.textAlign.toLowerCase();switch(p){case"left":case"center":case"right":break;case"end":p=AR.direction=="ltr"?"right":"left";break;case"start":p=AR.direction=="rtl"?"right":"left";break;default:p="left"}switch(this.textBaseline){case"hanging":case"top":AF.y=Z.size/1.75;break;case"middle":break;default:case null:case"alphabetic":case"ideographic":case"bottom":AF.y=-Z.size/2.25;break}switch(p){case"right":i=AQ;AP=0.05;break;case"center":i=AP=AQ/2;break}var AO=this.getCoords_(AI+AF.x,AH+AF.y);AE.push('<g_vml_:line from="',-i,' 0" to="',AP,' 0.05" ',' coordsize="100 100" coordorigin="0 0"',' filled="',!AG,'" stroked="',!!AG,'" style="position:absolute;width:1px;height:1px;">');if(AG){R(this,AE)}else{a(this,AE,{x:-i,y:0},{x:AP,y:Z.size})}var AL=AM[0][0].toFixed(3)+","+AM[1][0].toFixed(3)+","+AM[0][1].toFixed(3)+","+AM[1][1].toFixed(3)+",0,0";var AJ=K(AO.x/D)+","+K(AO.y/D);AE.push('<g_vml_:skew on="t" matrix="',AL,'" ',' offset="',AJ,'" origin="',i,' 0" />','<g_vml_:path textpathok="true" />','<g_vml_:textpath on="true" string="',AD(AK),'" style="v-text-align:',p,";font:",AD(j),'" /></g_vml_:line>');this.element_.insertAdjacentHTML("beforeEnd",AE.join(""))};M.fillText=function(j,Z,m,i){this.drawText_(j,Z,m,i,false)};M.strokeText=function(j,Z,m,i){this.drawText_(j,Z,m,i,true)};M.measureText=function(j){if(!this.textMeasureEl_){var Z='<span style="position:absolute;top:-20000px;left:0;padding:0;margin:0;border:none;white-space:pre;"></span>';this.element_.insertAdjacentHTML("beforeEnd",Z);this.textMeasureEl_=this.element_.lastChild}var i=this.element_.ownerDocument;this.textMeasureEl_.innerHTML="";this.textMeasureEl_.style.font=this.font;this.textMeasureEl_.appendChild(i.createTextNode(j));return{width:this.textMeasureEl_.offsetWidth}};M.clip=function(){};M.arcTo=function(){};M.createPattern=function(i,Z){return new u(i,Z)};function v(Z){this.type_=Z;this.x0_=0;this.y0_=0;this.r0_=0;this.x1_=0;this.y1_=0;this.r1_=0;this.colors_=[]}v.prototype.addColorStop=function(i,Z){Z=Y(Z);this.colors_.push({offset:i,color:Z.color,alpha:Z.alpha})};function u(i,Z){q(i);switch(Z){case"repeat":case null:case"":this.repetition_="repeat";break;case"repeat-x":case"repeat-y":case"no-repeat":this.repetition_=Z;break;default:n("SYNTAX_ERR")}this.src_=i.src;this.width_=i.width;this.height_=i.height}function n(Z){throw new o(Z)}function q(Z){if(!Z||Z.nodeType!=1||Z.tagName!="IMG"){n("TYPE_MISMATCH_ERR")}if(Z.readyState!="complete"){n("INVALID_STATE_ERR")}}function o(Z){this.code=this[Z];this.message=Z+": DOM Exception "+this.code}var x=o.prototype=new Error;x.INDEX_SIZE_ERR=1;x.DOMSTRING_SIZE_ERR=2;x.HIERARCHY_REQUEST_ERR=3;x.WRONG_DOCUMENT_ERR=4;x.INVALID_CHARACTER_ERR=5;x.NO_DATA_ALLOWED_ERR=6;x.NO_MODIFICATION_ALLOWED_ERR=7;x.NOT_FOUND_ERR=8;x.NOT_SUPPORTED_ERR=9;x.INUSE_ATTRIBUTE_ERR=10;x.INVALID_STATE_ERR=11;x.SYNTAX_ERR=12;x.INVALID_MODIFICATION_ERR=13;x.NAMESPACE_ERR=14;x.INVALID_ACCESS_ERR=15;x.VALIDATION_ERR=16;x.TYPE_MISMATCH_ERR=17;G_vmlCanvasManager=E;CanvasRenderingContext2D=W;CanvasGradient=v;CanvasPattern=u;DOMException=o})()};
|
@@ -0,0 +1,2513 @@
|
|
1
|
+
/* Javascript plotting library for jQuery, v. 0.6.
|
2
|
+
*
|
3
|
+
* Released under the MIT license by IOLA, December 2007.
|
4
|
+
*
|
5
|
+
*/
|
6
|
+
|
7
|
+
// first an inline dependency, jquery.colorhelpers.js, we inline it here
|
8
|
+
// for convenience
|
9
|
+
|
10
|
+
/* Plugin for jQuery for working with colors.
|
11
|
+
*
|
12
|
+
* Version 1.0.
|
13
|
+
*
|
14
|
+
* Inspiration from jQuery color animation plugin by John Resig.
|
15
|
+
*
|
16
|
+
* Released under the MIT license by Ole Laursen, October 2009.
|
17
|
+
*
|
18
|
+
* Examples:
|
19
|
+
*
|
20
|
+
* $.color.parse("#fff").scale('rgb', 0.25).add('a', -0.5).toString()
|
21
|
+
* var c = $.color.extract($("#mydiv"), 'background-color');
|
22
|
+
* console.log(c.r, c.g, c.b, c.a);
|
23
|
+
* $.color.make(100, 50, 25, 0.4).toString() // returns "rgba(100,50,25,0.4)"
|
24
|
+
*
|
25
|
+
* Note that .scale() and .add() work in-place instead of returning
|
26
|
+
* new objects.
|
27
|
+
*/
|
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]}})();
|
29
|
+
|
30
|
+
// the actual Flot code
|
31
|
+
(function($) {
|
32
|
+
function Plot(placeholder, data_, options_, plugins) {
|
33
|
+
// data is on the form:
|
34
|
+
// [ series1, series2 ... ]
|
35
|
+
// where series is either just the data as [ [x1, y1], [x2, y2], ... ]
|
36
|
+
// or { data: [ [x1, y1], [x2, y2], ... ], label: "some label", ... }
|
37
|
+
|
38
|
+
var series = [],
|
39
|
+
options = {
|
40
|
+
// the color theme used for graphs
|
41
|
+
colors: ["#edc240", "#afd8f8", "#cb4b4b", "#4da74d", "#9440ed"],
|
42
|
+
legend: {
|
43
|
+
show: true,
|
44
|
+
noColumns: 1, // number of colums in legend table
|
45
|
+
labelFormatter: null, // fn: string -> string
|
46
|
+
labelBoxBorderColor: "#ccc", // border color for the little label boxes
|
47
|
+
container: null, // container (as jQuery object) to put legend in, null means default on top of graph
|
48
|
+
position: "ne", // position of default legend container within plot
|
49
|
+
margin: 5, // distance from grid edge to default legend container within plot
|
50
|
+
backgroundColor: null, // null means auto-detect
|
51
|
+
backgroundOpacity: 0.85 // set to 0 to avoid background
|
52
|
+
},
|
53
|
+
xaxis: {
|
54
|
+
position: "bottom", // or "top"
|
55
|
+
mode: null, // null or "time"
|
56
|
+
color: null, // base color, labels, ticks
|
57
|
+
tickColor: null, // possibly different color of ticks, e.g. "rgba(0,0,0,0.15)"
|
58
|
+
transform: null, // null or f: number -> number to transform axis
|
59
|
+
inverseTransform: null, // if transform is set, this should be the inverse function
|
60
|
+
min: null, // min. value to show, null means set automatically
|
61
|
+
max: null, // max. value to show, null means set automatically
|
62
|
+
autoscaleMargin: null, // margin in % to add if auto-setting min/max
|
63
|
+
ticks: null, // either [1, 3] or [[1, "a"], 3] or (fn: axis info -> ticks) or app. number of ticks for auto-ticks
|
64
|
+
tickFormatter: null, // fn: number -> string
|
65
|
+
labelWidth: null, // size of tick labels in pixels
|
66
|
+
labelHeight: null,
|
67
|
+
tickLength: null, // size in pixels of ticks, or "full" for whole line
|
68
|
+
alignTicksWithAxis: null, // axis number or null for no sync
|
69
|
+
|
70
|
+
// mode specific options
|
71
|
+
tickDecimals: null, // no. of decimals, null means auto
|
72
|
+
tickSize: null, // number or [number, "unit"]
|
73
|
+
minTickSize: null, // number or [number, "unit"]
|
74
|
+
monthNames: null, // list of names of months
|
75
|
+
timeformat: null, // format string to use
|
76
|
+
twelveHourClock: false // 12 or 24 time in time mode
|
77
|
+
},
|
78
|
+
yaxis: {
|
79
|
+
autoscaleMargin: 0.02,
|
80
|
+
position: "left" // or "right"
|
81
|
+
},
|
82
|
+
xaxes: [],
|
83
|
+
yaxes: [],
|
84
|
+
series: {
|
85
|
+
points: {
|
86
|
+
show: false,
|
87
|
+
radius: 3,
|
88
|
+
lineWidth: 2, // in pixels
|
89
|
+
fill: true,
|
90
|
+
fillColor: "#ffffff",
|
91
|
+
symbol: "circle" // or callback
|
92
|
+
},
|
93
|
+
lines: {
|
94
|
+
// we don't put in show: false so we can see
|
95
|
+
// whether lines were actively disabled
|
96
|
+
lineWidth: 2, // in pixels
|
97
|
+
fill: false,
|
98
|
+
fillColor: null,
|
99
|
+
steps: false
|
100
|
+
},
|
101
|
+
bars: {
|
102
|
+
show: false,
|
103
|
+
lineWidth: 2, // in pixels
|
104
|
+
barWidth: 1, // in units of the x axis
|
105
|
+
fill: true,
|
106
|
+
fillColor: null,
|
107
|
+
align: "left", // or "center"
|
108
|
+
horizontal: false
|
109
|
+
},
|
110
|
+
shadowSize: 3
|
111
|
+
},
|
112
|
+
grid: {
|
113
|
+
show: true,
|
114
|
+
aboveData: false,
|
115
|
+
color: "#545454", // primary color used for outline and labels
|
116
|
+
backgroundColor: null, // null for transparent, else color
|
117
|
+
borderColor: null, // set if different from the grid color
|
118
|
+
tickColor: null, // color for the ticks, e.g. "rgba(0,0,0,0.15)"
|
119
|
+
labelMargin: 5, // in pixels
|
120
|
+
axisMargin: 8, // in pixels
|
121
|
+
borderWidth: 2, // in pixels
|
122
|
+
markings: null, // array of ranges or fn: axes -> array of ranges
|
123
|
+
markingsColor: "#f4f4f4",
|
124
|
+
markingsLineWidth: 2,
|
125
|
+
// interactive stuff
|
126
|
+
clickable: false,
|
127
|
+
hoverable: false,
|
128
|
+
autoHighlight: true, // highlight in case mouse is near
|
129
|
+
mouseActiveRadius: 10 // how far the mouse can be away to activate an item
|
130
|
+
},
|
131
|
+
hooks: {}
|
132
|
+
},
|
133
|
+
canvas = null, // the canvas for the plot itself
|
134
|
+
overlay = null, // canvas for interactive stuff on top of plot
|
135
|
+
eventHolder = null, // jQuery object that events should be bound to
|
136
|
+
ctx = null, octx = null,
|
137
|
+
xaxes = [], yaxes = [],
|
138
|
+
plotOffset = { left: 0, right: 0, top: 0, bottom: 0},
|
139
|
+
canvasWidth = 0, canvasHeight = 0,
|
140
|
+
plotWidth = 0, plotHeight = 0,
|
141
|
+
hooks = {
|
142
|
+
processOptions: [],
|
143
|
+
processRawData: [],
|
144
|
+
processDatapoints: [],
|
145
|
+
drawSeries: [],
|
146
|
+
draw: [],
|
147
|
+
bindEvents: [],
|
148
|
+
drawOverlay: []
|
149
|
+
},
|
150
|
+
plot = this;
|
151
|
+
|
152
|
+
// public functions
|
153
|
+
plot.setData = setData;
|
154
|
+
plot.setupGrid = setupGrid;
|
155
|
+
plot.draw = draw;
|
156
|
+
plot.getPlaceholder = function() { return placeholder; };
|
157
|
+
plot.getCanvas = function() { return canvas; };
|
158
|
+
plot.getPlotOffset = function() { return plotOffset; };
|
159
|
+
plot.width = function () { return plotWidth; };
|
160
|
+
plot.height = function () { return plotHeight; };
|
161
|
+
plot.offset = function () {
|
162
|
+
var o = eventHolder.offset();
|
163
|
+
o.left += plotOffset.left;
|
164
|
+
o.top += plotOffset.top;
|
165
|
+
return o;
|
166
|
+
};
|
167
|
+
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
|
+
plot.getAxes = function () {
|
175
|
+
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
|
+
|
187
|
+
return res;
|
188
|
+
};
|
189
|
+
plot.getXAxes = function () { return xaxes; };
|
190
|
+
plot.getYAxes = function () { return yaxes; };
|
191
|
+
plot.getUsedAxes = getUsedAxes; // return flat array with x and y axes that are in use
|
192
|
+
plot.c2p = canvasToAxisCoords;
|
193
|
+
plot.p2c = axisToCanvasCoords;
|
194
|
+
plot.getOptions = function () { return options; };
|
195
|
+
plot.highlight = highlight;
|
196
|
+
plot.unhighlight = unhighlight;
|
197
|
+
plot.triggerRedrawOverlay = triggerRedrawOverlay;
|
198
|
+
plot.pointOffset = function(point) {
|
199
|
+
return {
|
200
|
+
left: parseInt(xaxes[axisNumber(point, "x") - 1].p2c(+point.x) + plotOffset.left),
|
201
|
+
top: parseInt(yaxes[axisNumber(point, "y") - 1].p2c(+point.y) + plotOffset.top)
|
202
|
+
};
|
203
|
+
};
|
204
|
+
|
205
|
+
// public attributes
|
206
|
+
plot.hooks = hooks;
|
207
|
+
|
208
|
+
// initialize
|
209
|
+
initPlugins(plot);
|
210
|
+
parseOptions(options_);
|
211
|
+
constructCanvas();
|
212
|
+
setData(data_);
|
213
|
+
setupGrid();
|
214
|
+
draw();
|
215
|
+
bindEvents();
|
216
|
+
|
217
|
+
|
218
|
+
function executeHooks(hook, args) {
|
219
|
+
args = [plot].concat(args);
|
220
|
+
for (var i = 0; i < hook.length; ++i)
|
221
|
+
hook[i].apply(this, args);
|
222
|
+
}
|
223
|
+
|
224
|
+
function initPlugins() {
|
225
|
+
for (var i = 0; i < plugins.length; ++i) {
|
226
|
+
var p = plugins[i];
|
227
|
+
p.init(plot);
|
228
|
+
if (p.options)
|
229
|
+
$.extend(true, options, p.options);
|
230
|
+
}
|
231
|
+
}
|
232
|
+
|
233
|
+
function parseOptions(opts) {
|
234
|
+
var i;
|
235
|
+
|
236
|
+
$.extend(true, options, opts);
|
237
|
+
|
238
|
+
if (options.xaxis.color == null)
|
239
|
+
options.xaxis.color = options.grid.color;
|
240
|
+
if (options.yaxis.color == null)
|
241
|
+
options.yaxis.color = options.grid.color;
|
242
|
+
|
243
|
+
if (options.xaxis.tickColor == null) // backwards-compatibility
|
244
|
+
options.xaxis.tickColor = options.grid.tickColor;
|
245
|
+
if (options.yaxis.tickColor == null) // backwards-compatibility
|
246
|
+
options.yaxis.tickColor = options.grid.tickColor;
|
247
|
+
|
248
|
+
if (options.grid.borderColor == null)
|
249
|
+
options.grid.borderColor = options.grid.color;
|
250
|
+
if (options.grid.tickColor == null)
|
251
|
+
options.grid.tickColor = $.color.parse(options.grid.color).scale('a', 0.22).toString();
|
252
|
+
|
253
|
+
// fill in defaults in axes, copy at least always the
|
254
|
+
// first as the rest of the code assumes it'll be there
|
255
|
+
for (i = 0; i < Math.max(1, options.xaxes.length); ++i)
|
256
|
+
options.xaxes[i] = $.extend(true, {}, options.xaxis, options.xaxes[i]);
|
257
|
+
for (i = 0; i < Math.max(1, options.yaxes.length); ++i)
|
258
|
+
options.yaxes[i] = $.extend(true, {}, options.yaxis, options.yaxes[i]);
|
259
|
+
// backwards compatibility, to be removed in future
|
260
|
+
if (options.xaxis.noTicks && options.xaxis.ticks == null)
|
261
|
+
options.xaxis.ticks = options.xaxis.noTicks;
|
262
|
+
if (options.yaxis.noTicks && options.yaxis.ticks == null)
|
263
|
+
options.yaxis.ticks = options.yaxis.noTicks;
|
264
|
+
if (options.x2axis) {
|
265
|
+
options.y2axis.position = "top";
|
266
|
+
options.xaxes[1] = options.x2axis;
|
267
|
+
}
|
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;
|
273
|
+
}
|
274
|
+
if (options.grid.coloredAreas)
|
275
|
+
options.grid.markings = options.grid.coloredAreas;
|
276
|
+
if (options.grid.coloredAreasColor)
|
277
|
+
options.grid.markingsColor = options.grid.coloredAreasColor;
|
278
|
+
if (options.lines)
|
279
|
+
$.extend(true, options.series.lines, options.lines);
|
280
|
+
if (options.points)
|
281
|
+
$.extend(true, options.series.points, options.points);
|
282
|
+
if (options.bars)
|
283
|
+
$.extend(true, options.series.bars, options.bars);
|
284
|
+
if (options.shadowSize)
|
285
|
+
options.series.shadowSize = options.shadowSize;
|
286
|
+
|
287
|
+
for (i = 0; i < options.xaxes.length; ++i)
|
288
|
+
getOrCreateAxis(xaxes, i + 1).options = options.xaxes[i];
|
289
|
+
for (i = 0; i < options.yaxes.length; ++i)
|
290
|
+
getOrCreateAxis(yaxes, i + 1).options = options.yaxes[i];
|
291
|
+
|
292
|
+
// add hooks from options
|
293
|
+
for (var n in hooks)
|
294
|
+
if (options.hooks[n] && options.hooks[n].length)
|
295
|
+
hooks[n] = hooks[n].concat(options.hooks[n]);
|
296
|
+
|
297
|
+
executeHooks(hooks.processOptions, [options]);
|
298
|
+
}
|
299
|
+
|
300
|
+
function setData(d) {
|
301
|
+
series = parseData(d);
|
302
|
+
fillInSeriesOptions();
|
303
|
+
processData();
|
304
|
+
}
|
305
|
+
|
306
|
+
function parseData(d) {
|
307
|
+
var res = [];
|
308
|
+
for (var i = 0; i < d.length; ++i) {
|
309
|
+
var s = $.extend(true, {}, options.series);
|
310
|
+
|
311
|
+
if (d[i].data) {
|
312
|
+
s.data = d[i].data; // move the data instead of deep-copy
|
313
|
+
delete d[i].data;
|
314
|
+
|
315
|
+
$.extend(true, s, d[i]);
|
316
|
+
|
317
|
+
d[i].data = s.data;
|
318
|
+
}
|
319
|
+
else
|
320
|
+
s.data = d[i];
|
321
|
+
res.push(s);
|
322
|
+
}
|
323
|
+
|
324
|
+
return res;
|
325
|
+
}
|
326
|
+
|
327
|
+
function axisNumber(obj, coord) {
|
328
|
+
var a = obj[coord + "axis"];
|
329
|
+
if (typeof a == "object") // if we got a real axis, extract number
|
330
|
+
a = a.n;
|
331
|
+
if (typeof a != "number")
|
332
|
+
a = 1; // default to first axis
|
333
|
+
return a;
|
334
|
+
}
|
335
|
+
|
336
|
+
function canvasToAxisCoords(pos) {
|
337
|
+
// return an object with x/y corresponding to all used axes
|
338
|
+
var res = {}, i, axis;
|
339
|
+
for (i = 0; i < xaxes.length; ++i) {
|
340
|
+
axis = xaxes[i];
|
341
|
+
if (axis && axis.used)
|
342
|
+
res["x" + axis.n] = axis.c2p(pos.left);
|
343
|
+
}
|
344
|
+
|
345
|
+
for (i = 0; i < yaxes.length; ++i) {
|
346
|
+
axis = yaxes[i];
|
347
|
+
if (axis && axis.used)
|
348
|
+
res["y" + axis.n] = axis.c2p(pos.top);
|
349
|
+
}
|
350
|
+
|
351
|
+
if (res.x1 !== undefined)
|
352
|
+
res.x = res.x1;
|
353
|
+
if (res.y1 !== undefined)
|
354
|
+
res.y = res.y1;
|
355
|
+
|
356
|
+
return res;
|
357
|
+
}
|
358
|
+
|
359
|
+
function axisToCanvasCoords(pos) {
|
360
|
+
// get canvas coords from the first pair of x/y found in pos
|
361
|
+
var res = {}, i, axis, key;
|
362
|
+
|
363
|
+
for (i = 0; i < xaxes.length; ++i) {
|
364
|
+
axis = xaxes[i];
|
365
|
+
if (axis && axis.used) {
|
366
|
+
key = "x" + axis.n;
|
367
|
+
if (pos[key] == null && axis.n == 1)
|
368
|
+
key = "x";
|
369
|
+
|
370
|
+
if (pos[key]) {
|
371
|
+
res.left = axis.p2c(pos[key]);
|
372
|
+
break;
|
373
|
+
}
|
374
|
+
}
|
375
|
+
}
|
376
|
+
|
377
|
+
for (i = 0; i < yaxes.length; ++i) {
|
378
|
+
axis = yaxes[i];
|
379
|
+
if (axis && axis.used) {
|
380
|
+
key = "y" + axis.n;
|
381
|
+
if (pos[key] == null && axis.n == 1)
|
382
|
+
key = "y";
|
383
|
+
|
384
|
+
if (pos[key]) {
|
385
|
+
res.top = axis.p2c(pos[key]);
|
386
|
+
break;
|
387
|
+
}
|
388
|
+
}
|
389
|
+
}
|
390
|
+
|
391
|
+
return res;
|
392
|
+
}
|
393
|
+
|
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
|
+
function getOrCreateAxis(axes, number) {
|
410
|
+
if (!axes[number - 1])
|
411
|
+
axes[number - 1] = {
|
412
|
+
n: number, // save the number for future reference
|
413
|
+
direction: axes == xaxes ? "x" : "y",
|
414
|
+
options: $.extend(true, {}, axes == xaxes ? options.xaxis : options.yaxis)
|
415
|
+
};
|
416
|
+
|
417
|
+
return axes[number - 1];
|
418
|
+
}
|
419
|
+
|
420
|
+
function fillInSeriesOptions() {
|
421
|
+
var i;
|
422
|
+
|
423
|
+
// collect what we already got of colors
|
424
|
+
var neededColors = series.length,
|
425
|
+
usedColors = [],
|
426
|
+
assignedColors = [];
|
427
|
+
for (i = 0; i < series.length; ++i) {
|
428
|
+
var sc = series[i].color;
|
429
|
+
if (sc != null) {
|
430
|
+
--neededColors;
|
431
|
+
if (typeof sc == "number")
|
432
|
+
assignedColors.push(sc);
|
433
|
+
else
|
434
|
+
usedColors.push($.color.parse(series[i].color));
|
435
|
+
}
|
436
|
+
}
|
437
|
+
|
438
|
+
// we might need to generate more colors if higher indices
|
439
|
+
// are assigned
|
440
|
+
for (i = 0; i < assignedColors.length; ++i) {
|
441
|
+
neededColors = Math.max(neededColors, assignedColors[i] + 1);
|
442
|
+
}
|
443
|
+
|
444
|
+
// produce colors as needed
|
445
|
+
var colors = [], variation = 0;
|
446
|
+
i = 0;
|
447
|
+
while (colors.length < neededColors) {
|
448
|
+
var c;
|
449
|
+
if (options.colors.length == i) // check degenerate case
|
450
|
+
c = $.color.make(100, 100, 100);
|
451
|
+
else
|
452
|
+
c = $.color.parse(options.colors[i]);
|
453
|
+
|
454
|
+
// vary color if needed
|
455
|
+
var sign = variation % 2 == 1 ? -1 : 1;
|
456
|
+
c.scale('rgb', 1 + sign * Math.ceil(variation / 2) * 0.2)
|
457
|
+
|
458
|
+
// FIXME: if we're getting to close to something else,
|
459
|
+
// we should probably skip this one
|
460
|
+
colors.push(c);
|
461
|
+
|
462
|
+
++i;
|
463
|
+
if (i >= options.colors.length) {
|
464
|
+
i = 0;
|
465
|
+
++variation;
|
466
|
+
}
|
467
|
+
}
|
468
|
+
|
469
|
+
// fill in the options
|
470
|
+
var colori = 0, s;
|
471
|
+
for (i = 0; i < series.length; ++i) {
|
472
|
+
s = series[i];
|
473
|
+
|
474
|
+
// assign colors
|
475
|
+
if (s.color == null) {
|
476
|
+
s.color = colors[colori].toString();
|
477
|
+
++colori;
|
478
|
+
}
|
479
|
+
else if (typeof s.color == "number")
|
480
|
+
s.color = colors[s.color].toString();
|
481
|
+
|
482
|
+
// turn on lines automatically in case nothing is set
|
483
|
+
if (s.lines.show == null) {
|
484
|
+
var v, show = true;
|
485
|
+
for (v in s)
|
486
|
+
if (s[v] && s[v].show) {
|
487
|
+
show = false;
|
488
|
+
break;
|
489
|
+
}
|
490
|
+
if (show)
|
491
|
+
s.lines.show = true;
|
492
|
+
}
|
493
|
+
|
494
|
+
// setup axes
|
495
|
+
s.xaxis = getOrCreateAxis(xaxes, axisNumber(s, "x"));
|
496
|
+
s.yaxis = getOrCreateAxis(yaxes, axisNumber(s, "y"));
|
497
|
+
}
|
498
|
+
}
|
499
|
+
|
500
|
+
function processData() {
|
501
|
+
var topSentry = Number.POSITIVE_INFINITY,
|
502
|
+
bottomSentry = Number.NEGATIVE_INFINITY,
|
503
|
+
i, j, k, m, length,
|
504
|
+
s, points, ps, x, y, axis, val, f, p;
|
505
|
+
|
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
|
+
function updateAxis(axis, min, max) {
|
516
|
+
if (min < axis.datamin)
|
517
|
+
axis.datamin = min;
|
518
|
+
if (max > axis.datamax)
|
519
|
+
axis.datamax = max;
|
520
|
+
}
|
521
|
+
|
522
|
+
for (i = 0; i < xaxes.length; ++i)
|
523
|
+
initAxis(xaxes[i]);
|
524
|
+
for (i = 0; i < yaxes.length; ++i)
|
525
|
+
initAxis(yaxes[i]);
|
526
|
+
|
527
|
+
for (i = 0; i < series.length; ++i) {
|
528
|
+
s = series[i];
|
529
|
+
s.datapoints = { points: [] };
|
530
|
+
|
531
|
+
executeHooks(hooks.processRawData, [ s, s.data, s.datapoints ]);
|
532
|
+
}
|
533
|
+
|
534
|
+
// first pass: clean and copy data
|
535
|
+
for (i = 0; i < series.length; ++i) {
|
536
|
+
s = series[i];
|
537
|
+
|
538
|
+
var data = s.data, format = s.datapoints.format;
|
539
|
+
|
540
|
+
if (!format) {
|
541
|
+
format = [];
|
542
|
+
// find out how to copy
|
543
|
+
format.push({ x: true, number: true, required: true });
|
544
|
+
format.push({ y: true, number: true, required: true });
|
545
|
+
|
546
|
+
if (s.bars.show || (s.lines.show && s.lines.fill)) {
|
547
|
+
format.push({ y: true, number: true, required: false, defaultValue: 0 });
|
548
|
+
if (s.bars.horizontal) {
|
549
|
+
delete format[format.length - 1].y;
|
550
|
+
format[format.length - 1].x = true;
|
551
|
+
}
|
552
|
+
}
|
553
|
+
|
554
|
+
s.datapoints.format = format;
|
555
|
+
}
|
556
|
+
|
557
|
+
if (s.datapoints.pointsize != null)
|
558
|
+
continue; // already filled in
|
559
|
+
|
560
|
+
s.datapoints.pointsize = format.length;
|
561
|
+
|
562
|
+
ps = s.datapoints.pointsize;
|
563
|
+
points = s.datapoints.points;
|
564
|
+
|
565
|
+
insertSteps = s.lines.show && s.lines.steps;
|
566
|
+
s.xaxis.used = s.yaxis.used = true;
|
567
|
+
|
568
|
+
for (j = k = 0; j < data.length; ++j, k += ps) {
|
569
|
+
p = data[j];
|
570
|
+
|
571
|
+
var nullify = p == null;
|
572
|
+
if (!nullify) {
|
573
|
+
for (m = 0; m < ps; ++m) {
|
574
|
+
val = p[m];
|
575
|
+
f = format[m];
|
576
|
+
|
577
|
+
if (f) {
|
578
|
+
if (f.number && val != null) {
|
579
|
+
val = +val; // convert to number
|
580
|
+
if (isNaN(val))
|
581
|
+
val = null;
|
582
|
+
}
|
583
|
+
|
584
|
+
if (val == null) {
|
585
|
+
if (f.required)
|
586
|
+
nullify = true;
|
587
|
+
|
588
|
+
if (f.defaultValue != null)
|
589
|
+
val = f.defaultValue;
|
590
|
+
}
|
591
|
+
}
|
592
|
+
|
593
|
+
points[k + m] = val;
|
594
|
+
}
|
595
|
+
}
|
596
|
+
|
597
|
+
if (nullify) {
|
598
|
+
for (m = 0; m < ps; ++m) {
|
599
|
+
val = points[k + m];
|
600
|
+
if (val != null) {
|
601
|
+
f = format[m];
|
602
|
+
// extract min/max info
|
603
|
+
if (f.x)
|
604
|
+
updateAxis(s.xaxis, val, val);
|
605
|
+
if (f.y)
|
606
|
+
updateAxis(s.yaxis, val, val);
|
607
|
+
}
|
608
|
+
points[k + m] = null;
|
609
|
+
}
|
610
|
+
}
|
611
|
+
else {
|
612
|
+
// a little bit of line specific stuff that
|
613
|
+
// perhaps shouldn't be here, but lacking
|
614
|
+
// better means...
|
615
|
+
if (insertSteps && k > 0
|
616
|
+
&& points[k - ps] != null
|
617
|
+
&& points[k - ps] != points[k]
|
618
|
+
&& points[k - ps + 1] != points[k + 1]) {
|
619
|
+
// copy the point to make room for a middle point
|
620
|
+
for (m = 0; m < ps; ++m)
|
621
|
+
points[k + ps + m] = points[k + m];
|
622
|
+
|
623
|
+
// middle point has same y
|
624
|
+
points[k + 1] = points[k - ps + 1];
|
625
|
+
|
626
|
+
// we've added a point, better reflect that
|
627
|
+
k += ps;
|
628
|
+
}
|
629
|
+
}
|
630
|
+
}
|
631
|
+
}
|
632
|
+
|
633
|
+
// give the hooks a chance to run
|
634
|
+
for (i = 0; i < series.length; ++i) {
|
635
|
+
s = series[i];
|
636
|
+
|
637
|
+
executeHooks(hooks.processDatapoints, [ s, s.datapoints]);
|
638
|
+
}
|
639
|
+
|
640
|
+
// second pass: find datamax/datamin for auto-scaling
|
641
|
+
for (i = 0; i < series.length; ++i) {
|
642
|
+
s = series[i];
|
643
|
+
points = s.datapoints.points,
|
644
|
+
ps = s.datapoints.pointsize;
|
645
|
+
|
646
|
+
var xmin = topSentry, ymin = topSentry,
|
647
|
+
xmax = bottomSentry, ymax = bottomSentry;
|
648
|
+
|
649
|
+
for (j = 0; j < points.length; j += ps) {
|
650
|
+
if (points[j] == null)
|
651
|
+
continue;
|
652
|
+
|
653
|
+
for (m = 0; m < ps; ++m) {
|
654
|
+
val = points[j + m];
|
655
|
+
f = format[m];
|
656
|
+
if (!f)
|
657
|
+
continue;
|
658
|
+
|
659
|
+
if (f.x) {
|
660
|
+
if (val < xmin)
|
661
|
+
xmin = val;
|
662
|
+
if (val > xmax)
|
663
|
+
xmax = val;
|
664
|
+
}
|
665
|
+
if (f.y) {
|
666
|
+
if (val < ymin)
|
667
|
+
ymin = val;
|
668
|
+
if (val > ymax)
|
669
|
+
ymax = val;
|
670
|
+
}
|
671
|
+
}
|
672
|
+
}
|
673
|
+
|
674
|
+
if (s.bars.show) {
|
675
|
+
// make sure we got room for the bar on the dancing floor
|
676
|
+
var delta = s.bars.align == "left" ? 0 : -s.bars.barWidth/2;
|
677
|
+
if (s.bars.horizontal) {
|
678
|
+
ymin += delta;
|
679
|
+
ymax += delta + s.bars.barWidth;
|
680
|
+
}
|
681
|
+
else {
|
682
|
+
xmin += delta;
|
683
|
+
xmax += delta + s.bars.barWidth;
|
684
|
+
}
|
685
|
+
}
|
686
|
+
|
687
|
+
updateAxis(s.xaxis, xmin, xmax);
|
688
|
+
updateAxis(s.yaxis, ymin, ymax);
|
689
|
+
}
|
690
|
+
|
691
|
+
$.each(getUsedAxes(), function (i, axis) {
|
692
|
+
if (axis.datamin == topSentry)
|
693
|
+
axis.datamin = null;
|
694
|
+
if (axis.datamax == bottomSentry)
|
695
|
+
axis.datamax = null;
|
696
|
+
});
|
697
|
+
}
|
698
|
+
|
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
|
+
}
|
708
|
+
|
709
|
+
canvasWidth = placeholder.width();
|
710
|
+
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
|
+
|
715
|
+
if (canvasWidth <= 0 || canvasHeight <= 0)
|
716
|
+
throw "Invalid dimensions for plot, width = " + canvasWidth + ", height = " + canvasHeight;
|
717
|
+
|
718
|
+
if (window.G_vmlCanvasManager) // excanvas hack
|
719
|
+
window.G_vmlCanvasManager.init_(document); // make sure everything is setup
|
720
|
+
|
721
|
+
// the canvas
|
722
|
+
canvas = $(makeCanvas(canvasWidth, canvasHeight)).appendTo(placeholder).get(0);
|
723
|
+
ctx = canvas.getContext("2d");
|
724
|
+
|
725
|
+
// overlay canvas for interactive features
|
726
|
+
overlay = $(makeCanvas(canvasWidth, canvasHeight)).css({ position: 'absolute', left: 0, top: 0 }).appendTo(placeholder).get(0);
|
727
|
+
octx = overlay.getContext("2d");
|
728
|
+
octx.stroke();
|
729
|
+
}
|
730
|
+
|
731
|
+
function bindEvents() {
|
732
|
+
// we include the canvas in the event holder too, because IE 7
|
733
|
+
// sometimes has trouble with the stacking order
|
734
|
+
eventHolder = $([overlay, canvas]);
|
735
|
+
|
736
|
+
// bind events
|
737
|
+
if (options.grid.hoverable)
|
738
|
+
eventHolder.mousemove(onMouseMove);
|
739
|
+
|
740
|
+
if (options.grid.clickable)
|
741
|
+
eventHolder.click(onClick);
|
742
|
+
|
743
|
+
executeHooks(hooks.bindEvents, [eventHolder]);
|
744
|
+
}
|
745
|
+
|
746
|
+
function setTransformationHelpers(axis) {
|
747
|
+
// set helper functions on the axis, assumes plot area
|
748
|
+
// has been computed already
|
749
|
+
|
750
|
+
function identity(x) { return x; }
|
751
|
+
|
752
|
+
var s, m, t = axis.options.transform || identity,
|
753
|
+
it = axis.options.inverseTransform;
|
754
|
+
|
755
|
+
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); };
|
771
|
+
}
|
772
|
+
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); };
|
784
|
+
}
|
785
|
+
}
|
786
|
+
|
787
|
+
function measureTickLabels(axis) {
|
788
|
+
if (!axis)
|
789
|
+
return;
|
790
|
+
|
791
|
+
var opts = axis.options, i, ticks = axis.ticks || [], labels = [],
|
792
|
+
l, w = opts.labelWidth, h = opts.labelHeight, dummyDiv;
|
793
|
+
|
794
|
+
function makeDummyDiv(labels, width) {
|
795
|
+
return $('<div style="position:absolute;top:-10000px;' + width + 'font-size:smaller">' +
|
796
|
+
'<div class="' + axis.direction + 'Axis ' + axis.direction + axis.n + 'Axis">'
|
797
|
+
+ labels.join("") + '</div></div>')
|
798
|
+
.appendTo(placeholder);
|
799
|
+
}
|
800
|
+
|
801
|
+
if (axis.direction == "x") {
|
802
|
+
// to avoid measuring the widths of the labels (it's slow), we
|
803
|
+
// construct fixed-size boxes and put the labels inside
|
804
|
+
// them, we don't need the exact figures and the
|
805
|
+
// fixed-size box content is easy to center
|
806
|
+
if (w == null)
|
807
|
+
w = Math.floor(canvasWidth / (ticks.length > 0 ? ticks.length : 1));
|
808
|
+
|
809
|
+
// measure x label heights
|
810
|
+
if (h == null) {
|
811
|
+
labels = [];
|
812
|
+
for (i = 0; i < ticks.length; ++i) {
|
813
|
+
l = ticks[i].label;
|
814
|
+
if (l)
|
815
|
+
labels.push('<div class="tickLabel" style="float:left;width:' + w + 'px">' + l + '</div>');
|
816
|
+
}
|
817
|
+
|
818
|
+
if (labels.length > 0) {
|
819
|
+
// stick them all in the same div and measure
|
820
|
+
// collective height
|
821
|
+
labels.push('<div style="clear:left"></div>');
|
822
|
+
dummyDiv = makeDummyDiv(labels, "width:10000px;");
|
823
|
+
h = dummyDiv.height();
|
824
|
+
dummyDiv.remove();
|
825
|
+
}
|
826
|
+
}
|
827
|
+
}
|
828
|
+
else if (w == null || h == null) {
|
829
|
+
// calculate y label dimensions
|
830
|
+
for (i = 0; i < ticks.length; ++i) {
|
831
|
+
l = ticks[i].label;
|
832
|
+
if (l)
|
833
|
+
labels.push('<div class="tickLabel">' + l + '</div>');
|
834
|
+
}
|
835
|
+
|
836
|
+
if (labels.length > 0) {
|
837
|
+
dummyDiv = makeDummyDiv(labels, "");
|
838
|
+
if (w == null)
|
839
|
+
w = dummyDiv.children().width();
|
840
|
+
if (h == null)
|
841
|
+
h = dummyDiv.find("div.tickLabel").height();
|
842
|
+
dummyDiv.remove();
|
843
|
+
}
|
844
|
+
}
|
845
|
+
|
846
|
+
if (w == null)
|
847
|
+
w = 0;
|
848
|
+
if (h == null)
|
849
|
+
h = 0;
|
850
|
+
|
851
|
+
axis.labelWidth = w;
|
852
|
+
axis.labelHeight = h;
|
853
|
+
}
|
854
|
+
|
855
|
+
function computeAxisBox(axis) {
|
856
|
+
if (!axis || (!axis.used && !(axis.labelWidth || axis.labelHeight)))
|
857
|
+
return;
|
858
|
+
|
859
|
+
// find the bounding box of the axis by looking at label
|
860
|
+
// widths/heights and ticks, make room by diminishing the
|
861
|
+
// plotOffset
|
862
|
+
|
863
|
+
var lw = axis.labelWidth,
|
864
|
+
lh = axis.labelHeight,
|
865
|
+
pos = axis.options.position,
|
866
|
+
tickLength = axis.options.tickLength,
|
867
|
+
axismargin = options.grid.axisMargin,
|
868
|
+
padding = options.grid.labelMargin,
|
869
|
+
all = axis.direction == "x" ? xaxes : yaxes,
|
870
|
+
index;
|
871
|
+
|
872
|
+
// determine axis margin
|
873
|
+
var samePosition = $.grep(all, function (a) {
|
874
|
+
return a && a.options.position == pos && (a.labelHeight || a.labelWidth);
|
875
|
+
});
|
876
|
+
if ($.inArray(axis, samePosition) == samePosition.length - 1)
|
877
|
+
axismargin = 0; // outermost
|
878
|
+
|
879
|
+
// determine tick length - if we're innermost, we can use "full"
|
880
|
+
if (tickLength == null)
|
881
|
+
tickLength = "full";
|
882
|
+
|
883
|
+
var sameDirection = $.grep(all, function (a) {
|
884
|
+
return a && (a.labelHeight || a.labelWidth);
|
885
|
+
});
|
886
|
+
|
887
|
+
var innermost = $.inArray(axis, sameDirection) == 0;
|
888
|
+
if (!innermost && tickLength == "full")
|
889
|
+
tickLength = 5;
|
890
|
+
|
891
|
+
if (!isNaN(+tickLength))
|
892
|
+
padding += +tickLength;
|
893
|
+
|
894
|
+
// compute box
|
895
|
+
if (axis.direction == "x") {
|
896
|
+
lh += padding;
|
897
|
+
|
898
|
+
if (pos == "bottom") {
|
899
|
+
plotOffset.bottom += lh + axismargin;
|
900
|
+
axis.box = { top: canvasHeight - plotOffset.bottom, height: lh };
|
901
|
+
}
|
902
|
+
else {
|
903
|
+
axis.box = { top: plotOffset.top + axismargin, height: lh };
|
904
|
+
plotOffset.top += lh + axismargin;
|
905
|
+
}
|
906
|
+
}
|
907
|
+
else {
|
908
|
+
lw += padding;
|
909
|
+
|
910
|
+
if (pos == "left") {
|
911
|
+
axis.box = { left: plotOffset.left + axismargin, width: lw };
|
912
|
+
plotOffset.left += lw + axismargin;
|
913
|
+
}
|
914
|
+
else {
|
915
|
+
plotOffset.right += lw + axismargin;
|
916
|
+
axis.box = { left: canvasWidth - plotOffset.right, width: lw };
|
917
|
+
}
|
918
|
+
}
|
919
|
+
|
920
|
+
// save for future reference
|
921
|
+
axis.position = pos;
|
922
|
+
axis.tickLength = tickLength;
|
923
|
+
axis.box.padding = padding;
|
924
|
+
axis.innermost = innermost;
|
925
|
+
}
|
926
|
+
|
927
|
+
function fixupAxisBox(axis) {
|
928
|
+
// set remaining bounding box coordinates
|
929
|
+
if (axis.direction == "x") {
|
930
|
+
axis.box.left = plotOffset.left;
|
931
|
+
axis.box.width = plotWidth;
|
932
|
+
}
|
933
|
+
else {
|
934
|
+
axis.box.top = plotOffset.top;
|
935
|
+
axis.box.height = plotHeight;
|
936
|
+
}
|
937
|
+
}
|
938
|
+
|
939
|
+
function setupGrid() {
|
940
|
+
var axes = getUsedAxes(), j, k;
|
941
|
+
|
942
|
+
// compute axis intervals
|
943
|
+
for (k = 0; k < axes.length; ++k)
|
944
|
+
setRange(axes[k]);
|
945
|
+
|
946
|
+
|
947
|
+
plotOffset.left = plotOffset.right = plotOffset.top = plotOffset.bottom = 0;
|
948
|
+
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
|
+
}
|
955
|
+
|
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]);
|
969
|
+
|
970
|
+
// make sure we've got enough space for things that
|
971
|
+
// 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
|
+
|
976
|
+
for (var a in plotOffset) {
|
977
|
+
plotOffset[a] += options.grid.borderWidth;
|
978
|
+
plotOffset[a] = Math.max(maxOutset, plotOffset[a]);
|
979
|
+
}
|
980
|
+
}
|
981
|
+
|
982
|
+
plotWidth = canvasWidth - plotOffset.left - plotOffset.right;
|
983
|
+
plotHeight = canvasHeight - plotOffset.bottom - plotOffset.top;
|
984
|
+
|
985
|
+
// now we got the proper plotWidth/Height, we can compute the scaling
|
986
|
+
for (k = 0; k < axes.length; ++k)
|
987
|
+
setTransformationHelpers(axes[k]);
|
988
|
+
|
989
|
+
if (options.grid.show) {
|
990
|
+
for (k = 0; k < axes.length; ++k)
|
991
|
+
fixupAxisBox(axes[k]);
|
992
|
+
|
993
|
+
insertAxisLabels();
|
994
|
+
}
|
995
|
+
|
996
|
+
insertLegend();
|
997
|
+
}
|
998
|
+
|
999
|
+
function setRange(axis) {
|
1000
|
+
var opts = axis.options,
|
1001
|
+
min = +(opts.min != null ? opts.min : axis.datamin),
|
1002
|
+
max = +(opts.max != null ? opts.max : axis.datamax),
|
1003
|
+
delta = max - min;
|
1004
|
+
|
1005
|
+
if (delta == 0.0) {
|
1006
|
+
// degenerate case
|
1007
|
+
var widen = max == 0 ? 1 : 0.01;
|
1008
|
+
|
1009
|
+
if (opts.min == null)
|
1010
|
+
min -= widen;
|
1011
|
+
// alway widen max if we couldn't widen min to ensure we
|
1012
|
+
// don't fall into min == max which doesn't work
|
1013
|
+
if (opts.max == null || opts.min != null)
|
1014
|
+
max += widen;
|
1015
|
+
}
|
1016
|
+
else {
|
1017
|
+
// consider autoscaling
|
1018
|
+
var margin = opts.autoscaleMargin;
|
1019
|
+
if (margin != null) {
|
1020
|
+
if (opts.min == null) {
|
1021
|
+
min -= delta * margin;
|
1022
|
+
// make sure we don't go below zero if all values
|
1023
|
+
// are positive
|
1024
|
+
if (min < 0 && axis.datamin != null && axis.datamin >= 0)
|
1025
|
+
min = 0;
|
1026
|
+
}
|
1027
|
+
if (opts.max == null) {
|
1028
|
+
max += delta * margin;
|
1029
|
+
if (max > 0 && axis.datamax != null && axis.datamax <= 0)
|
1030
|
+
max = 0;
|
1031
|
+
}
|
1032
|
+
}
|
1033
|
+
}
|
1034
|
+
axis.min = min;
|
1035
|
+
axis.max = max;
|
1036
|
+
}
|
1037
|
+
|
1038
|
+
function setupTickGeneration(axis) {
|
1039
|
+
var opts = axis.options;
|
1040
|
+
|
1041
|
+
// estimate number of ticks
|
1042
|
+
var noTicks;
|
1043
|
+
if (typeof opts.ticks == "number" && opts.ticks > 0)
|
1044
|
+
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
|
+
else
|
1050
|
+
noTicks = 0.3 * Math.sqrt(canvasHeight);
|
1051
|
+
|
1052
|
+
var delta = (axis.max - axis.min) / noTicks,
|
1053
|
+
size, generator, unit, formatter, i, magn, norm;
|
1054
|
+
|
1055
|
+
if (opts.mode == "time") {
|
1056
|
+
// pretty handling of time
|
1057
|
+
|
1058
|
+
// map of app. size of time units in milliseconds
|
1059
|
+
var timeUnitSize = {
|
1060
|
+
"second": 1000,
|
1061
|
+
"minute": 60 * 1000,
|
1062
|
+
"hour": 60 * 60 * 1000,
|
1063
|
+
"day": 24 * 60 * 60 * 1000,
|
1064
|
+
"month": 30 * 24 * 60 * 60 * 1000,
|
1065
|
+
"year": 365.2425 * 24 * 60 * 60 * 1000
|
1066
|
+
};
|
1067
|
+
|
1068
|
+
|
1069
|
+
// the allowed tick sizes, after 1 year we use
|
1070
|
+
// an integer algorithm
|
1071
|
+
var spec = [
|
1072
|
+
[1, "second"], [2, "second"], [5, "second"], [10, "second"],
|
1073
|
+
[30, "second"],
|
1074
|
+
[1, "minute"], [2, "minute"], [5, "minute"], [10, "minute"],
|
1075
|
+
[30, "minute"],
|
1076
|
+
[1, "hour"], [2, "hour"], [4, "hour"],
|
1077
|
+
[8, "hour"], [12, "hour"],
|
1078
|
+
[1, "day"], [2, "day"], [3, "day"],
|
1079
|
+
[0.25, "month"], [0.5, "month"], [1, "month"],
|
1080
|
+
[2, "month"], [3, "month"], [6, "month"],
|
1081
|
+
[1, "year"]
|
1082
|
+
];
|
1083
|
+
|
1084
|
+
var minSize = 0;
|
1085
|
+
if (opts.minTickSize != null) {
|
1086
|
+
if (typeof opts.tickSize == "number")
|
1087
|
+
minSize = opts.tickSize;
|
1088
|
+
else
|
1089
|
+
minSize = opts.minTickSize[0] * timeUnitSize[opts.minTickSize[1]];
|
1090
|
+
}
|
1091
|
+
|
1092
|
+
for (var i = 0; i < spec.length - 1; ++i)
|
1093
|
+
if (delta < (spec[i][0] * timeUnitSize[spec[i][1]]
|
1094
|
+
+ spec[i + 1][0] * timeUnitSize[spec[i + 1][1]]) / 2
|
1095
|
+
&& spec[i][0] * timeUnitSize[spec[i][1]] >= minSize)
|
1096
|
+
break;
|
1097
|
+
size = spec[i][0];
|
1098
|
+
unit = spec[i][1];
|
1099
|
+
|
1100
|
+
// special-case the possibility of several years
|
1101
|
+
if (unit == "year") {
|
1102
|
+
magn = Math.pow(10, Math.floor(Math.log(delta / timeUnitSize.year) / Math.LN10));
|
1103
|
+
norm = (delta / timeUnitSize.year) / magn;
|
1104
|
+
if (norm < 1.5)
|
1105
|
+
size = 1;
|
1106
|
+
else if (norm < 3)
|
1107
|
+
size = 2;
|
1108
|
+
else if (norm < 7.5)
|
1109
|
+
size = 5;
|
1110
|
+
else
|
1111
|
+
size = 10;
|
1112
|
+
|
1113
|
+
size *= magn;
|
1114
|
+
}
|
1115
|
+
|
1116
|
+
axis.tickSize = opts.tickSize || [size, unit];
|
1117
|
+
|
1118
|
+
generator = function(axis) {
|
1119
|
+
var ticks = [],
|
1120
|
+
tickSize = axis.tickSize[0], unit = axis.tickSize[1],
|
1121
|
+
d = new Date(axis.min);
|
1122
|
+
|
1123
|
+
var step = tickSize * timeUnitSize[unit];
|
1124
|
+
|
1125
|
+
if (unit == "second")
|
1126
|
+
d.setUTCSeconds(floorInBase(d.getUTCSeconds(), tickSize));
|
1127
|
+
if (unit == "minute")
|
1128
|
+
d.setUTCMinutes(floorInBase(d.getUTCMinutes(), tickSize));
|
1129
|
+
if (unit == "hour")
|
1130
|
+
d.setUTCHours(floorInBase(d.getUTCHours(), tickSize));
|
1131
|
+
if (unit == "month")
|
1132
|
+
d.setUTCMonth(floorInBase(d.getUTCMonth(), tickSize));
|
1133
|
+
if (unit == "year")
|
1134
|
+
d.setUTCFullYear(floorInBase(d.getUTCFullYear(), tickSize));
|
1135
|
+
|
1136
|
+
// reset smaller components
|
1137
|
+
d.setUTCMilliseconds(0);
|
1138
|
+
if (step >= timeUnitSize.minute)
|
1139
|
+
d.setUTCSeconds(0);
|
1140
|
+
if (step >= timeUnitSize.hour)
|
1141
|
+
d.setUTCMinutes(0);
|
1142
|
+
if (step >= timeUnitSize.day)
|
1143
|
+
d.setUTCHours(0);
|
1144
|
+
if (step >= timeUnitSize.day * 4)
|
1145
|
+
d.setUTCDate(1);
|
1146
|
+
if (step >= timeUnitSize.year)
|
1147
|
+
d.setUTCMonth(0);
|
1148
|
+
|
1149
|
+
|
1150
|
+
var carry = 0, v = Number.NaN, prev;
|
1151
|
+
do {
|
1152
|
+
prev = v;
|
1153
|
+
v = d.getTime();
|
1154
|
+
ticks.push(v);
|
1155
|
+
if (unit == "month") {
|
1156
|
+
if (tickSize < 1) {
|
1157
|
+
// a bit complicated - we'll divide the month
|
1158
|
+
// up but we need to take care of fractions
|
1159
|
+
// so we don't end up in the middle of a day
|
1160
|
+
d.setUTCDate(1);
|
1161
|
+
var start = d.getTime();
|
1162
|
+
d.setUTCMonth(d.getUTCMonth() + 1);
|
1163
|
+
var end = d.getTime();
|
1164
|
+
d.setTime(v + carry * timeUnitSize.hour + (end - start) * tickSize);
|
1165
|
+
carry = d.getUTCHours();
|
1166
|
+
d.setUTCHours(0);
|
1167
|
+
}
|
1168
|
+
else
|
1169
|
+
d.setUTCMonth(d.getUTCMonth() + tickSize);
|
1170
|
+
}
|
1171
|
+
else if (unit == "year") {
|
1172
|
+
d.setUTCFullYear(d.getUTCFullYear() + tickSize);
|
1173
|
+
}
|
1174
|
+
else
|
1175
|
+
d.setTime(v + step);
|
1176
|
+
} while (v < axis.max && v != prev);
|
1177
|
+
|
1178
|
+
return ticks;
|
1179
|
+
};
|
1180
|
+
|
1181
|
+
formatter = function (v, axis) {
|
1182
|
+
var d = new Date(v);
|
1183
|
+
|
1184
|
+
// first check global format
|
1185
|
+
if (opts.timeformat != null)
|
1186
|
+
return $.plot.formatDate(d, opts.timeformat, opts.monthNames);
|
1187
|
+
|
1188
|
+
var t = axis.tickSize[0] * timeUnitSize[axis.tickSize[1]];
|
1189
|
+
var span = axis.max - axis.min;
|
1190
|
+
var suffix = (opts.twelveHourClock) ? " %p" : "";
|
1191
|
+
|
1192
|
+
if (t < timeUnitSize.minute)
|
1193
|
+
fmt = "%h:%M:%S" + suffix;
|
1194
|
+
else if (t < timeUnitSize.day) {
|
1195
|
+
if (span < 2 * timeUnitSize.day)
|
1196
|
+
fmt = "%h:%M" + suffix;
|
1197
|
+
else
|
1198
|
+
fmt = "%b %d %h:%M" + suffix;
|
1199
|
+
}
|
1200
|
+
else if (t < timeUnitSize.month)
|
1201
|
+
fmt = "%b %d";
|
1202
|
+
else if (t < timeUnitSize.year) {
|
1203
|
+
if (span < timeUnitSize.year)
|
1204
|
+
fmt = "%b";
|
1205
|
+
else
|
1206
|
+
fmt = "%b %y";
|
1207
|
+
}
|
1208
|
+
else
|
1209
|
+
fmt = "%y";
|
1210
|
+
|
1211
|
+
return $.plot.formatDate(d, fmt, opts.monthNames);
|
1212
|
+
};
|
1213
|
+
}
|
1214
|
+
else {
|
1215
|
+
// pretty rounding of base-10 numbers
|
1216
|
+
var maxDec = opts.tickDecimals;
|
1217
|
+
var dec = -Math.floor(Math.log(delta) / Math.LN10);
|
1218
|
+
if (maxDec != null && dec > maxDec)
|
1219
|
+
dec = maxDec;
|
1220
|
+
|
1221
|
+
magn = Math.pow(10, -dec);
|
1222
|
+
norm = delta / magn; // norm is between 1.0 and 10.0
|
1223
|
+
|
1224
|
+
if (norm < 1.5)
|
1225
|
+
size = 1;
|
1226
|
+
else if (norm < 3) {
|
1227
|
+
size = 2;
|
1228
|
+
// special case for 2.5, requires an extra decimal
|
1229
|
+
if (norm > 2.25 && (maxDec == null || dec + 1 <= maxDec)) {
|
1230
|
+
size = 2.5;
|
1231
|
+
++dec;
|
1232
|
+
}
|
1233
|
+
}
|
1234
|
+
else if (norm < 7.5)
|
1235
|
+
size = 5;
|
1236
|
+
else
|
1237
|
+
size = 10;
|
1238
|
+
|
1239
|
+
size *= magn;
|
1240
|
+
|
1241
|
+
if (opts.minTickSize != null && size < opts.minTickSize)
|
1242
|
+
size = opts.minTickSize;
|
1243
|
+
|
1244
|
+
axis.tickDecimals = Math.max(0, maxDec != null ? maxDec : dec);
|
1245
|
+
axis.tickSize = opts.tickSize || size;
|
1246
|
+
|
1247
|
+
generator = function (axis) {
|
1248
|
+
var ticks = [];
|
1249
|
+
|
1250
|
+
// spew out all possible ticks
|
1251
|
+
var start = floorInBase(axis.min, axis.tickSize),
|
1252
|
+
i = 0, v = Number.NaN, prev;
|
1253
|
+
do {
|
1254
|
+
prev = v;
|
1255
|
+
v = start + i * axis.tickSize;
|
1256
|
+
ticks.push(v);
|
1257
|
+
++i;
|
1258
|
+
} while (v < axis.max && v != prev);
|
1259
|
+
return ticks;
|
1260
|
+
};
|
1261
|
+
|
1262
|
+
formatter = function (v, axis) {
|
1263
|
+
return v.toFixed(axis.tickDecimals);
|
1264
|
+
};
|
1265
|
+
}
|
1266
|
+
|
1267
|
+
if (opts.alignTicksWithAxis != null) {
|
1268
|
+
var otherAxis = (axis.direction == "x" ? xaxes : yaxes)[opts.alignTicksWithAxis - 1];
|
1269
|
+
if (otherAxis && otherAxis.used && otherAxis != axis) {
|
1270
|
+
// consider snapping min/max to outermost nice ticks
|
1271
|
+
var niceTicks = generator(axis);
|
1272
|
+
if (niceTicks.length > 0) {
|
1273
|
+
if (opts.min == null)
|
1274
|
+
axis.min = Math.min(axis.min, niceTicks[0]);
|
1275
|
+
if (opts.max == null && niceTicks.length > 1)
|
1276
|
+
axis.max = Math.max(axis.max, niceTicks[niceTicks.length - 1]);
|
1277
|
+
}
|
1278
|
+
|
1279
|
+
generator = function (axis) {
|
1280
|
+
// copy ticks, scaled to this axis
|
1281
|
+
var ticks = [], v, i;
|
1282
|
+
for (i = 0; i < otherAxis.ticks.length; ++i) {
|
1283
|
+
v = (otherAxis.ticks[i].v - otherAxis.min) / (otherAxis.max - otherAxis.min);
|
1284
|
+
v = axis.min + v * (axis.max - axis.min);
|
1285
|
+
ticks.push(v);
|
1286
|
+
}
|
1287
|
+
return ticks;
|
1288
|
+
};
|
1289
|
+
|
1290
|
+
// we might need an extra decimal since forced
|
1291
|
+
// ticks don't necessarily fit naturally
|
1292
|
+
if (axis.mode != "time" && opts.tickDecimals == null) {
|
1293
|
+
var extraDec = Math.max(0, -Math.floor(Math.log(delta) / Math.LN10) + 1),
|
1294
|
+
ts = generator(axis);
|
1295
|
+
|
1296
|
+
// only proceed if the tick interval rounded
|
1297
|
+
// with an extra decimal doesn't give us a
|
1298
|
+
// zero at end
|
1299
|
+
if (!(ts.length > 1 && /\..*0$/.test((ts[1] - ts[0]).toFixed(extraDec))))
|
1300
|
+
axis.tickDecimals = extraDec;
|
1301
|
+
}
|
1302
|
+
}
|
1303
|
+
}
|
1304
|
+
|
1305
|
+
axis.tickGenerator = generator;
|
1306
|
+
if ($.isFunction(opts.tickFormatter))
|
1307
|
+
axis.tickFormatter = function (v, axis) { return "" + opts.tickFormatter(v, axis); };
|
1308
|
+
else
|
1309
|
+
axis.tickFormatter = formatter;
|
1310
|
+
}
|
1311
|
+
|
1312
|
+
function setTicks(axis) {
|
1313
|
+
axis.ticks = [];
|
1314
|
+
|
1315
|
+
var oticks = axis.options.ticks, ticks = null;
|
1316
|
+
if (oticks == null || (typeof oticks == "number" && oticks > 0))
|
1317
|
+
ticks = axis.tickGenerator(axis);
|
1318
|
+
else if (oticks) {
|
1319
|
+
if ($.isFunction(oticks))
|
1320
|
+
// generate the ticks
|
1321
|
+
ticks = oticks({ min: axis.min, max: axis.max });
|
1322
|
+
else
|
1323
|
+
ticks = oticks;
|
1324
|
+
}
|
1325
|
+
|
1326
|
+
// clean up/labelify the supplied ticks, copy them over
|
1327
|
+
var i, v;
|
1328
|
+
for (i = 0; i < ticks.length; ++i) {
|
1329
|
+
var label = null;
|
1330
|
+
var t = ticks[i];
|
1331
|
+
if (typeof t == "object") {
|
1332
|
+
v = t[0];
|
1333
|
+
if (t.length > 1)
|
1334
|
+
label = t[1];
|
1335
|
+
}
|
1336
|
+
else
|
1337
|
+
v = t;
|
1338
|
+
if (label == null)
|
1339
|
+
label = axis.tickFormatter(v, axis);
|
1340
|
+
axis.ticks[i] = { v: v, label: label };
|
1341
|
+
}
|
1342
|
+
}
|
1343
|
+
|
1344
|
+
function snapRangeToTicks(axis, ticks) {
|
1345
|
+
if (axis.options.autoscaleMargin != null && ticks.length > 0) {
|
1346
|
+
// snap to ticks
|
1347
|
+
if (axis.options.min == null)
|
1348
|
+
axis.min = Math.min(axis.min, ticks[0].v);
|
1349
|
+
if (axis.options.max == null && ticks.length > 1)
|
1350
|
+
axis.max = Math.max(axis.max, ticks[ticks.length - 1].v);
|
1351
|
+
}
|
1352
|
+
}
|
1353
|
+
|
1354
|
+
function draw() {
|
1355
|
+
ctx.clearRect(0, 0, canvasWidth, canvasHeight);
|
1356
|
+
|
1357
|
+
var grid = options.grid;
|
1358
|
+
|
1359
|
+
if (grid.show && !grid.aboveData)
|
1360
|
+
drawGrid();
|
1361
|
+
|
1362
|
+
for (var i = 0; i < series.length; ++i) {
|
1363
|
+
executeHooks(hooks.drawSeries, [ctx, series[i]]);
|
1364
|
+
drawSeries(series[i]);
|
1365
|
+
}
|
1366
|
+
|
1367
|
+
executeHooks(hooks.draw, [ctx]);
|
1368
|
+
|
1369
|
+
if (grid.show && grid.aboveData)
|
1370
|
+
drawGrid();
|
1371
|
+
}
|
1372
|
+
|
1373
|
+
function extractRange(ranges, coord) {
|
1374
|
+
var axis, from, to, axes, key;
|
1375
|
+
|
1376
|
+
axes = getUsedAxes();
|
1377
|
+
for (i = 0; i < axes.length; ++i) {
|
1378
|
+
axis = axes[i];
|
1379
|
+
if (axis.direction == coord) {
|
1380
|
+
key = coord + axis.n + "axis";
|
1381
|
+
if (!ranges[key] && axis.n == 1)
|
1382
|
+
key = coord + "axis"; // support x1axis as xaxis
|
1383
|
+
if (ranges[key]) {
|
1384
|
+
from = ranges[key].from;
|
1385
|
+
to = ranges[key].to;
|
1386
|
+
break;
|
1387
|
+
}
|
1388
|
+
}
|
1389
|
+
}
|
1390
|
+
|
1391
|
+
// backwards-compat stuff - to be removed in future
|
1392
|
+
if (!ranges[key]) {
|
1393
|
+
axis = coord == "x" ? xaxes[0] : yaxes[0];
|
1394
|
+
from = ranges[coord + "1"];
|
1395
|
+
to = ranges[coord + "2"];
|
1396
|
+
}
|
1397
|
+
|
1398
|
+
// auto-reverse as an added bonus
|
1399
|
+
if (from != null && to != null && from > to) {
|
1400
|
+
var tmp = from;
|
1401
|
+
from = to;
|
1402
|
+
to = tmp;
|
1403
|
+
}
|
1404
|
+
|
1405
|
+
return { from: from, to: to, axis: axis };
|
1406
|
+
}
|
1407
|
+
|
1408
|
+
function drawGrid() {
|
1409
|
+
var i;
|
1410
|
+
|
1411
|
+
ctx.save();
|
1412
|
+
ctx.translate(plotOffset.left, plotOffset.top);
|
1413
|
+
|
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
|
+
// draw markings
|
1421
|
+
var markings = options.grid.markings;
|
1422
|
+
if (markings) {
|
1423
|
+
if ($.isFunction(markings)) {
|
1424
|
+
var axes = plot.getAxes();
|
1425
|
+
// xmin etc. is backwards compatibility, to be
|
1426
|
+
// removed in the future
|
1427
|
+
axes.xmin = axes.xaxis.min;
|
1428
|
+
axes.xmax = axes.xaxis.max;
|
1429
|
+
axes.ymin = axes.yaxis.min;
|
1430
|
+
axes.ymax = axes.yaxis.max;
|
1431
|
+
|
1432
|
+
markings = markings(axes);
|
1433
|
+
}
|
1434
|
+
|
1435
|
+
for (i = 0; i < markings.length; ++i) {
|
1436
|
+
var m = markings[i],
|
1437
|
+
xrange = extractRange(m, "x"),
|
1438
|
+
yrange = extractRange(m, "y");
|
1439
|
+
|
1440
|
+
// fill in missing
|
1441
|
+
if (xrange.from == null)
|
1442
|
+
xrange.from = xrange.axis.min;
|
1443
|
+
if (xrange.to == null)
|
1444
|
+
xrange.to = xrange.axis.max;
|
1445
|
+
if (yrange.from == null)
|
1446
|
+
yrange.from = yrange.axis.min;
|
1447
|
+
if (yrange.to == null)
|
1448
|
+
yrange.to = yrange.axis.max;
|
1449
|
+
|
1450
|
+
// clip
|
1451
|
+
if (xrange.to < xrange.axis.min || xrange.from > xrange.axis.max ||
|
1452
|
+
yrange.to < yrange.axis.min || yrange.from > yrange.axis.max)
|
1453
|
+
continue;
|
1454
|
+
|
1455
|
+
xrange.from = Math.max(xrange.from, xrange.axis.min);
|
1456
|
+
xrange.to = Math.min(xrange.to, xrange.axis.max);
|
1457
|
+
yrange.from = Math.max(yrange.from, yrange.axis.min);
|
1458
|
+
yrange.to = Math.min(yrange.to, yrange.axis.max);
|
1459
|
+
|
1460
|
+
if (xrange.from == xrange.to && yrange.from == yrange.to)
|
1461
|
+
continue;
|
1462
|
+
|
1463
|
+
// then draw
|
1464
|
+
xrange.from = xrange.axis.p2c(xrange.from);
|
1465
|
+
xrange.to = xrange.axis.p2c(xrange.to);
|
1466
|
+
yrange.from = yrange.axis.p2c(yrange.from);
|
1467
|
+
yrange.to = yrange.axis.p2c(yrange.to);
|
1468
|
+
|
1469
|
+
if (xrange.from == xrange.to || yrange.from == yrange.to) {
|
1470
|
+
// draw line
|
1471
|
+
ctx.beginPath();
|
1472
|
+
ctx.strokeStyle = m.color || options.grid.markingsColor;
|
1473
|
+
ctx.lineWidth = m.lineWidth || options.grid.markingsLineWidth;
|
1474
|
+
ctx.moveTo(xrange.from, yrange.from);
|
1475
|
+
ctx.lineTo(xrange.to, yrange.to);
|
1476
|
+
ctx.stroke();
|
1477
|
+
}
|
1478
|
+
else {
|
1479
|
+
// fill area
|
1480
|
+
ctx.fillStyle = m.color || options.grid.markingsColor;
|
1481
|
+
ctx.fillRect(xrange.from, yrange.to,
|
1482
|
+
xrange.to - xrange.from,
|
1483
|
+
yrange.from - yrange.to);
|
1484
|
+
}
|
1485
|
+
}
|
1486
|
+
}
|
1487
|
+
|
1488
|
+
// draw the ticks
|
1489
|
+
var axes = getUsedAxes(), bw = options.grid.borderWidth;
|
1490
|
+
|
1491
|
+
for (var j = 0; j < axes.length; ++j) {
|
1492
|
+
var axis = axes[j], box = axis.box,
|
1493
|
+
t = axis.tickLength, x, y, xoff, yoff;
|
1494
|
+
|
1495
|
+
ctx.strokeStyle = axis.options.tickColor || $.color.parse(axis.options.color).scale('a', 0.22).toString();
|
1496
|
+
ctx.lineWidth = 1;
|
1497
|
+
|
1498
|
+
// find the edges
|
1499
|
+
if (axis.direction == "x") {
|
1500
|
+
x = 0;
|
1501
|
+
if (t == "full")
|
1502
|
+
y = (axis.position == "top" ? 0 : plotHeight);
|
1503
|
+
else
|
1504
|
+
y = box.top - plotOffset.top + (axis.position == "top" ? box.height : 0);
|
1505
|
+
}
|
1506
|
+
else {
|
1507
|
+
y = 0;
|
1508
|
+
if (t == "full")
|
1509
|
+
x = (axis.position == "left" ? 0 : plotWidth);
|
1510
|
+
else
|
1511
|
+
x = box.left - plotOffset.left + (axis.position == "left" ? box.width : 0);
|
1512
|
+
}
|
1513
|
+
|
1514
|
+
// draw tick bar
|
1515
|
+
if (!axis.innermost) {
|
1516
|
+
ctx.beginPath();
|
1517
|
+
xoff = yoff = 0;
|
1518
|
+
if (axis.direction == "x")
|
1519
|
+
xoff = plotWidth;
|
1520
|
+
else
|
1521
|
+
yoff = plotHeight;
|
1522
|
+
|
1523
|
+
if (ctx.lineWidth == 1) {
|
1524
|
+
x = Math.floor(x) + 0.5;
|
1525
|
+
y = Math.floor(y) + 0.5;
|
1526
|
+
}
|
1527
|
+
|
1528
|
+
ctx.moveTo(x, y);
|
1529
|
+
ctx.lineTo(x + xoff, y + yoff);
|
1530
|
+
ctx.stroke();
|
1531
|
+
}
|
1532
|
+
|
1533
|
+
// draw ticks
|
1534
|
+
ctx.beginPath();
|
1535
|
+
for (i = 0; i < axis.ticks.length; ++i) {
|
1536
|
+
var v = axis.ticks[i].v;
|
1537
|
+
|
1538
|
+
xoff = yoff = 0;
|
1539
|
+
|
1540
|
+
if (v < axis.min || v > axis.max
|
1541
|
+
// skip those lying on the axes if we got a border
|
1542
|
+
|| (t == "full" && bw > 0
|
1543
|
+
&& (v == axis.min || v == axis.max)))
|
1544
|
+
continue;
|
1545
|
+
|
1546
|
+
if (axis.direction == "x") {
|
1547
|
+
x = axis.p2c(v);
|
1548
|
+
yoff = t == "full" ? -plotHeight : t;
|
1549
|
+
|
1550
|
+
if (axis.position == "top")
|
1551
|
+
yoff = -yoff;
|
1552
|
+
}
|
1553
|
+
else {
|
1554
|
+
y = axis.p2c(v);
|
1555
|
+
xoff = t == "full" ? -plotWidth : t;
|
1556
|
+
|
1557
|
+
if (axis.position == "left")
|
1558
|
+
xoff = -xoff;
|
1559
|
+
}
|
1560
|
+
|
1561
|
+
if (ctx.lineWidth == 1) {
|
1562
|
+
if (axis.direction == "x")
|
1563
|
+
x = Math.floor(x) + 0.5;
|
1564
|
+
else
|
1565
|
+
y = Math.floor(y) + 0.5;
|
1566
|
+
}
|
1567
|
+
|
1568
|
+
ctx.moveTo(x, y);
|
1569
|
+
ctx.lineTo(x + xoff, y + yoff);
|
1570
|
+
}
|
1571
|
+
|
1572
|
+
ctx.stroke();
|
1573
|
+
}
|
1574
|
+
|
1575
|
+
|
1576
|
+
// draw border
|
1577
|
+
if (bw) {
|
1578
|
+
ctx.lineWidth = bw;
|
1579
|
+
ctx.strokeStyle = options.grid.borderColor;
|
1580
|
+
ctx.strokeRect(-bw/2, -bw/2, plotWidth + bw, plotHeight + bw);
|
1581
|
+
}
|
1582
|
+
|
1583
|
+
ctx.restore();
|
1584
|
+
}
|
1585
|
+
|
1586
|
+
function insertAxisLabels() {
|
1587
|
+
placeholder.find(".tickLabels").remove();
|
1588
|
+
|
1589
|
+
var html = ['<div class="tickLabels" style="font-size:smaller">'];
|
1590
|
+
|
1591
|
+
var axes = getUsedAxes();
|
1592
|
+
for (var j = 0; j < axes.length; ++j) {
|
1593
|
+
var axis = axes[j], box = axis.box;
|
1594
|
+
//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
|
+
html.push('<div class="' + axis.direction + 'Axis ' + axis.direction + axis.n + 'Axis" style="color:' + axis.options.color + '">');
|
1596
|
+
for (var i = 0; i < axis.ticks.length; ++i) {
|
1597
|
+
var tick = axis.ticks[i];
|
1598
|
+
if (!tick.label || tick.v < axis.min || tick.v > axis.max)
|
1599
|
+
continue;
|
1600
|
+
|
1601
|
+
var pos = {}, align;
|
1602
|
+
|
1603
|
+
if (axis.direction == "x") {
|
1604
|
+
align = "center";
|
1605
|
+
pos.left = Math.round(plotOffset.left + axis.p2c(tick.v) - axis.labelWidth/2);
|
1606
|
+
if (axis.position == "bottom")
|
1607
|
+
pos.top = box.top + box.padding;
|
1608
|
+
else
|
1609
|
+
pos.bottom = canvasHeight - (box.top + box.height - box.padding);
|
1610
|
+
}
|
1611
|
+
else {
|
1612
|
+
pos.top = Math.round(plotOffset.top + axis.p2c(tick.v) - axis.labelHeight/2);
|
1613
|
+
if (axis.position == "left") {
|
1614
|
+
pos.right = canvasWidth - (box.left + box.width - box.padding)
|
1615
|
+
align = "right";
|
1616
|
+
}
|
1617
|
+
else {
|
1618
|
+
pos.left = box.left + box.padding;
|
1619
|
+
align = "left";
|
1620
|
+
}
|
1621
|
+
}
|
1622
|
+
|
1623
|
+
pos.width = axis.labelWidth;
|
1624
|
+
|
1625
|
+
var style = ["position:absolute", "text-align:" + align ];
|
1626
|
+
for (var a in pos)
|
1627
|
+
style.push(a + ":" + pos[a] + "px")
|
1628
|
+
|
1629
|
+
html.push('<div class="tickLabel" style="' + style.join(';') + '">' + tick.label + '</div>');
|
1630
|
+
}
|
1631
|
+
html.push('</div>');
|
1632
|
+
}
|
1633
|
+
|
1634
|
+
html.push('</div>');
|
1635
|
+
|
1636
|
+
placeholder.append(html.join(""));
|
1637
|
+
}
|
1638
|
+
|
1639
|
+
function drawSeries(series) {
|
1640
|
+
if (series.lines.show)
|
1641
|
+
drawSeriesLines(series);
|
1642
|
+
if (series.bars.show)
|
1643
|
+
drawSeriesBars(series);
|
1644
|
+
if (series.points.show)
|
1645
|
+
drawSeriesPoints(series);
|
1646
|
+
}
|
1647
|
+
|
1648
|
+
function drawSeriesLines(series) {
|
1649
|
+
function plotLine(datapoints, xoffset, yoffset, axisx, axisy) {
|
1650
|
+
var points = datapoints.points,
|
1651
|
+
ps = datapoints.pointsize,
|
1652
|
+
prevx = null, prevy = null;
|
1653
|
+
|
1654
|
+
ctx.beginPath();
|
1655
|
+
for (var i = ps; i < points.length; i += ps) {
|
1656
|
+
var x1 = points[i - ps], y1 = points[i - ps + 1],
|
1657
|
+
x2 = points[i], y2 = points[i + 1];
|
1658
|
+
|
1659
|
+
if (x1 == null || x2 == null)
|
1660
|
+
continue;
|
1661
|
+
|
1662
|
+
// clip with ymin
|
1663
|
+
if (y1 <= y2 && y1 < axisy.min) {
|
1664
|
+
if (y2 < axisy.min)
|
1665
|
+
continue; // line segment is outside
|
1666
|
+
// compute new intersection point
|
1667
|
+
x1 = (axisy.min - y1) / (y2 - y1) * (x2 - x1) + x1;
|
1668
|
+
y1 = axisy.min;
|
1669
|
+
}
|
1670
|
+
else if (y2 <= y1 && y2 < axisy.min) {
|
1671
|
+
if (y1 < axisy.min)
|
1672
|
+
continue;
|
1673
|
+
x2 = (axisy.min - y1) / (y2 - y1) * (x2 - x1) + x1;
|
1674
|
+
y2 = axisy.min;
|
1675
|
+
}
|
1676
|
+
|
1677
|
+
// clip with ymax
|
1678
|
+
if (y1 >= y2 && y1 > axisy.max) {
|
1679
|
+
if (y2 > axisy.max)
|
1680
|
+
continue;
|
1681
|
+
x1 = (axisy.max - y1) / (y2 - y1) * (x2 - x1) + x1;
|
1682
|
+
y1 = axisy.max;
|
1683
|
+
}
|
1684
|
+
else if (y2 >= y1 && y2 > axisy.max) {
|
1685
|
+
if (y1 > axisy.max)
|
1686
|
+
continue;
|
1687
|
+
x2 = (axisy.max - y1) / (y2 - y1) * (x2 - x1) + x1;
|
1688
|
+
y2 = axisy.max;
|
1689
|
+
}
|
1690
|
+
|
1691
|
+
// clip with xmin
|
1692
|
+
if (x1 <= x2 && x1 < axisx.min) {
|
1693
|
+
if (x2 < axisx.min)
|
1694
|
+
continue;
|
1695
|
+
y1 = (axisx.min - x1) / (x2 - x1) * (y2 - y1) + y1;
|
1696
|
+
x1 = axisx.min;
|
1697
|
+
}
|
1698
|
+
else if (x2 <= x1 && x2 < axisx.min) {
|
1699
|
+
if (x1 < axisx.min)
|
1700
|
+
continue;
|
1701
|
+
y2 = (axisx.min - x1) / (x2 - x1) * (y2 - y1) + y1;
|
1702
|
+
x2 = axisx.min;
|
1703
|
+
}
|
1704
|
+
|
1705
|
+
// clip with xmax
|
1706
|
+
if (x1 >= x2 && x1 > axisx.max) {
|
1707
|
+
if (x2 > axisx.max)
|
1708
|
+
continue;
|
1709
|
+
y1 = (axisx.max - x1) / (x2 - x1) * (y2 - y1) + y1;
|
1710
|
+
x1 = axisx.max;
|
1711
|
+
}
|
1712
|
+
else if (x2 >= x1 && x2 > axisx.max) {
|
1713
|
+
if (x1 > axisx.max)
|
1714
|
+
continue;
|
1715
|
+
y2 = (axisx.max - x1) / (x2 - x1) * (y2 - y1) + y1;
|
1716
|
+
x2 = axisx.max;
|
1717
|
+
}
|
1718
|
+
|
1719
|
+
if (x1 != prevx || y1 != prevy)
|
1720
|
+
ctx.moveTo(axisx.p2c(x1) + xoffset, axisy.p2c(y1) + yoffset);
|
1721
|
+
|
1722
|
+
prevx = x2;
|
1723
|
+
prevy = y2;
|
1724
|
+
ctx.lineTo(axisx.p2c(x2) + xoffset, axisy.p2c(y2) + yoffset);
|
1725
|
+
}
|
1726
|
+
ctx.stroke();
|
1727
|
+
}
|
1728
|
+
|
1729
|
+
function plotLineArea(datapoints, axisx, axisy) {
|
1730
|
+
var points = datapoints.points,
|
1731
|
+
ps = datapoints.pointsize,
|
1732
|
+
bottom = Math.min(Math.max(0, axisy.min), axisy.max),
|
1733
|
+
i = 0, top, areaOpen = false,
|
1734
|
+
ypos = 1, segmentStart = 0, segmentEnd = 0;
|
1735
|
+
|
1736
|
+
// we process each segment in two turns, first forward
|
1737
|
+
// direction to sketch out top, then once we hit the
|
1738
|
+
// end we go backwards to sketch the bottom
|
1739
|
+
while (true) {
|
1740
|
+
if (ps > 0 && i > points.length + ps)
|
1741
|
+
break;
|
1742
|
+
|
1743
|
+
i += ps; // ps is negative if going backwards
|
1744
|
+
|
1745
|
+
var x1 = points[i - ps],
|
1746
|
+
y1 = points[i - ps + ypos],
|
1747
|
+
x2 = points[i], y2 = points[i + ypos];
|
1748
|
+
|
1749
|
+
if (areaOpen) {
|
1750
|
+
if (ps > 0 && x1 != null && x2 == null) {
|
1751
|
+
// at turning point
|
1752
|
+
segmentEnd = i;
|
1753
|
+
ps = -ps;
|
1754
|
+
ypos = 2;
|
1755
|
+
continue;
|
1756
|
+
}
|
1757
|
+
|
1758
|
+
if (ps < 0 && i == segmentStart + ps) {
|
1759
|
+
// done with the reverse sweep
|
1760
|
+
ctx.fill();
|
1761
|
+
areaOpen = false;
|
1762
|
+
ps = -ps;
|
1763
|
+
ypos = 1;
|
1764
|
+
i = segmentStart = segmentEnd + ps;
|
1765
|
+
continue;
|
1766
|
+
}
|
1767
|
+
}
|
1768
|
+
|
1769
|
+
if (x1 == null || x2 == null)
|
1770
|
+
continue;
|
1771
|
+
|
1772
|
+
// clip x values
|
1773
|
+
|
1774
|
+
// clip with xmin
|
1775
|
+
if (x1 <= x2 && x1 < axisx.min) {
|
1776
|
+
if (x2 < axisx.min)
|
1777
|
+
continue;
|
1778
|
+
y1 = (axisx.min - x1) / (x2 - x1) * (y2 - y1) + y1;
|
1779
|
+
x1 = axisx.min;
|
1780
|
+
}
|
1781
|
+
else if (x2 <= x1 && x2 < axisx.min) {
|
1782
|
+
if (x1 < axisx.min)
|
1783
|
+
continue;
|
1784
|
+
y2 = (axisx.min - x1) / (x2 - x1) * (y2 - y1) + y1;
|
1785
|
+
x2 = axisx.min;
|
1786
|
+
}
|
1787
|
+
|
1788
|
+
// clip with xmax
|
1789
|
+
if (x1 >= x2 && x1 > axisx.max) {
|
1790
|
+
if (x2 > axisx.max)
|
1791
|
+
continue;
|
1792
|
+
y1 = (axisx.max - x1) / (x2 - x1) * (y2 - y1) + y1;
|
1793
|
+
x1 = axisx.max;
|
1794
|
+
}
|
1795
|
+
else if (x2 >= x1 && x2 > axisx.max) {
|
1796
|
+
if (x1 > axisx.max)
|
1797
|
+
continue;
|
1798
|
+
y2 = (axisx.max - x1) / (x2 - x1) * (y2 - y1) + y1;
|
1799
|
+
x2 = axisx.max;
|
1800
|
+
}
|
1801
|
+
|
1802
|
+
if (!areaOpen) {
|
1803
|
+
// open area
|
1804
|
+
ctx.beginPath();
|
1805
|
+
ctx.moveTo(axisx.p2c(x1), axisy.p2c(bottom));
|
1806
|
+
areaOpen = true;
|
1807
|
+
}
|
1808
|
+
|
1809
|
+
// now first check the case where both is outside
|
1810
|
+
if (y1 >= axisy.max && y2 >= axisy.max) {
|
1811
|
+
ctx.lineTo(axisx.p2c(x1), axisy.p2c(axisy.max));
|
1812
|
+
ctx.lineTo(axisx.p2c(x2), axisy.p2c(axisy.max));
|
1813
|
+
continue;
|
1814
|
+
}
|
1815
|
+
else if (y1 <= axisy.min && y2 <= axisy.min) {
|
1816
|
+
ctx.lineTo(axisx.p2c(x1), axisy.p2c(axisy.min));
|
1817
|
+
ctx.lineTo(axisx.p2c(x2), axisy.p2c(axisy.min));
|
1818
|
+
continue;
|
1819
|
+
}
|
1820
|
+
|
1821
|
+
// else it's a bit more complicated, there might
|
1822
|
+
// be a flat maxed out rectangle first, then a
|
1823
|
+
// triangular cutout or reverse; to find these
|
1824
|
+
// keep track of the current x values
|
1825
|
+
var x1old = x1, x2old = x2;
|
1826
|
+
|
1827
|
+
// clip the y values, without shortcutting, we
|
1828
|
+
// go through all cases in turn
|
1829
|
+
|
1830
|
+
// clip with ymin
|
1831
|
+
if (y1 <= y2 && y1 < axisy.min && y2 >= axisy.min) {
|
1832
|
+
x1 = (axisy.min - y1) / (y2 - y1) * (x2 - x1) + x1;
|
1833
|
+
y1 = axisy.min;
|
1834
|
+
}
|
1835
|
+
else if (y2 <= y1 && y2 < axisy.min && y1 >= axisy.min) {
|
1836
|
+
x2 = (axisy.min - y1) / (y2 - y1) * (x2 - x1) + x1;
|
1837
|
+
y2 = axisy.min;
|
1838
|
+
}
|
1839
|
+
|
1840
|
+
// clip with ymax
|
1841
|
+
if (y1 >= y2 && y1 > axisy.max && y2 <= axisy.max) {
|
1842
|
+
x1 = (axisy.max - y1) / (y2 - y1) * (x2 - x1) + x1;
|
1843
|
+
y1 = axisy.max;
|
1844
|
+
}
|
1845
|
+
else if (y2 >= y1 && y2 > axisy.max && y1 <= axisy.max) {
|
1846
|
+
x2 = (axisy.max - y1) / (y2 - y1) * (x2 - x1) + x1;
|
1847
|
+
y2 = axisy.max;
|
1848
|
+
}
|
1849
|
+
|
1850
|
+
// if the x value was changed we got a rectangle
|
1851
|
+
// to fill
|
1852
|
+
if (x1 != x1old) {
|
1853
|
+
ctx.lineTo(axisx.p2c(x1old), axisy.p2c(y1));
|
1854
|
+
// it goes to (x1, y1), but we fill that below
|
1855
|
+
}
|
1856
|
+
|
1857
|
+
// fill triangular section, this sometimes result
|
1858
|
+
// in redundant points if (x1, y1) hasn't changed
|
1859
|
+
// from previous line to, but we just ignore that
|
1860
|
+
ctx.lineTo(axisx.p2c(x1), axisy.p2c(y1));
|
1861
|
+
ctx.lineTo(axisx.p2c(x2), axisy.p2c(y2));
|
1862
|
+
|
1863
|
+
// fill the other rectangle if it's there
|
1864
|
+
if (x2 != x2old) {
|
1865
|
+
ctx.lineTo(axisx.p2c(x2), axisy.p2c(y2));
|
1866
|
+
ctx.lineTo(axisx.p2c(x2old), axisy.p2c(y2));
|
1867
|
+
}
|
1868
|
+
}
|
1869
|
+
}
|
1870
|
+
|
1871
|
+
ctx.save();
|
1872
|
+
ctx.translate(plotOffset.left, plotOffset.top);
|
1873
|
+
ctx.lineJoin = "round";
|
1874
|
+
|
1875
|
+
var lw = series.lines.lineWidth,
|
1876
|
+
sw = series.shadowSize;
|
1877
|
+
// FIXME: consider another form of shadow when filling is turned on
|
1878
|
+
if (lw > 0 && sw > 0) {
|
1879
|
+
// draw shadow as a thick and thin line with transparency
|
1880
|
+
ctx.lineWidth = sw;
|
1881
|
+
ctx.strokeStyle = "rgba(0,0,0,0.1)";
|
1882
|
+
// position shadow at angle from the mid of line
|
1883
|
+
var angle = Math.PI/18;
|
1884
|
+
plotLine(series.datapoints, Math.sin(angle) * (lw/2 + sw/2), Math.cos(angle) * (lw/2 + sw/2), series.xaxis, series.yaxis);
|
1885
|
+
ctx.lineWidth = sw/2;
|
1886
|
+
plotLine(series.datapoints, Math.sin(angle) * (lw/2 + sw/4), Math.cos(angle) * (lw/2 + sw/4), series.xaxis, series.yaxis);
|
1887
|
+
}
|
1888
|
+
|
1889
|
+
ctx.lineWidth = lw;
|
1890
|
+
ctx.strokeStyle = series.color;
|
1891
|
+
var fillStyle = getFillStyle(series.lines, series.color, 0, plotHeight);
|
1892
|
+
if (fillStyle) {
|
1893
|
+
ctx.fillStyle = fillStyle;
|
1894
|
+
plotLineArea(series.datapoints, series.xaxis, series.yaxis);
|
1895
|
+
}
|
1896
|
+
|
1897
|
+
if (lw > 0)
|
1898
|
+
plotLine(series.datapoints, 0, 0, series.xaxis, series.yaxis);
|
1899
|
+
ctx.restore();
|
1900
|
+
}
|
1901
|
+
|
1902
|
+
function drawSeriesPoints(series) {
|
1903
|
+
function plotPoints(datapoints, radius, fillStyle, offset, shadow, axisx, axisy, symbol) {
|
1904
|
+
var points = datapoints.points, ps = datapoints.pointsize;
|
1905
|
+
|
1906
|
+
for (var i = 0; i < points.length; i += ps) {
|
1907
|
+
var x = points[i], y = points[i + 1];
|
1908
|
+
if (x == null || x < axisx.min || x > axisx.max || y < axisy.min || y > axisy.max)
|
1909
|
+
continue;
|
1910
|
+
|
1911
|
+
ctx.beginPath();
|
1912
|
+
x = axisx.p2c(x);
|
1913
|
+
y = axisy.p2c(y) + offset;
|
1914
|
+
if (symbol == "circle")
|
1915
|
+
ctx.arc(x, y, radius, 0, shadow ? Math.PI : Math.PI * 2, false);
|
1916
|
+
else
|
1917
|
+
symbol(ctx, x, y, radius, shadow);
|
1918
|
+
ctx.closePath();
|
1919
|
+
|
1920
|
+
if (fillStyle) {
|
1921
|
+
ctx.fillStyle = fillStyle;
|
1922
|
+
ctx.fill();
|
1923
|
+
}
|
1924
|
+
ctx.stroke();
|
1925
|
+
}
|
1926
|
+
}
|
1927
|
+
|
1928
|
+
ctx.save();
|
1929
|
+
ctx.translate(plotOffset.left, plotOffset.top);
|
1930
|
+
|
1931
|
+
var lw = series.points.lineWidth,
|
1932
|
+
sw = series.shadowSize,
|
1933
|
+
radius = series.points.radius,
|
1934
|
+
symbol = series.points.symbol;
|
1935
|
+
if (lw > 0 && sw > 0) {
|
1936
|
+
// draw shadow in two steps
|
1937
|
+
var w = sw / 2;
|
1938
|
+
ctx.lineWidth = w;
|
1939
|
+
ctx.strokeStyle = "rgba(0,0,0,0.1)";
|
1940
|
+
plotPoints(series.datapoints, radius, null, w + w/2, true,
|
1941
|
+
series.xaxis, series.yaxis, symbol);
|
1942
|
+
|
1943
|
+
ctx.strokeStyle = "rgba(0,0,0,0.2)";
|
1944
|
+
plotPoints(series.datapoints, radius, null, w/2, true,
|
1945
|
+
series.xaxis, series.yaxis, symbol);
|
1946
|
+
}
|
1947
|
+
|
1948
|
+
ctx.lineWidth = lw;
|
1949
|
+
ctx.strokeStyle = series.color;
|
1950
|
+
plotPoints(series.datapoints, radius,
|
1951
|
+
getFillStyle(series.points, series.color), 0, false,
|
1952
|
+
series.xaxis, series.yaxis, symbol);
|
1953
|
+
ctx.restore();
|
1954
|
+
}
|
1955
|
+
|
1956
|
+
function drawBar(x, y, b, barLeft, barRight, offset, fillStyleCallback, axisx, axisy, c, horizontal, lineWidth) {
|
1957
|
+
var left, right, bottom, top,
|
1958
|
+
drawLeft, drawRight, drawTop, drawBottom,
|
1959
|
+
tmp;
|
1960
|
+
|
1961
|
+
// in horizontal mode, we start the bar from the left
|
1962
|
+
// instead of from the bottom so it appears to be
|
1963
|
+
// horizontal rather than vertical
|
1964
|
+
if (horizontal) {
|
1965
|
+
drawBottom = drawRight = drawTop = true;
|
1966
|
+
drawLeft = false;
|
1967
|
+
left = b;
|
1968
|
+
right = x;
|
1969
|
+
top = y + barLeft;
|
1970
|
+
bottom = y + barRight;
|
1971
|
+
|
1972
|
+
// account for negative bars
|
1973
|
+
if (right < left) {
|
1974
|
+
tmp = right;
|
1975
|
+
right = left;
|
1976
|
+
left = tmp;
|
1977
|
+
drawLeft = true;
|
1978
|
+
drawRight = false;
|
1979
|
+
}
|
1980
|
+
}
|
1981
|
+
else {
|
1982
|
+
drawLeft = drawRight = drawTop = true;
|
1983
|
+
drawBottom = false;
|
1984
|
+
left = x + barLeft;
|
1985
|
+
right = x + barRight;
|
1986
|
+
bottom = b;
|
1987
|
+
top = y;
|
1988
|
+
|
1989
|
+
// account for negative bars
|
1990
|
+
if (top < bottom) {
|
1991
|
+
tmp = top;
|
1992
|
+
top = bottom;
|
1993
|
+
bottom = tmp;
|
1994
|
+
drawBottom = true;
|
1995
|
+
drawTop = false;
|
1996
|
+
}
|
1997
|
+
}
|
1998
|
+
|
1999
|
+
// clip
|
2000
|
+
if (right < axisx.min || left > axisx.max ||
|
2001
|
+
top < axisy.min || bottom > axisy.max)
|
2002
|
+
return;
|
2003
|
+
|
2004
|
+
if (left < axisx.min) {
|
2005
|
+
left = axisx.min;
|
2006
|
+
drawLeft = false;
|
2007
|
+
}
|
2008
|
+
|
2009
|
+
if (right > axisx.max) {
|
2010
|
+
right = axisx.max;
|
2011
|
+
drawRight = false;
|
2012
|
+
}
|
2013
|
+
|
2014
|
+
if (bottom < axisy.min) {
|
2015
|
+
bottom = axisy.min;
|
2016
|
+
drawBottom = false;
|
2017
|
+
}
|
2018
|
+
|
2019
|
+
if (top > axisy.max) {
|
2020
|
+
top = axisy.max;
|
2021
|
+
drawTop = false;
|
2022
|
+
}
|
2023
|
+
|
2024
|
+
left = axisx.p2c(left);
|
2025
|
+
bottom = axisy.p2c(bottom);
|
2026
|
+
right = axisx.p2c(right);
|
2027
|
+
top = axisy.p2c(top);
|
2028
|
+
|
2029
|
+
// fill the bar
|
2030
|
+
if (fillStyleCallback) {
|
2031
|
+
c.beginPath();
|
2032
|
+
c.moveTo(left, bottom);
|
2033
|
+
c.lineTo(left, top);
|
2034
|
+
c.lineTo(right, top);
|
2035
|
+
c.lineTo(right, bottom);
|
2036
|
+
c.fillStyle = fillStyleCallback(bottom, top);
|
2037
|
+
c.fill();
|
2038
|
+
}
|
2039
|
+
|
2040
|
+
// draw outline
|
2041
|
+
if (lineWidth > 0 && (drawLeft || drawRight || drawTop || drawBottom)) {
|
2042
|
+
c.beginPath();
|
2043
|
+
|
2044
|
+
// FIXME: inline moveTo is buggy with excanvas
|
2045
|
+
c.moveTo(left, bottom + offset);
|
2046
|
+
if (drawLeft)
|
2047
|
+
c.lineTo(left, top + offset);
|
2048
|
+
else
|
2049
|
+
c.moveTo(left, top + offset);
|
2050
|
+
if (drawTop)
|
2051
|
+
c.lineTo(right, top + offset);
|
2052
|
+
else
|
2053
|
+
c.moveTo(right, top + offset);
|
2054
|
+
if (drawRight)
|
2055
|
+
c.lineTo(right, bottom + offset);
|
2056
|
+
else
|
2057
|
+
c.moveTo(right, bottom + offset);
|
2058
|
+
if (drawBottom)
|
2059
|
+
c.lineTo(left, bottom + offset);
|
2060
|
+
else
|
2061
|
+
c.moveTo(left, bottom + offset);
|
2062
|
+
c.stroke();
|
2063
|
+
}
|
2064
|
+
}
|
2065
|
+
|
2066
|
+
function drawSeriesBars(series) {
|
2067
|
+
function plotBars(datapoints, barLeft, barRight, offset, fillStyleCallback, axisx, axisy) {
|
2068
|
+
var points = datapoints.points, ps = datapoints.pointsize;
|
2069
|
+
|
2070
|
+
for (var i = 0; i < points.length; i += ps) {
|
2071
|
+
if (points[i] == null)
|
2072
|
+
continue;
|
2073
|
+
drawBar(points[i], points[i + 1], points[i + 2], barLeft, barRight, offset, fillStyleCallback, axisx, axisy, ctx, series.bars.horizontal, series.bars.lineWidth);
|
2074
|
+
}
|
2075
|
+
}
|
2076
|
+
|
2077
|
+
ctx.save();
|
2078
|
+
ctx.translate(plotOffset.left, plotOffset.top);
|
2079
|
+
|
2080
|
+
// FIXME: figure out a way to add shadows (for instance along the right edge)
|
2081
|
+
ctx.lineWidth = series.bars.lineWidth;
|
2082
|
+
ctx.strokeStyle = series.color;
|
2083
|
+
var barLeft = series.bars.align == "left" ? 0 : -series.bars.barWidth/2;
|
2084
|
+
var fillStyleCallback = series.bars.fill ? function (bottom, top) { return getFillStyle(series.bars, series.color, bottom, top); } : null;
|
2085
|
+
plotBars(series.datapoints, barLeft, barLeft + series.bars.barWidth, 0, fillStyleCallback, series.xaxis, series.yaxis);
|
2086
|
+
ctx.restore();
|
2087
|
+
}
|
2088
|
+
|
2089
|
+
function getFillStyle(filloptions, seriesColor, bottom, top) {
|
2090
|
+
var fill = filloptions.fill;
|
2091
|
+
if (!fill)
|
2092
|
+
return null;
|
2093
|
+
|
2094
|
+
if (filloptions.fillColor)
|
2095
|
+
return getColorOrGradient(filloptions.fillColor, bottom, top, seriesColor);
|
2096
|
+
|
2097
|
+
var c = $.color.parse(seriesColor);
|
2098
|
+
c.a = typeof fill == "number" ? fill : 0.4;
|
2099
|
+
c.normalize();
|
2100
|
+
return c.toString();
|
2101
|
+
}
|
2102
|
+
|
2103
|
+
function insertLegend() {
|
2104
|
+
placeholder.find(".legend").remove();
|
2105
|
+
|
2106
|
+
if (!options.legend.show)
|
2107
|
+
return;
|
2108
|
+
|
2109
|
+
var fragments = [], rowStarted = false,
|
2110
|
+
lf = options.legend.labelFormatter, s, label;
|
2111
|
+
for (var i = 0; i < series.length; ++i) {
|
2112
|
+
s = series[i];
|
2113
|
+
label = s.label;
|
2114
|
+
if (!label)
|
2115
|
+
continue;
|
2116
|
+
|
2117
|
+
if (i % options.legend.noColumns == 0) {
|
2118
|
+
if (rowStarted)
|
2119
|
+
fragments.push('</tr>');
|
2120
|
+
fragments.push('<tr>');
|
2121
|
+
rowStarted = true;
|
2122
|
+
}
|
2123
|
+
|
2124
|
+
if (lf)
|
2125
|
+
label = lf(label, s);
|
2126
|
+
|
2127
|
+
fragments.push(
|
2128
|
+
'<td class="legendColorBox"><div style="border:1px solid ' + options.legend.labelBoxBorderColor + ';padding:1px"><div style="width:4px;height:0;border:5px solid ' + s.color + ';overflow:hidden"></div></div></td>' +
|
2129
|
+
'<td class="legendLabel">' + label + '</td>');
|
2130
|
+
}
|
2131
|
+
if (rowStarted)
|
2132
|
+
fragments.push('</tr>');
|
2133
|
+
|
2134
|
+
if (fragments.length == 0)
|
2135
|
+
return;
|
2136
|
+
|
2137
|
+
var table = '<table style="font-size:smaller;color:' + options.grid.color + '">' + fragments.join("") + '</table>';
|
2138
|
+
if (options.legend.container != null)
|
2139
|
+
$(options.legend.container).html(table);
|
2140
|
+
else {
|
2141
|
+
var pos = "",
|
2142
|
+
p = options.legend.position,
|
2143
|
+
m = options.legend.margin;
|
2144
|
+
if (m[0] == null)
|
2145
|
+
m = [m, m];
|
2146
|
+
if (p.charAt(0) == "n")
|
2147
|
+
pos += 'top:' + (m[1] + plotOffset.top) + 'px;';
|
2148
|
+
else if (p.charAt(0) == "s")
|
2149
|
+
pos += 'bottom:' + (m[1] + plotOffset.bottom) + 'px;';
|
2150
|
+
if (p.charAt(1) == "e")
|
2151
|
+
pos += 'right:' + (m[0] + plotOffset.right) + 'px;';
|
2152
|
+
else if (p.charAt(1) == "w")
|
2153
|
+
pos += 'left:' + (m[0] + plotOffset.left) + 'px;';
|
2154
|
+
var legend = $('<div class="legend">' + table.replace('style="', 'style="position:absolute;' + pos +';') + '</div>').appendTo(placeholder);
|
2155
|
+
if (options.legend.backgroundOpacity != 0.0) {
|
2156
|
+
// put in the transparent background
|
2157
|
+
// separately to avoid blended labels and
|
2158
|
+
// label boxes
|
2159
|
+
var c = options.legend.backgroundColor;
|
2160
|
+
if (c == null) {
|
2161
|
+
c = options.grid.backgroundColor;
|
2162
|
+
if (c && typeof c == "string")
|
2163
|
+
c = $.color.parse(c);
|
2164
|
+
else
|
2165
|
+
c = $.color.extract(legend, 'background-color');
|
2166
|
+
c.a = 1;
|
2167
|
+
c = c.toString();
|
2168
|
+
}
|
2169
|
+
var div = legend.children();
|
2170
|
+
$('<div style="position:absolute;width:' + div.width() + 'px;height:' + div.height() + 'px;' + pos +'background-color:' + c + ';"> </div>').prependTo(legend).css('opacity', options.legend.backgroundOpacity);
|
2171
|
+
}
|
2172
|
+
}
|
2173
|
+
}
|
2174
|
+
|
2175
|
+
|
2176
|
+
// interactive features
|
2177
|
+
|
2178
|
+
var highlights = [],
|
2179
|
+
redrawTimeout = null;
|
2180
|
+
|
2181
|
+
// returns the data item the mouse is over, or null if none is found
|
2182
|
+
function findNearbyItem(mouseX, mouseY, seriesFilter) {
|
2183
|
+
var maxDistance = options.grid.mouseActiveRadius,
|
2184
|
+
smallestDistance = maxDistance * maxDistance + 1,
|
2185
|
+
item = null, foundPoint = false, i, j;
|
2186
|
+
|
2187
|
+
for (i = series.length - 1; i >= 0; --i) {
|
2188
|
+
if (!seriesFilter(series[i]))
|
2189
|
+
continue;
|
2190
|
+
|
2191
|
+
var s = series[i],
|
2192
|
+
axisx = s.xaxis,
|
2193
|
+
axisy = s.yaxis,
|
2194
|
+
points = s.datapoints.points,
|
2195
|
+
ps = s.datapoints.pointsize,
|
2196
|
+
mx = axisx.c2p(mouseX), // precompute some stuff to make the loop faster
|
2197
|
+
my = axisy.c2p(mouseY),
|
2198
|
+
maxx = maxDistance / axisx.scale,
|
2199
|
+
maxy = maxDistance / axisy.scale;
|
2200
|
+
|
2201
|
+
if (s.lines.show || s.points.show) {
|
2202
|
+
for (j = 0; j < points.length; j += ps) {
|
2203
|
+
var x = points[j], y = points[j + 1];
|
2204
|
+
if (x == null)
|
2205
|
+
continue;
|
2206
|
+
|
2207
|
+
// For points and lines, the cursor must be within a
|
2208
|
+
// certain distance to the data point
|
2209
|
+
if (x - mx > maxx || x - mx < -maxx ||
|
2210
|
+
y - my > maxy || y - my < -maxy)
|
2211
|
+
continue;
|
2212
|
+
|
2213
|
+
// We have to calculate distances in pixels, not in
|
2214
|
+
// data units, because the scales of the axes may be different
|
2215
|
+
var dx = Math.abs(axisx.p2c(x) - mouseX),
|
2216
|
+
dy = Math.abs(axisy.p2c(y) - mouseY),
|
2217
|
+
dist = dx * dx + dy * dy; // we save the sqrt
|
2218
|
+
|
2219
|
+
// use <= to ensure last point takes precedence
|
2220
|
+
// (last generally means on top of)
|
2221
|
+
if (dist < smallestDistance) {
|
2222
|
+
smallestDistance = dist;
|
2223
|
+
item = [i, j / ps];
|
2224
|
+
}
|
2225
|
+
}
|
2226
|
+
}
|
2227
|
+
|
2228
|
+
if (s.bars.show && !item) { // no other point can be nearby
|
2229
|
+
var barLeft = s.bars.align == "left" ? 0 : -s.bars.barWidth/2,
|
2230
|
+
barRight = barLeft + s.bars.barWidth;
|
2231
|
+
|
2232
|
+
for (j = 0; j < points.length; j += ps) {
|
2233
|
+
var x = points[j], y = points[j + 1], b = points[j + 2];
|
2234
|
+
if (x == null)
|
2235
|
+
continue;
|
2236
|
+
|
2237
|
+
// for a bar graph, the cursor must be inside the bar
|
2238
|
+
if (series[i].bars.horizontal ?
|
2239
|
+
(mx <= Math.max(b, x) && mx >= Math.min(b, x) &&
|
2240
|
+
my >= y + barLeft && my <= y + barRight) :
|
2241
|
+
(mx >= x + barLeft && mx <= x + barRight &&
|
2242
|
+
my >= Math.min(b, y) && my <= Math.max(b, y)))
|
2243
|
+
item = [i, j / ps];
|
2244
|
+
}
|
2245
|
+
}
|
2246
|
+
}
|
2247
|
+
|
2248
|
+
if (item) {
|
2249
|
+
i = item[0];
|
2250
|
+
j = item[1];
|
2251
|
+
ps = series[i].datapoints.pointsize;
|
2252
|
+
|
2253
|
+
return { datapoint: series[i].datapoints.points.slice(j * ps, (j + 1) * ps),
|
2254
|
+
dataIndex: j,
|
2255
|
+
series: series[i],
|
2256
|
+
seriesIndex: i };
|
2257
|
+
}
|
2258
|
+
|
2259
|
+
return null;
|
2260
|
+
}
|
2261
|
+
|
2262
|
+
function onMouseMove(e) {
|
2263
|
+
if (options.grid.hoverable)
|
2264
|
+
triggerClickHoverEvent("plothover", e,
|
2265
|
+
function (s) { return s["hoverable"] != false; });
|
2266
|
+
}
|
2267
|
+
|
2268
|
+
function onClick(e) {
|
2269
|
+
triggerClickHoverEvent("plotclick", e,
|
2270
|
+
function (s) { return s["clickable"] != false; });
|
2271
|
+
}
|
2272
|
+
|
2273
|
+
// trigger click or hover event (they send the same parameters
|
2274
|
+
// so we share their code)
|
2275
|
+
function triggerClickHoverEvent(eventname, event, seriesFilter) {
|
2276
|
+
var offset = eventHolder.offset(),
|
2277
|
+
canvasX = event.pageX - offset.left - plotOffset.left,
|
2278
|
+
canvasY = event.pageY - offset.top - plotOffset.top,
|
2279
|
+
pos = canvasToAxisCoords({ left: canvasX, top: canvasY });
|
2280
|
+
|
2281
|
+
pos.pageX = event.pageX;
|
2282
|
+
pos.pageY = event.pageY;
|
2283
|
+
|
2284
|
+
var item = findNearbyItem(canvasX, canvasY, seriesFilter);
|
2285
|
+
|
2286
|
+
if (item) {
|
2287
|
+
// fill in mouse pos for any listeners out there
|
2288
|
+
item.pageX = parseInt(item.series.xaxis.p2c(item.datapoint[0]) + offset.left + plotOffset.left);
|
2289
|
+
item.pageY = parseInt(item.series.yaxis.p2c(item.datapoint[1]) + offset.top + plotOffset.top);
|
2290
|
+
}
|
2291
|
+
|
2292
|
+
if (options.grid.autoHighlight) {
|
2293
|
+
// clear auto-highlights
|
2294
|
+
for (var i = 0; i < highlights.length; ++i) {
|
2295
|
+
var h = highlights[i];
|
2296
|
+
if (h.auto == eventname &&
|
2297
|
+
!(item && h.series == item.series && h.point == item.datapoint))
|
2298
|
+
unhighlight(h.series, h.point);
|
2299
|
+
}
|
2300
|
+
|
2301
|
+
if (item)
|
2302
|
+
highlight(item.series, item.datapoint, eventname);
|
2303
|
+
}
|
2304
|
+
|
2305
|
+
placeholder.trigger(eventname, [ pos, item ]);
|
2306
|
+
}
|
2307
|
+
|
2308
|
+
function triggerRedrawOverlay() {
|
2309
|
+
if (!redrawTimeout)
|
2310
|
+
redrawTimeout = setTimeout(drawOverlay, 30);
|
2311
|
+
}
|
2312
|
+
|
2313
|
+
function drawOverlay() {
|
2314
|
+
redrawTimeout = null;
|
2315
|
+
|
2316
|
+
// draw highlights
|
2317
|
+
octx.save();
|
2318
|
+
octx.clearRect(0, 0, canvasWidth, canvasHeight);
|
2319
|
+
octx.translate(plotOffset.left, plotOffset.top);
|
2320
|
+
|
2321
|
+
var i, hi;
|
2322
|
+
for (i = 0; i < highlights.length; ++i) {
|
2323
|
+
hi = highlights[i];
|
2324
|
+
|
2325
|
+
if (hi.series.bars.show)
|
2326
|
+
drawBarHighlight(hi.series, hi.point);
|
2327
|
+
else
|
2328
|
+
drawPointHighlight(hi.series, hi.point);
|
2329
|
+
}
|
2330
|
+
octx.restore();
|
2331
|
+
|
2332
|
+
executeHooks(hooks.drawOverlay, [octx]);
|
2333
|
+
}
|
2334
|
+
|
2335
|
+
function highlight(s, point, auto) {
|
2336
|
+
if (typeof s == "number")
|
2337
|
+
s = series[s];
|
2338
|
+
|
2339
|
+
if (typeof point == "number") {
|
2340
|
+
var ps = s.datapoints.pointsize;
|
2341
|
+
point = s.datapoints.points.slice(ps * point, ps * (point + 1));
|
2342
|
+
}
|
2343
|
+
|
2344
|
+
var i = indexOfHighlight(s, point);
|
2345
|
+
if (i == -1) {
|
2346
|
+
highlights.push({ series: s, point: point, auto: auto });
|
2347
|
+
|
2348
|
+
triggerRedrawOverlay();
|
2349
|
+
}
|
2350
|
+
else if (!auto)
|
2351
|
+
highlights[i].auto = false;
|
2352
|
+
}
|
2353
|
+
|
2354
|
+
function unhighlight(s, point) {
|
2355
|
+
if (s == null && point == null) {
|
2356
|
+
highlights = [];
|
2357
|
+
triggerRedrawOverlay();
|
2358
|
+
}
|
2359
|
+
|
2360
|
+
if (typeof s == "number")
|
2361
|
+
s = series[s];
|
2362
|
+
|
2363
|
+
if (typeof point == "number")
|
2364
|
+
point = s.data[point];
|
2365
|
+
|
2366
|
+
var i = indexOfHighlight(s, point);
|
2367
|
+
if (i != -1) {
|
2368
|
+
highlights.splice(i, 1);
|
2369
|
+
|
2370
|
+
triggerRedrawOverlay();
|
2371
|
+
}
|
2372
|
+
}
|
2373
|
+
|
2374
|
+
function indexOfHighlight(s, p) {
|
2375
|
+
for (var i = 0; i < highlights.length; ++i) {
|
2376
|
+
var h = highlights[i];
|
2377
|
+
if (h.series == s && h.point[0] == p[0]
|
2378
|
+
&& h.point[1] == p[1])
|
2379
|
+
return i;
|
2380
|
+
}
|
2381
|
+
return -1;
|
2382
|
+
}
|
2383
|
+
|
2384
|
+
function drawPointHighlight(series, point) {
|
2385
|
+
var x = point[0], y = point[1],
|
2386
|
+
axisx = series.xaxis, axisy = series.yaxis;
|
2387
|
+
|
2388
|
+
if (x < axisx.min || x > axisx.max || y < axisy.min || y > axisy.max)
|
2389
|
+
return;
|
2390
|
+
|
2391
|
+
var pointRadius = series.points.radius + series.points.lineWidth / 2;
|
2392
|
+
octx.lineWidth = pointRadius;
|
2393
|
+
octx.strokeStyle = $.color.parse(series.color).scale('a', 0.5).toString();
|
2394
|
+
var radius = 1.5 * pointRadius,
|
2395
|
+
x = axisx.p2c(x),
|
2396
|
+
y = axisy.p2c(y);
|
2397
|
+
|
2398
|
+
octx.beginPath();
|
2399
|
+
if (series.points.symbol == "circle")
|
2400
|
+
octx.arc(x, y, radius, 0, 2 * Math.PI, false);
|
2401
|
+
else
|
2402
|
+
series.points.symbol(octx, x, y, radius, false);
|
2403
|
+
octx.closePath();
|
2404
|
+
octx.stroke();
|
2405
|
+
}
|
2406
|
+
|
2407
|
+
function drawBarHighlight(series, point) {
|
2408
|
+
octx.lineWidth = series.bars.lineWidth;
|
2409
|
+
octx.strokeStyle = $.color.parse(series.color).scale('a', 0.5).toString();
|
2410
|
+
var fillStyle = $.color.parse(series.color).scale('a', 0.5).toString();
|
2411
|
+
var barLeft = series.bars.align == "left" ? 0 : -series.bars.barWidth/2;
|
2412
|
+
drawBar(point[0], point[1], point[2] || 0, barLeft, barLeft + series.bars.barWidth,
|
2413
|
+
0, function () { return fillStyle; }, series.xaxis, series.yaxis, octx, series.bars.horizontal, series.bars.lineWidth);
|
2414
|
+
}
|
2415
|
+
|
2416
|
+
function getColorOrGradient(spec, bottom, top, defaultColor) {
|
2417
|
+
if (typeof spec == "string")
|
2418
|
+
return spec;
|
2419
|
+
else {
|
2420
|
+
// assume this is a gradient spec; IE currently only
|
2421
|
+
// supports a simple vertical gradient properly, so that's
|
2422
|
+
// what we support too
|
2423
|
+
var gradient = ctx.createLinearGradient(0, top, 0, bottom);
|
2424
|
+
|
2425
|
+
for (var i = 0, l = spec.colors.length; i < l; ++i) {
|
2426
|
+
var c = spec.colors[i];
|
2427
|
+
if (typeof c != "string") {
|
2428
|
+
var co = $.color.parse(defaultColor);
|
2429
|
+
if (c.brightness != null)
|
2430
|
+
co = co.scale('rgb', c.brightness)
|
2431
|
+
if (c.opacity != null)
|
2432
|
+
co.a *= c.opacity;
|
2433
|
+
c = co.toString();
|
2434
|
+
}
|
2435
|
+
gradient.addColorStop(i / (l - 1), c);
|
2436
|
+
}
|
2437
|
+
|
2438
|
+
return gradient;
|
2439
|
+
}
|
2440
|
+
}
|
2441
|
+
}
|
2442
|
+
|
2443
|
+
$.plot = function(placeholder, data, options) {
|
2444
|
+
//var t0 = new Date();
|
2445
|
+
var plot = new Plot($(placeholder), data, options, $.plot.plugins);
|
2446
|
+
//(window.console ? console.log : alert)("time used (msecs): " + ((new Date()).getTime() - t0.getTime()));
|
2447
|
+
return plot;
|
2448
|
+
};
|
2449
|
+
|
2450
|
+
$.plot.plugins = [];
|
2451
|
+
|
2452
|
+
// returns a string with the date d formatted according to fmt
|
2453
|
+
$.plot.formatDate = function(d, fmt, monthNames) {
|
2454
|
+
var leftPad = function(n) {
|
2455
|
+
n = "" + n;
|
2456
|
+
return n.length == 1 ? "0" + n : n;
|
2457
|
+
};
|
2458
|
+
|
2459
|
+
var r = [];
|
2460
|
+
var escape = false, padNext = false;
|
2461
|
+
var hours = d.getUTCHours();
|
2462
|
+
var isAM = hours < 12;
|
2463
|
+
if (monthNames == null)
|
2464
|
+
monthNames = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
|
2465
|
+
|
2466
|
+
if (fmt.search(/%p|%P/) != -1) {
|
2467
|
+
if (hours > 12) {
|
2468
|
+
hours = hours - 12;
|
2469
|
+
} else if (hours == 0) {
|
2470
|
+
hours = 12;
|
2471
|
+
}
|
2472
|
+
}
|
2473
|
+
for (var i = 0; i < fmt.length; ++i) {
|
2474
|
+
var c = fmt.charAt(i);
|
2475
|
+
|
2476
|
+
if (escape) {
|
2477
|
+
switch (c) {
|
2478
|
+
case 'h': c = "" + hours; break;
|
2479
|
+
case 'H': c = leftPad(hours); break;
|
2480
|
+
case 'M': c = leftPad(d.getUTCMinutes()); break;
|
2481
|
+
case 'S': c = leftPad(d.getUTCSeconds()); break;
|
2482
|
+
case 'd': c = "" + d.getUTCDate(); break;
|
2483
|
+
case 'm': c = "" + (d.getUTCMonth() + 1); break;
|
2484
|
+
case 'y': c = "" + d.getUTCFullYear(); break;
|
2485
|
+
case 'b': c = "" + monthNames[d.getUTCMonth()]; break;
|
2486
|
+
case 'p': c = (isAM) ? ("" + "am") : ("" + "pm"); break;
|
2487
|
+
case 'P': c = (isAM) ? ("" + "AM") : ("" + "PM"); break;
|
2488
|
+
case '0': c = ""; padNext = true; break;
|
2489
|
+
}
|
2490
|
+
if (c && padNext) {
|
2491
|
+
c = leftPad(c);
|
2492
|
+
padNext = false;
|
2493
|
+
}
|
2494
|
+
r.push(c);
|
2495
|
+
if (!padNext)
|
2496
|
+
escape = false;
|
2497
|
+
}
|
2498
|
+
else {
|
2499
|
+
if (c == "%")
|
2500
|
+
escape = true;
|
2501
|
+
else
|
2502
|
+
r.push(c);
|
2503
|
+
}
|
2504
|
+
}
|
2505
|
+
return r.join("");
|
2506
|
+
};
|
2507
|
+
|
2508
|
+
// round to nearby lower multiple of base
|
2509
|
+
function floorInBase(n, base) {
|
2510
|
+
return base * Math.floor(n / base);
|
2511
|
+
}
|
2512
|
+
|
2513
|
+
})(jQuery);
|