blacklight_range_limit 1.0.0pre1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (26) hide show
  1. data/.gitignore +21 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.rdoc +141 -0
  4. data/Rakefile +5 -0
  5. data/VERSION +1 -0
  6. data/app/helpers/range_limit_helper.rb +89 -0
  7. data/app/views/blacklight_range_limit/_range_limit_panel.html.erb +74 -0
  8. data/app/views/blacklight_range_limit/_range_segments.html.erb +14 -0
  9. data/blacklight_range_limit.gemspec +23 -0
  10. data/config/routes.rb +6 -0
  11. data/lib/blacklight_range_limit.rb +55 -0
  12. data/lib/blacklight_range_limit/controller_override.rb +139 -0
  13. data/lib/blacklight_range_limit/engine.rb +17 -0
  14. data/lib/blacklight_range_limit/route_sets.rb +12 -0
  15. data/lib/blacklight_range_limit/segment_calculation.rb +103 -0
  16. data/lib/blacklight_range_limit/version.rb +10 -0
  17. data/lib/blacklight_range_limit/view_helper_override.rb +82 -0
  18. data/lib/generators/blacklight_range_limit/assets_generator.rb +25 -0
  19. data/lib/generators/blacklight_range_limit/blacklight_range_limit_generator.rb +11 -0
  20. data/lib/generators/blacklight_range_limit/templates/public/javascripts/flot/excanvas.min.js +1 -0
  21. data/lib/generators/blacklight_range_limit/templates/public/javascripts/flot/jquery.flot.js +2513 -0
  22. data/lib/generators/blacklight_range_limit/templates/public/javascripts/flot/jquery.flot.selection.js +328 -0
  23. data/lib/generators/blacklight_range_limit/templates/public/javascripts/range_limit_distro_facets.js +211 -0
  24. data/lib/generators/blacklight_range_limit/templates/public/javascripts/range_limit_slider.js +83 -0
  25. data/lib/generators/blacklight_range_limit/templates/public/stylesheets/blacklight_range_limit.css +13 -0
  26. 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,12 @@
1
+ module BlacklightRangeLimit
2
+ module RouteSets
3
+ protected
4
+ def catalog
5
+ add_routes do |options|
6
+ match 'catalog/range_limit' => 'catalog#range_limit'
7
+ end
8
+
9
+ super
10
+ end
11
+ end
12
+ 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,10 @@
1
+ module BlacklightRangeLimit
2
+ unless BlacklightRangeLimit.const_defined? :VERSION
3
+ def self.version
4
+ @version ||= File.read(File.join(File.dirname(__FILE__), '..', '..', 'VERSION')).chomp
5
+ end
6
+
7
+ VERSION = self.version
8
+ end
9
+ end
10
+
@@ -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,"&amp;").replace(/"/g,"&quot;")}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);