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.
- data/README.markdown +71 -0
- data/examples/sincos.rb +26 -0
- data/lib/excanvas.js +785 -0
- data/lib/excanvas.pack.js +1 -0
- data/lib/flotr.rb +170 -0
- data/lib/interacting.rhtml +77 -0
- data/lib/jquery.flot.js +2187 -0
- data/lib/jquery.min.js +32 -0
- data/lib/layout.css +5 -0
- data/lib/zooming.rhtml +120 -0
- metadata +74 -0
@@ -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_})()}
|
data/lib/flotr.rb
ADDED
@@ -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>
|
data/lib/jquery.flot.js
ADDED
@@ -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 );
|