mirror of
https://github.com/wu736139669/vczero.github.io.git
synced 2026-08-05 06:23:38 +00:00
2121 lines
80 KiB
JavaScript
2121 lines
80 KiB
JavaScript
window.JingleChart = JChart = {
|
|
version : '0.1',
|
|
animationOptions : {
|
|
linear : function (t){
|
|
return t;
|
|
},
|
|
easeInQuad: function (t) {
|
|
return t*t;
|
|
},
|
|
easeOutQuad: function (t) {
|
|
return -1 *t*(t-2);
|
|
},
|
|
easeInOutQuad: function (t) {
|
|
if ((t/=1/2) < 1) return 1/2*t*t;
|
|
return -1/2 * ((--t)*(t-2) - 1);
|
|
},
|
|
easeInCubic: function (t) {
|
|
return t*t*t;
|
|
},
|
|
easeOutCubic: function (t) {
|
|
return 1*((t=t/1-1)*t*t + 1);
|
|
},
|
|
easeInOutCubic: function (t) {
|
|
if ((t/=1/2) < 1) return 1/2*t*t*t;
|
|
return 1/2*((t-=2)*t*t + 2);
|
|
},
|
|
easeInQuart: function (t) {
|
|
return t*t*t*t;
|
|
},
|
|
easeOutQuart: function (t) {
|
|
return -1 * ((t=t/1-1)*t*t*t - 1);
|
|
},
|
|
easeInOutQuart: function (t) {
|
|
if ((t/=1/2) < 1) return 1/2*t*t*t*t;
|
|
return -1/2 * ((t-=2)*t*t*t - 2);
|
|
},
|
|
easeInQuint: function (t) {
|
|
return 1*(t/=1)*t*t*t*t;
|
|
},
|
|
easeOutQuint: function (t) {
|
|
return 1*((t=t/1-1)*t*t*t*t + 1);
|
|
},
|
|
easeInOutQuint: function (t) {
|
|
if ((t/=1/2) < 1) return 1/2*t*t*t*t*t;
|
|
return 1/2*((t-=2)*t*t*t*t + 2);
|
|
},
|
|
easeInSine: function (t) {
|
|
return -1 * Math.cos(t/1 * (Math.PI/2)) + 1;
|
|
},
|
|
easeOutSine: function (t) {
|
|
return 1 * Math.sin(t/1 * (Math.PI/2));
|
|
},
|
|
easeInOutSine: function (t) {
|
|
return -1/2 * (Math.cos(Math.PI*t/1) - 1);
|
|
},
|
|
easeInExpo: function (t) {
|
|
return (t==0) ? 1 : 1 * Math.pow(2, 10 * (t/1 - 1));
|
|
},
|
|
easeOutExpo: function (t) {
|
|
return (t==1) ? 1 : 1 * (-Math.pow(2, -10 * t/1) + 1);
|
|
},
|
|
easeInOutExpo: function (t) {
|
|
if (t==0) return 0;
|
|
if (t==1) return 1;
|
|
if ((t/=1/2) < 1) return 1/2 * Math.pow(2, 10 * (t - 1));
|
|
return 1/2 * (-Math.pow(2, -10 * --t) + 2);
|
|
},
|
|
easeInCirc: function (t) {
|
|
if (t>=1) return t;
|
|
return -1 * (Math.sqrt(1 - (t/=1)*t) - 1);
|
|
},
|
|
easeOutCirc: function (t) {
|
|
return 1 * Math.sqrt(1 - (t=t/1-1)*t);
|
|
},
|
|
easeInOutCirc: function (t) {
|
|
if ((t/=1/2) < 1) return -1/2 * (Math.sqrt(1 - t*t) - 1);
|
|
return 1/2 * (Math.sqrt(1 - (t-=2)*t) + 1);
|
|
},
|
|
easeInElastic: function (t) {
|
|
var s=1.70158;var p=0;var a=1;
|
|
if (t==0) return 0; if ((t/=1)==1) return 1; if (!p) p=1*.3;
|
|
if (a < Math.abs(1)) { a=1; var s=p/4; }
|
|
else var s = p/(2*Math.PI) * Math.asin (1/a);
|
|
return -(a*Math.pow(2,10*(t-=1)) * Math.sin( (t*1-s)*(2*Math.PI)/p ));
|
|
},
|
|
easeOutElastic: function (t) {
|
|
var s=1.70158;var p=0;var a=1;
|
|
if (t==0) return 0; if ((t/=1)==1) return 1; if (!p) p=1*.3;
|
|
if (a < Math.abs(1)) { a=1; var s=p/4; }
|
|
else var s = p/(2*Math.PI) * Math.asin (1/a);
|
|
return a*Math.pow(2,-10*t) * Math.sin( (t*1-s)*(2*Math.PI)/p ) + 1;
|
|
},
|
|
easeInOutElastic: function (t) {
|
|
var s=1.70158;var p=0;var a=1;
|
|
if (t==0) return 0; if ((t/=1/2)==2) return 1; if (!p) p=1*(.3*1.5);
|
|
if (a < Math.abs(1)) { a=1; var s=p/4; }
|
|
else var s = p/(2*Math.PI) * Math.asin (1/a);
|
|
if (t < 1) return -.5*(a*Math.pow(2,10*(t-=1)) * Math.sin( (t*1-s)*(2*Math.PI)/p ));
|
|
return a*Math.pow(2,-10*(t-=1)) * Math.sin( (t*1-s)*(2*Math.PI)/p )*.5 + 1;
|
|
},
|
|
easeInBack: function (t) {
|
|
var s = 1.70158;
|
|
return 1*(t/=1)*t*((s+1)*t - s);
|
|
},
|
|
easeOutBack: function (t) {
|
|
var s = 1.70158;
|
|
return 1*((t=t/1-1)*t*((s+1)*t + s) + 1);
|
|
},
|
|
easeInOutBack: function (t) {
|
|
var s = 1.70158;
|
|
if ((t/=1/2) < 1) return 1/2*(t*t*(((s*=(1.525))+1)*t - s));
|
|
return 1/2*((t-=2)*t*(((s*=(1.525))+1)*t + s) + 2);
|
|
},
|
|
easeInBounce: function (t) {
|
|
return 1 - JChart.animationOptions.easeOutBounce (1-t);
|
|
},
|
|
easeOutBounce: function (t) {
|
|
if ((t/=1) < (1/2.75)) {
|
|
return 1*(7.5625*t*t);
|
|
} else if (t < (2/2.75)) {
|
|
return 1*(7.5625*(t-=(1.5/2.75))*t + .75);
|
|
} else if (t < (2.5/2.75)) {
|
|
return 1*(7.5625*(t-=(2.25/2.75))*t + .9375);
|
|
} else {
|
|
return 1*(7.5625*(t-=(2.625/2.75))*t + .984375);
|
|
}
|
|
},
|
|
easeInOutBounce: function (t) {
|
|
if (t < 1/2) return JChart.animationOptions.easeInBounce (t*2) * .5;
|
|
return JChart.animationOptions.easeOutBounce (t*2-1) * .5 + 1*.5;
|
|
}
|
|
},
|
|
/**
|
|
* 通用的计时控制器
|
|
*/
|
|
requestAnimFrame : (function(){
|
|
return window.requestAnimationFrame ||
|
|
window.webkitRequestAnimationFrame ||
|
|
window.mozRequestAnimationFrame ||
|
|
window.oRequestAnimationFrame ||
|
|
window.msRequestAnimationFrame ||
|
|
function(callback) {
|
|
window.setTimeout(callback, 1000 / 60);
|
|
};
|
|
})(),
|
|
isNumber : function(n){
|
|
return !isNaN(parseFloat(n)) && isFinite(n);
|
|
},
|
|
isEqual : function(number1, number2, digits){
|
|
digits = digits == undefined? 10: digits; // 默认精度为10
|
|
return number1.toFixed(digits) === number2.toFixed(digits);
|
|
},
|
|
/**
|
|
* 取有效区域内的值
|
|
* @param valueToCap
|
|
* @param maxValue
|
|
* @param minValue
|
|
* @return {*}
|
|
*/
|
|
capValue : function(valueToCap, maxValue, minValue){
|
|
var value;
|
|
if(this.isNumber(maxValue) && valueToCap > maxValue) {
|
|
return maxValue;
|
|
}
|
|
if(this.isNumber(minValue) && valueToCap < minValue ){
|
|
return minValue;
|
|
}
|
|
return valueToCap;
|
|
},
|
|
getDecimalPlaces : function(num){
|
|
if (num%1!=0){
|
|
return num.toString().split(".")[1].length
|
|
}
|
|
else{
|
|
return 0;
|
|
}
|
|
},
|
|
extend : function(target){
|
|
var args = Array.prototype.slice.call(arguments,1);
|
|
this.each(args,function(v,i){
|
|
extend(target,v);
|
|
});
|
|
function extend(target,source){
|
|
for(var key in source){
|
|
var o = source[key];
|
|
if(o instanceof Array){
|
|
target[key] = extend([], o);
|
|
}else if(o instanceof Object){
|
|
target[key] = extend({},o);
|
|
}else{
|
|
target[key] = o;
|
|
}
|
|
}
|
|
return target;
|
|
}
|
|
return target;
|
|
|
|
},
|
|
clone : function(obj){
|
|
var o;
|
|
if (typeof obj == "object") {
|
|
if (obj === null) {
|
|
o = null;
|
|
} else {
|
|
if (obj instanceof Array) {
|
|
o = [];
|
|
for (var i = 0, len = obj.length; i < len; i++) {
|
|
o.push(this.clone(obj[i]));
|
|
}
|
|
} else {
|
|
o = {};
|
|
for (var j in obj) {
|
|
o[j] = this.clone(obj[j]);
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
o = obj;
|
|
}
|
|
return o;
|
|
},
|
|
//只对array有效
|
|
each : function(array,fn,context){
|
|
for(var i = 0,len=array.length;i<len;i++){
|
|
var result = fn.call(context,array[i],i,array);
|
|
if(result === true){
|
|
continue;
|
|
}else if(result === false){
|
|
break;
|
|
}
|
|
}
|
|
},
|
|
getOffset : function(el){
|
|
var box = el.getBoundingClientRect(),
|
|
doc = el.ownerDocument,
|
|
body = doc.body,
|
|
html = doc.documentElement,
|
|
clientTop = html.clientTop || body.clientTop || 0,
|
|
clientLeft = html.clientLeft || body.clientLeft || 0,
|
|
top = box.top + (self.pageYOffset || html.scrollTop || body.scrollTop ) - clientTop,
|
|
left = box.left + (self.pageXOffset || html.scrollLeft || body.scrollLeft) - clientLeft
|
|
return { 'top': top, 'left': left };
|
|
},
|
|
/**
|
|
* 将颜色代码转换成RGB颜色
|
|
* @param color
|
|
* @return {*}
|
|
*/
|
|
hex2Rgb : function(color,alpha){
|
|
var r, g, b;
|
|
// 参数为RGB模式时不做进制转换,直接截取字符串即可
|
|
if( /rgb/.test(color) ){
|
|
var arr = color.match( /\d+/g );
|
|
r = parseInt( arr[0] );
|
|
g = parseInt( arr[1] );
|
|
b = parseInt( arr[2] );
|
|
}else if( /#/.test(color) ){// 参数为十六进制时需要做进制转换
|
|
var len = color.length;
|
|
if( len === 7 ){// 非简写模式 #0066cc
|
|
r = parseInt( color.slice(1, 3), 16 );
|
|
g = parseInt( color.slice(3, 5), 16 );
|
|
b = parseInt( color.slice(5), 16 );
|
|
}else if( len === 4 ){ // 简写模式 #06c
|
|
r = parseInt( color.charAt(1) + val.charAt(1), 16 );
|
|
g = parseInt( color.charAt(2) + val.charAt(2), 16 );
|
|
b = parseInt( color.charAt(3) + val.charAt(3), 16 );
|
|
}
|
|
}
|
|
else{
|
|
return color;
|
|
}
|
|
return 'rgba('+r+','+g+','+ b + ','+(alpha?alpha:1)+')';
|
|
},
|
|
tmpl : (function(){
|
|
//Javascript micro templating by John Resig - source at http://ejohn.org/blog/javascript-micro-templating/
|
|
var cache = {};
|
|
function tmpl(str, data){
|
|
// Figure out if we're getting a template, or if we need to
|
|
// load the template - and be sure to cache the result.
|
|
var fn = !/\W/.test(str) ?
|
|
cache[str] = cache[str] ||
|
|
tmpl(document.getElementById(str).innerHTML) :
|
|
|
|
// Generate a reusable function that will serve as a template
|
|
// generator (and which will be cached).
|
|
new Function("obj",
|
|
"var p=[],print=function(){p.push.apply(p,arguments);};" +
|
|
|
|
// Introduce the data as local variables using with(){}
|
|
"with(obj){p.push('" +
|
|
|
|
// Convert the template into pure JavaScript
|
|
str
|
|
.replace(/[\r\t\n]/g, " ")
|
|
.split("<%").join("\t")
|
|
.replace(/((^|%>)[^\t]*)'/g, "$1\r")
|
|
.replace(/\t=(.*?)%>/g, "',$1,'")
|
|
.split("\t").join("');")
|
|
.split("%>").join("p.push('")
|
|
.split("\r").join("\\'")
|
|
+ "');}return p.join('');");
|
|
|
|
// Provide some basic currying to the user
|
|
return data ? fn( data ) : fn;
|
|
};
|
|
return tmpl;
|
|
})()
|
|
};
|
|
|
|
|
|
;(function(_){
|
|
function Bar(data,cfg){
|
|
_.Scale.apply(this);
|
|
var barRanges = [];//记录柱状图的占据的位置
|
|
this._type_ = 'bar';
|
|
var _this = this;
|
|
this.data = data;//所有的数据
|
|
this.chartData = null;//图表当前展示的数据
|
|
//配置项
|
|
_.extend(this.config,{
|
|
//是否显示bar的边框
|
|
showBarBorder : true,
|
|
//bar边框宽度
|
|
barBorderWidth : 2,
|
|
//每两个bar之间的间距
|
|
barSpacing : 1,
|
|
//每两组bar之间的间距
|
|
barSetSpacing : 5,
|
|
//是否可以对数据进行拖动
|
|
datasetGesture : false,
|
|
//每次显示的数据条数
|
|
datasetShowNumber : 12
|
|
});
|
|
/**
|
|
* 绑定canvas dom元素上的事件 如:click、touch
|
|
*/
|
|
this.bindEvents = function(){
|
|
this.on('_tap',function(x,y){tapHandler(x,y,'tap.bar')});
|
|
//this.on('_doubleTap',function(x,y){tapHandler(x,y,'doubleTap.bar')});
|
|
this.on('_longTap',function(x,y){tapHandler(x,y,'longTap.bar')});
|
|
if(this.config.datasetGesture){
|
|
this.bindDataGestureEvent();
|
|
}
|
|
}
|
|
/**
|
|
* 初始化部分元素值
|
|
*/
|
|
this.draw = function(noAnim){
|
|
if(this.config.datasetGesture && this.data.labels.length > _this.config.datasetShowNumber){
|
|
this.chartData = this.sliceData(this.data,0,this.data.labels.length,this.config.datasetShowNumber);
|
|
}else{
|
|
this.chartData = this.data;
|
|
}
|
|
this.mergeFont(['scaleFont','textFont']);
|
|
this.initScale(true);
|
|
if(noAnim){
|
|
this.drawScale();
|
|
this.drawBars(1);
|
|
}else{
|
|
this.doAnim(this.drawScale,this.drawBars);
|
|
}
|
|
}
|
|
this.redraw = function(data){
|
|
this.chartData = data;
|
|
this.clear();
|
|
this.initScale(true);
|
|
this.drawScale();
|
|
this.drawBars(1);
|
|
}
|
|
|
|
this.drawBars = function(animPc){
|
|
if(animPc >= 1)barRanges = [];
|
|
var ctx = _this.ctx,cfg = _this.config,scale = _this.scaleData;
|
|
_.each(_this.chartData.datasets,function(set,i){
|
|
if(!cfg.showBarBorder)borderColor = null;
|
|
_.each(set.data,function(d,j){
|
|
var x = scale.x + cfg.barSetSpacing + scale.xHop*j + scale.barWidth*i + cfg.barSpacing*i + cfg.barBorderWidth* i,
|
|
y = scale.y,width = scale.barWidth,height = animPc*_this.calcOffset(d,scale.yScaleValue,scale.yHop)+(cfg.barBorderWidth/2),
|
|
color = set.color,borderColor,bgColor = _.hex2Rgb(color,0.6);
|
|
if(cfg.showBarBorder){
|
|
//边框颜色默认与设置颜色一致
|
|
borderColor = set.borderColor || color;
|
|
}
|
|
ctx.rect(x,y,width,-height,bgColor,borderColor,cfg.barBorderWidth);
|
|
if(animPc >= 1){
|
|
barRanges.push([x,x+width,y,y-height,j,i]);
|
|
}
|
|
cfg.showText && _this.drawText(d,x+width/2,y-height-3,[j,i]);
|
|
|
|
});
|
|
})
|
|
}
|
|
|
|
function tapHandler(x,y,event){
|
|
var p = isInBarRange(x,y);
|
|
if(p){
|
|
_this.trigger(event,[_this.chartData.datasets[p[5]].data[p[4]],p[4],p[5]]);
|
|
}
|
|
}
|
|
|
|
function isInBarRange(x,y){
|
|
var range;
|
|
_.each(barRanges,function(r){
|
|
if(x >= r[0] && x <= r[1] && y >= r[3] && y <= r[2]){
|
|
range = r;
|
|
return false;
|
|
}
|
|
});
|
|
return range;
|
|
}
|
|
//初始化参数
|
|
if(cfg)this.initial(cfg);
|
|
}
|
|
_.Bar = Bar;
|
|
})(JChart)
|
|
/**
|
|
* 简单的Canvas帮助类,使canvas支持类似于jquery的链式操作,支持CanvasRenderingContext2D所有的方法,并提供一些常用的工具方法
|
|
*/
|
|
;(function(_){
|
|
function Chain(el){
|
|
//需要返回结果的方法,这些方法将不能进行后续的链式调用
|
|
var needReturnValueFn = ['isPointInPath','measureText','getImageData','createLinearGradient','createPattern','createRadialGradient','isPointInPath'];
|
|
function Canvas(){
|
|
this.el = el = (typeof el === 'string') ? document.getElementById(el) : el;
|
|
this.ctx = el.getContext('2d');
|
|
this.width = el.width;
|
|
this.height = el.height;
|
|
addProtoFunc(this.ctx);
|
|
}
|
|
|
|
//添加canvas原生方法到prototype中
|
|
function addProtoFunc(ctx){
|
|
for(var fn in CanvasRenderingContext2D.prototype){
|
|
if(Canvas.prototype[fn])continue;
|
|
Canvas.prototype[fn] = function(fn){
|
|
return function(){
|
|
var args = Array.prototype.slice.call(arguments);
|
|
var result = ctx[fn].apply(ctx,args);
|
|
if(needReturnValueFn.indexOf(fn)>-1){
|
|
return result;
|
|
}
|
|
return this;
|
|
}
|
|
}(fn);
|
|
}
|
|
}
|
|
|
|
Canvas.prototype = {
|
|
/**
|
|
* 设置context的属性值
|
|
* @param name 属性名
|
|
* @param value 属性值
|
|
* @return this
|
|
*/
|
|
set : function(name,value){
|
|
if(typeof name == 'object'){
|
|
for(var p in name){
|
|
this.ctx[p] && (this.ctx[p] = name[p]);
|
|
}
|
|
}else{
|
|
this.ctx[name] && (this.ctx[name] = value);
|
|
}
|
|
return this;
|
|
},
|
|
/**
|
|
* 获取context的属性值
|
|
* @param name 属性名
|
|
* @return value 属性值
|
|
*/
|
|
get : function(name){
|
|
return this.ctx[name];
|
|
},
|
|
/**
|
|
* context填充
|
|
* @param color 填充颜色
|
|
* @return this
|
|
*/
|
|
fill : function (color) {
|
|
color && this.set('fillStyle', color);
|
|
this.ctx.fill();
|
|
return this;
|
|
},
|
|
/**
|
|
* context描边
|
|
* @param color 描边颜色
|
|
* @return this
|
|
*/
|
|
stroke : function (color,width) {
|
|
if (color) {
|
|
this.set('strokeStyle', color);
|
|
width && this.set('lineWidth',width);
|
|
}
|
|
this.ctx.stroke();
|
|
return this;
|
|
},
|
|
/**
|
|
* 填充文本,在文本中加入\n可实现换行
|
|
* @param text
|
|
* @param x
|
|
* @param y
|
|
* @param style
|
|
* @return {*}
|
|
*/
|
|
fillText : function(text,x,y,style){
|
|
this.ctx.save();
|
|
if(style && typeof style == 'object'){
|
|
for(var p in style){
|
|
this.set(p,style[p]);
|
|
}
|
|
}
|
|
var texts = (text+'').split('\n');
|
|
if(texts.length > 1){
|
|
var fontsize = this.getFontSize();
|
|
for(var i=0;i<texts.length;i++){
|
|
this.ctx.fillText(texts[i],x,y+i*fontsize);
|
|
}
|
|
}else{
|
|
this.ctx.fillText(text,x,y);
|
|
}
|
|
this.ctx.restore();
|
|
return this;
|
|
},
|
|
/**
|
|
* 清除矩形
|
|
* @param x
|
|
* @param y
|
|
* @param w
|
|
* @param h
|
|
* @return this
|
|
*/
|
|
clear : function (x, y, w, h) {
|
|
var c = this.el;
|
|
x = x || 0;
|
|
y = y || 0;
|
|
w = w || this.el.width;
|
|
h = h || this.el.height;
|
|
this.ctx.clearRect(x, y, w, h);
|
|
//android4.1+下经常会出现clear无效,大多数情况出现在loop函数中
|
|
c.style.opacity = 0.99;
|
|
setTimeout(function() {
|
|
c.style.opacity = 1;
|
|
}, 1);
|
|
return this;
|
|
},
|
|
/**
|
|
* 重新设置大小
|
|
* @param width 宽
|
|
* @param height 高
|
|
* @return this
|
|
*/
|
|
resize : function (width, height) {
|
|
this.el.width = width;
|
|
this.el.height = height;
|
|
this.width = width;
|
|
this.height = height;
|
|
return this;
|
|
},
|
|
/**
|
|
* 画线
|
|
*/
|
|
line : function(x,y,x1,y1,stroke,strokeWidth){
|
|
this.beginPath().moveTo(x,y).lineTo(x1,y1);
|
|
stroke && this.stroke(stroke,strokeWidth);
|
|
return this;
|
|
},
|
|
/**
|
|
* 画矩形
|
|
* @param x
|
|
* @param y
|
|
* @param w
|
|
* @param h
|
|
* @param fill 填充颜色
|
|
* @param stroke 描边颜色
|
|
* @param strokeWidth 描边宽度
|
|
* @return this
|
|
*/
|
|
rect : function (x, y, w, h, fill, stroke,strokeWidth) {
|
|
this.ctx.beginPath();
|
|
this.ctx.rect(x, y, w, h);
|
|
this.ctx.closePath();
|
|
fill && this.fill(fill);
|
|
stroke && this.stroke(stroke,strokeWidth);
|
|
return this;
|
|
},
|
|
/**
|
|
* 画圆形
|
|
* @param x
|
|
* @param y
|
|
* @param r 半径
|
|
* @param fill 填充颜色
|
|
* @param stroke 描边颜色
|
|
* @param strokeWidth 描边宽度
|
|
* @return this
|
|
*/
|
|
circle : function (x, y, r, fill, stroke,strokeWidth) {
|
|
this.beginPath();
|
|
this.ctx.arc(x, y, r, 0, 2 * Math.PI, false);
|
|
this.closePath();
|
|
fill && this.fill(fill);
|
|
stroke && this.stroke(stroke,strokeWidth);
|
|
return this;
|
|
},
|
|
/**
|
|
* 画扇形
|
|
* @param x
|
|
* @param y
|
|
* @param r 半径
|
|
* @param start 开始角度
|
|
* @param end 结束角度
|
|
* @param fill 填充颜色
|
|
* @param stroke 描边颜色
|
|
* @param strokeWidth 描边宽度
|
|
* @reutrn this
|
|
*/
|
|
sector : function(x,y,r,start,end,fill,stroke,strokeWidth){
|
|
this.beginPath()
|
|
.arc(x,y,r,start, end, false)
|
|
.lineTo(x,y)
|
|
.closePath();
|
|
fill && this.fill(fill);
|
|
stroke && this.stroke(stroke,strokeWidth);
|
|
return this;
|
|
},
|
|
/**
|
|
* 环形扇形
|
|
* @param x
|
|
* @param y
|
|
* @param ir 内半径
|
|
* @param or 外半径
|
|
* @param start 开始角度
|
|
* @param end 结束角度
|
|
* @param fill 填充颜色
|
|
* @param stroke 描边颜色
|
|
* @param strokeWidth 描边宽度
|
|
* @reutrn this
|
|
*/
|
|
dountSector : function(x,y,ir,or,start,end,fill,stroke,strokeWidth){
|
|
this.beginPath()
|
|
.arc(x,y,or,start, end, false)
|
|
.arc(x,y,ir,end,start,true)
|
|
.closePath();
|
|
fill && this.fill(fill);
|
|
stroke && this.stroke(stroke,strokeWidth);
|
|
return this;
|
|
},
|
|
/**
|
|
* 加载一张图片
|
|
* @param img
|
|
* @return this
|
|
*/
|
|
image : function (img) {
|
|
var _self = this;
|
|
var args = Array.prototype.slice.call(arguments);
|
|
var cb = function () {
|
|
_self.ctx.drawImage.apply(_self.ctx, args);
|
|
};
|
|
|
|
if (typeof img === 'string') {
|
|
args[0] = new Image();
|
|
args[0].onload = cb;
|
|
args[0].src = img;
|
|
} else {
|
|
cb();
|
|
}
|
|
return this;
|
|
},
|
|
/**
|
|
* 获取canvas当前的字体大小
|
|
* @return {Boolean}
|
|
*/
|
|
getFontSize : function(){
|
|
var size = this.ctx.font.match(/\d+(?=px)/i);
|
|
if(size){
|
|
size = parseInt(size[0]);
|
|
}
|
|
return size;
|
|
}
|
|
}
|
|
return new Canvas();
|
|
}
|
|
|
|
_.Canvas = Chain;
|
|
})(JChart || window);
|
|
;(function(_){
|
|
var Chart = function(){
|
|
//当前动画的状态
|
|
this.isAnimating = false;
|
|
this.config = {
|
|
width : 0,
|
|
height : 0,
|
|
bgColor : '#fff',
|
|
//优先画刻度
|
|
drawScaleFirst : true,
|
|
//文本字体属性
|
|
showText : true,
|
|
textFont : {},
|
|
//是否开启动画
|
|
animation : true,
|
|
//动画帧数
|
|
animationSteps : 10,
|
|
//动画函数
|
|
animationEasing : "linear"
|
|
}
|
|
this.defaultFont = {
|
|
family : 'Arial',
|
|
size : 16,
|
|
style : 'normal',
|
|
color : '#5b5b5b',
|
|
textAlign : 'center',
|
|
textBaseline : 'middle'
|
|
}
|
|
this.events = {};
|
|
/**
|
|
* 初始化参数
|
|
*/
|
|
this.initial = function(cfg){
|
|
//合并设置参数
|
|
if(typeof cfg == 'string'){
|
|
this.config.id = cfg;
|
|
}else{
|
|
_.extend(this.config,cfg);
|
|
}
|
|
this.ctx = _.Canvas(this.config.id);
|
|
var canvas = this.ctx.el;
|
|
this.config.width && (canvas.width = this.config.width);
|
|
this.config.height && (canvas.height = this.config.height);
|
|
if(this.config.fit){
|
|
//todo 自动计算高度宽度
|
|
//todo 检测 转屏 事件
|
|
}
|
|
this.width = canvas.width;
|
|
this.height = canvas.height;
|
|
//设置canvas背景颜色
|
|
canvas.style.background = this.config.bgColor;
|
|
|
|
//High pixel density displays - multiply the size of the canvas height/width by the device pixel ratio, then scale.
|
|
//如果设备为视网膜屏,将canvas按照设备像素比放大像素,然后再等比缩小
|
|
if (window.devicePixelRatio) {
|
|
canvas.style.width = this.width + "px";
|
|
canvas.style.height = this.height + "px";
|
|
canvas.height = this.height * window.devicePixelRatio;
|
|
canvas.width = this.width * window.devicePixelRatio;
|
|
this.ctx.scale(window.devicePixelRatio, window.devicePixelRatio);
|
|
}
|
|
this.bindTouchEvents();
|
|
this.bindEvents();
|
|
};
|
|
this.resize = function(w,h){
|
|
|
|
},
|
|
/**
|
|
* 清空画布后重新设置画布的背景
|
|
*/
|
|
this.clear = function(){
|
|
this.ctx.clear();
|
|
};
|
|
/**
|
|
* 重新刷新图表
|
|
*/
|
|
this.refresh = function(config){
|
|
this.update(null,config,true);
|
|
};
|
|
/**
|
|
* 加载数据
|
|
* @param data
|
|
* @param config
|
|
*/
|
|
this.load = function(data){
|
|
this.update(data,null,false);
|
|
}
|
|
/**
|
|
* 更新图表
|
|
* @param data
|
|
* @param config
|
|
* @param animation
|
|
*/
|
|
this.update = function(data,config,animation){
|
|
config && _.extend(this.config,config);
|
|
data && (this.data = data);
|
|
this.dataOffset = 0;
|
|
this.clear();
|
|
this.draw(!animation);
|
|
}
|
|
this.mergeFont = function(key){
|
|
if(key instanceof Array){
|
|
_.each(key,function(v){
|
|
this.mergeFont(v);
|
|
},this);
|
|
}else{
|
|
var of = this.config[key];
|
|
var f = _.extend({},this.defaultFont,of);
|
|
f.font = f.style + " " + f.size+"px " + f.family;
|
|
f.fillStyle = f.color;
|
|
this.config[key] = f;
|
|
}
|
|
}
|
|
/**
|
|
* 动画函数
|
|
* @param drawScale 缩放动画 函数
|
|
* @param drawData 增长式动画 函数
|
|
* @param callback 执行成功回调函数
|
|
*/
|
|
this.doAnim = function(drawScale,drawData,callback){
|
|
this.isAnimating = true;
|
|
var config = this.config,_this = this;
|
|
// 1/动画帧数
|
|
var animFrameAmount = (config.animation)? 1/ _.capValue(config.animationSteps,1000,1) : 1,
|
|
//动画效果
|
|
easingFunction = _.animationOptions[config.animationEasing],
|
|
//动画完成率
|
|
percentAnimComplete =(config.animation)? 0 : 1,
|
|
_this = this;
|
|
|
|
if (typeof drawScale !== "function") drawScale = function(){};
|
|
_.requestAnimFrame.call(window,animLoop);
|
|
function animLoop(){
|
|
percentAnimComplete += animFrameAmount;
|
|
animateFrame();
|
|
if (percentAnimComplete <= 1){
|
|
_.requestAnimFrame.call(window,animLoop);
|
|
}else{
|
|
_this.isAnimating = false;
|
|
callback && callback.call(_this);
|
|
_this.trigger('animationComplete');
|
|
}
|
|
};
|
|
function animateFrame(){
|
|
_this.clear();
|
|
var animPercent =(config.animation)? _.capValue(easingFunction(percentAnimComplete),1,0) : 1;
|
|
drawData.call(_this,animPercent);
|
|
if(_this.config.drawScaleFirst){
|
|
drawScale.call(_this);
|
|
drawData.call(_this,animPercent);
|
|
}else{
|
|
drawData.call(_this,animPercent);
|
|
drawScale.call(_this);
|
|
}
|
|
};
|
|
}
|
|
/**
|
|
* 简易的事件绑定
|
|
*/
|
|
this.on = function(event,callback){
|
|
this.events[event] = callback;
|
|
};
|
|
/**
|
|
* 调用事件函数
|
|
* @param event 事件名称
|
|
* @param data 参数(数组形式)
|
|
*/
|
|
this.trigger = function(event,data){
|
|
var callback = this.events[event];
|
|
if(callback){
|
|
return callback.apply(this,data);
|
|
}else{
|
|
return null;
|
|
}
|
|
};
|
|
this.drawText = function(text,x,y,args,style){
|
|
this.ctx.set(this.config.textFont);
|
|
style && this.ctx.set(style);
|
|
args = args ? [text].concat(args) : [text];
|
|
var t = this.trigger('renderText',args);
|
|
t = (t == null)?text:t;
|
|
this.ctx.fillText(t,x,y);
|
|
};
|
|
//给chart添加tap longTap doubleTap事件
|
|
this.bindTouchEvents = function(){
|
|
var touch = {},touchTimeout,longTapDelay = 750, longTapTimeout,now, delta,offset,
|
|
hasTouch = 'ontouchstart' in window,
|
|
START_EV = hasTouch ? 'touchstart' : 'mousedown',
|
|
MOVE_EV = hasTouch ? 'touchmove' : 'mousemove',
|
|
END_EV = hasTouch ? 'touchend' : 'mouseup',
|
|
CANCEL_EV = hasTouch ? 'touchcancel' : 'mouseup',
|
|
_this = this;
|
|
|
|
this.ctx.el.addEventListener(START_EV,touchstart);
|
|
this.ctx.el.addEventListener(MOVE_EV,touchmove);
|
|
this.ctx.el.addEventListener(END_EV,touchend);
|
|
this.ctx.el.addEventListener(CANCEL_EV,cancelAll);
|
|
|
|
function touchstart(e){
|
|
now = Date.now();
|
|
e = e.touches ? e.touches[0] : e;
|
|
delta = now - (touch.last || now);
|
|
touchTimeout && clearTimeout(touchTimeout);
|
|
offset = _.getOffset(_this.ctx.el);
|
|
touch.x1 = e.pageX - offset.left;
|
|
touch.y1 = e.pageY - offset.top;
|
|
if (delta > 0 && delta <= 250) touch.isDoubleTap = true;
|
|
touch.last = now;
|
|
longTapTimeout = setTimeout(longTap, longTapDelay);
|
|
}
|
|
function touchmove(e){
|
|
if(!touch.last)return;
|
|
var ev = e.touches ? e.touches[0] : e;
|
|
touch.x2 = ev.pageX - offset.left;
|
|
touch.y2 = ev.pageY - offset.top;
|
|
if (Math.abs(touch.x1 - touch.x2) > 15){
|
|
e.preventDefault();
|
|
cancelAll();
|
|
}
|
|
}
|
|
function touchend(e){
|
|
cancelLongTap();
|
|
if ('last' in touch){
|
|
//tap事件,单击/双击都会触发,0延迟,建议在不使用doubleTap的环境中使用,如果要同时使用tap和doubleTap,请使用singleTap
|
|
_this.trigger('_tap',[touch.x1,touch.y1]);
|
|
_this.trigger('tap',[touch.x1,touch.y1]);
|
|
if (touch.isDoubleTap) {
|
|
cancelAll();
|
|
_this.trigger('_doubleTap',[touch.x1,touch.y1]);
|
|
_this.trigger('doubleTap',[touch.x1,touch.y1]);
|
|
}else {
|
|
touchTimeout = setTimeout(function(){
|
|
touchTimeout = null;
|
|
_this.trigger('_singleTap',[touch.x1,touch.y1]);
|
|
_this.trigger('singleTap',[touch.x1,touch.y1]);
|
|
touch = {};
|
|
}, 250)
|
|
|
|
}
|
|
};
|
|
}
|
|
|
|
function longTap() {
|
|
longTapTimeout = null;
|
|
if (touch.last) {
|
|
_this.trigger('_longTap',[touch.x1,touch.y1]);
|
|
_this.trigger('longTap',[touch.x1,touch.y1]);
|
|
touch = {};
|
|
}
|
|
}
|
|
|
|
function cancelLongTap() {
|
|
if (longTapTimeout) clearTimeout(longTapTimeout);
|
|
longTapTimeout = null;
|
|
}
|
|
|
|
function cancelAll() {
|
|
if (touchTimeout) clearTimeout(touchTimeout);
|
|
if (longTapTimeout) clearTimeout(longTapTimeout);
|
|
touchTimeout = longTapTimeout = null;
|
|
touch = {};
|
|
}
|
|
}
|
|
}
|
|
_.Chart = Chart;
|
|
})(JChart);
|
|
;(function(_){
|
|
function Line(data,cfg){
|
|
_.Scale.apply(this);
|
|
var pointRanges = [];//记录线的节点位置 (for click 事件)
|
|
this._type_ = 'line';
|
|
this.data = data;
|
|
this.chartData = null;
|
|
var _this = this;
|
|
_.extend(this.config,{
|
|
//平滑曲线
|
|
smooth : true,
|
|
//是否显示线的连接点
|
|
showPoint : true,
|
|
//连接圆点半径
|
|
pointRadius : 4,
|
|
//连接点的边框宽度
|
|
pointBorderWidth : 2,
|
|
//连接点的点击范围(方便手指触摸)
|
|
pointClickBounds : 20,
|
|
//连接线的宽度
|
|
lineWidth : 2,
|
|
//是否填充为面积图
|
|
fill : true,
|
|
//是否可以对数据进行拖动
|
|
datasetGesture : false,
|
|
//每次显示的数据条数
|
|
datasetShowNumber : 12
|
|
});
|
|
/**
|
|
* 绑定canvas dom元素上的事件 如:click、touch
|
|
*/
|
|
this.bindEvents = function(){
|
|
//this.ctx.canvas.addEventListener('click',tapHandler);
|
|
this.on('_tap',tapHandler);
|
|
if(this.config.datasetGesture){
|
|
this.bindDataGestureEvent();
|
|
}
|
|
}
|
|
/**
|
|
* 初始化部分元素值
|
|
*/
|
|
this.draw = function(noAnim){
|
|
this.mergeFont(['textFont','scaleFont']);
|
|
if(this.config.datasetGesture && this.data.labels.length > _this.config.datasetShowNumber){
|
|
this.chartData = this.sliceData(this.data,0,this.data.labels.length,this.config.datasetShowNumber);
|
|
}else{
|
|
this.chartData = this.data;
|
|
}
|
|
_this.initScale(true);
|
|
if(noAnim){
|
|
this.drawScale();
|
|
this.drawLines(1);
|
|
}else{
|
|
this.doAnim(this.drawScale,this.drawLines);
|
|
}
|
|
}
|
|
this.redraw = function(data){
|
|
this.chartData = data;
|
|
this.clear();
|
|
this.initScale(true);
|
|
this.drawScale();
|
|
this.drawLines(1);
|
|
}
|
|
this.drawLines = function(animPc){
|
|
if(animPc >= 1)pointRanges = [];
|
|
var ctx = _this.ctx,cfg = _this.config,dataset = _this.chartData.datasets,scale = _this.scaleData;
|
|
_.each(dataset,function(set,i){
|
|
//画连接线
|
|
ctx.beginPath().moveTo(scale.x, yPos(i,0));
|
|
_.each(set.data,function(d,j){
|
|
var pointX = xPos(j),pointY = yPos(i,j);
|
|
if (cfg.smooth){//贝塞尔曲线
|
|
ctx.bezierCurveTo(xPos(j-0.5),yPos(i,j-1),xPos(j-0.5),pointY,pointX,pointY);
|
|
}else{
|
|
ctx.lineTo(pointX,pointY);
|
|
}
|
|
if(animPc >= 1){
|
|
pointRanges.push([pointX,pointY,j,i]);
|
|
}
|
|
});
|
|
ctx.stroke(set.color,cfg.lineWidth);
|
|
//填充区域
|
|
cfg.fill ? ctx.lineTo(scale.x + (scale.xHop*(set.data.length-1)),scale.y).lineTo(scale.x,scale.y).closePath()
|
|
.fill(set.fillColor?set.fillColor : _.hex2Rgb(set.color,0.6)) : ctx.closePath();
|
|
|
|
//画点以及点上文本
|
|
_.each(set.data,function(d,k){
|
|
var x = xPos(k),y = yPos(i,k);
|
|
cfg.showPoint && _this.drawPoint(x,y,set);
|
|
cfg.showText && _this.drawText(d,x,y-3,[k,i]);
|
|
});
|
|
});
|
|
|
|
function yPos(i,j){
|
|
return scale.y - animPc*(_this.calcOffset(dataset[i].data[j],scale.yScaleValue,scale.yHop));
|
|
}
|
|
function xPos(i){
|
|
return scale.x + (scale.xHop * i);
|
|
}
|
|
}
|
|
function tapHandler(x,y){
|
|
var p = isInPointRange(x,y);
|
|
if(p){
|
|
_this.trigger('tap.point',[_this.chartData.datasets[p[3]].data[p[2]],p[2],p[3]]);
|
|
}
|
|
}
|
|
|
|
function isInPointRange(x,y){
|
|
var point,pb = _this.config.pointClickBounds;
|
|
_.each(pointRanges,function(p){
|
|
if(x >= p[0] - pb && x <= p[0] + pb && y >= p[1]-pb && y <= p[1] + pb){
|
|
point = p;
|
|
return false;
|
|
}
|
|
});
|
|
return point;
|
|
}
|
|
|
|
//初始化参数
|
|
if(cfg)this.initial(cfg);
|
|
}
|
|
_.Line = Line;
|
|
})(JChart)
|
|
;(function(_){
|
|
function Pie(data,cfg){
|
|
_.Chart.apply(this);
|
|
var angleRanges;//记录每个扇形的起始角度(从0开始)
|
|
var _this = this;
|
|
this.data = data;
|
|
var radius,totalData = 0,startAngle = 0,rotateAngle = 0,currentOutIndex = -1,origin = {};
|
|
//覆盖配置项
|
|
_.extend(this.config,{
|
|
//border
|
|
showBorder : true,
|
|
//border color
|
|
borderColor : "#fff",
|
|
//border width
|
|
borderWidth : 2,
|
|
//开始角度,默认为12点钟方向
|
|
startAngle : -Math.PI/2,
|
|
//旋转扇形,使其中线对应的角度
|
|
rotateAngle : Math.PI/2,
|
|
//扇形弹出距离
|
|
pullOutDistance : 10,
|
|
//点击扇形默认触发的事件类型
|
|
clickType : 'pullOut',// pullOut||rotate
|
|
//环形图
|
|
isDount : false,
|
|
dountRadiusPercent :0.4,
|
|
totalAngle : Math.PI*2,
|
|
dountText : '',
|
|
dountFont : {
|
|
size : 20,
|
|
style : 600,
|
|
color : '#3498DB'
|
|
}
|
|
});
|
|
/**
|
|
* 计算各个扇形的起始角度
|
|
* @param data
|
|
*/
|
|
function calcAngel(){
|
|
var angle = 0;
|
|
angleRanges = [];
|
|
_.each(_this.data,function(d,i){
|
|
var start = angle;
|
|
var percent = d.value/totalData;
|
|
angle = angle + percent * _this.config.totalAngle;
|
|
var end = angle;
|
|
angleRanges.push([start,end,d,i,percent]);
|
|
})
|
|
}
|
|
|
|
function animRotate(percent){
|
|
drawPie(percent,'rotate');
|
|
}
|
|
|
|
/**
|
|
* 画饼图
|
|
* @param percent 动画比例
|
|
*/
|
|
function drawPie (percent,type){
|
|
_this.clear();
|
|
percent = _this.config.animation ? percent : 1;
|
|
_.each(angleRanges,function(a){
|
|
drawSector(a,percent,type);
|
|
});
|
|
_this.config.isDount && _this.config.dountText && drawDountText();
|
|
}
|
|
|
|
/**
|
|
* 计算扇形真实的角度
|
|
*/
|
|
function calcSectorAngle(r,p,t){
|
|
var start = r[0],end = r[1];
|
|
if(t == 'rotate'){
|
|
//旋转
|
|
start = start + startAngle + rotateAngle*p;
|
|
end = end + startAngle + rotateAngle*p;
|
|
}else{
|
|
//默认动画
|
|
start = start*p + startAngle;
|
|
end = end*p + startAngle
|
|
}
|
|
return {
|
|
start : start,
|
|
end : end
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 画扇形
|
|
* @param i
|
|
* @param animPercent
|
|
*/
|
|
function drawSector(r,p,t){
|
|
var x = origin.x,y = origin.y,cfg = _this.config,
|
|
index = r[3],angle = calcSectorAngle(r,p,t);
|
|
|
|
if(index == currentOutIndex){
|
|
var midAngle = (r[0] + r[1])/2+startAngle;
|
|
x += Math.cos(midAngle) * cfg.pullOutDistance;
|
|
y += Math.sin(midAngle) * cfg.pullOutDistance;
|
|
}
|
|
if(cfg.isDount){
|
|
_this.ctx.dountSector(x,y,radius*cfg.dountRadiusPercent,radius,angle.start,angle.end,_this.data[index].color);
|
|
}else{
|
|
_this.ctx.sector(x,y,radius,angle.start,angle.end,_this.data[index].color);
|
|
}
|
|
cfg.showBorder && _this.ctx.stroke(cfg.borderColor,cfg.borderWidth);
|
|
cfg.showText && drawText(x,y,radius,angle.start,angle.end,r);
|
|
}
|
|
|
|
function drawText(x,y,r,start,end,data){
|
|
//计算文本位置
|
|
var middAngle = (start+end)/ 2, dis = r/ 2,
|
|
percent = data[4],d = data[2];
|
|
if(_this.config.isDount){
|
|
dis = r/2 + r*_this.config.dountRadiusPercent/2;
|
|
}
|
|
percent = (percent * 100).toFixed(1)+'%';
|
|
var xaxis = Math.cos(middAngle) * dis + x, yaxis = Math.sin(middAngle) * dis + y;
|
|
_this.drawText(percent,xaxis,yaxis,[d,data[3],data[4]]);
|
|
}
|
|
function drawDountText(){
|
|
_this.ctx.fillText(_this.config.dountText,origin.x,origin.y,_this.config.dountFont);
|
|
}
|
|
/**
|
|
* 绑定canvas dom元素上的事件 如:click、touch
|
|
*/
|
|
this.bindEvents = function(){
|
|
this.on('_tap',function(x,y){tapHandler(x,y,'tap.pie')});
|
|
//暂时关闭doubleTap事件
|
|
//this.on('_doubleTap',function(x,y){tapHandler(x,y,'doubleTap.pie')});
|
|
this.on('_longTap',function(x,y){tapHandler(x,y,'longTap.pie')});
|
|
//添加一个默认点击事件
|
|
this.on('tap.pie',function(){return true;})
|
|
}
|
|
|
|
function tapHandler(x,y,event){
|
|
var type = _this.config.clickType;
|
|
var angle = isInSegment(x,y);
|
|
if(angle){
|
|
if(event == 'tap.pie'){//处理一些默认行为
|
|
if(!_this.trigger(event,[angle[2],angle[3]]))return;
|
|
if(type == 'rotate'){
|
|
_this.rotate(angle[3]);
|
|
}else if(type == 'pullOut'){
|
|
_this.toggleSegment(angle[3]);
|
|
}
|
|
}else{
|
|
_this.trigger(type,[angle[2],angle[3]]);
|
|
}
|
|
}
|
|
}
|
|
|
|
function isInSegment(offsetX,offsetY){
|
|
var angle;
|
|
var x = offsetX - origin.x;
|
|
var y = offsetY - origin.y;
|
|
//距离圆点的距离
|
|
var dfc = Math.sqrt( Math.pow( Math.abs(x), 2 ) + Math.pow( Math.abs(y), 2 ) );
|
|
var isInPie = (dfc <= radius);
|
|
if(isInPie && _this.config.isDount){//排除dount图中心区
|
|
isInPie = (dfc >= radius*_this.config.dountRadiusPercent);
|
|
}
|
|
if(!isInPie)return;
|
|
var clickAngle = Math.atan2(y, x)-startAngle;
|
|
if ( clickAngle < 0 ) clickAngle += 2 * Math.PI;
|
|
if(clickAngle > 2 * Math.PI) clickAngle -= 2 * Math.PI;
|
|
|
|
_.each(angleRanges,function(a){
|
|
if(clickAngle >= a[0] && clickAngle < a[1]){
|
|
angle = a;
|
|
return false;
|
|
}
|
|
});
|
|
return angle;
|
|
}
|
|
|
|
/**
|
|
* 弹出/收起扇形块
|
|
* @param i 扇形索引
|
|
*/
|
|
this.toggleSegment = function(i){
|
|
if(i == currentOutIndex){
|
|
this.pushIn();
|
|
}else{
|
|
this.pullOut(i);
|
|
}
|
|
}
|
|
/**
|
|
* 收起所有弹出的扇形块
|
|
*/
|
|
this.pushIn = function(){
|
|
currentOutIndex = -1;
|
|
drawPie(1);
|
|
this.trigger('pushIn');
|
|
}
|
|
/**
|
|
* 弹出指定的扇形块
|
|
* @param i 扇形索引
|
|
*/
|
|
this.pullOut = function(i){
|
|
if ( currentOutIndex == i ) return;
|
|
currentOutIndex = i;
|
|
drawPie(1);
|
|
this.trigger('pullOut',[_this.data[i],i,angleRanges[i][4]]);
|
|
}
|
|
/**
|
|
* 旋转扇形块的中线指向6点钟方向
|
|
* @param i 扇形索引
|
|
*/
|
|
this.rotate = function(i){
|
|
if(_this.isAnimating)return;
|
|
var middAngle = (angleRanges[i][0] + angleRanges[i][1]) / 2 + startAngle;
|
|
var newRotateAngle = _this.config.rotateAngle-middAngle;
|
|
if(_.isEqual(newRotateAngle,0))return;
|
|
this.pushIn();
|
|
rotateAngle = newRotateAngle;
|
|
this.doAnim(null,animRotate,function(){
|
|
startAngle += rotateAngle;
|
|
_this.trigger('rotate',[_this.data[i],i,angleRanges[i][4]]);
|
|
});
|
|
}
|
|
this.setDountText = function(text){
|
|
_this.config.dountText = text;
|
|
drawPie(1);
|
|
}
|
|
/**
|
|
* 画图
|
|
*/
|
|
this.draw = function(noAnim){
|
|
this.mergeFont(['textFont','dountFont']);
|
|
calcOrigin();
|
|
totalData = 0;
|
|
currentOutIndex = -1;
|
|
_.each(_this.data,function(d){
|
|
totalData += d.value;
|
|
});
|
|
calcAngel();
|
|
startAngle = _this.config.startAngle;
|
|
if(noAnim){
|
|
drawPie(1);
|
|
}else{
|
|
this.doAnim(null,drawPie);
|
|
}
|
|
}
|
|
//计算原点位置及半径
|
|
function calcOrigin(){
|
|
if(_this.config.totalAngle == Math.PI){
|
|
origin = {
|
|
x : _this.width/2,
|
|
y : _this.height - 20
|
|
}
|
|
radius = Math.min(origin.x,origin.y) - 10;
|
|
}else{
|
|
origin = {x:_this.width/2,y:_this.height/2};
|
|
radius = Math.min(origin.x,origin.y) - 10;
|
|
}
|
|
}
|
|
//初始化参数
|
|
if(cfg)this.initial(cfg);
|
|
}
|
|
_.Pie = Pie;
|
|
}(JChart));
|
|
|
|
;(function(_){
|
|
function Polar(data,cfg){
|
|
_.Scale.apply(this);
|
|
var _this = this;
|
|
this.data = this.chartData = data;
|
|
//配置项
|
|
_.extend(this.config,{
|
|
drawScaleFirst : false,
|
|
//是否显示刻度文本背景
|
|
showScaleLabelBackdrop : true,
|
|
//刻度背景颜色
|
|
scaleBackdropColor : "rgba(255,255,255,0.75)",
|
|
//刻度padding-top bottom
|
|
scaleBackdropPaddingY : 2,
|
|
//刻度padding-left right
|
|
scaleBackdropPaddingX : 2,
|
|
//是否显示角度分割线
|
|
showAngleLine : true,
|
|
//分割线颜色
|
|
angleLineColor : "rgba(0,0,0,.1)",
|
|
showBorder : true,
|
|
borderColor : '#fff',
|
|
borderWidth : 1,
|
|
textFont : {
|
|
size : 16,
|
|
color : '#666',
|
|
textBaseline : 'middle'
|
|
},
|
|
//分割线宽度
|
|
angleLineWidth : 1,
|
|
//是否开启旋转动画
|
|
animateRotate : true,
|
|
//是否开启缩放动画
|
|
animateScale : false
|
|
});
|
|
/**
|
|
* 绑定canvas dom元素上的事件
|
|
*/
|
|
this.bindEvents = function(){
|
|
this.on('_tap',tapHandler);
|
|
}
|
|
|
|
this.draw = function(noAnim){
|
|
this.mergeFont(['scaleFont','textFont']);
|
|
this.initScale();
|
|
if(noAnim){
|
|
this.drawAllSegments(1);
|
|
this.drawScale();
|
|
}
|
|
this.doAnim(this.drawScale,this.drawAllSegments);
|
|
}
|
|
function tapHandler(x,y){
|
|
var i = isInSegment(x,y);
|
|
if(i>-1){
|
|
this.trigger('tap.pie',[this.data[i],i]);
|
|
}
|
|
}
|
|
this.calcDrawingSizes = function(){
|
|
var maxSize = Math.min(this.width,this.height)/2,
|
|
cfg = this.config,size = cfg.scaleFont.size,lh = size*2;
|
|
|
|
maxSize -= Math.max(size*0.5,cfg.scaleLineWidth*0.5);
|
|
if (cfg.showScaleLabelBackdrop){
|
|
lh += (2 * cfg.scaleBackdropPaddingY);
|
|
maxSize -= cfg.scaleBackdropPaddingY*1.5;
|
|
}
|
|
this.scaleData.yHeight = maxSize - 10;
|
|
this.scaleData.yLabelHeight = lh;
|
|
}
|
|
|
|
this.drawScale = function(){
|
|
var cfg = this.config,scale = this.scaleData,x = this.width/2, y = this.height/2
|
|
size = cfg.scaleFont.size,px = cfg.scaleBackdropPaddingX,py = cfg.scaleBackdropPaddingY;
|
|
this.ctx.save().translate(x,y);
|
|
|
|
//画圆圈
|
|
for (var i=0; i<scale.yScaleValue.step; i++){
|
|
var hop = scale.yHop * (i + 1);
|
|
cfg.showGridLine && this.ctx.circle(0, 0, hop,false,cfg.gridLineColor,cfg.gridLineWidth);
|
|
if (cfg.showScaleLabel){
|
|
var label = scale.yScaleValue.labels[i];
|
|
if (cfg.showScaleLabelBackdrop){
|
|
var textWidth = this.ctx.measureText(label).width;
|
|
this.ctx.rect(
|
|
Math.round(- textWidth/2 - px), //X
|
|
Math.round(- hop - size/2 - py),//Y
|
|
Math.round(textWidth + px*2), //Width
|
|
Math.round(size + py*2), //Height
|
|
cfg.scaleBackdropColor
|
|
);
|
|
}
|
|
this.ctx.fillText(label,0,-hop,cfg.scaleFont);
|
|
}
|
|
}
|
|
//画角度分割线
|
|
var len = this.data.labels.length,rotateAngle = (2*Math.PI)/len;
|
|
this.ctx.rotate(-Math.PI/2-rotateAngle);
|
|
_.each(this.data.labels,function(label,i){
|
|
this.ctx.rotate(rotateAngle);
|
|
if(cfg.showAngleLine){
|
|
this.ctx.line(0,0,scale.yHeight,0,cfg.angleLineColor,cfg.angleLineWidth);
|
|
}
|
|
if(cfg.showLabel){
|
|
this.ctx.save().translate(scale.yHeight+10,0).rotate(Math.PI/2 - rotateAngle*i);
|
|
this.ctx.fillText(label,0,0,cfg.textFont);
|
|
this.ctx.restore();
|
|
}
|
|
},this);
|
|
this.ctx.restore();
|
|
}
|
|
|
|
this.drawAllSegments = function(animPc){
|
|
var startAngle = -Math.PI/2,angleStep = (Math.PI*2)/this.data.datasets.length,
|
|
scaleAnim = 1,rotateAnim = 1,
|
|
scale = this.scaleData,cfg = this.config,
|
|
borderColor,borderWidth;
|
|
if (cfg.animation) {
|
|
cfg.animateScale && (scaleAnim = animPc);
|
|
cfg.animateRotate && (rotateAnim = animPc);
|
|
}
|
|
if(cfg.showBorder){
|
|
borderColor = cfg.borderColor;
|
|
borderWidth = cfg.borderWidth;
|
|
}
|
|
_.each(this.data.datasets,function(d){
|
|
var r = scaleAnim * this.calcOffset(d.value,scale.yScaleValue,scale.yHop);
|
|
this.ctx.sector(this.width/2,this.height/2,r,startAngle, startAngle + rotateAnim*angleStep, _.hex2Rgb(d.color,.6),borderColor,borderWidth);
|
|
startAngle += rotateAnim*angleStep;
|
|
},this);
|
|
}
|
|
|
|
this.getValueBounds = function(data){
|
|
var upperValue = Number.MIN_VALUE;
|
|
var lowerValue = Number.MAX_VALUE;
|
|
for (var i=0; i<data.length; i++){
|
|
if (data[i].value > upperValue) {upperValue = data[i].value;}
|
|
if (data[i].value < lowerValue) {lowerValue = data[i].value;}
|
|
};
|
|
var yh = this.scaleData.yHeight;
|
|
var lh = this.scaleData.yLabelHeight;
|
|
var maxSteps = Math.floor((yh/(lh*0.66)));
|
|
var minSteps = Math.floor((yh/lh*0.5));
|
|
return {
|
|
maxValue : upperValue,
|
|
minValue : lowerValue,
|
|
maxSteps : maxSteps,
|
|
minSteps : minSteps
|
|
};
|
|
}
|
|
|
|
function isInSegment(x,y){
|
|
var startAngle = -Math.PI/2,
|
|
angleStep = (Math.PI*2)/this.data.length;
|
|
var x = x-_this.width/ 2,y = y-_this.height/2;
|
|
//距离圆点的距离
|
|
var dfc = Math.sqrt( Math.pow( Math.abs(x), 2 ) + Math.pow( Math.abs(y), 2 ) );
|
|
var isInPie = (dfc <= _this.scaleData.yHeight);
|
|
if(!isInPie)return -1;
|
|
var clickAngle = Math.atan2(y, x)-startAngle;
|
|
if ( clickAngle < 0 ) clickAngle = 2 * Math.PI + clickAngle;
|
|
if(clickAngle > 2 * Math.PI) clickAngle = clickAngle - 2 * Math.PI;
|
|
return Math.floor(clickAngle/angleStep);
|
|
}
|
|
|
|
//初始化参数
|
|
if(cfg)this.initial(cfg);
|
|
|
|
}
|
|
_.Polar = Polar;
|
|
})(JChart);
|
|
;(function (_) {
|
|
function Radar(data, cfg) {
|
|
_.Scale.apply(this);
|
|
var pointRanges = [];//记录线的节点位置 (for click 事件)
|
|
var _this = this;
|
|
this.data = this.chartData = data;
|
|
//配置项
|
|
_.extend(this.config, {
|
|
drawScaleFirst : false,
|
|
//是否显示刻度文本背景
|
|
scaleShowLabelBackdrop:true,
|
|
//刻度背景颜色
|
|
scaleBackdropColor:"rgba(255,255,255,0.75)",
|
|
//刻度padding-top bottom
|
|
scaleBackdropPaddingY:2,
|
|
//刻度padding-left right
|
|
scaleBackdropPaddingX:2,
|
|
//图形形状,菱形 diamond,圆形 circle
|
|
graphShape:'circle',
|
|
//是否显示角度分割线
|
|
showAngleLine:true,
|
|
//角度分割线颜色
|
|
angleLineColor:"rgba(0,0,0,.1)",
|
|
//角度分割线宽度
|
|
angleLineWidth:1,
|
|
//是否显示线的连接点
|
|
showPoint:true,
|
|
//连接圆点半径
|
|
pointRadius:3,
|
|
//连接点的边框宽度
|
|
pointBorderWidth:1,
|
|
//连接点的点击范围(方便手指触摸)
|
|
pointClickBounds:20,
|
|
//连接线的宽度
|
|
lineWidth:2,
|
|
//是否填充为面积图
|
|
fill:true,
|
|
showScaleLabel:true,
|
|
showText : false,
|
|
gridLineColor:'rgb(0,0,0,.5)'
|
|
});
|
|
|
|
/**
|
|
* 绑定canvas dom元素上的事件 如:click、touch
|
|
*/
|
|
this.bindEvents = function () {
|
|
this.on('_tap', tapHandler);
|
|
}
|
|
|
|
this.draw = function (noAnim) {
|
|
this.mergeFont(['scaleFont','textFont']);
|
|
this.initScale();
|
|
if (noAnim) {
|
|
this.drawAllDataPoints(1);
|
|
this.drawScale();
|
|
} else {
|
|
this.doAnim(this.drawScale, this.drawAllDataPoints);
|
|
}
|
|
}
|
|
|
|
function tapHandler(x, y) {
|
|
var p = isInPointRange(x,y);
|
|
if(p){
|
|
_this.trigger('tap.point',[_this.data.datasets[p[3]].data[p[2]],p[2],p[3]]);
|
|
}
|
|
}
|
|
|
|
this.calcDrawingSizes = function () {
|
|
var maxSize = (Math.min(this.width, this.height) / 2),
|
|
cfg = this.config,
|
|
labelHeight = cfg.scaleFont.size * 2;
|
|
var labelLength = 0;
|
|
_.each(_this.data.labels, function (label) {
|
|
this.ctx.set(cfg.textFont);
|
|
var w = this.ctx.measureText(label).width;
|
|
if (w > labelLength) labelLength = w;
|
|
}, this);
|
|
maxSize -= Math.max(labelLength, ((cfg.textFont.size/2) * 1.5));
|
|
maxSize -= cfg.textFont.size;
|
|
maxSize = _.capValue(maxSize, null, 0);
|
|
this.scaleData.yHeight = maxSize;
|
|
this.scaleData.yLabelHeight = labelHeight;
|
|
}
|
|
|
|
this.drawScale = function () {
|
|
var ctx = this.ctx, cfg = this.config, scale = this.scaleData,scaleSize = cfg.scaleFont.size,textSize = cfg.textFont.size,
|
|
dataLen = this.data.labels.length,px = cfg.scaleBackdropPaddingX,py = cfg.scaleBackdropPaddingY;
|
|
//计算每条数据的角度
|
|
var rotationDegree = (2 * Math.PI) / dataLen;
|
|
ctx.save().translate(this.width / 2, this.height / 2);
|
|
//显示角度分割线
|
|
if (cfg.showAngleLine) {
|
|
var w = scale.yHeight - (scale.yHeight % scale.yHop);
|
|
//画每个角度的分割线
|
|
for (var h = 0; h < dataLen; h++) {
|
|
ctx.rotate(rotationDegree).line(0,0,0,-w,cfg.angleLineColor,cfg.angleLineWidth);
|
|
}
|
|
}
|
|
//画刻度线
|
|
for (var i = 0; i < scale.yScaleValue.step; i++) {
|
|
var hop = scale.yHop * (i + 1);
|
|
ctx.beginPath();
|
|
if (cfg.showGridLine) {
|
|
ctx.set({strokeStyle : cfg.gridLineColor,lineWidth : cfg.gridLineWidth})
|
|
if (cfg.graphShape == 'diamond') {
|
|
ctx.moveTo(0, -hop);
|
|
for (var j = 0; j < dataLen; j++) {
|
|
ctx.rotate(rotationDegree).lineTo(0, -hop);
|
|
}
|
|
} else {
|
|
ctx.circle(0, 0, hop);
|
|
}
|
|
ctx.closePath().stroke();
|
|
}
|
|
//画刻度值
|
|
if (cfg.showScaleLabel) {
|
|
var label = scale.yScaleValue.labels[i];
|
|
//显示刻度值的背景
|
|
if (cfg.showScaleLabelBackdrop){
|
|
var textWidth = this.ctx.measureText(label).width;
|
|
this.ctx.rect(
|
|
Math.round(-textWidth/2 - px), //X
|
|
Math.round(-hop - scaleSize/2 - py),//Y
|
|
Math.round(textWidth + px*2), //Width
|
|
Math.round(scaleSize + py*2), //Height
|
|
cfg.scaleBackdropColor
|
|
);
|
|
}
|
|
this.ctx.fillText(label,0,-hop,cfg.scaleFont);
|
|
}
|
|
|
|
}
|
|
|
|
//设置文本样式
|
|
this.ctx.set(cfg.textFont);
|
|
//显示数据标签文本
|
|
for (var k = 0; k < dataLen; k++) {
|
|
var opposite = Math.sin(rotationDegree * k) * (scale.yHeight + textSize);
|
|
var adjacent = Math.cos(rotationDegree * k) * (scale.yHeight + textSize);
|
|
var align;
|
|
if (rotationDegree * k == Math.PI || rotationDegree * k == 0) {
|
|
align = 'center';
|
|
} else if (rotationDegree * k > Math.PI) {
|
|
align = 'right';
|
|
}else {
|
|
align = 'left';
|
|
}
|
|
this.ctx.fillText(this.data.labels[k], opposite, -adjacent,{textAlign:align});
|
|
}
|
|
ctx.restore();
|
|
}
|
|
|
|
this.drawAllDataPoints = function (animPc) {
|
|
if (animPc >= 1)pointRanges = [];
|
|
var dataLen = data.datasets[0].data.length,
|
|
rotationDegree = (2 * Math.PI) / dataLen,
|
|
scale = this.scaleData,
|
|
ctx = this.ctx, cfg = this.config;
|
|
ctx.save().translate(this.width / 2, this.height / 2);
|
|
_.each(this.data.datasets, function (set, i) {
|
|
ctx.beginPath().moveTo(0, getY(set.data[0]));
|
|
//画连接线
|
|
_.each(set.data, function (d, j) {
|
|
if (j == 0)return true;
|
|
ctx.rotate(rotationDegree).lineTo(0, getY(d));
|
|
});
|
|
ctx.closePath();
|
|
|
|
cfg.fill && ctx.fill(set.fillColor||_.hex2Rgb(set.color,0.6));
|
|
ctx.stroke(set.color,cfg.lineWidth);
|
|
|
|
//画连接点
|
|
_.each(set.data,function(d,j){
|
|
var y = getY(d);
|
|
if (cfg.showPoint) {
|
|
ctx.rotate(rotationDegree).circle(0, y, cfg.pointRadius,set.pointColor,set.pointBorderColor,cfg.pointBorderWidth);
|
|
}
|
|
if(animPc >= 1){
|
|
var p = getPosition(y,j);
|
|
pointRanges.push([p[0],p[1],j,i]);
|
|
}
|
|
});
|
|
ctx.rotate(rotationDegree);
|
|
|
|
}, this);
|
|
ctx.restore();
|
|
if(cfg.showText){
|
|
drawText();
|
|
}
|
|
function getY(d){
|
|
return -animPc * _this.calcOffset(d, scale.yScaleValue, scale.yHop);
|
|
}
|
|
function getPosition(radius,i){
|
|
radius = Math.abs(radius);
|
|
var x,y;
|
|
var angel = -Math.PI/2 + i * rotationDegree;
|
|
x = Math.cos(angel)*radius + _this.width/2;
|
|
y = Math.sin(angel)*radius + _this.height/2;
|
|
return [x,y];
|
|
}
|
|
}
|
|
|
|
function drawText(){
|
|
_.each(pointRanges,function(p){
|
|
var y = p[1];
|
|
if(y > _this.height/2){
|
|
y += 6;
|
|
}
|
|
_this.drawText(_this.data.datasets[p[3]].data[p[2]],p[0],y,[p[2],p[3]]);
|
|
});
|
|
|
|
}
|
|
|
|
function isInPointRange(x,y){
|
|
var point,pb = _this.config.pointClickBounds;
|
|
_.each(pointRanges,function(p){
|
|
if(x >= p[0] - pb && x <= p[0] + pb && y >= p[1]-pb && y <= p[1] + pb){
|
|
point = p;
|
|
return false;
|
|
}
|
|
});
|
|
return point;
|
|
}
|
|
//初始化参数
|
|
if (cfg)this.initial(cfg);
|
|
}
|
|
_.Radar = Radar;
|
|
})(JChart);
|
|
;(function(_){
|
|
/**
|
|
* 抽象类-刻度值
|
|
* 用来初始化XY轴各项数据
|
|
* @constructor
|
|
*/
|
|
function Scale(){
|
|
var P_T = 5,//图表顶部空白
|
|
P_R = 5,//图表右侧空白
|
|
P_Y = 20,//y轴左侧空白
|
|
P_X = 10;//x轴文本与x之间的间距
|
|
_.Chart.apply(this);
|
|
_.extend(this.config,{
|
|
/**
|
|
* @Object
|
|
* Y轴刻度值,默认为null,会自动生成,也可以自己指定
|
|
* {
|
|
* step : 10,//刻度个数,必选项
|
|
* stepValue : 10//每两个刻度线之间的差值,必选项
|
|
* start : 0//起始刻度值,默认为0
|
|
* }
|
|
*/
|
|
scale : null,
|
|
//xy轴刻度线的颜色
|
|
scaleLineColor : "rgba(0,0,0,.3)",
|
|
//刻度线宽度
|
|
scaleLineWidth:1,
|
|
//是否显示Y轴刻度值
|
|
showScaleLabel : true,
|
|
//是否显示X轴刻度值
|
|
showLabel : true,
|
|
//刻度值字体属性
|
|
scaleFont : {
|
|
size:12,
|
|
color : '#666'
|
|
},
|
|
textFont : {
|
|
size : 14,
|
|
textBaseline : 'bottom'
|
|
},
|
|
//是否显示网格线
|
|
showGridLine : true,
|
|
//网格线颜色
|
|
gridLineColor : "rgba(0,0,0,.1)",
|
|
//网格线宽度
|
|
gridLineWidth : 1,
|
|
//水平线{value : 50,color : #fff,width : 1}
|
|
horizonLine : null
|
|
|
|
});
|
|
//数据偏移量-已经偏移
|
|
this.dataOffset = 0;
|
|
this.scaleData = {
|
|
x : 0,//圆点坐标
|
|
y : 0,
|
|
xHop : 0,//x轴数据项宽度
|
|
yHop : 0,//y轴每个刻度的高度
|
|
xLength : 0,//x轴长度
|
|
yHeight : 0,//y轴高度
|
|
yLabelHeight : 0,//y轴刻度文本高度
|
|
yScaleValue : null,//y轴刻度指标
|
|
labelRotate : 0,//x轴label旋转角度
|
|
xLabelWidth : 0,//x轴label宽度
|
|
xLabelHeight : 0,//x轴label宽度
|
|
barWidth : 0//柱形图柱子宽度
|
|
}
|
|
/**
|
|
* 计算X轴文本宽度、旋转角度及Y轴高度
|
|
*/
|
|
this.calcDrawingSizes = function(){
|
|
var maxSize = this.height,widestX = 0,scaleFontSize = this.config.scaleFont.size, xLabelWidth = 0,xLabelHeight = scaleFontSize,
|
|
labelRotate = 0,dataLen = this.chartData.labels.length;
|
|
//计算X轴,如果发现数据宽度超过总宽度,需要将label进行旋转
|
|
this.ctx.set(this.config.scaleFont);
|
|
//找出最宽的label
|
|
_.each(this.chartData.labels,function(o){
|
|
var w = this.ctx.measureText(o).width;
|
|
widestX = (w > widestX)? w : widestX;
|
|
},this);
|
|
xLabelWidth = widestX;
|
|
if (this.width/dataLen < widestX){
|
|
labelRotate = 45;
|
|
xLabelWidth = Math.cos(labelRotate*Math.PI/180) * widestX;
|
|
xLabelHeight = Math.sin(labelRotate*Math.PI/180) * widestX ;
|
|
if (this.width/dataLen < xLabelHeight){
|
|
labelRotate = 90;
|
|
xLabelWidth = scaleFontSize;
|
|
xLabelHeight = widestX;
|
|
}
|
|
}
|
|
//减去x轴label的高度
|
|
maxSize -= xLabelHeight;
|
|
//减去x轴文本与x轴之间的间距
|
|
maxSize -= P_X;
|
|
//给Y轴顶部留一点空白
|
|
maxSize -= P_T;
|
|
maxSize -= this.config.showText?scaleFontSize:0;
|
|
//y轴高度
|
|
this.scaleData.yHeight = maxSize;
|
|
//y轴刻度高度
|
|
this.scaleData.yLabelHeight = scaleFontSize;
|
|
//x轴文本旋转角度
|
|
this.scaleData.labelRotate = labelRotate;
|
|
//x轴文本的宽度
|
|
this.scaleData.xLabelWidth = xLabelWidth;
|
|
//x轴文本的高度
|
|
this.scaleData.xLabelHeight = xLabelHeight;
|
|
}
|
|
|
|
/**
|
|
* 计算Y轴刻度的边界及刻度步数
|
|
* @return {Object}
|
|
*/
|
|
this.getValueBounds = function(dataset) {
|
|
var upperValue = Number.MIN_VALUE;
|
|
var lowerValue = Number.MAX_VALUE;
|
|
_.each(dataset,function(o){
|
|
_.each(o.data,function(obj){
|
|
if(obj > upperValue){upperValue = obj};
|
|
if (obj < lowerValue) { lowerValue = obj};
|
|
});
|
|
})
|
|
var yh = this.scaleData.yHeight;
|
|
var lh = this.scaleData.yLabelHeight;
|
|
var maxSteps = Math.floor((yh/(lh*0.66)));
|
|
var minSteps = Math.floor((yh/lh*0.5));
|
|
|
|
return {
|
|
maxValue : upperValue,
|
|
minValue : lowerValue,
|
|
maxSteps : maxSteps,
|
|
minSteps : minSteps
|
|
};
|
|
}
|
|
|
|
/**
|
|
* 计算Y轴刻度的各项数据
|
|
*/
|
|
this.calcYAxis = function(){
|
|
var scale = this.config.scale;
|
|
if (scale){
|
|
scale.start = scale.start || 0;
|
|
scale.labels = this.populateLabels(scale.step,scale.start,scale.stepValue);
|
|
}else {
|
|
var bounds = this.getValueBounds(this.chartData.datasets);
|
|
scale = this.calcScale(this.scaleData.yHeight,bounds.maxSteps,bounds.minSteps,bounds.maxValue,bounds.minValue);
|
|
}
|
|
this.scaleData.yScaleValue = scale;
|
|
this.scaleData.yHop = Math.floor(this.scaleData.yHeight/scale.step);
|
|
}
|
|
|
|
/**
|
|
* 计算X轴宽度,每个数据项宽度大小及坐标原点
|
|
*/
|
|
this.calcXAxis = function(){
|
|
var config = this.config,scale = this.scaleData,yLabelWidth = 0,xAxisLength,valueHop, x,y;
|
|
if (config.showScaleLabel){
|
|
//找出Y轴刻度的最宽值
|
|
_.each(scale.yScaleValue.labels,function(o){
|
|
var w = this.ctx.measureText(o).width;
|
|
yLabelWidth = (w > yLabelWidth)? w : yLabelWidth;
|
|
},this);
|
|
yLabelWidth += P_Y;
|
|
}
|
|
//x轴的宽度
|
|
xAxisLength = this.width - yLabelWidth-P_R-(this.config.showText?this.config.textFont.size:0);
|
|
|
|
if(this._type_ == 'bar'){//计算柱形图柱子宽度,柱形图x轴文本居中显示,需要重新计算数据项宽度
|
|
valueHop = Math.floor(xAxisLength/this.chartData.labels.length);
|
|
var len = this.chartData.datasets.length;
|
|
scale.barWidth = (valueHop - config.gridLineWidth*2 - (config.barSetSpacing*2) - (config.barSpacing*len-1) - ((config.barBorderWidth/2)*len-1))/len;
|
|
}else{
|
|
valueHop = Math.floor(xAxisLength/(this.chartData.labels.length-1));
|
|
}
|
|
scale.x = yLabelWidth;
|
|
scale.y = this.height - scale.xLabelHeight - P_X;
|
|
scale.xWidth = xAxisLength;
|
|
scale.xHop = valueHop;
|
|
}
|
|
|
|
this.drawScale = function(){
|
|
var ctx = this.ctx,cfg = this.config,scale = this.scaleData,align;
|
|
ctx.set({
|
|
strokeStyle : cfg.scaleLineColor,
|
|
lineWidth : cfg.scaleLineWidth
|
|
})
|
|
//画X轴
|
|
ctx.line(scale.x-3, scale.y, scale.x+scale.xWidth, scale.y, true);
|
|
//画Y轴
|
|
ctx.line(scale.x,scale.y+3, scale.x,scale.y-scale.yHeight, true);
|
|
|
|
//画X轴刻度文本
|
|
if (scale.labelRotate > 0){
|
|
ctx.save();
|
|
align = 'right';
|
|
}else{
|
|
align = 'center';
|
|
}
|
|
ctx.set({
|
|
fillStyle : cfg.scaleFont.color,
|
|
textAlign : align,
|
|
textBaseline : 'hanging',
|
|
strokeStyle : cfg.gridLineColor,
|
|
lineWidth : cfg.gridLineWidth
|
|
});
|
|
_.each(this.chartData.labels,function(label,i){
|
|
ctx.save();
|
|
var cx = scale.x + i*scale.xHop,labelY = scale.y + P_X/ 2,
|
|
labelX = this._type_ == 'bar'?cx + scale.xHop/2 : cx;
|
|
if (scale.labelRotate > 0){
|
|
ctx.translate(labelX,labelY).rotate(-(scale.labelRotate * (Math.PI/180))).fillText(label,0,0).restore();
|
|
}else{
|
|
ctx.fillText(label, labelX,labelY);
|
|
}
|
|
//画纵向的网格线
|
|
if(cfg.showGridLine){
|
|
var x = (this._type_ == 'bar')?cx + scale.xHop : cx;
|
|
ctx.line(x, scale.y, x, scale.y-scale.yHeight,true);
|
|
}
|
|
},this);
|
|
|
|
//画横向网格线
|
|
ctx.set({textAlign:'right',textBaseline:'middle'});
|
|
for (var j=0; j<scale.yScaleValue.step; j++){
|
|
var y = scale.y - ((j+1) * scale.yHop);
|
|
cfg.showGridLine && ctx.line(scale.x,y,scale.x + scale.xWidth,y, true);
|
|
cfg.showScaleLabel && ctx.fillText(scale.yScaleValue.labels[j],scale.x-P_Y/2,y);
|
|
}
|
|
//绘制水平线
|
|
if(cfg.horizonLine){
|
|
var y = scale.y - this.calcOffset(cfg.horizonLine.value,scale.yScaleValue,scale.yHop);
|
|
ctx.line(scale.x,y,scale.x + scale.xWidth,y, cfg.horizonLine.color || '#E74C3C',cfg.horizonLine.width||1);
|
|
}
|
|
|
|
}
|
|
|
|
this.initScale = function(showX){
|
|
this.calcDrawingSizes();
|
|
this.calcYAxis();
|
|
showX && this.calcXAxis();
|
|
}
|
|
this.drawPoint = function(x,y,d){
|
|
//填充色默认为白色,边框颜色默认与线条颜色一致
|
|
this.ctx.beginPath().circle(x,y,this.config.pointRadius,d.pointColor || '#fff',d.pointBorderColor || d.color,this.config.pointBorderWidth);
|
|
}
|
|
|
|
/**
|
|
* 计算坐标轴的刻度
|
|
* @param drawingHeight
|
|
* @param maxSteps
|
|
* @param minSteps
|
|
* @param maxValue
|
|
* @param minValue
|
|
*/
|
|
this.calcScale = function(drawingHeight,maxSteps,minSteps,maxValue,minValue){
|
|
var min,max,range,stepValue,step,valueRange,rangeOrderOfMagnitude;
|
|
|
|
valueRange = maxValue - minValue;
|
|
|
|
rangeOrderOfMagnitude = calculateOrderOfMagnitude(valueRange);
|
|
|
|
//min = Math.floor(minValue / (1 * Math.pow(10, rangeOrderOfMagnitude))) * Math.pow(10, rangeOrderOfMagnitude);
|
|
min = 0;//固定起始点为0
|
|
|
|
max = Math.ceil(maxValue / (1 * Math.pow(10, rangeOrderOfMagnitude))) * Math.pow(10, rangeOrderOfMagnitude);
|
|
|
|
range = max - min;
|
|
|
|
stepValue = Math.pow(10, rangeOrderOfMagnitude);
|
|
|
|
step = Math.round(range / stepValue);
|
|
|
|
//Compare number of steps to the max and min for that size graph, and add in half steps if need be.
|
|
while(step < minSteps || step > maxSteps) {
|
|
if (step < minSteps){
|
|
stepValue /= 2;
|
|
step = Math.round(range/stepValue);
|
|
}
|
|
else{
|
|
stepValue *=2;
|
|
step = Math.round(range/stepValue);
|
|
}
|
|
};
|
|
var labels = this.populateLabels(step, min, stepValue);;
|
|
return {
|
|
step : step,
|
|
stepValue : stepValue,
|
|
start : min,
|
|
labels : labels
|
|
}
|
|
function calculateOrderOfMagnitude(val){
|
|
return Math.floor(Math.log(val) / Math.LN10);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 构造刻度值
|
|
* @param labels
|
|
* @param numberOfSteps
|
|
* @param graphMin
|
|
* @param stepValue
|
|
*/
|
|
this.populateLabels = function (step, start, stepValue) {
|
|
var labels = [];
|
|
for (var i = 1; i < step + 1; i++) {
|
|
if(!this.config.showScaleLabel){
|
|
labels.push('');
|
|
continue;
|
|
}
|
|
//小数点位数与stepValue后的小数点一致
|
|
var value = (start + (stepValue * i)).toFixed(_.getDecimalPlaces(stepValue));
|
|
var text = this.trigger('renderYLabel',[value]);
|
|
text = text ? text : value;
|
|
labels.push(text);
|
|
}
|
|
return labels;
|
|
},
|
|
this.calcOffset = function(val,scale,scaleHop){
|
|
var outerValue = scale.step * scale.stepValue;
|
|
var adjustedValue = val - scale.start;
|
|
var scalingFactor = _.capValue(adjustedValue/outerValue,1,0);
|
|
return (scaleHop*scale.step) * scalingFactor;
|
|
},
|
|
|
|
this.sliceData = function(data,offset,len,num){
|
|
var newdata = _.clone(data);
|
|
var min = offset,max = offset + num;
|
|
if(max > len){
|
|
min = len - num;
|
|
max = len;
|
|
}
|
|
newdata.labels = newdata.labels.slice(min,max);
|
|
_.each(newdata.datasets,function(d){
|
|
d.data = d.data.slice(min,max)
|
|
});
|
|
return newdata;
|
|
}
|
|
this.bindDataGestureEvent = function(){
|
|
var _this = this,
|
|
touchDistanceX,//手指滑动偏移量
|
|
startPosition,//触摸初始位置记录
|
|
currentOffset = 0,//当前一次滑动的偏移量
|
|
dataNum = this.config.datasetShowNumber,//每屏显示的数据条数
|
|
gestureStarted,
|
|
hasTouch = 'ontouchstart' in window,
|
|
START_EV = hasTouch ? 'touchstart' : 'mousedown',
|
|
MOVE_EV = hasTouch ? 'touchmove' : 'mousemove',
|
|
END_EV = hasTouch ? 'touchend' : 'mouseup';
|
|
|
|
this.ctx.el.addEventListener(START_EV,touchstart);
|
|
this.ctx.el.addEventListener(MOVE_EV,touchmove);
|
|
this.ctx.el.addEventListener(END_EV,touchend);
|
|
|
|
function touchstart(e){
|
|
e = e.touches ? e.touches[0] : e;
|
|
startPosition = {
|
|
x : e.pageX,
|
|
y : e.pageY
|
|
}
|
|
touchDistanceX = 0;
|
|
gestureStarted = true;
|
|
}
|
|
function touchmove(e){
|
|
if(!gestureStarted || !_this.config.datasetGesture)return;
|
|
e = e.touches ? e.touches[0] : e;
|
|
var x = e.pageX;
|
|
var y = e.pageY;
|
|
touchDistanceX = x - startPosition.x;
|
|
//每滑动xHop加载下一组数据
|
|
var totalLen = _this.data.labels.length;//数据总长度
|
|
var offset = _this.dataOffset - Math.floor(touchDistanceX/_this.scaleData.xHop);
|
|
if(offset < 0 || offset == currentOffset||(offset+dataNum > totalLen))return;
|
|
currentOffset = offset;
|
|
console.log(offset);
|
|
//将操作加入系统队列,解决android系统下touchmove的bug
|
|
setTimeout(function(){
|
|
_this.redraw(_this.sliceData(_this.data,offset,totalLen,dataNum));
|
|
},0)
|
|
}
|
|
function touchend(event){
|
|
gestureStarted = false;
|
|
_this.dataOffset = currentOffset;
|
|
}
|
|
}
|
|
}
|
|
_.Scale = Scale;
|
|
})(JChart); |