flotr 1.3.8

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1 @@
1
+ if(!window.CanvasRenderingContext2D){(function(){var m=Math;var mr=m.round;var ms=m.sin;var mc=m.cos;var Z=10;var Z2=Z/2;var G_vmlCanvasManager_={init:function(opt_doc){var doc=opt_doc||document;if(/MSIE/.test(navigator.userAgent)&&!window.opera){var self=this;doc.attachEvent("onreadystatechange",function(){self.init_(doc)})}},init_:function(doc){if(doc.readyState=="complete"){if(!doc.namespaces["g_vml_"]){doc.namespaces.add("g_vml_","urn:schemas-microsoft-com:vml")}var ss=doc.createStyleSheet();ss.cssText="canvas{display:inline-block;overflow:hidden;"+"text-align:left;width:300px;height:150px}"+"g_vml_\\:*{behavior:url(#default#VML)}";var els=doc.getElementsByTagName("canvas");for(var i=0;i<els.length;i++){if(!els[i].getContext){this.initElement(els[i])}}}},fixElement_:function(el){var outerHTML=el.outerHTML;var newEl=el.ownerDocument.createElement(outerHTML);if(outerHTML.slice(-2)!="/>"){var tagName="/"+el.tagName;var ns;while((ns=el.nextSibling)&&ns.tagName!=tagName){ns.removeNode()}if(ns){ns.removeNode()}}el.parentNode.replaceChild(newEl,el);return newEl},initElement:function(el){el=this.fixElement_(el);el.getContext=function(){if(this.context_){return this.context_}return this.context_=new CanvasRenderingContext2D_(this)};el.attachEvent('onpropertychange',onPropertyChange);el.attachEvent('onresize',onResize);var attrs=el.attributes;if(attrs.width&&attrs.width.specified){el.style.width=attrs.width.nodeValue+"px"}else{el.width=el.clientWidth}if(attrs.height&&attrs.height.specified){el.style.height=attrs.height.nodeValue+"px"}else{el.height=el.clientHeight}return el}};function onPropertyChange(e){var el=e.srcElement;switch(e.propertyName){case'width':el.style.width=el.attributes.width.nodeValue+"px";el.getContext().clearRect();break;case'height':el.style.height=el.attributes.height.nodeValue+"px";el.getContext().clearRect();break}}function onResize(e){var el=e.srcElement;if(el.firstChild){el.firstChild.style.width=el.clientWidth+'px';el.firstChild.style.height=el.clientHeight+'px'}}G_vmlCanvasManager_.init();var dec2hex=[];for(var i=0;i<16;i++){for(var j=0;j<16;j++){dec2hex[i*16+j]=i.toString(16)+j.toString(16)}}function createMatrixIdentity(){return[[1,0,0],[0,1,0],[0,0,1]]}function matrixMultiply(m1,m2){var result=createMatrixIdentity();for(var x=0;x<3;x++){for(var y=0;y<3;y++){var sum=0;for(var z=0;z<3;z++){sum+=m1[x][z]*m2[z][y]}result[x][y]=sum}}return result}function copyState(o1,o2){o2.fillStyle=o1.fillStyle;o2.lineCap=o1.lineCap;o2.lineJoin=o1.lineJoin;o2.lineWidth=o1.lineWidth;o2.miterLimit=o1.miterLimit;o2.shadowBlur=o1.shadowBlur;o2.shadowColor=o1.shadowColor;o2.shadowOffsetX=o1.shadowOffsetX;o2.shadowOffsetY=o1.shadowOffsetY;o2.strokeStyle=o1.strokeStyle;o2.arcScaleX_=o1.arcScaleX_;o2.arcScaleY_=o1.arcScaleY_}function processStyle(styleString){var str,alpha=1;styleString=String(styleString);if(styleString.substring(0,3)=="rgb"){var start=styleString.indexOf("(",3);var end=styleString.indexOf(")",start+1);var guts=styleString.substring(start+1,end).split(",");str="#";for(var i=0;i<3;i++){str+=dec2hex[Number(guts[i])]}if((guts.length==4)&&(styleString.substr(3,1)=="a")){alpha=guts[3]}}else{str=styleString}return[str,alpha]}function processLineCap(lineCap){switch(lineCap){case"butt":return"flat";case"round":return"round";case"square":default:return"square"}}function CanvasRenderingContext2D_(surfaceElement){this.m_=createMatrixIdentity();this.mStack_=[];this.aStack_=[];this.currentPath_=[];this.strokeStyle="#000";this.fillStyle="#000";this.lineWidth=1;this.lineJoin="miter";this.lineCap="butt";this.miterLimit=Z*1;this.globalAlpha=1;this.canvas=surfaceElement;var el=surfaceElement.ownerDocument.createElement('div');el.style.width=surfaceElement.clientWidth+'px';el.style.height=surfaceElement.clientHeight+'px';el.style.overflow='hidden';el.style.position='absolute';surfaceElement.appendChild(el);this.element_=el;this.arcScaleX_=1;this.arcScaleY_=1}var contextPrototype=CanvasRenderingContext2D_.prototype;contextPrototype.clearRect=function(){this.element_.innerHTML="";this.currentPath_=[]};contextPrototype.beginPath=function(){this.currentPath_=[]};contextPrototype.moveTo=function(aX,aY){this.currentPath_.push({type:"moveTo",x:aX,y:aY});this.currentX_=aX;this.currentY_=aY};contextPrototype.lineTo=function(aX,aY){this.currentPath_.push({type:"lineTo",x:aX,y:aY});this.currentX_=aX;this.currentY_=aY};contextPrototype.bezierCurveTo=function(aCP1x,aCP1y,aCP2x,aCP2y,aX,aY){this.currentPath_.push({type:"bezierCurveTo",cp1x:aCP1x,cp1y:aCP1y,cp2x:aCP2x,cp2y:aCP2y,x:aX,y:aY});this.currentX_=aX;this.currentY_=aY};contextPrototype.quadraticCurveTo=function(aCPx,aCPy,aX,aY){var cp1x=this.currentX_+2.0/3.0*(aCPx-this.currentX_);var cp1y=this.currentY_+2.0/3.0*(aCPy-this.currentY_);var cp2x=cp1x+(aX-this.currentX_)/3.0;var cp2y=cp1y+(aY-this.currentY_)/3.0;this.bezierCurveTo(cp1x,cp1y,cp2x,cp2y,aX,aY)};contextPrototype.arc=function(aX,aY,aRadius,aStartAngle,aEndAngle,aClockwise){aRadius*=Z;var arcType=aClockwise?"at":"wa";var xStart=aX+(mc(aStartAngle)*aRadius)-Z2;var yStart=aY+(ms(aStartAngle)*aRadius)-Z2;var xEnd=aX+(mc(aEndAngle)*aRadius)-Z2;var yEnd=aY+(ms(aEndAngle)*aRadius)-Z2;if(xStart==xEnd&&!aClockwise){xStart+=0.125}this.currentPath_.push({type:arcType,x:aX,y:aY,radius:aRadius,xStart:xStart,yStart:yStart,xEnd:xEnd,yEnd:yEnd})};contextPrototype.rect=function(aX,aY,aWidth,aHeight){this.moveTo(aX,aY);this.lineTo(aX+aWidth,aY);this.lineTo(aX+aWidth,aY+aHeight);this.lineTo(aX,aY+aHeight);this.closePath()};contextPrototype.strokeRect=function(aX,aY,aWidth,aHeight){this.beginPath();this.moveTo(aX,aY);this.lineTo(aX+aWidth,aY);this.lineTo(aX+aWidth,aY+aHeight);this.lineTo(aX,aY+aHeight);this.closePath();this.stroke()};contextPrototype.fillRect=function(aX,aY,aWidth,aHeight){this.beginPath();this.moveTo(aX,aY);this.lineTo(aX+aWidth,aY);this.lineTo(aX+aWidth,aY+aHeight);this.lineTo(aX,aY+aHeight);this.closePath();this.fill()};contextPrototype.createLinearGradient=function(aX0,aY0,aX1,aY1){var gradient=new CanvasGradient_("gradient");return gradient};contextPrototype.createRadialGradient=function(aX0,aY0,aR0,aX1,aY1,aR1){var gradient=new CanvasGradient_("gradientradial");gradient.radius1_=aR0;gradient.radius2_=aR1;gradient.focus_.x=aX0;gradient.focus_.y=aY0;return gradient};contextPrototype.drawImage=function(image,var_args){var dx,dy,dw,dh,sx,sy,sw,sh;var oldRuntimeWidth=image.runtimeStyle.width;var oldRuntimeHeight=image.runtimeStyle.height;image.runtimeStyle.width='auto';image.runtimeStyle.height='auto';var w=image.width;var h=image.height;image.runtimeStyle.width=oldRuntimeWidth;image.runtimeStyle.height=oldRuntimeHeight;if(arguments.length==3){dx=arguments[1];dy=arguments[2];sx=sy=0;sw=dw=w;sh=dh=h}else if(arguments.length==5){dx=arguments[1];dy=arguments[2];dw=arguments[3];dh=arguments[4];sx=sy=0;sw=w;sh=h}else if(arguments.length==9){sx=arguments[1];sy=arguments[2];sw=arguments[3];sh=arguments[4];dx=arguments[5];dy=arguments[6];dw=arguments[7];dh=arguments[8]}else{throw"Invalid number of arguments";}var d=this.getCoords_(dx,dy);var w2=sw/2;var h2=sh/2;var vmlStr=[];var W=10;var H=10;vmlStr.push(' <g_vml_:group',' coordsize="',Z*W,',',Z*H,'"',' coordorigin="0,0"',' style="width:',W,';height:',H,';position:absolute;');if(this.m_[0][0]!=1||this.m_[0][1]){var filter=[];filter.push("M11='",this.m_[0][0],"',","M12='",this.m_[1][0],"',","M21='",this.m_[0][1],"',","M22='",this.m_[1][1],"',","Dx='",mr(d.x/Z),"',","Dy='",mr(d.y/Z),"'");var max=d;var c2=this.getCoords_(dx+dw,dy);var c3=this.getCoords_(dx,dy+dh);var c4=this.getCoords_(dx+dw,dy+dh);max.x=Math.max(max.x,c2.x,c3.x,c4.x);max.y=Math.max(max.y,c2.y,c3.y,c4.y);vmlStr.push("padding:0 ",mr(max.x/Z),"px ",mr(max.y/Z),"px 0;filter:progid:DXImageTransform.Microsoft.Matrix(",filter.join(""),", sizingmethod='clip');")}else{vmlStr.push("top:",mr(d.y/Z),"px;left:",mr(d.x/Z),"px;")}vmlStr.push(' ">','<g_vml_:image src="',image.src,'"',' style="width:',Z*dw,';',' height:',Z*dh,';"',' cropleft="',sx/w,'"',' croptop="',sy/h,'"',' cropright="',(w-sx-sw)/w,'"',' cropbottom="',(h-sy-sh)/h,'"',' />','</g_vml_:group>');this.element_.insertAdjacentHTML("BeforeEnd",vmlStr.join(""))};contextPrototype.stroke=function(aFill){var lineStr=[];var lineOpen=false;var a=processStyle(aFill?this.fillStyle:this.strokeStyle);var color=a[0];var opacity=a[1]*this.globalAlpha;var W=10;var H=10;lineStr.push('<g_vml_:shape',' fillcolor="',color,'"',' filled="',Boolean(aFill),'"',' style="position:absolute;width:',W,';height:',H,';"',' coordorigin="0 0" coordsize="',Z*W,' ',Z*H,'"',' stroked="',!aFill,'"',' strokeweight="',this.lineWidth,'"',' strokecolor="',color,'"',' path="');var newSeq=false;var min={x:null,y:null};var max={x:null,y:null};for(var i=0;i<this.currentPath_.length;i++){var p=this.currentPath_[i];if(p.type=="moveTo"){lineStr.push(" m ");var c=this.getCoords_(p.x,p.y);lineStr.push(mr(c.x),",",mr(c.y))}else if(p.type=="lineTo"){lineStr.push(" l ");var c=this.getCoords_(p.x,p.y);lineStr.push(mr(c.x),",",mr(c.y))}else if(p.type=="close"){lineStr.push(" x ")}else if(p.type=="bezierCurveTo"){lineStr.push(" c ");var c=this.getCoords_(p.x,p.y);var c1=this.getCoords_(p.cp1x,p.cp1y);var c2=this.getCoords_(p.cp2x,p.cp2y);lineStr.push(mr(c1.x),",",mr(c1.y),",",mr(c2.x),",",mr(c2.y),",",mr(c.x),",",mr(c.y))}else if(p.type=="at"||p.type=="wa"){lineStr.push(" ",p.type," ");var c=this.getCoords_(p.x,p.y);var cStart=this.getCoords_(p.xStart,p.yStart);var cEnd=this.getCoords_(p.xEnd,p.yEnd);lineStr.push(mr(c.x-this.arcScaleX_*p.radius),",",mr(c.y-this.arcScaleY_*p.radius)," ",mr(c.x+this.arcScaleX_*p.radius),",",mr(c.y+this.arcScaleY_*p.radius)," ",mr(cStart.x),",",mr(cStart.y)," ",mr(cEnd.x),",",mr(cEnd.y))}if(c){if(min.x==null||c.x<min.x){min.x=c.x}if(max.x==null||c.x>max.x){max.x=c.x}if(min.y==null||c.y<min.y){min.y=c.y}if(max.y==null||c.y>max.y){max.y=c.y}}}lineStr.push(' ">');if(typeof this.fillStyle=="object"){var focus={x:"50%",y:"50%"};var width=(max.x-min.x);var height=(max.y-min.y);var dimension=(width>height)?width:height;focus.x=mr((this.fillStyle.focus_.x/width)*100+50)+"%";focus.y=mr((this.fillStyle.focus_.y/height)*100+50)+"%";var colors=[];if(this.fillStyle.type_=="gradientradial"){var inside=(this.fillStyle.radius1_/dimension*100);var expansion=(this.fillStyle.radius2_/dimension*100)-inside}else{var inside=0;var expansion=100}var insidecolor={offset:null,color:null};var outsidecolor={offset:null,color:null};this.fillStyle.colors_.sort(function(cs1,cs2){return cs1.offset-cs2.offset});for(var i=0;i<this.fillStyle.colors_.length;i++){var fs=this.fillStyle.colors_[i];colors.push((fs.offset*expansion)+inside,"% ",fs.color,",");if(fs.offset>insidecolor.offset||insidecolor.offset==null){insidecolor.offset=fs.offset;insidecolor.color=fs.color}if(fs.offset<outsidecolor.offset||outsidecolor.offset==null){outsidecolor.offset=fs.offset;outsidecolor.color=fs.color}}colors.pop();lineStr.push('<g_vml_:fill',' color="',outsidecolor.color,'"',' color2="',insidecolor.color,'"',' type="',this.fillStyle.type_,'"',' focusposition="',focus.x,', ',focus.y,'"',' colors="',colors.join(""),'"',' opacity="',opacity,'" />')}else if(aFill){lineStr.push('<g_vml_:fill color="',color,'" opacity="',opacity,'" />')}else{lineStr.push('<g_vml_:stroke',' opacity="',opacity,'"',' joinstyle="',this.lineJoin,'"',' miterlimit="',this.miterLimit,'"',' endcap="',processLineCap(this.lineCap),'"',' weight="',this.lineWidth,'px"',' color="',color,'" />')}lineStr.push("</g_vml_:shape>");this.element_.insertAdjacentHTML("beforeEnd",lineStr.join(""))};contextPrototype.fill=function(){this.stroke(true)};contextPrototype.closePath=function(){this.currentPath_.push({type:"close"})};contextPrototype.getCoords_=function(aX,aY){return{x:Z*(aX*this.m_[0][0]+aY*this.m_[1][0]+this.m_[2][0])-Z2,y:Z*(aX*this.m_[0][1]+aY*this.m_[1][1]+this.m_[2][1])-Z2}};contextPrototype.save=function(){var o={};copyState(this,o);this.aStack_.push(o);this.mStack_.push(this.m_);this.m_=matrixMultiply(createMatrixIdentity(),this.m_)};contextPrototype.restore=function(){copyState(this.aStack_.pop(),this);this.m_=this.mStack_.pop()};contextPrototype.translate=function(aX,aY){var m1=[[1,0,0],[0,1,0],[aX,aY,1]];this.m_=matrixMultiply(m1,this.m_)};contextPrototype.rotate=function(aRot){var c=mc(aRot);var s=ms(aRot);var m1=[[c,s,0],[-s,c,0],[0,0,1]];this.m_=matrixMultiply(m1,this.m_)};contextPrototype.scale=function(aX,aY){this.arcScaleX_*=aX;this.arcScaleY_*=aY;var m1=[[aX,0,0],[0,aY,0],[0,0,1]];this.m_=matrixMultiply(m1,this.m_)};contextPrototype.clip=function(){};contextPrototype.arcTo=function(){};contextPrototype.createPattern=function(){return new CanvasPattern_};function CanvasGradient_(aType){this.type_=aType;this.radius1_=0;this.radius2_=0;this.colors_=[];this.focus_={x:0,y:0}}CanvasGradient_.prototype.addColorStop=function(aOffset,aColor){aColor=processStyle(aColor);this.colors_.push({offset:1-aOffset,color:aColor})};function CanvasPattern_(){}G_vmlCanvasManager=G_vmlCanvasManager_;CanvasRenderingContext2D=CanvasRenderingContext2D_;CanvasGradient=CanvasGradient_;CanvasPattern=CanvasPattern_})()}
@@ -0,0 +1,170 @@
1
+ #!/usr/bin/env ruby
2
+ #
3
+ # Created by Paolo Bosetti on 2009-04-27.
4
+ # Copyright (c) 2009 University of Trento. All rights
5
+ # reserved.
6
+ require "rubygems"
7
+ require "erubis"
8
+ require "cgi"
9
+
10
+ class String
11
+ def escapeHTML
12
+ CGI.escapeHTML self
13
+ end
14
+ end
15
+
16
+ =begin rdoc
17
+ Flotr namespace.
18
+ =end
19
+ module Flotr
20
+ BASENAME = File.dirname(__FILE__)
21
+ OUTPUT_FILE = "flotr.html"
22
+ STD_TEMPLATE = "zooming.rhtml"
23
+
24
+
25
+ =begin rdoc
26
+ Series to be plotted are instance of the Flotr::Data class. Initialize a new
27
+ Data object with a set of OPTIONS, then add points with the << method.
28
+ =end
29
+ class Data
30
+ OPTIONS = [:color, :label, :lines, :bars, :points, :hints, :shadowSize]
31
+ attr_accessor *OPTIONS
32
+ attr_accessor :data
33
+
34
+ =begin rdoc
35
+ Creates a new serie of points. opts is a Hash with a subset of the keys
36
+ given in OPTIONS.
37
+ =end
38
+ def initialize(opts={})
39
+ @data = []
40
+ opts.each do |k,v|
41
+ if OPTIONS.include? k
42
+ self.send(k.to_s+'=', v)
43
+ else
44
+ warn "Warning: data option '#{k}' does not exist, ignored"
45
+ end
46
+ end
47
+ end
48
+
49
+ =begin rdoc
50
+ Provides a JavaScript-formatted description of the data array.
51
+ =end
52
+ def inspect
53
+ options = ""
54
+ OPTIONS.each do |o|
55
+ if self.send o then
56
+ options += "#{o}: \"#{self.send o}\", "
57
+ end
58
+ end
59
+ "{ #{options}data: #{@data.inspect}}"
60
+ end
61
+
62
+ =begin rdoc
63
+ Adds other to the data array. other must be a two element array like [0,0].
64
+ =end
65
+ def <<(other)
66
+ other = other.to_a
67
+ raise ArgumentError unless other[0].kind_of? Numeric
68
+ raise ArgumentError unless other[1].kind_of? Numeric
69
+ @data << other
70
+ end
71
+
72
+ end
73
+
74
+ =begin rdoc
75
+ The Plot object. Serves as a container for the data series (Data objects)
76
+ and for the plot options. Call the plot/show methods to generate the
77
+ plot file Flotr::OUTPUT_FILE.
78
+ =end
79
+ class Plot
80
+ attr_accessor :series, :title, :comment, :template, :options
81
+ attr_accessor :width, :height
82
+ attr_accessor :label, :xlim, :ylim
83
+
84
+ =begin rdoc
85
+ Creates a new plot. You only have to provide a title for the plot, that
86
+ will (probably) be used as a page title by the active template.
87
+ =end
88
+ def initialize(title = "Flotr Plot")
89
+ @title = title
90
+ @template = "#{BASENAME}/#{STD_TEMPLATE}"
91
+ @series = []
92
+ @options = {}
93
+ @width, @height = 600, 300
94
+ @label = @xlim = @ylim = {}
95
+ end
96
+
97
+ =begin rdoc
98
+ Lists the available standard templates. These are .rhtml files located in
99
+ the lib floder inside the gem. The extension .rhtml is dropped.
100
+ =end
101
+ def std_templates
102
+ Dir.glob("#{BASENAME}/*.rhtml").map {|f| File.basename(f, ".rhtml")}
103
+ end
104
+
105
+ =begin rdoc
106
+ Selects the active template. It has to be one of those included in the
107
+ result of the std_templates method.
108
+ =end
109
+ def std_template=(template)
110
+ raise "Template #{template} does not exist!" unless self.std_templates.include?(template)
111
+ @template = "#{BASENAME}/#{template}.rhtml"
112
+ end
113
+
114
+ =begin rdoc
115
+ Returns the currently selected standard template, or nil if the active
116
+ template is a custom one. Use Plot#template=(full/path/to/template.rhtml)
117
+ to set a custom template.
118
+ =end
119
+ def std_template
120
+ if File.dirname(@template) == BASENAME
121
+ File.basename(@template, ".rhtml")
122
+ else
123
+ nil
124
+ end
125
+ end
126
+
127
+ =begin rdoc
128
+ Generates the html file containing the plot and fires up a preferred
129
+ browser window with the plot loaded.
130
+ =end
131
+ def plot
132
+ eruby = Erubis::Eruby.new(File.read(@template))
133
+ File.open(OUTPUT_FILE, 'w') do |f|
134
+ f.print eruby.result(binding())
135
+ end
136
+ case RUBY_PLATFORM
137
+ when /darwin/
138
+ system "open \"#{OUTPUT_FILE}\""
139
+ when /mswin/
140
+ system "start #{OUTPUT_FILE}"
141
+ else
142
+ puts "open #{OUTPUT_FILE} in your preferred browser"
143
+ end
144
+ end
145
+
146
+ alias show plot
147
+
148
+ =begin rdoc
149
+ Adds a Data serie to the Plot. It also returns self, so multiple calls
150
+ can be concatenated as in plot << series1 << series2 << series3.
151
+ =end
152
+ def <<(serie)
153
+ if serie.instance_of? Data
154
+ @series << serie
155
+ else
156
+ raise ArgumentError
157
+ end
158
+ end
159
+
160
+ =begin rdoc
161
+ Provides a JavaScript-formatted description of the all the series.
162
+ =end
163
+ def inspect
164
+ data = @series.map {|s| s.inspect}
165
+ "[ #{data*', '} ]"
166
+ end
167
+ end
168
+
169
+ end
170
+
@@ -0,0 +1,77 @@
1
+ <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
2
+ "http://www.w3.org/TR/html4/loose.dtd">
3
+
4
+ <html>
5
+ <head>
6
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
7
+ <title><%= title %></title>
8
+ <link href="<%= BASENAME%>/layout.css" rel="stylesheet" type="text/css">
9
+ <!--[if IE]><script type="text/javascript" src="<%= BASENAME%>/excanvas.pack.js"></script><![endif]-->
10
+ <script type="text/javascript" src="<%= BASENAME%>/jquery.min.js"></script>
11
+ <script type="text/javascript" src="<%= BASENAME%>/jquery.flot.js"></script>
12
+ <script type="text/javascript">
13
+ $(document).ready( function() {
14
+ // create some datasets
15
+ var data = <%= self.inspect%>;
16
+
17
+ // plot the graph into '#placeholder'
18
+ plot = $.plot(
19
+ $("#placeholder"),
20
+ data,
21
+ { xaxis: {
22
+ label: <%= "\"#{@label[:X]}\"" or 'null'%>,
23
+ min: <%= @xlim[:min] or 'null'%>,
24
+ max: <%= @xlim[:max] or 'null'%>
25
+ },
26
+ yaxis: {
27
+ label: <%= "\"#{@label[:Y]}\"" or 'null'%>,
28
+ min: <%= @ylim[:min] or 'null'%>,
29
+ max: <%= @ylim[:max] or 'null'%>
30
+ },
31
+ hints: {
32
+ show: true,
33
+ hintFormatter: function( datapoint ) {
34
+ hintStr = "";
35
+ for( var key in datapoint ) {
36
+ if( key[0] == '_' ) { continue; } // skip internal members
37
+ hintStr += "<strong>" + key + ":</strong> " +
38
+ datapoint[key].toFixed(2) + "<br/>";
39
+ }
40
+ return hintStr;
41
+ } },
42
+ points: { show: <%= @options[:points] or 'true'%> },
43
+ lines: { show: <%= @options[:lines] or 'true'%> },
44
+ legend: { position: '<%= @options[:legend_position] or 'sw'%>' },
45
+ grid: {
46
+ clickable: true,
47
+ hoverable: true,
48
+ hoverFill: '#444',
49
+ hoverRadius: 5
50
+ }
51
+ } );
52
+
53
+ // setup event handlers
54
+ $("#placeholder").bind( 'plotclick', function( e, pos ) {
55
+ if( !pos.selected ) { return; }
56
+ plot.highlight( pos.selected );
57
+ x = pos.selected.x.toFixed( 2 );
58
+ y = pos.selected.y.toFixed( 2 );
59
+ $("#result").text( 'You clicked on (' + x + ', ' + y + ')' );
60
+ } );
61
+
62
+ $("#placeholder").bind( 'plotmousemove', function( e, pos ) {
63
+ if( !pos.selected ) { return; }
64
+ plot.highlight( pos.selected );
65
+ } );
66
+ });
67
+ </script>
68
+ </head>
69
+ <body>
70
+ <h1><%= title %></h1>
71
+
72
+ <div id="placeholder" style="width: <%= @width %>px; height: <%= @height %>px;"></div>
73
+
74
+ <p><%= comment %></p>
75
+ <p><span id="result"></span></p>
76
+ </body>
77
+ </html>
@@ -0,0 +1,2187 @@
1
+ /*
2
+ * Flot v0.9.0
3
+ *
4
+ * Released under the MIT license.
5
+ */
6
+
7
+ ( function( $ ) {
8
+ function Plot( target_, data_, options_ ) {
9
+ var series = [];
10
+ var options = {
11
+ // the color theme used for graphs
12
+ colors: ["#edc240", "#afd8f8", "#cb4b4b", "#4da74d", "#9440ed"],
13
+ legend: {
14
+ show: true,
15
+ noColumns: 1, // number of colums in legend table
16
+ labelFormatter: null, // fn: string -> string
17
+ labelBoxBorderColor: "#ccc", // border color for the little label boxes
18
+ container: null, // container (as jQuery object) to put legend in, null means default on top of graph
19
+ position: "ne", // position of default legend container within plot
20
+ margin: 5, // distance from grid edge to default legend container within plot
21
+ backgroundColor: null, // null means auto-detect
22
+ backgroundOpacity: 0.85 // set to 0 to avoid background
23
+ },
24
+ xaxis: {
25
+ label: null,
26
+ showLabels: true,
27
+ mode: null, // null or "time"
28
+ min: null, // min. value to show, null means set automatically
29
+ max: null, // max. value to show, null means set automatically
30
+ autoscaleMargin: null, // margin in % to add if auto-setting min/max
31
+ ticks: null, // either [1, 3] or [[1, "a"], 3] or (fn: axis info -> ticks) or app. number of ticks for auto-ticks
32
+ tickFormatter: null, // fn: number -> string
33
+ labelWidth: null, // size of tick labels in pixels
34
+ labelHeight: null,
35
+
36
+ // mode specific options
37
+ tickDecimals: null, // no. of decimals, null means auto
38
+ tickSize: null, // number or [number, "unit"]
39
+ minTickSize: null, // number or [number, "unit"]
40
+ monthNames: null, // list of names of months
41
+ timeformat: null // format string to use
42
+ },
43
+ yaxis: {
44
+ label: null,
45
+ showLabels: true,
46
+ autoscaleMargin: 0.02
47
+ },
48
+ points: {
49
+ show: false,
50
+ radius: 3,
51
+ lineWidth: 2, // in pixels
52
+ fill: true,
53
+ fillColor: "#ffffff"
54
+ },
55
+ lines: {
56
+ show: false,
57
+ lineWidth: 2, // in pixels
58
+ fill: false,
59
+ fillColor: null
60
+ },
61
+ bars: {
62
+ show: false,
63
+ lineWidth: 2, // in pixels
64
+ barWidth: 1, // in units of the x axis
65
+ fill: true,
66
+ fillOpacity: 0.4,
67
+ fillColor: null
68
+ },
69
+ deltas: {
70
+ show: false,
71
+ color: { above: '#A00', below: '#00A', equal: '#D52' },
72
+ markerWidth: 3
73
+ },
74
+ grid: {
75
+ showLines: 'both',
76
+ showBorder: true,
77
+ markers: [], // see API.txt for details
78
+ labelFontSize: 16, // default is 16px font size for axis labels
79
+ color: "#545454", // primary color used for outline and labels
80
+ backgroundColor: null, // null for transparent, else color
81
+ tickColor: "#dddddd", // color used for the ticks
82
+ tickWidth: 1, // thickness of grid lines
83
+ labelMargin: 3, // in pixels
84
+ borderWidth: 2,
85
+ clickable: null,
86
+ hoverable: false,
87
+ hoverColor: null,
88
+ hoverFill: null,
89
+ hoverRadius: null,
90
+ mouseCatchingArea: 15,
91
+ coloredAreas: null, // array of { x1, y1, x2, y2 } or fn: plot area -> areas
92
+ coloredAreasColor: "#f4f4f4"
93
+ },
94
+ hints: {
95
+ show: false,
96
+ showColorBox: true,
97
+ showSeriesLabel: true,
98
+ labelFormatter: defaultLabelFormatter,
99
+ hintFormatter: defaultHintFormatter,
100
+ backgroundColor: "#DDD", // null means auto-detect
101
+ backgroundOpacity: 0.7, // set to 0 to avoid background
102
+ borderColor: "#BBB" // set to 'transparent' for none
103
+ },
104
+ selection: {
105
+ snapToTicks: false, // boolean for if we should snap to ticks on selection
106
+ mode: null, // one of null, "x", "y" or "xy"
107
+ color: "#e8cfac"
108
+ },
109
+ shadowSize: 4,
110
+ sortData: true
111
+ };
112
+
113
+ var canvas = null, overlay = null, eventHolder = null,
114
+ ctx = null, octx = null,
115
+ target = target_,
116
+ xaxis = {}, yaxis = {},
117
+ plotOffset = { left: 0, right: 0, top: 0, bottom: 0},
118
+ canvasWidth = 0, canvasHeight = 0,
119
+ plotWidth = 0, plotHeight = 0,
120
+ hozScale = 0, vertScale = 0,
121
+ hintDiv = null, hintBackground = null,
122
+ lastMarker = null,
123
+ // dedicated to storing data for buggy standard compliance cases
124
+ workarounds = {},
125
+ // buffer constants
126
+ RIGHT_SIDE_BUFFER = 10,
127
+ BOTTOM_SIDE_BUFFER = 10;
128
+
129
+ this.setData = setData;
130
+ this.setupGrid = setupGrid;
131
+ this.highlight = highlight;
132
+ this.draw = draw;
133
+ this.cleanup = cleanup;
134
+ this.clearSelection = clearSelection;
135
+ this.setSelection = setSelection;
136
+ this.getCanvas = function () { return canvas; };
137
+ this.getPlotOffset = function () { return plotOffset; };
138
+ this.getData = function () { return series; };
139
+ this.getAxes = function () { return { xaxis: xaxis, yaxis: yaxis }; };
140
+
141
+ // initialize
142
+ $.extend( true, options, options_ );
143
+ setData( data_ );
144
+ constructCanvas();
145
+ setupGrid();
146
+ draw();
147
+
148
+ // kill hints and highlighted points when the mouse leaves the graph
149
+ if( options.grid.hoverable ) $(target).mouseout( cleanup );
150
+
151
+ function setData( d ) {
152
+ series = parseData(d);
153
+ fillInSeriesOptions();
154
+ processData();
155
+ }
156
+
157
+ // normalize the data given to the call to $.plot. If we're
158
+ // going to be monitoring mousemove's then sort the data
159
+ function parseData( d ) {
160
+ function sortData( a, b ) {
161
+ if( !a || !b ) return 0;
162
+ if( a.x > b.x ) return 1;
163
+ else if( a.x < b.x ) return -1;
164
+ else return 0;
165
+ }
166
+
167
+ var res = [];
168
+ for( var i = 0; i < d.length; ++i ) {
169
+ var s = {};
170
+ if( d[i].data ) {
171
+ for( var v in d[i] ) s[v] = d[i][v];
172
+ }
173
+ else {
174
+ s.data = d[i];
175
+ }
176
+ res.push( s );
177
+ }
178
+
179
+ // normalize the old style [x,y] data format
180
+ for( var i in res ) {
181
+ for( var j in res[i].data ) {
182
+ var datapoint = res[i].data[j];
183
+ if( datapoint != null && datapoint.x == undefined ) {
184
+ res[i].data[j] = { x: datapoint[0], y: datapoint[1] };
185
+ }
186
+ }
187
+ if( options.sortData ) res[i].data.sort( sortData );
188
+ }
189
+ return res;
190
+ }
191
+
192
+ function fillInSeriesOptions() {
193
+ var i;
194
+
195
+ // collect what we already got of colors
196
+ var neededColors = series.length;
197
+ var usedColors = [];
198
+ var assignedColors = [];
199
+ for( i = 0; i < series.length; ++i ) {
200
+ var sc = series[i].color;
201
+ if( sc != null ) {
202
+ --neededColors;
203
+ if( typeof sc == "number" ) { assignedColors.push( sc ); }
204
+ else { usedColors.push( parseColor( series[i].color ) ); }
205
+ }
206
+ }
207
+
208
+ // we might need to generate more colors if higher indices
209
+ // are assigned
210
+ for( i = 0; i < assignedColors.length; ++i ) {
211
+ neededColors = Math.max( neededColors, assignedColors[i] + 1 );
212
+ }
213
+
214
+ // produce colors as needed
215
+ var colors = [];
216
+ var variation = 0;
217
+ i = 0;
218
+ while( colors.length < neededColors ) {
219
+ var c;
220
+ if( options.colors.length == i ) { c = new Color( 100, 100, 100 ); }
221
+ else { c = parseColor( options.colors[i] ); }
222
+
223
+ // vary color if needed
224
+ var sign = variation % 2 == 1 ? -1 : 1;
225
+ var factor = 1 + sign * Math.ceil( variation / 2 ) * 0.2;
226
+ c.scale( factor, factor, factor );
227
+
228
+ // FIXME: if we're getting to close to something else,
229
+ // we should probably skip this one
230
+ colors.push( c );
231
+
232
+ ++i;
233
+ if( i >= options.colors.length ) {
234
+ i = 0;
235
+ ++variation;
236
+ }
237
+ }
238
+
239
+ // fill in the options
240
+ var colori = 0, s;
241
+ for( i = 0; i < series.length; ++i ) {
242
+ s = series[i];
243
+
244
+ // assign colors
245
+ if( s.color == null ) {
246
+ s.color = colors[colori].toString();
247
+ ++colori;
248
+ }
249
+ else if( typeof s.color == "number" ) {
250
+ s.color = colors[s.color].toString();
251
+ }
252
+
253
+ // copy the rest
254
+ s.lines = $.extend(true, {}, options.lines, s.lines);
255
+ s.points = $.extend(true, {}, options.points, s.points);
256
+ s.bars = $.extend(true, {}, options.bars, s.bars);
257
+ s.deltas = $.extend(true, {}, options.deltas, s.deltas);
258
+ s.hints = $.extend(true, {}, options.hints, s.hints);
259
+ if (s.shadowSize == null) s.shadowSize = options.shadowSize;
260
+ }
261
+ }
262
+
263
+ function processData() {
264
+ var top_sentry = Number.POSITIVE_INFINITY,
265
+ bottom_sentry = Number.NEGATIVE_INFINITY;
266
+
267
+ xaxis.datamin = yaxis.datamin = top_sentry;
268
+ xaxis.datamax = yaxis.datamax = bottom_sentry;
269
+
270
+ for( var i = 0; i < series.length; ++i ) {
271
+ var data = series[i].data;
272
+ for( var j = 0; j < data.length; ++j ) {
273
+ if( data[j] == null ) continue;
274
+
275
+ var x = data[j].x, y = data[j].y;
276
+
277
+ // convert to number
278
+ if( x == null || y == null ||
279
+ isNaN( x = +x ) || isNaN( y = +y ) ) {
280
+ data[j] = null; // mark this point as invalid
281
+ continue;
282
+ }
283
+
284
+ if( x < xaxis.datamin ) xaxis.datamin = x;
285
+ if( x > xaxis.datamax ) xaxis.datamax = x;
286
+ if( y < yaxis.datamin ) yaxis.datamin = y;
287
+ if( y > yaxis.datamax ) yaxis.datamax = y;
288
+ }
289
+ }
290
+
291
+ if( xaxis.datamin == top_sentry ) xaxis.datamin = 0;
292
+ if( yaxis.datamin == top_sentry ) yaxis.datamin = 0;
293
+ if( xaxis.datamax == bottom_sentry ) xaxis.datamax = 1;
294
+ if( yaxis.datamax == bottom_sentry ) yaxis.datamax = 1;
295
+ }
296
+
297
+ function constructCanvas() {
298
+ canvasWidth = target.width();
299
+ canvasHeight = target.height();
300
+ target.html( '' ).css( 'position', 'relative' );
301
+
302
+ if( canvasWidth <= 0 || canvasHeight <= 0 ) {
303
+ throw "Invalid dimensions for plot, width = " + canvasWidth + ", height = " + canvasHeight;
304
+ }
305
+
306
+ // the canvas
307
+ canvas = $('<canvas width="' + canvasWidth + '" height="' + canvasHeight + '"></canvas>').appendTo( target ).get( 0 );
308
+ if( $.browser.msie ) { canvas = window.G_vmlCanvasManager.initElement( canvas ); }
309
+ ctx = canvas.getContext( '2d' );
310
+
311
+ // overlay canvas for interactive features
312
+ overlay = $('<canvas style="position:absolute;left:0px;top:0px;" width="' + canvasWidth + '" height="' + canvasHeight + '"></canvas>').appendTo( target ).get( 0 );
313
+ if( $.browser.msie ) { overlay = window.G_vmlCanvasManager.initElement( overlay ); }
314
+ octx = overlay.getContext( '2d' );
315
+
316
+ // we include the canvas in the event holder too, because IE 7
317
+ // sometimes has trouble with the stacking order
318
+ eventHolder = $( [ overlay, canvas ] );
319
+
320
+ // bind events
321
+ if( options.selection.mode != null ) {
322
+ eventHolder.mousedown( onMouseDown ).mousemove( onMouseMove );
323
+ }
324
+
325
+ if( options.grid.hoverable ) {
326
+ eventHolder.mousemove( onMouseMove );
327
+ }
328
+
329
+ if( options.grid.clickable ) {
330
+ eventHolder.click( onClick );
331
+ }
332
+ }
333
+
334
+ function setupGrid() {
335
+ // x axis
336
+ setRange( xaxis, options.xaxis );
337
+ prepareTickGeneration( xaxis, options.xaxis );
338
+ setTicks( xaxis, options.xaxis );
339
+ extendXRangeIfNeededByBar();
340
+
341
+ // y axis
342
+ setRange( yaxis, options.yaxis );
343
+ prepareTickGeneration( yaxis, options.yaxis );
344
+ setTicks( yaxis, options.yaxis );
345
+
346
+ setSpacing();
347
+ insertTickLabels();
348
+ insertLegend();
349
+ insertAxisLabels();
350
+ }
351
+
352
+ function setRange( axis, axisOptions ) {
353
+ var min = axisOptions.min != null ? axisOptions.min : axis.datamin;
354
+ var max = axisOptions.max != null ? axisOptions.max : axis.datamax;
355
+
356
+ if( max - min == 0.0 ) {
357
+ // degenerate case
358
+ var widen;
359
+ if( max == 0.0 ) widen = 1.0;
360
+ else widen = 0.01;
361
+
362
+ min -= widen;
363
+ max += widen;
364
+ }
365
+ else {
366
+ // consider autoscaling
367
+ var margin = axisOptions.autoscaleMargin;
368
+ if( margin != null ) {
369
+ if( axisOptions.min == null ) {
370
+ min -= ( max - min ) * margin;
371
+ // make sure we don't go below zero if all values
372
+ // are positive
373
+ if( min < 0 && axis.datamin >= 0 ) min = 0;
374
+ }
375
+ if( axisOptions.max == null ) {
376
+ max += ( max - min ) * margin;
377
+ if( max > 0 && axis.datamax <= 0 ) max = 0;
378
+ }
379
+ }
380
+ }
381
+ axis.min = min;
382
+ axis.max = max;
383
+ }
384
+
385
+ function prepareTickGeneration( axis, axisOptions ) {
386
+ // estimate number of ticks
387
+ var noTicks;
388
+ if( typeof axisOptions.ticks == "number" && axisOptions.ticks > 0 ) {
389
+ noTicks = axisOptions.ticks;
390
+ }
391
+ else if( axis == xaxis ) {
392
+ noTicks = canvasWidth / 100;
393
+ }
394
+ else {
395
+ noTicks = canvasHeight / 60;
396
+ }
397
+
398
+ var delta = ( axis.max - axis.min ) / noTicks;
399
+ var size, generator, unit, formatter, i, magn, norm;
400
+
401
+ if( axisOptions.mode == "time" ) {
402
+ // pretty handling of time
403
+
404
+ function formatDate( d, fmt, monthNames ) {
405
+ var leftPad = function( n ) {
406
+ n = '' + n;
407
+ return n.length == 1 ? '0' + n : n;
408
+ };
409
+
410
+ var r = [];
411
+ var escape = false;
412
+ if( monthNames == null ) {
413
+ monthNames = [ "Jan", "Feb", "Mar", "Apr", "May",
414
+ "Jun", "Jul", "Aug", "Sep", "Oct",
415
+ "Nov", "Dec" ];
416
+ }
417
+ for( var i = 0; i < fmt.length; ++i ) {
418
+ var c = fmt.charAt( i );
419
+
420
+ if( escape ) {
421
+ switch (c) {
422
+ case 'h': c = "" + d.getUTCHours(); break;
423
+ case 'H': c = leftPad( d.getUTCHours() ); break;
424
+ case 'M': c = leftPad( d.getUTCMinutes() ); break;
425
+ case 'S': c = leftPad( d.getUTCSeconds() ); break;
426
+ case 'd': c = "" + d.getUTCDate(); break;
427
+ case 'm': c = "" + ( d.getUTCMonth() + 1 ); break;
428
+ case 'y': c = "" + d.getUTCFullYear(); break;
429
+ case 'b': c = "" + monthNames[d.getUTCMonth()]; break;
430
+ }
431
+ r.push( c );
432
+ escape = false;
433
+ }
434
+ else {
435
+ if( c == "%" ) escape = true;
436
+ else r.push( c );
437
+ }
438
+ }
439
+ return r.join( '' );
440
+ }
441
+
442
+ // map of app. size of time units in milliseconds
443
+ var timeUnitSize = {
444
+ "second": 1000,
445
+ "minute": 60 * 1000,
446
+ "hour": 60 * 60 * 1000,
447
+ "day": 24 * 60 * 60 * 1000,
448
+ "month": 30 * 24 * 60 * 60 * 1000,
449
+ "year": 365.2425 * 24 * 60 * 60 * 1000
450
+ };
451
+
452
+ // the allowed tick sizes, after 1 year we use
453
+ // an integer algorithm
454
+ var spec = [
455
+ [ 1, "second" ], [ 2, "second" ], [ 5, "second" ],
456
+ [ 10, "second" ], [ 30, "second" ], [ 1, "minute" ],
457
+ [ 2, "minute" ], [ 5, "minute" ], [ 10, "minute" ],
458
+ [ 30, "minute" ], [ 1, "hour" ], [ 2, "hour" ],
459
+ [ 4, "hour" ], [ 8, "hour" ], [ 12, "hour" ],
460
+ [ 1, "day" ], [ 2, "day" ], [ 3, "day" ],
461
+ [ 0.25, "month" ], [ 0.5, "month" ], [ 1, "month" ],
462
+ [ 2, "month" ], [ 3, "month" ], [ 6, "month" ],
463
+ [ 1, "year" ]
464
+ ];
465
+
466
+ var minSize = 0;
467
+ if( axisOptions.minTickSize != null ) {
468
+ minSize = typeof axisOptions.tickSize == 'number' ?
469
+ axisOptions.tickSize:
470
+ axisOptions.minTickSize[0] *
471
+ timeUnitSize[axisOptions.minTickSize[1]];
472
+ }
473
+
474
+ for( i = 0; i < spec.length - 1; ++i ) {
475
+ var d = spec[i][0] * timeUnitSize[spec[i][1]] +
476
+ spec[i + 1][0] * timeUnitSize[spec[i + 1][1]];
477
+ if( delta < d / 2 &&
478
+ spec[i][0] * timeUnitSize[spec[i][1]] >= minSize ) {
479
+ break;
480
+ }
481
+ }
482
+
483
+ size = spec[i][0];
484
+ unit = spec[i][1];
485
+
486
+ // special-case the possibility of several years
487
+ if( unit == "year" ) {
488
+ magn = Math.pow( 10,
489
+ Math.floor(
490
+ Math.log( delta / timeUnitSize.year ) / Math.LN10
491
+ )
492
+ );
493
+ norm = ( delta / timeUnitSize.year ) / magn;
494
+ if( norm < 1.5 ) size = 1;
495
+ else if( norm < 3 ) size = 2;
496
+ else if( norm < 7.5 ) size = 5;
497
+ else size = 10;
498
+
499
+ size *= magn;
500
+ }
501
+
502
+ if( axisOptions.tickSize ) {
503
+ size = axisOptions.tickSize[0];
504
+ unit = axisOptions.tickSize[1];
505
+ }
506
+
507
+ generator = function( axis ) {
508
+ var ticks = [],
509
+ tickSize = axis.tickSize[0],
510
+ unit = axis.tickSize[1],
511
+ d = new Date( axis.min );
512
+
513
+ var step = tickSize * timeUnitSize[unit];
514
+
515
+ if( unit == 'second' ) d.setUTCSeconds( floorInBase( d.getUTCSeconds(), tickSize ) );
516
+ if( unit == 'minute' ) d.setUTCMinutes( floorInBase( d.getUTCMinutes(), tickSize ) );
517
+ if( unit == 'hour' ) d.setUTCHours( floorInBase( d.getUTCHours(), tickSize ) );
518
+ if( unit == 'month' ) d.setUTCMonth( floorInBase( d.getUTCMonth(), tickSize ) );
519
+ if( unit == 'year' ) d.setUTCFullYear( floorInBase( d.getUTCFullYear(), tickSize ) );
520
+
521
+ // reset smaller components
522
+ d.setUTCMilliseconds( 0 );
523
+ if( step >= timeUnitSize.minute ) d.setUTCSeconds( 0 );
524
+ if( step >= timeUnitSize.hour ) d.setUTCMinutes( 0 );
525
+ if( step >= timeUnitSize.day ) d.setUTCHours( 0 );
526
+ if( step >= timeUnitSize.day * 4 ) d.setUTCDate( 1 );
527
+ if( step >= timeUnitSize.year ) d.setUTCMonth( 0 );
528
+
529
+ var carry = 0,
530
+ v = Number.NaN,
531
+ prev;
532
+
533
+ do {
534
+ prev = v;
535
+ v = d.getTime();
536
+ ticks.push( { v: v, label: axis.tickFormatter( v, axis ) } );
537
+ if( unit == 'month' ) {
538
+ if( tickSize < 1 ) {
539
+ // a bit complicated - we'll divide the month
540
+ // up but we need to take care of fractions
541
+ // so we don't end up in the middle of a day
542
+ d.setUTCDate( 1 );
543
+ var start = d.getTime();
544
+ d.setUTCMonth( d.getUTCMonth() + 1 );
545
+ var end = d.getTime();
546
+ d.setTime( v + carry * timeUnitSize.hour +
547
+ ( end - start ) * tickSize );
548
+ carry = d.getUTCHours();
549
+ d.setUTCHours( 0 );
550
+ }
551
+ else {
552
+ d.setUTCMonth( d.getUTCMonth() + tickSize );
553
+ }
554
+ }
555
+ else if( unit == 'year' ) {
556
+ d.setUTCFullYear( d.getUTCFullYear() + tickSize );
557
+ }
558
+ else {
559
+ d.setTime( v + step );
560
+ }
561
+ } while( v < axis.max && v != prev );
562
+
563
+ return ticks;
564
+ };
565
+
566
+ formatter = function( v, axis ) {
567
+ var d = new Date( v );
568
+
569
+ // first check global format
570
+ if( axisOptions.timeformat ) {
571
+ return formatDate( d, axisOptions.timeformat, axisOptions.monthNames );
572
+ }
573
+
574
+ var t = axis.tickSize[0] * timeUnitSize[axis.tickSize[1]];
575
+ var span = axis.max - axis.min;
576
+
577
+ if( t < timeUnitSize.minute ) { fmt = "%h:%M:%S"; }
578
+ else if( t < timeUnitSize.day ) {
579
+ if( span < 2 * timeUnitSize.day ) fmt = "%h:%M";
580
+ else fmt = "%b %d %h:%M";
581
+ }
582
+ else if( t < timeUnitSize.month ) { fmt = "%b %d"; }
583
+ else if( t < timeUnitSize.year ) {
584
+ if( span < timeUnitSize.year ) fmt = "%b";
585
+ else fmt = "%b %y";
586
+ }
587
+ else { fmt = "%y"; }
588
+
589
+ return formatDate( d, fmt, axisOptions.monthNames );
590
+ };
591
+ }
592
+ else {
593
+ // pretty rounding of base-10 numbers
594
+ var maxDec = axisOptions.tickDecimals;
595
+ var dec = -Math.floor( Math.log( delta ) / Math.LN10 );
596
+ if( maxDec && dec > maxDec ) dec = maxDec;
597
+
598
+ magn = Math.pow( 10, -dec );
599
+ norm = delta / magn; // norm is between 1.0 and 10.0
600
+
601
+ if( norm < 1.5 ) { size = 1; }
602
+ else if( norm < 3 ) {
603
+ size = 2;
604
+ // special case for 2.5, requires an extra decimal
605
+ if( norm > 2.25 && ( !maxDec || dec + 1 <= maxDec ) ) {
606
+ size = 2.5;
607
+ ++dec;
608
+ }
609
+ }
610
+ else if( norm < 7.5 ) { size = 5; }
611
+ else { size = 10; }
612
+
613
+ size *= magn;
614
+
615
+ if( axisOptions.minTickSize && size < axisOptions.minTickSize ) {
616
+ size = axisOptions.minTickSize;
617
+ }
618
+
619
+ if( axisOptions.tickSize ) {
620
+ size = axisOptions.tickSize;
621
+ }
622
+
623
+ axis.tickDecimals = Math.max( 0, ( maxDec ) ? maxDec : dec );
624
+
625
+ generator = function( axis ) {
626
+ var ticks = [];
627
+ var start = floorInBase( axis.min, axis.tickSize );
628
+ // then spew out all possible ticks
629
+ var i = 0,
630
+ v = Number.NaN,
631
+ prev;
632
+
633
+ do {
634
+ prev = v;
635
+ v = start + i * axis.tickSize;
636
+ ticks.push( { v: v, label: axis.tickFormatter( v, axis ) } );
637
+ ++i;
638
+ } while( v < axis.max && v != prev );
639
+ return ticks;
640
+ };
641
+
642
+ formatter = function( v, axis ) {
643
+ return v.toFixed( axis.tickDecimals );
644
+ };
645
+ }
646
+
647
+ axis.tickSize = unit ? [size, unit] : size;
648
+ axis.tickGenerator = generator;
649
+ if( $.isFunction( axisOptions.tickFormatter ) ) {
650
+ axis.tickFormatter = function( v, axis ) {
651
+ return '' + axisOptions.tickFormatter( v, axis );
652
+ };
653
+ }
654
+ else {
655
+ axis.tickFormatter = formatter;
656
+ }
657
+
658
+ if( axisOptions.labelWidth ) axis.labelWidth = axisOptions.labelWidth;
659
+ if( axisOptions.labelHeight ) axis.labelHeight = axisOptions.labelHeight;
660
+ }
661
+
662
+ function extendXRangeIfNeededByBar() {
663
+ if( !options.xaxis.max ) {
664
+ // great, we're autoscaling, check if we might need a bump
665
+ var newmax = xaxis.max;
666
+ for( var i = 0; i < series.length; ++i ) {
667
+ if( series[i].bars.show && series[i].bars.barWidth +
668
+ xaxis.datamax > newmax ) {
669
+ newmax = xaxis.datamax + series[i].bars.barWidth;
670
+ }
671
+ }
672
+ xaxis.max = newmax;
673
+ }
674
+ }
675
+
676
+ function setTicks( axis, axisOptions ) {
677
+ axis.ticks = [];
678
+
679
+ if( !axisOptions.ticks ) {
680
+ axis.ticks = axis.tickGenerator( axis );
681
+ }
682
+ else if( typeof axisOptions.ticks == 'number' ) {
683
+ if( axisOptions.ticks > 0 ) axis.ticks = axis.tickGenerator( axis );
684
+ }
685
+ else if( axisOptions.ticks ) {
686
+ var ticks = axisOptions.ticks;
687
+
688
+ if( $.isFunction( ticks ) ) {
689
+ // generate the ticks
690
+ ticks = ticks( { min: axis.min, max: axis.max } );
691
+ }
692
+
693
+ // clean up the user-supplied ticks, copy them over
694
+ var i, v;
695
+ for( i = 0; i < ticks.length; ++i ) {
696
+ var label = null;
697
+ var t = ticks[i];
698
+ if( typeof t == 'object' ) {
699
+ v = t[0];
700
+ if( t.length > 1 ) label = t[1];
701
+ }
702
+ else {
703
+ v = t;
704
+ }
705
+
706
+ if( !label ) label = axis.tickFormatter( v, axis );
707
+ axis.ticks[i] = { v: v, label: label };
708
+ }
709
+ }
710
+
711
+ if( axisOptions.autoscaleMargin && axis.ticks.length > 0 ) {
712
+ // snap to ticks
713
+ if( !axisOptions.min ) {
714
+ axis.min = Math.min( axis.min, axis.ticks[0].v );
715
+ }
716
+ if( !axisOptions.max && axis.ticks.length > 1 ) {
717
+ axis.max = Math.min( axis.max, axis.ticks[axis.ticks.length - 1].v );
718
+ }
719
+ }
720
+ }
721
+
722
+ function setSpacing() {
723
+ var i, l,
724
+ labels = [];
725
+
726
+ if( !yaxis.labelWidth || !yaxis.labelHeight ) {
727
+ // calculate y label dimensions
728
+ for( i = 0; i < yaxis.ticks.length; ++i ) {
729
+ l = yaxis.ticks[i].label;
730
+ if( l ) labels.push( '<div class="tickLabel">' + l + '</div>' );
731
+ }
732
+
733
+ if( labels.length > 0 ) {
734
+ var dummyDiv = $( '<div style="position:absolute;top:-10000px;font-size:smaller">' +
735
+ labels.join('') + '</div>' ).appendTo( target );
736
+ if( !yaxis.labelWidth ) yaxis.labelWidth = dummyDiv.width();
737
+ if( !yaxis.labelHeight ) yaxis.labelHeight = dummyDiv.find('div').height();
738
+ dummyDiv.remove();
739
+ }
740
+
741
+ if( !yaxis.labelWidth ) yaxis.labelWidth = 0;
742
+ if( !yaxis.labelHeight ) yaxis.labelHeight = 0;
743
+ }
744
+
745
+ var maxOutset = options.grid.borderWidth / 2;
746
+ if( options.points.show ) {
747
+ maxOutset = Math.max( maxOutset, options.points.radius +
748
+ options.points.lineWidth / 2 );
749
+ }
750
+ for( i = 0; i < series.length; ++i ) {
751
+ if( series[i].points.show ) {
752
+ maxOutset = Math.max( maxOutset, series[i].points.radius +
753
+ series[i].points.lineWidth / 2 );
754
+ }
755
+ }
756
+
757
+ plotOffset.left = plotOffset.right = plotOffset.top = plotOffset.bottom = maxOutset;
758
+
759
+ if( yaxis.labelWidth > 0 && options.xaxis.showLabels ) {
760
+ plotOffset.left += yaxis.labelWidth + options.grid.labelMargin;
761
+ }
762
+
763
+ plotWidth = canvasWidth - plotOffset.left - plotOffset.right - RIGHT_SIDE_BUFFER;
764
+
765
+ // set width for labels; to avoid measuring the widths of
766
+ // the labels, we construct fixed-size boxes and put the
767
+ // labels inside them, the fixed-size boxes are easy to
768
+ // mid-align
769
+ if( !xaxis.labelWidth ) xaxis.labelWidth = plotWidth / 6;
770
+
771
+ if( !xaxis.labelHeight ) {
772
+ // measure x label heights
773
+ labels = [];
774
+ for( i = 0; i < xaxis.ticks.length; ++i ) {
775
+ l = xaxis.ticks[i].label;
776
+ if( l ) labels.push( '<span class="tickLabel" width="' + xaxis.labelWidth + '">' + l + '</span>' );
777
+ }
778
+
779
+ xaxis.labelHeight = 0;
780
+ if( labels.length > 0 ) {
781
+ var dummyDiv = $( '<div style="position:absolute;top:-10000px;font-size:smaller">' +
782
+ labels.join('') + '</div>' ).appendTo( target );
783
+ xaxis.labelHeight = dummyDiv.height();
784
+ dummyDiv.remove();
785
+ }
786
+ }
787
+
788
+ if( xaxis.labelHeight > 0 && options.yaxis.showLabels ) {
789
+ plotOffset.bottom += xaxis.labelHeight + options.grid.labelMargin;
790
+ }
791
+
792
+ // add a bit of extra buffer on the bottom of the graph to account
793
+ // for the axis label, if there is one
794
+ if( options.xaxis.label ) plotOffset.bottom += BOTTOM_SIDE_BUFFER;
795
+
796
+ plotHeight = canvasHeight - plotOffset.bottom - BOTTOM_SIDE_BUFFER - plotOffset.top;
797
+ hozScale = plotWidth / ( xaxis.max - xaxis.min );
798
+ vertScale = plotHeight / ( yaxis.max - yaxis.min );
799
+ }
800
+
801
+ function draw() {
802
+ drawGrid();
803
+ drawMarkers();
804
+ for( var i = 0; i < series.length; i++ ) {
805
+ drawSeries( series[i] );
806
+ }
807
+ }
808
+
809
+ function tHoz( x ) { return ( x - xaxis.min ) * hozScale; }
810
+ function tVert( y ) { return plotHeight - ( y - yaxis.min ) * vertScale; }
811
+
812
+ function drawGrid() {
813
+ var i;
814
+
815
+ ctx.save();
816
+ ctx.clearRect( 0, 0, canvasWidth, canvasHeight );
817
+ ctx.translate( plotOffset.left, plotOffset.top );
818
+
819
+ // draw background, if any
820
+ if( options.grid.backgroundColor ) {
821
+ ctx.fillStyle = options.grid.backgroundColor;
822
+ ctx.fillRect( 0, 0, plotWidth, plotHeight );
823
+ }
824
+
825
+ // draw colored areas
826
+ if( options.grid.coloredAreas ) {
827
+ var areas = options.grid.coloredAreas;
828
+ if( $.isFunction( areas ) ) {
829
+ areas = areas( { xmin: xaxis.min, xmax: xaxis.max,
830
+ ymin: yaxis.min, ymax: yaxis.max } );
831
+ }
832
+
833
+ for( i = 0; i < areas.length; ++i ) {
834
+ var a = areas[i];
835
+
836
+ // clip
837
+ if( !a.x1 || a.x1 < xaxis.min ) a.x1 = xaxis.min;
838
+ if( !a.x2 || a.x2 > xaxis.max ) a.x2 = xaxis.max;
839
+ if( !a.y1 || a.y1 < yaxis.min ) a.y1 = yaxis.min;
840
+ if( !a.y2 || a.y2 > yaxis.max ) a.y2 = yaxis.max;
841
+
842
+ var tmp;
843
+ if( a.x1 > a.x2 ) {
844
+ tmp = a.x1;
845
+ a.x1 = a.x2;
846
+ a.x2 = tmp;
847
+ }
848
+ if (a.y1 > a.y2) {
849
+ tmp = a.y1;
850
+ a.y1 = a.y2;
851
+ a.y2 = tmp;
852
+ }
853
+
854
+ if( a.x1 >= xaxis.max || a.x2 <= xaxis.min || a.x1 == a.x2 ||
855
+ a.y1 >= yaxis.max || a.y2 <= yaxis.min || a.y1 == a.y2 ) {
856
+ continue;
857
+ }
858
+
859
+ ctx.fillStyle = a.color || options.grid.coloredAreasColor;
860
+ ctx.fillRect( Math.floor( tHoz( a.x1 ) ),
861
+ Math.floor( tVert( a.y2 ) ),
862
+ Math.floor( tHoz( a.x2 ) - tHoz( a.x1 ) ),
863
+ Math.floor( tVert( a.y1 ) - tVert( a.y2 ) ) );
864
+ }
865
+ }
866
+
867
+ // draw the inner grid
868
+ ctx.lineWidth = options.grid.tickWidth;
869
+ ctx.strokeStyle = options.grid.tickColor;
870
+ ctx.beginPath();
871
+ var v;
872
+ if( options.grid.showLines == 'x' || options.grid.showLines == 'both' ) {
873
+ for( i = 0; i < xaxis.ticks.length; ++i ) {
874
+ v = xaxis.ticks[i].v;
875
+ // skip those lying on the axes
876
+ if( v <= xaxis.min || v >= xaxis.max ) continue;
877
+
878
+ ctx.moveTo( Math.floor( tHoz( v ) ) + ctx.lineWidth / 2, 0 );
879
+ ctx.lineTo( Math.floor( tHoz( v ) ) + ctx.lineWidth / 2, plotHeight );
880
+ }
881
+ }
882
+
883
+ if( options.grid.showLines == 'y' || options.grid.showLines == 'both' ) {
884
+ for( i = 0; i < yaxis.ticks.length; ++i ) {
885
+ v = yaxis.ticks[i].v;
886
+ if( v <= yaxis.min || v >= yaxis.max ) continue;
887
+
888
+ ctx.moveTo( 0, Math.floor( tVert( v ) ) + ctx.lineWidth / 2 );
889
+ ctx.lineTo( plotWidth, Math.floor( tVert( v ) ) + ctx.lineWidth / 2 );
890
+ }
891
+ }
892
+
893
+ ctx.stroke();
894
+
895
+ if( options.grid.showBorder && options.grid.borderWidth ) {
896
+ // draw border
897
+ ctx.lineWidth = options.grid.borderWidth;
898
+ ctx.strokeStyle = options.grid.color;
899
+ ctx.lineJoin = 'round';
900
+ ctx.strokeRect( 0, 0, plotWidth, plotHeight );
901
+ ctx.restore();
902
+ }
903
+ }
904
+
905
+ function insertTickLabels() {
906
+ target.find('.tickLabels').remove();
907
+
908
+ var i, tick;
909
+ var html = '<div class="tickLabels" style="font-size:smaller;color:' + options.grid.color + '">';
910
+
911
+ // do the x-axis
912
+ if( options.xaxis.showLabels ) {
913
+ for( i = 0; i < xaxis.ticks.length; ++i ) {
914
+ tick = xaxis.ticks[i];
915
+ if( !tick.label || tick.v < xaxis.min || tick.v > xaxis.max ) continue;
916
+ html += '<div style="position:absolute;top:' +
917
+ ( plotOffset.top + plotHeight + options.grid.labelMargin ) +
918
+ 'px;left:' + ( plotOffset.left + tHoz( tick.v ) - xaxis.labelWidth / 2) +
919
+ 'px;width:' + xaxis.labelWidth + 'px;text-align:center" class="tickLabel">' +
920
+ tick.label + "</div>";
921
+ }
922
+ }
923
+
924
+ // do the y-axis
925
+ if( options.yaxis.showLabels ) {
926
+ for( i = 0; i < yaxis.ticks.length; ++i ) {
927
+ tick = yaxis.ticks[i];
928
+ if( !tick.label || tick.v < yaxis.min || tick.v > yaxis.max ) continue;
929
+ html += '<div style="position:absolute;top:' +
930
+ ( plotOffset.top + tVert( tick.v ) - yaxis.labelHeight / 2 ) +
931
+ 'px;left:0;width:' + yaxis.labelWidth + 'px;text-align:right" class="tickLabel">' +
932
+ tick.label + "</div>";
933
+ }
934
+ }
935
+
936
+ html += '</div>';
937
+
938
+ target.append( html );
939
+ }
940
+
941
+ function insertAxisLabels() {
942
+ if( options.xaxis.label ) {
943
+ yLocation = plotOffset.top + plotHeight + ( xaxis.labelHeight * 1.5 );
944
+ xLocation = plotOffset.left;
945
+ target.find('#xaxislabel').remove();
946
+ target.append( "<div id='xaxislabel' style='color:" +
947
+ options.grid.color + ";width:" + plotWidth +
948
+ "px;text-align:center;position:absolute;top:" +
949
+ yLocation + "px;left:" + xLocation + "px;'>" +
950
+ options.xaxis.label + "</div>" );
951
+ }
952
+ if( options.yaxis.label ) {
953
+ var element;
954
+ if( $.browser.msie ) {
955
+ element = "<span class='yaxis axislabel' style='writing-mode: tb-rl;filter: flipV flipH;'>" +
956
+ options.yaxis.label + "</span>";
957
+ }
958
+ else {
959
+ // we'll use svg instead
960
+ var element = document.createElement( 'object' );
961
+ element.setAttribute( 'type', 'image/svg+xml' );
962
+ xAxisHeight = $('#xaxislabel').height();
963
+ string = '<svg:svg baseProfile="full" height="' + plotHeight +
964
+ '" width="' + xAxisHeight * 1.5 +
965
+ '" xmlns:svg="http://www.w3.org/2000/svg" ' +
966
+ 'xmlns="http://www.w3.org/2000/svg" xmlns:xlink=' +
967
+ '"http://www.w3.org/1999/xlink"><svg:g>';
968
+ string += '<svg:text text-anchor="middle" style="fill:#545454; ' +
969
+ 'stroke:none" x="' + options.grid.labelFontSize + '" y="' +
970
+ plotHeight / 2 + '" ' + 'transform="rotate(-90,' +
971
+ options.grid.labelFontSize + ',' + plotHeight / 2 +
972
+ ')" font-size="' + options.grid.labelFontSize + '">' +
973
+ options.yaxis.label + '</svg:text></svg:g></svg:svg>';
974
+ element.setAttribute( 'data', 'data:image/svg+xml,' + string );
975
+ }
976
+
977
+ xLocation = plotOffset.left - ( yaxis.labelWidth * 1.5 ) -
978
+ options.grid.labelFontSize;
979
+ yLocation = plotOffset.top;
980
+
981
+ var yAxisLabel = $("<div id='yaxislabel' style='color:" +
982
+ options.grid.color + ";height:" + plotHeight +
983
+ "px;text-align:center;position:absolute;top:" +
984
+ yLocation + "px;left:" + xLocation + "px;'</div>");
985
+ yAxisLabel.append( element );
986
+
987
+ target.find('#yaxislabel').remove().end().append( yAxisLabel );
988
+ }
989
+ }
990
+
991
+ function drawSeries( series ) {
992
+ if( series.lines.show || ( !series.bars.show && !series.points.show && !series.deltas.show ) ) {
993
+ drawSeriesLines( series );
994
+ }
995
+ if( series.bars.show ) drawSeriesBars( series );
996
+ if( series.points.show ) drawSeriesPoints( series );
997
+ if( series.deltas.show ) drawSeriesDeltas( series );
998
+ }
999
+
1000
+ function drawSeriesLines( series ) {
1001
+ function plotLine( data, offset ) {
1002
+ var prev = cur = drawx = drawy = null;
1003
+
1004
+ ctx.beginPath();
1005
+ for( var i = 0; i < data.length; ++i ) {
1006
+ prev = cur;
1007
+ cur = data[i];
1008
+
1009
+ if( !prev || !cur ) continue;
1010
+
1011
+ var x1 = prev.x, y1 = prev.y,
1012
+ x2 = cur.x, y2 = cur.y;
1013
+
1014
+ // clip with ymin
1015
+ if( y1 <= y2 && y1 < yaxis.min ) {
1016
+ if( y2 < yaxis.min ) continue; // line segment is outside
1017
+ // compute new intersection point
1018
+ x1 = ( yaxis.min - y1 ) / ( y2 - y1 ) * ( x2 - x1 ) + x1;
1019
+ y1 = yaxis.min;
1020
+ }
1021
+ else if( y2 <= y1 && y2 < yaxis.min ) {
1022
+ if( y1 < yaxis.min ) continue;
1023
+ x2 = ( yaxis.min - y1 ) / ( y2 - y1 ) * ( x2 - x1 ) + x1;
1024
+ y2 = yaxis.min;
1025
+ }
1026
+
1027
+ // clip with ymax
1028
+ if( y1 >= y2 && y1 > yaxis.max ) {
1029
+ if( y2 > yaxis.max ) continue;
1030
+ x1 = ( yaxis.max - y1 ) / ( y2 - y1 ) * ( x2 - x1 ) + x1;
1031
+ y1 = yaxis.max;
1032
+ }
1033
+ else if( y2 >= y1 && y2 > yaxis.max ) {
1034
+ if( y1 > yaxis.max ) continue;
1035
+ x2 = ( yaxis.max - y1 ) / ( y2 - y1 ) * ( x2 - x1 ) + x1;
1036
+ y2 = yaxis.max;
1037
+ }
1038
+
1039
+ // clip with xmin
1040
+ if( x1 <= x2 && x1 < xaxis.min ) {
1041
+ if( x2 < xaxis.min ) continue;
1042
+ y1 = ( xaxis.min - x1 ) / ( x2 - x1 ) * ( y2 - y1 ) + y1;
1043
+ x1 = xaxis.min;
1044
+ }
1045
+ else if( x2 <= x1 && x2 < xaxis.min ) {
1046
+ if( x1 < xaxis.min ) continue;
1047
+ y2 = ( xaxis.min - x1 ) / ( x2 - x1 ) * ( y2 - y1 ) + y1;
1048
+ x2 = xaxis.min;
1049
+ }
1050
+
1051
+ // clip with xmax
1052
+ if( x1 >= x2 && x1 > xaxis.max ) {
1053
+ if( x2 > xaxis.max ) continue;
1054
+ y1 = ( xaxis.max - x1 ) / ( x2 - x1 ) * ( y2 - y1 ) + y1;
1055
+ x1 = xaxis.max;
1056
+ }
1057
+ else if( x2 >= x1 && x2 > xaxis.max ) {
1058
+ if( x1 > xaxis.max ) continue;
1059
+ y2 = ( xaxis.max - x1 ) / ( x2 - x1 ) * ( y2 - y1 ) + y1;
1060
+ x2 = xaxis.max;
1061
+ }
1062
+
1063
+ if( drawx != tHoz( x1 ) || drawy != tVert( y1 ) + offset ) {
1064
+ ctx.moveTo( tHoz( x1 ), tVert( y1 ) + offset );
1065
+ }
1066
+
1067
+ drawx = tHoz( x2 );
1068
+ drawy = tVert( y2 ) + offset;
1069
+ ctx.lineTo( drawx, drawy );
1070
+ }
1071
+ ctx.stroke();
1072
+ }
1073
+
1074
+ function plotLineArea( data ) {
1075
+ var prev = cur = null;
1076
+ var bottom = Math.min( Math.max( 0, yaxis.min ), yaxis.max );
1077
+ var top, lastX = 0;
1078
+ var areaOpen = false;
1079
+
1080
+ for( var i = 0; i < data.length; ++i ) {
1081
+ prev = cur;
1082
+ cur = data[i];
1083
+
1084
+ if( areaOpen && prev && !cur ) {
1085
+ // close area
1086
+ ctx.lineTo( tHoz( lastX ), tVert( bottom ) );
1087
+ ctx.fill();
1088
+ areaOpen = false;
1089
+ continue;
1090
+ }
1091
+
1092
+ if( !prev || !cur ) continue;
1093
+
1094
+ var x1 = prev.x, y1 = prev.y,
1095
+ x2 = cur.x, y2 = cur.y;
1096
+
1097
+ // clip with xmin
1098
+ if( x1 <= x2 && x1 < xaxis.min ) {
1099
+ if( x2 < xaxis.min ) continue;
1100
+ y1 = ( xaxis.min - x1 ) / ( x2 - x1 ) * ( y2 - y1 ) + y1;
1101
+ x1 = xaxis.min;
1102
+ }
1103
+ else if( x2 <= x1 && x2 < xaxis.min ) {
1104
+ if( x1 < xaxis.min ) continue;
1105
+ y2 = ( xaxis.min - x1 ) / (x2 - x1) * (y2 - y1) + y1;
1106
+ x2 = xaxis.min;
1107
+ }
1108
+
1109
+ // clip with xmax
1110
+ if( x1 >= x2 && x1 > xaxis.max ) {
1111
+ if( x2 > xaxis.max ) continue;
1112
+ y1 = ( xaxis.max - x1 ) / ( x2 - x1 ) * ( y2 - y1 ) + y1;
1113
+ x1 = xaxis.max;
1114
+ }
1115
+ else if( x2 >= x1 && x2 > xaxis.max ) {
1116
+ if( x1 > xaxis.max ) continue;
1117
+ y2 = ( xaxis.max - x1 ) / ( x2 - x1 ) * ( y2 - y1 ) + y1;
1118
+ x2 = xaxis.max;
1119
+ }
1120
+
1121
+ if( !areaOpen ) {
1122
+ // open area
1123
+ ctx.beginPath();
1124
+ ctx.moveTo( tHoz( x1 ), tVert( bottom ) );
1125
+ areaOpen = true;
1126
+ }
1127
+
1128
+ // now first check the case where both is outside
1129
+ if( y1 >= yaxis.max && y2 >= yaxis.max ) {
1130
+ ctx.lineTo( tHoz( x1 ), tVert( yaxis.max ) );
1131
+ ctx.lineTo( tHoz( x2 ), tVert( yaxis.max ) );
1132
+ continue;
1133
+ }
1134
+ else if( y1 <= yaxis.min && y2 <= yaxis.min ) {
1135
+ ctx.lineTo( tHoz( x1 ), tVert( yaxis.min ) );
1136
+ ctx.lineTo( tHoz( x2 ), tVert( yaxis.min ) );
1137
+ continue;
1138
+ }
1139
+
1140
+ // else it's a bit more complicated, there might
1141
+ // be two rectangles and two triangles we need to fill
1142
+ // in; to find these keep track of the current x values
1143
+ var x1old = x1,
1144
+ x2old = x2;
1145
+
1146
+ // and clip the y values, without shortcutting
1147
+
1148
+ // clip with ymin
1149
+ if( y1 <= y2 && y1 < yaxis.min && y2 >= yaxis.min ) {
1150
+ x1 = ( yaxis.min - y1 ) / ( y2 - y1 ) * ( x2 - x1 ) + x1;
1151
+ y1 = yaxis.min;
1152
+ }
1153
+ else if( y2 <= y1 && y2 < yaxis.min && y1 >= yaxis.min ) {
1154
+ x2 = ( yaxis.min - y1 ) / ( y2 - y1 ) * ( x2 - x1 ) + x1;
1155
+ y2 = yaxis.min;
1156
+ }
1157
+
1158
+ // clip with ymax
1159
+ if( y1 >= y2 && y1 > yaxis.max && y2 <= yaxis.max ) {
1160
+ x1 = ( yaxis.max - y1 ) / ( y2 - y1 ) * ( x2 - x1 ) + x1;
1161
+ y1 = yaxis.max;
1162
+ }
1163
+ else if( y2 >= y1 && y2 > yaxis.max && y1 <= yaxis.max ) {
1164
+ x2 = ( yaxis.max - y1 ) / ( y2 - y1 ) * ( x2 - x1 ) + x1;
1165
+ y2 = yaxis.max;
1166
+ }
1167
+
1168
+ // if the x value was changed we got a rectangle to fill
1169
+ if( x1 != x1old ) {
1170
+ top = y1 <= yaxis.min ? yaxis.min : yaxis.max;
1171
+ ctx.lineTo( tHoz( x1old ), tVert( top ) );
1172
+ ctx.lineTo( tHoz( x1 ), tVert( top ) );
1173
+ }
1174
+
1175
+ // fill the triangles
1176
+ ctx.lineTo( tHoz( x1 ), tVert( y1 ) );
1177
+ ctx.lineTo( tHoz( x2 ), tVert( y2 ) );
1178
+
1179
+ // fill the other rectangle if it's there
1180
+ if( x2 != x2old ) {
1181
+ top = y2 <= yaxis.min ? yaxis.min : yaxis.max;
1182
+ ctx.lineTo( tHoz( x2old ), tVert( top ) );
1183
+ ctx.lineTo( tHoz( x2 ), tVert( top ) );
1184
+ }
1185
+
1186
+ lastX = Math.max( x2, x2old );
1187
+ }
1188
+
1189
+ if( areaOpen ) {
1190
+ ctx.lineTo( tHoz( lastX ), tVert( bottom ) );
1191
+ ctx.fill();
1192
+ }
1193
+ }
1194
+
1195
+ ctx.save();
1196
+ ctx.translate( plotOffset.left, plotOffset.top );
1197
+ ctx.lineJoin = 'round';
1198
+
1199
+ var lw = series.lines.lineWidth;
1200
+ var sw = series.shadowSize;
1201
+
1202
+ // FIXME: consider another form of shadow when filling is turned on
1203
+ if( sw > 0 ) {
1204
+ // draw shadow in two steps
1205
+ ctx.lineWidth = sw / 2;
1206
+ ctx.strokeStyle = 'rgba(0,0,0,0.1)';
1207
+ plotLine( series.data, lw / 2 + sw / 2 + ctx.lineWidth / 2 );
1208
+
1209
+ ctx.lineWidth = sw / 2;
1210
+ ctx.strokeStyle = 'rgba(0,0,0,0.2)';
1211
+ plotLine( series.data, lw / 2 + ctx.lineWidth / 2 );
1212
+ }
1213
+
1214
+ ctx.lineWidth = lw;
1215
+ ctx.strokeStyle = series.color;
1216
+ setFillStyle( series.lines, series.color );
1217
+ if( series.lines.fill ) plotLineArea( series.data, 0 );
1218
+ plotLine( series.data, 0 );
1219
+ ctx.restore();
1220
+ }
1221
+
1222
+ function drawSeriesPoints( series ) {
1223
+ function plotPoints( data, radius, fill ) {
1224
+ for( var i = 0; i < data.length; ++i ) {
1225
+ if( !data[i] ) continue;
1226
+
1227
+ var x = data[i].x,
1228
+ y = data[i].y;
1229
+
1230
+ if( x < xaxis.min || x > xaxis.max ||
1231
+ y < yaxis.min || y > yaxis.max ) { continue; }
1232
+
1233
+ ctx.beginPath();
1234
+ ctx.arc( tHoz(x), tVert( y ), radius, 0, 2 * Math.PI, true );
1235
+ if( fill ) ctx.fill();
1236
+ ctx.stroke();
1237
+ }
1238
+ }
1239
+
1240
+ function plotPointShadows( data, offset, radius ) {
1241
+ for( var i = 0; i < data.length; ++i ) {
1242
+ if( !data[i] ) continue;
1243
+
1244
+ var x = data[i].x,
1245
+ y = data[i].y;
1246
+ if( x < xaxis.min || x > xaxis.max ||
1247
+ y < yaxis.min || y > yaxis.max ) { continue; }
1248
+
1249
+ ctx.beginPath();
1250
+ ctx.arc( tHoz( x ), tVert( y ) + offset, radius, 0, Math.PI, false );
1251
+ ctx.stroke();
1252
+ }
1253
+ }
1254
+
1255
+ ctx.save();
1256
+ ctx.translate( plotOffset.left, plotOffset.top );
1257
+
1258
+ var lw = series.lines.lineWidth;
1259
+ var sw = series.shadowSize;
1260
+ if( sw > 0 ) {
1261
+ // draw shadow in two steps
1262
+ ctx.lineWidth = sw / 2;
1263
+ ctx.strokeStyle = 'rgba(0,0,0,0.1)';
1264
+ plotPointShadows( series.data, sw / 2 + ctx.lineWidth / 2, series.points.radius );
1265
+
1266
+ ctx.lineWidth = sw / 2;
1267
+ ctx.strokeStyle = 'rgba(0,0,0,0.2)';
1268
+ plotPointShadows( series.data, ctx.lineWidth / 2, series.points.radius );
1269
+ }
1270
+
1271
+ ctx.lineWidth = series.points.lineWidth;
1272
+ ctx.strokeStyle = series.color;
1273
+ setFillStyle( series.points, series.color );
1274
+ plotPoints( series.data, series.points.radius, series.points.fill );
1275
+ ctx.restore();
1276
+ }
1277
+
1278
+ function drawSeriesDeltas( series ) {
1279
+ function plotPoints( data, radius, fill ) {
1280
+ for( var i = 0; i < data.length; ++i ) {
1281
+ if( !data[i] ) continue;
1282
+
1283
+ var x = data[i].x,
1284
+ y = data[i].y,
1285
+ d = data[i].delta;
1286
+
1287
+ if( x < xaxis.min || x > xaxis.max ||
1288
+ y < yaxis.min || y > yaxis.max ||
1289
+ d < yaxis.min || d > yaxis.max ) { continue; }
1290
+
1291
+ ctx.beginPath();
1292
+ ctx.arc( tHoz( x ), tVert( y ), radius, 0, 2 * Math.PI, true );
1293
+ if( fill ) ctx.fill();
1294
+ ctx.stroke();
1295
+ }
1296
+ }
1297
+
1298
+ function plotDeltas( data, settings ) {
1299
+ for( var i = 0; i < data.length; ++i ) {
1300
+ if( !data[i] ) { continue; }
1301
+
1302
+ var x = data[i].x,
1303
+ y = data[i].y,
1304
+ d = data[i].delta;
1305
+
1306
+ if( x < xaxis.min || x > xaxis.max ||
1307
+ y < yaxis.min || y > yaxis.max ||
1308
+ d < yaxis.min || d > yaxis.max ) { continue; }
1309
+
1310
+ if( y < d ) ctx.strokeStyle = settings.color.below;
1311
+ else if( y > d ) ctx.strokeStyle = settings.color.above;
1312
+ else ctx.strokeStyle = settings.color.equal;
1313
+
1314
+ ctx.beginPath();
1315
+ ctx.moveTo( tHoz( x ), tVert( y ) );
1316
+ ctx.lineTo( tHoz( x ), tVert( d ) );
1317
+ ctx.stroke();
1318
+
1319
+ // draw the markers for the deltas (horizontal line)
1320
+ var markerLeft = tHoz( x ) - ( ctx.lineWidth * settings.markerWidth );
1321
+ var markerRight = tHoz( x ) + ( ctx.lineWidth * settings.markerWidth );
1322
+
1323
+ ctx.beginPath();
1324
+ ctx.moveTo( markerLeft, tVert( d ) );
1325
+ ctx.lineTo( markerRight, tVert( d ) );
1326
+ ctx.stroke();
1327
+ }
1328
+ }
1329
+
1330
+ function plotPointShadows( data, offset, radius ) {
1331
+ for( var i = 0; i < data.length; ++i ) {
1332
+ if( !data[i] ) continue;
1333
+
1334
+ var x = data[i].x,
1335
+ y = data[i].y,
1336
+ d = data[i].delta;
1337
+
1338
+ if( x < xaxis.min || x > xaxis.max ||
1339
+ y < yaxis.min || y > yaxis.max ||
1340
+ d < yaxis.min || d > yaxis.max ) { continue; }
1341
+
1342
+ ctx.beginPath();
1343
+ ctx.arc( tHoz( x ), tVert( y ) + offset, radius, 0, Math.PI, false );
1344
+ ctx.stroke();
1345
+ }
1346
+ }
1347
+
1348
+ ctx.save();
1349
+ ctx.translate( plotOffset.left, plotOffset.top );
1350
+
1351
+ var lw = series.lines.lineWidth;
1352
+ var sw = series.shadowSize;
1353
+ if( sw > 0 ) {
1354
+ // draw shadow in two steps
1355
+ ctx.lineWidth = sw / 2;
1356
+ ctx.strokeStyle = 'rgba(0,0,0,0.1)';
1357
+ plotPointShadows( series.data, sw / 2 + ctx.lineWidth / 2, series.points.radius );
1358
+
1359
+ ctx.lineWidth = sw / 2;
1360
+ ctx.strokeStyle = 'rgba(0,0,0,0.2)';
1361
+ plotPointShadows( series.data, ctx.lineWidth / 2, series.points.radius );
1362
+ }
1363
+
1364
+ ctx.lineWidth = series.points.lineWidth;
1365
+
1366
+ // draw the delta lines and markers
1367
+ plotDeltas( series.data, series.deltas );
1368
+
1369
+ // draw the actual datapoints
1370
+ ctx.strokeStyle = series.color;
1371
+ setFillStyle( series.points, series.color );
1372
+ plotPoints( series.data, series.points.radius, series.points.fill );
1373
+
1374
+ ctx.restore();
1375
+ }
1376
+
1377
+ function drawSeriesBars( series ) {
1378
+ function plotBars( data, barWidth, offset, fill ) {
1379
+ for( var i = 0; i < data.length; i++ ) {
1380
+ if( !data[i] ) continue;
1381
+
1382
+ var x = data[i].x,
1383
+ y = data[i].y,
1384
+ drawLeft = true,
1385
+ drawTop = true,
1386
+ drawRight = true;
1387
+
1388
+ // determine the co-ordinates of the bar, account for negative bars having
1389
+ // flipped top/bottom and draw/don't draw accordingly
1390
+ var halfBar = barWidth / 2;
1391
+ var left = x - halfBar,
1392
+ right = x + halfBar,
1393
+ bottom = ( y < 0 ? y : 0 ),
1394
+ top = ( y < 0 ? 0 : y );
1395
+
1396
+ if( right < xaxis.min || left > xaxis.max ||
1397
+ top < yaxis.min || bottom > yaxis.max ) { continue; }
1398
+
1399
+ // clip
1400
+ if( left < xaxis.min ) {
1401
+ left = xaxis.min;
1402
+ drawLeft = false;
1403
+ }
1404
+
1405
+ if ( right > xaxis.max ) {
1406
+ right = xaxis.max;
1407
+ drawRight = false;
1408
+ }
1409
+
1410
+ if( bottom < yaxis.min ) {
1411
+ bottom = yaxis.min;
1412
+ }
1413
+
1414
+ if (top > yaxis.max) {
1415
+ top = yaxis.max;
1416
+ drawTop = false;
1417
+ }
1418
+
1419
+ // fill the bar
1420
+ if( fill ) {
1421
+ ctx.beginPath();
1422
+ ctx.moveTo( tHoz( left ), tVert( bottom) + offset );
1423
+ ctx.lineTo( tHoz( left ), tVert( top) + offset );
1424
+ ctx.lineTo( tHoz( right ), tVert( top) + offset );
1425
+ ctx.lineTo( tHoz( right ), tVert( bottom) + offset );
1426
+ ctx.fill();
1427
+ }
1428
+
1429
+ // draw outline
1430
+ if( drawLeft || drawRight || drawTop ) {
1431
+ ctx.beginPath();
1432
+ ctx.moveTo( tHoz( left ), tVert( bottom ) + offset );
1433
+ if( drawLeft ) ctx.lineTo( tHoz( left ), tVert( top) + offset );
1434
+ else ctx.moveTo( tHoz( left ), tVert( top) + offset );
1435
+ if( drawTop ) ctx.lineTo( tHoz( right ), tVert( top ) + offset );
1436
+ else ctx.moveTo( tHoz( right ), tVert( top ) + offset );
1437
+ if( drawRight ) ctx.lineTo( tHoz( right ), tVert( bottom ) + offset );
1438
+ else ctx.moveTo( tHoz( right ), tVert( bottom ) + offset );
1439
+ ctx.stroke();
1440
+ }
1441
+ }
1442
+ }
1443
+
1444
+ ctx.save();
1445
+ ctx.translate( plotOffset.left, plotOffset.top );
1446
+ ctx.lineJoin = 'round';
1447
+
1448
+ var bw = series.bars.barWidth;
1449
+ var lw = Math.min( series.bars.lineWidth, bw );
1450
+
1451
+ ctx.lineWidth = lw;
1452
+ ctx.strokeStyle = series.color;
1453
+ setFillStyle( series.bars, series.color );
1454
+ plotBars( series.data, bw, 0, series.bars.fill );
1455
+ ctx.restore();
1456
+ }
1457
+
1458
+ function setFillStyle( obj, seriesColor ) {
1459
+ var opacity = obj.fillOpacity;
1460
+ if( obj.fill ) {
1461
+ if( obj.fillColor ) {
1462
+ ctx.fillStyle = obj.fillColor;
1463
+ }
1464
+ else {
1465
+ var c = parseColor( seriesColor );
1466
+ c.a = typeof fill == 'number' ? obj.fill : ( opacity ? opacity : 0.4 );
1467
+ c.normalize();
1468
+ ctx.fillStyle = c.toString();
1469
+ }
1470
+ }
1471
+ }
1472
+
1473
+ function drawMarkers() {
1474
+ if( !options.grid.markers.length ) return;
1475
+
1476
+ for( var i = 0; i < options.grid.markers.length; i++ ) {
1477
+ marker = options.grid.markers[i];
1478
+ if( marker.value < yaxis.max && marker.value > yaxis.min ) {
1479
+ ctx.lineWidth = marker.width;
1480
+ ctx.strokeStyle = marker.color;
1481
+ ctx.beginPath();
1482
+
1483
+ if( marker.axis == 'x' ) {
1484
+ ctx.moveTo( tHoz( xaxis.min ) + plotOffset.left,
1485
+ tVert( marker.value ) + plotOffset.top );
1486
+ ctx.lineTo( tHoz( xaxis.max ) + plotOffset.left,
1487
+ tVert( marker.value ) + plotOffset.top );
1488
+ }
1489
+ else if( marker.axis == 'y' ) {
1490
+ ctx.moveTo( tHoz( marker.value ) + plotOffset.left,
1491
+ tVert( yaxis.min ) + plotOffset.top );
1492
+ ctx.lineTo( tHoz( marker.value ) + plotOffset.left,
1493
+ tVert( yaxis.max ) + plotOffset.top );
1494
+ }
1495
+
1496
+ ctx.stroke();
1497
+ }
1498
+ }
1499
+ }
1500
+
1501
+ function insertLegend() {
1502
+ // remove legends from the appropriate container
1503
+ if( options.legend.container ) {
1504
+ options.legend.container.find('table.legend_table').remove();
1505
+ }
1506
+ else {
1507
+ target.find('.legend').remove();
1508
+ }
1509
+
1510
+ if( !options.legend.show ) { return; }
1511
+
1512
+ var fragments = [];
1513
+ var rowStarted = false;
1514
+ for( i = 0; i < series.length; ++i ) {
1515
+ if( !series[i].label ) continue;
1516
+
1517
+ if( i % options.legend.noColumns == 0 ) {
1518
+ if( rowStarted ) fragments.push( '</tr>' );
1519
+ fragments.push( '<tr>' );
1520
+ rowStarted = true;
1521
+ }
1522
+
1523
+ var label = series[i].label;
1524
+ if( options.legend.labelFormatter ) label = options.legend.labelFormatter( label );
1525
+
1526
+ fragments.push(
1527
+ '<td class="legendColorBox"><div style="border:1px solid ' +
1528
+ options.legend.labelBoxBorderColor +
1529
+ ';padding:1px"><div style="width:14px;height:10px;background-color:' +
1530
+ series[i].color + ';overflow:hidden"></div></div></td>' +
1531
+ '<td class="legendLabel">' + label + '</td>'
1532
+ );
1533
+ }
1534
+
1535
+ if( rowStarted ) fragments.push( '</tr>' );
1536
+
1537
+ if( fragments.length > 0 ) {
1538
+ var table = '<table class="legend_table" style="font-size:smaller;color:' +
1539
+ options.grid.color + '">' + fragments.join('') + '</table>';
1540
+
1541
+ if( options.legend.container ) {
1542
+ options.legend.container.append( table );
1543
+ }
1544
+ else {
1545
+ var pos = '';
1546
+ var p = options.legend.position,
1547
+ m = options.legend.margin;
1548
+
1549
+ if( p.charAt( 0 ) == 'n' ) {
1550
+ pos += 'top:' + ( m + plotOffset.top ) + 'px;';
1551
+ }
1552
+ else if( p.charAt( 0 ) == 's' ) {
1553
+ pos += 'bottom:' + ( m + plotOffset.bottom + BOTTOM_SIDE_BUFFER ) + 'px;';
1554
+ }
1555
+ if( p.charAt( 1 ) == 'e' ) {
1556
+ pos += 'right:' + ( m + plotOffset.right + RIGHT_SIDE_BUFFER ) + 'px;';
1557
+ }
1558
+ else if( p.charAt( 1 ) == 'w' ) {
1559
+ pos += 'left:' + ( m + plotOffset.left ) + 'px;';
1560
+ }
1561
+
1562
+ var legend = $('<div class="legend">' +
1563
+ table.replace(
1564
+ 'style="', 'style="position:absolute;' + pos +';'
1565
+ ) + '</div>').appendTo( target );
1566
+
1567
+ if( options.legend.backgroundOpacity != 0.0 ) {
1568
+ // put in the transparent background
1569
+ // separately to avoid blended labels and
1570
+ // label boxes
1571
+ var c = options.legend.backgroundColor;
1572
+ if( !c ) {
1573
+ tmp = options.grid.backgroundColor ?
1574
+ options.grid.backgroundColor :
1575
+ extractColor( legend );
1576
+ c = parseColor( tmp ).adjust( null, null, null, 1 ).toString();
1577
+ }
1578
+ var div = legend.children();
1579
+ $('<div style="position:absolute;width:' + div.width() +
1580
+ 'px;height:' + div.height() + 'px;' + pos +'background-color:' +
1581
+ c + ';"> </div>')
1582
+ .prependTo( legend )
1583
+ .css( 'opacity', options.legend.backgroundOpacity );
1584
+ }
1585
+ }
1586
+ }
1587
+ }
1588
+
1589
+ var lastMousePos = { pageX: null, pageY: null },
1590
+ selection = { first: { x: -1, y: -1 }, second: { x: -1, y: -1 } },
1591
+ prevSelection = null,
1592
+ selectionInterval = null,
1593
+ ignoreClick = false;
1594
+
1595
+ // Returns the data item the mouse is over, or null if none is found
1596
+ function findSelectedItem( mouseX, mouseY ) {
1597
+ // How close do we need to be to an item in order to select it?
1598
+ // The clickCatchingArea parameter is the radius of the circle, in pixels.
1599
+ var lowestDistance = Math.pow( options.grid.mouseCatchingArea, 2 );
1600
+ selectedItem = null;
1601
+
1602
+ for( var i = 0; i < series.length; ++i ) {
1603
+ var data = series[i].data;
1604
+
1605
+ if( options.sortData && data.length > 1 ) {
1606
+ var half = tHoz( data[( data.length / 2 ).toFixed(0)].x ).toFixed( 0 );
1607
+ if( mouseX < half ) {
1608
+ start = 0;
1609
+ end = ( data.length / 2 ).toFixed( 0 ) + 5;
1610
+ }
1611
+ else {
1612
+ start = ( data.length / 2 ).toFixed( 0 ) - 5;
1613
+ end = data.length;
1614
+ }
1615
+ }
1616
+ else {
1617
+ // either we haven't sorted the data (and so we can't split it for
1618
+ // searching) or there's only 1 data point, so it doesn't matter
1619
+ start = 0;
1620
+ end = data.length;
1621
+ }
1622
+
1623
+ for( var j = start; j < end; ++j ) {
1624
+ if( !data[j] ) continue;
1625
+
1626
+ // We have to calculate distances in pixels, not in data units, because
1627
+ // the scale of the axes may be different
1628
+ var x = data[j].x,
1629
+ y = data[j].y;
1630
+
1631
+ xDistance = Math.abs( tHoz( x ) - mouseX );
1632
+ yDistance = Math.abs(tVert(y)-mouseY);
1633
+ if( xDistance > options.grid.mouseCatchingArea ) continue;
1634
+ if( yDistance > options.grid.mouseCatchingArea ) continue;
1635
+
1636
+ sqrDistance = Math.pow( xDistance, 2 ) + Math.pow( yDistance, 2 );
1637
+ if( sqrDistance < lowestDistance ) {
1638
+ selectedItem = data[j];
1639
+ selectedItem._data = series[i];
1640
+ lowestDistance = sqrDistance;
1641
+ }
1642
+ }
1643
+ }
1644
+
1645
+ return selectedItem;
1646
+ }
1647
+
1648
+ function onMouseMove( ev ) {
1649
+ // FIXME: temp. work-around until jQuery bug 1871 is fixed
1650
+ var e = ev || window.event;
1651
+ if( !e.pageX && e.clientX ) {
1652
+ var de = document.documentElement,
1653
+ b = document.body;
1654
+ lastMousePos.pageX = e.clientX + ( de && de.scrollLeft || b.scrollLeft || 0 );
1655
+ lastMousePos.pageY = e.clientY + ( de && de.scrollTop || b.scrollTop || 0 );
1656
+ }
1657
+ else {
1658
+ lastMousePos.pageX = e.pageX;
1659
+ lastMousePos.pageY = e.pageY;
1660
+ }
1661
+
1662
+ if( options.grid.hoverable ) {
1663
+ var offset = eventHolder.offset();
1664
+ result = { raw: {
1665
+ x: lastMousePos.pageX - offset.left - plotOffset.left,
1666
+ y: lastMousePos.pageY - offset.top - plotOffset.top
1667
+ } };
1668
+ result.selected = findSelectedItem( result.raw.x, result.raw.y );
1669
+
1670
+ // display the tooltip/hint if requested
1671
+ if( !$.browser.msie && result.selected && result.selected._data.hints.show ) {
1672
+ showHintDiv( result.selected );
1673
+ }
1674
+
1675
+ if( !result.selected ) cleanup();
1676
+ target.trigger( 'plotmousemove', [ result ] );
1677
+ }
1678
+ }
1679
+
1680
+ function onMouseDown( e ) {
1681
+ if( e.which != 1 ) return; // left click
1682
+
1683
+ // cancel out any text selections
1684
+ document.body.focus();
1685
+
1686
+ // prevent text selection and drag in old-school browsers
1687
+ if( document.onselectstart !== undefined && !workarounds.onselectstart ) {
1688
+ workarounds.onselectstart = document.onselectstart;
1689
+ document.onselectstart = function() { return false; };
1690
+ }
1691
+ if( document.ondrag !== undefined && !workarounds.ondrag ) {
1692
+ workarounds.ondrag = document.ondrag;
1693
+ document.ondrag = function() { return false; };
1694
+ }
1695
+
1696
+ setSelectionPos( selection.first, e );
1697
+ clearInterval( selectionInterval );
1698
+ lastMousePos.pageX = null;
1699
+ selectionInterval = setInterval( updateSelectionOnMouseMove, 200 );
1700
+ $(document).one( 'mouseup', onSelectionMouseUp );
1701
+ }
1702
+
1703
+ function onClick( e ) {
1704
+ if( ignoreClick ) {
1705
+ ignoreClick = false;
1706
+ return;
1707
+ }
1708
+
1709
+ var offset = eventHolder.offset();
1710
+ var canvasX = e.pageX - offset.left - plotOffset.left;
1711
+ var canvasY = e.pageY - offset.top - plotOffset.top;
1712
+
1713
+ var result = { raw: {
1714
+ x: xaxis.min + canvasX / hozScale,
1715
+ y: yaxis.max - canvasY / vertScale
1716
+ } };
1717
+ result.selected = findSelectedItem( canvasX, canvasY );
1718
+
1719
+ target.trigger( 'plotclick', [ result ] );
1720
+ }
1721
+
1722
+ function triggerSelectedEvent() {
1723
+ var x1, x2, y1, y2;
1724
+ if( selection.first.x <= selection.second.x ) {
1725
+ x1 = selection.first.x;
1726
+ x2 = selection.second.x;
1727
+ }
1728
+ else {
1729
+ x1 = selection.second.x;
1730
+ x2 = selection.first.x;
1731
+ }
1732
+
1733
+ if( selection.first.y >= selection.second.y ) {
1734
+ y1 = selection.first.y;
1735
+ y2 = selection.second.y;
1736
+ }
1737
+ else {
1738
+ y1 = selection.second.y;
1739
+ y2 = selection.first.y;
1740
+ }
1741
+
1742
+ x1 = xaxis.min + x1 / hozScale;
1743
+ x2 = xaxis.min + x2 / hozScale;
1744
+
1745
+ y1 = yaxis.max - y1 / vertScale;
1746
+ y2 = yaxis.max - y2 / vertScale;
1747
+
1748
+ target.trigger( 'plotselected', [ { x1: x1, y1: y1, x2: x2, y2: y2 } ] );
1749
+ }
1750
+
1751
+ function onSelectionMouseUp( e ) {
1752
+ if( document.onselectstart !== undefined ) document.onselectstart = workarounds.onselectstart;
1753
+ if( document.ondrag !== undefined ) document.ondrag = workarounds.ondrag;
1754
+
1755
+ if( selectionInterval ) {
1756
+ clearInterval( selectionInterval );
1757
+ selectionInterval = null;
1758
+ }
1759
+
1760
+ setSelectionPos( selection.second, e );
1761
+ clearSelection();
1762
+ if( !selectionIsSane() || e.which != 1 ) return false;
1763
+
1764
+ drawSelection();
1765
+ triggerSelectedEvent();
1766
+ ignoreClick = true;
1767
+
1768
+ return false;
1769
+ }
1770
+
1771
+ function setSelectionPos( pos, e ) {
1772
+ var offset = $(overlay).offset();
1773
+ if( options.selection.mode == 'y' ) {
1774
+ pos.x = ( pos == selection.first ) ? 0 : plotWidth;
1775
+ }
1776
+ else {
1777
+ pos.x = e.pageX - offset.left - plotOffset.left;
1778
+ pos.x = Math.min( Math.max( 0, pos.x ), plotWidth );
1779
+
1780
+ if( options.selection.snapToTicks ) {
1781
+ // find our current location in terms of the xaxis
1782
+ var x = xaxis.min + pos.x / hozScale;
1783
+
1784
+ // determine if we're moving left or right on the xaxis
1785
+ if( selection.first.x - selection.second.x < 0 ||
1786
+ selection.first.x == -1 ) {
1787
+ // to the right
1788
+ idx = pos == selection.first ? -1 : 0
1789
+ for( var i = 0; i < xaxis.ticks.length; i++ ) {
1790
+ if( x <= xaxis.ticks[i].v ) {
1791
+ pos.x = Math.floor( ( xaxis.ticks[i+idx].v - xaxis.min ) *
1792
+ hozScale);
1793
+ break;
1794
+ }
1795
+ }
1796
+ }
1797
+ else {
1798
+ // to the left
1799
+ idx = pos == selection.first ? 1 : 0
1800
+ for( var i = xaxis.ticks.length - 1; i >= 0; i-- ) {
1801
+ if( x >= xaxis.ticks[i].v ) {
1802
+ pos.x = Math.floor( ( xaxis.ticks[i+idx].v - xaxis.min ) *
1803
+ hozScale);
1804
+ break;
1805
+ }
1806
+ }
1807
+ }
1808
+ }
1809
+ }
1810
+
1811
+ if( options.selection.mode == 'x' ) {
1812
+ pos.y = ( pos == selection.first ) ? 0 : plotHeight;
1813
+ }
1814
+ else {
1815
+ pos.y = e.pageY - offset.top - plotOffset.top;
1816
+ pos.y = Math.min( Math.max( 0, pos.y ), plotHeight );
1817
+ }
1818
+ }
1819
+
1820
+ function updateSelectionOnMouseMove() {
1821
+ if( !lastMousePos.pageX ) { return; }
1822
+ setSelectionPos( selection.second, lastMousePos );
1823
+ clearSelection();
1824
+ if( selectionIsSane() ) { drawSelection(); }
1825
+ }
1826
+
1827
+ function clearSelection() {
1828
+ if( !prevSelection ) { return; }
1829
+
1830
+ var x = Math.min( prevSelection.first.x, prevSelection.second.x ),
1831
+ y = Math.min( prevSelection.first.y, prevSelection.second.y ),
1832
+ w = Math.abs( prevSelection.second.x - prevSelection.first.x ),
1833
+ h = Math.abs( prevSelection.second.y - prevSelection.first.y );
1834
+
1835
+ octx.clearRect( x + plotOffset.left - octx.lineWidth,
1836
+ y + plotOffset.top - octx.lineWidth,
1837
+ w + octx.lineWidth * 2,
1838
+ h + octx.lineWidth * 2 );
1839
+
1840
+ prevSelection = null;
1841
+ }
1842
+
1843
+ function setSelection( area ) {
1844
+ clearSelection();
1845
+
1846
+ if( options.selection.mode == 'x' ) {
1847
+ selection.first.y = 0;
1848
+ selection.second.y = plotHeight;
1849
+ }
1850
+ else {
1851
+ selection.first.y = ( yaxis.max - area.y1 ) * vertScale;
1852
+ selection.second.y = ( yaxis.max - area.y2 ) * vertScale;
1853
+ }
1854
+
1855
+ if( options.selection.mode == 'y' ) {
1856
+ selection.first.x = 0;
1857
+ selection.second.x = plotWidth;
1858
+ }
1859
+ else {
1860
+ selection.first.x = ( area.x1 - xaxis.min ) * hozScale;
1861
+ selection.second.x = ( area.x2 - xaxis.min ) * hozScale;
1862
+ }
1863
+
1864
+ drawSelection();
1865
+ triggerSelectedEvent();
1866
+ }
1867
+
1868
+ function highlight( marker ) {
1869
+ // prevent unnecessary work
1870
+ if( marker == lastMarker ) { return; }
1871
+ else { lastMarker = marker; }
1872
+
1873
+ // draw a marker on the graph over the point that the mouse is hovering over
1874
+ if( marker ) {
1875
+ var color = options.grid.hoverColor ? options.grid.hoverColor : marker._data.color;
1876
+ var fill = options.grid.hoverFill ? options.grid.hoverFill : 'white';
1877
+ var radius = options.grid.hoverRadius ? options.grid.hoverRadius : marker._data.points.radius;
1878
+
1879
+ var temp_series = {
1880
+ shadowSize: options.shadowSize,
1881
+ lines: { show: false },
1882
+ points: $.extend( true, options.points,
1883
+ { fillColor: fill,
1884
+ radius: radius } ),
1885
+ color: color,
1886
+ data: [ { x: marker.x, y: marker.y } ]
1887
+ };
1888
+ draw();
1889
+ drawSeriesPoints( temp_series );
1890
+ }
1891
+ else {
1892
+ draw();
1893
+ }
1894
+ }
1895
+
1896
+ function drawSelection() {
1897
+ if( prevSelection &&
1898
+ selection.first.x == prevSelection.first.x &&
1899
+ selection.first.y == prevSelection.first.y &&
1900
+ selection.second.x == prevSelection.second.x &&
1901
+ selection.second.y == prevSelection.second.y ) { return; }
1902
+
1903
+ octx.strokeStyle = parseColor( options.selection.color ).scale( null, null, null, 0.8 ).toString();
1904
+ octx.lineWidth = 1;
1905
+ ctx.lineJoin = 'round';
1906
+ octx.fillStyle = parseColor( options.selection.color ).scale( null, null, null, 0.4 ).toString();
1907
+
1908
+ prevSelection = { first: { x: selection.first.x,
1909
+ y: selection.first.y },
1910
+ second: { x: selection.second.x,
1911
+ y: selection.second.y } };
1912
+
1913
+ var x = Math.min( selection.first.x, selection.second.x ),
1914
+ y = Math.min( selection.first.y, selection.second.y ),
1915
+ w = Math.abs( selection.second.x - selection.first.x ),
1916
+ h = Math.abs( selection.second.y - selection.first.y );
1917
+
1918
+ octx.fillRect( x + plotOffset.left, y + plotOffset.top, w, h );
1919
+ octx.strokeRect( x + plotOffset.left, y + plotOffset.top, w, h );
1920
+ }
1921
+
1922
+ function selectionIsSane() {
1923
+ var minSize = 5;
1924
+ return Math.abs( selection.second.x - selection.first.x ) >= minSize &&
1925
+ Math.abs( selection.second.y - selection.first.y ) >= minSize;
1926
+ }
1927
+
1928
+ function showHintDiv(selected) {
1929
+ var offset = $(overlay).offset();
1930
+ if( $('.hint-wrapper').length > 0 &&
1931
+ $('.hint-wrapper:first').attr( 'name' ) == selected.x + ":" + selected.y ) {
1932
+ var hintDiv = $('div.plot-hint');
1933
+ var hintBackground = $('div.hint-background');
1934
+ }
1935
+ else {
1936
+ cleanup();
1937
+ var fragments = [];
1938
+ var hintWrapper = $('<div class="hint-wrapper" name="' +
1939
+ selected.x + ':' + selected.y + '"></div>');
1940
+ hintWrapper.appendTo( target );
1941
+
1942
+ fragments.push( '<tbody><tr>' );
1943
+ if( selected._data.hints.showColorBox ) {
1944
+ fragments.push( '<td class="legendColorBox"><div style="border:1px solid ' +
1945
+ options.legend.labelBoxBorderColor +
1946
+ ';padding:1px"><div style="width:14px;height:10px;background-color:' +
1947
+ selected._data.color + '"></div></div></td>' );
1948
+ }
1949
+
1950
+ if( selected._data.hints.showSeriesLabel && selected._data.label ) {
1951
+ var label = selected._data.hints.labelFormatter( selected._data.label );
1952
+ fragments.push( '<td class="legendLabel" style="padding: 0px 4px">' +
1953
+ label + '</td>');
1954
+ }
1955
+ fragments.push( '<td class="hintData" style="padding-left: 4px;"></td>' );
1956
+ fragments.push( '</tr></tbody>' );
1957
+
1958
+ hintDiv = $('<div class="plot-hint" style="border: 1px solid ' + options.hints.borderColor +
1959
+ ';padding: 1px;z-index:5;position:absolute;top:1px;left:1px;display:none;"></div>')
1960
+ .appendTo(hintWrapper);
1961
+
1962
+ var table = $('<table style="font-size:smaller;white-space: nowrap;color:' +
1963
+ options.grid.color + '">' + fragments.join('') + '</table>');
1964
+ hintDiv.append( table );
1965
+
1966
+ if( selected._data.hints.backgroundOpacity != 0.0 ) {
1967
+ var c = selected._data.hints.backgroundColor;
1968
+ if( !c ){
1969
+ tmp = options.grid.backgroundColor ? options.grid.backgroundColor : extractColor( hintDiv );
1970
+ c = parseColor( tmp ).adjust( null, null, null, 1 ).toString();
1971
+ }
1972
+ hintBackground = $('<div class="hint-background" style="padding: 2px;' +
1973
+ 'z-index:4;position:absolute;display:none;background-color:' +
1974
+ c + ';"> </div>')
1975
+ .appendTo( hintWrapper )
1976
+ .css( 'opacity', selected._data.hints.backgroundOpacity );
1977
+ }
1978
+
1979
+ var hintDataContainer = hintDiv.find('.hintData');
1980
+ $(hintDataContainer).html( selected._data.hints.hintFormatter( selected ) );
1981
+ }
1982
+
1983
+ leftEdge = lastMousePos.pageX - offset.left + 15;
1984
+ if( hintDiv.width() + leftEdge > target.width() ) {
1985
+ leftEdge = leftEdge - 30 - hintDiv.width();
1986
+ }
1987
+ hintDiv.css( { left: leftEdge,
1988
+ top: lastMousePos.pageY - offset.top + 15 } ).show();
1989
+ hintBackground.css( { left: leftEdge,
1990
+ top: lastMousePos.pageY - offset.top + 15,
1991
+ width: hintDiv.width(),
1992
+ height: hintDiv.height() } ).show();
1993
+ }
1994
+
1995
+ function cleanup() {
1996
+ $('.hint-wrapper').remove();
1997
+ draw();
1998
+ }
1999
+
2000
+ function defaultHintFormatter( datapoint ) {
2001
+ hintStr = '';
2002
+ for( var key in datapoint ) {
2003
+ if( key[0] == '_' ) { continue; } // skip internal members
2004
+ hintStr += "<strong>" + key + ":</strong> " + datapoint[key] + "<br/>";
2005
+ }
2006
+ return hintStr;
2007
+ }
2008
+
2009
+ function defaultLabelFormatter( label ) {
2010
+ return "<span style='font-size:1.2em;'>" + label + "</span>";
2011
+ }
2012
+ }
2013
+
2014
+ $.plot = function( target, data, options ) {
2015
+ var plot = new Plot( target, data, options );
2016
+ /*var t0 = new Date();
2017
+ var t1 = new Date();
2018
+ var tstr = "time used (msecs): " + (t1.getTime() - t0.getTime())
2019
+ if (window.console)
2020
+ console.log(tstr);
2021
+ else
2022
+ alert(tstr);*/
2023
+ return plot;
2024
+ };
2025
+
2026
+ // round to nearby lower multiple of base
2027
+ function floorInBase( n, base ) {
2028
+ return base * Math.floor( n / base );
2029
+ }
2030
+
2031
+ // color helpers, inspiration from the jquery color animation
2032
+ // plugin by John Resig
2033
+ function Color( r, g, b, a ) {
2034
+ var rgba = [ 'r', 'g', 'b', 'a' ];
2035
+ var x = 4; //rgba.length
2036
+
2037
+ while( -1 < --x ) {
2038
+ this[rgba[x]] = arguments[x] || ( ( x == 3 ) ? 1.0 : 0 );
2039
+ }
2040
+
2041
+ this.toString = function() {
2042
+ if( this.a >= 1.0 ) {
2043
+ return "rgb(" + [ this.r, this.g, this.b ].join( ',' ) + ")";
2044
+ }
2045
+ else {
2046
+ return "rgba(" + [ this.r, this.g, this.b, this.a ].join( ',' ) + ")";
2047
+ }
2048
+ };
2049
+
2050
+ this.scale = function( rf, gf, bf, af ) {
2051
+ x = 4; //rgba.length
2052
+ while( -1 < --x ) {
2053
+ if( arguments[x] ) this[rgba[x]] *= arguments[x];
2054
+ }
2055
+ return this.normalize();
2056
+ };
2057
+
2058
+ this.adjust = function( rd, gd, bd, ad ) {
2059
+ x = 4; //rgba.length
2060
+ while( -1 < --x ) {
2061
+ if( arguments[x] ) this[rgba[x]] += arguments[x];
2062
+ }
2063
+ return this.normalize();
2064
+ };
2065
+
2066
+ this.clone = function() {
2067
+ return new Color( this.r, this.b, this.g, this.a );
2068
+ };
2069
+
2070
+ var limit = function( val, minVal, maxVal ) {
2071
+ return Math.max( Math.min( val, maxVal ), minVal );
2072
+ };
2073
+
2074
+ this.normalize = function() {
2075
+ this.r = limit( parseInt( this.r ), 0, 255 );
2076
+ this.g = limit( parseInt( this.g ), 0, 255 );
2077
+ this.b = limit( parseInt( this.b ), 0, 255 );
2078
+ this.a = limit( this.a, 0, 1 );
2079
+ return this;
2080
+ };
2081
+
2082
+ this.normalize();
2083
+ }
2084
+
2085
+ var lookupColors = {
2086
+ aqua: [ 0, 255, 255 ],
2087
+ azure: [ 240, 255, 255 ],
2088
+ beige: [ 245, 245, 220 ],
2089
+ black: [ 0, 0, 0 ],
2090
+ blue: [ 0, 0, 255 ],
2091
+ brown: [ 165, 42, 42 ],
2092
+ cyan: [ 0, 255, 255 ],
2093
+ darkblue: [ 0, 0, 139 ],
2094
+ darkcyan: [ 0, 139, 139 ],
2095
+ darkgrey: [ 169, 169, 169 ],
2096
+ darkgreen: [ 0, 100, 0 ],
2097
+ darkkhaki: [ 189, 183, 107 ],
2098
+ darkmagenta: [ 139, 0, 139 ],
2099
+ darkolivegreen: [ 85, 107, 47 ],
2100
+ darkorange: [ 255, 140, 0 ],
2101
+ darkorchid: [ 153, 50, 204 ],
2102
+ darkred: [ 139, 0, 0 ],
2103
+ darksalmon: [ 233, 150, 122 ],
2104
+ darkviolet: [ 148, 0, 211 ],
2105
+ fuchsia: [ 255, 0, 255 ],
2106
+ gold: [ 255, 215, 0 ],
2107
+ green: [ 0, 128, 0 ],
2108
+ indigo: [ 75, 0, 130 ],
2109
+ khaki: [ 240, 230, 140 ],
2110
+ lightblue: [ 173, 216, 230 ],
2111
+ lightcyan: [ 224, 255, 255 ],
2112
+ lightgreen: [ 144, 238, 144 ],
2113
+ lightgrey: [ 211, 211, 211 ],
2114
+ lightpink: [ 255, 182, 193 ],
2115
+ lightyellow: [ 255, 255, 224 ],
2116
+ lime: [ 0, 255, 0 ],
2117
+ magenta: [ 255, 0, 255 ],
2118
+ maroon: [ 128, 0, 0 ],
2119
+ navy: [ 0, 0, 128 ],
2120
+ olive: [ 128, 128, 0 ],
2121
+ orange: [ 255, 165, 0 ],
2122
+ pink: [ 255, 192, 203 ],
2123
+ purple: [ 128, 0, 128 ],
2124
+ violet: [ 128, 0, 128 ],
2125
+ red: [ 255, 0, 0 ],
2126
+ silver: [ 192, 192, 192 ],
2127
+ white: [ 255, 255, 255 ],
2128
+ yellow: [ 255, 255, 0 ]
2129
+ };
2130
+
2131
+ function extractColor( element ) {
2132
+ var color,
2133
+ elem = element;
2134
+
2135
+ do {
2136
+ color = elem.css( 'background-color' ).toLowerCase();
2137
+ // keep going until we find an element that has color, or
2138
+ // we hit the body
2139
+ if( color != '' && color != 'transparent' ) break;
2140
+ elem = elem.parent();
2141
+ } while( !$.nodeName( elem.get( 0 ), 'body' ) );
2142
+
2143
+ // catch Safari's way of signalling transparent
2144
+ if( color == 'rgba(0, 0, 0, 0)' ) return 'transparent';
2145
+ return color;
2146
+ }
2147
+
2148
+ // parse string, returns Color
2149
+ function parseColor( str ) {
2150
+ var result;
2151
+
2152
+ // Try to lookup the color first before going mad with regexes
2153
+ var name = $.trim( str ).toLowerCase();
2154
+ if (name == 'transparent') {
2155
+ return new Color( 255, 255, 255, 0 );
2156
+ }
2157
+ else if( !name.match( /^(rgb|#)/ ) ) {
2158
+ result = lookupColors[name];
2159
+ return new Color( result[0], result[1], result[2] );
2160
+ }
2161
+
2162
+ // Look for rgb(num,num,num)
2163
+ if( result = /rgb\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*\)/.exec( str ) ) {
2164
+ return new Color( parseInt( result[1], 10 ), parseInt( result[2], 10 ), parseInt( result[3], 10 ) );
2165
+ }
2166
+ // Look for rgba(num,num,num,num)
2167
+ if( result = /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( str ) ) {
2168
+ return new Color( parseInt( result[1], 10 ), parseInt( result[2], 10 ), parseInt( result[3], 10 ), parseFloat( result[4] ) );
2169
+ }
2170
+ // Look for rgb(num%,num%,num%)
2171
+ if( result = /rgb\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*\)/.exec( str ) ) {
2172
+ return new Color( parseFloat( result[1] ) * 2.55, parseFloat( result[2] ) * 2.55, parseFloat( result[3] ) * 2.55 );
2173
+ }
2174
+ // Look for rgba(num%,num%,num%,num)
2175
+ if( result = /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( str ) ) {
2176
+ return new Color( parseFloat( result[1] ) * 2.55, parseFloat( result[2]) * 2.55, parseFloat( result[3] ) * 2.55, parseFloat( result[4] ) );
2177
+ }
2178
+ // Look for #a0b1c2
2179
+ if( result = /#([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})/.exec( str ) ) {
2180
+ return new Color( parseInt( result[1], 16 ), parseInt( result[2], 16 ), parseInt( result[3], 16 ) );
2181
+ }
2182
+ // Look for #fff
2183
+ if( result = /#([a-fA-F0-9])([a-fA-F0-9])([a-fA-F0-9])/.exec( str ) ) {
2184
+ return new Color( parseInt( result[1] + result[1], 16 ), parseInt( result[2] + result[2], 16 ), parseInt( result[3] + result[3], 16 ) );
2185
+ }
2186
+ }
2187
+ } )( jQuery );