blacklight_range_limit 1.0.0pre1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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);