flotr 1.3.8

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