由于项目中需要使用到speedo meter chart,但是又不能使用flash(因为要支持iphone),而speedo的js插件网上貌似很难找到,找到了一个但是却不是很满意,因为没办法灵活的定义UI(如果有需要链接在这里http://plugins.jquery.com/project/speedometer),因为实现起来不是很复杂,而且刚好的flot插件中也有一个pie的plugin,想了一下通过修改这个plugin来实现的话就可以了,下面给出这个代码的api定义:
// 这里的API是flot相关的,具体还需参见flot的API定义
series: {
meter:{ // 定义了一个meter object,所有的参数定义都在这里 show: true, // 是否显示该chart label: { // 是否显示各个段的百分比及名称 show: false },
radius: 1, // 圆的半径定义,数值是0-1,是一个比例值,基数是整个chart的宽度的一半,默认是1
innerRadius: 0.5, // 内圆的半径定义,也就是空白部分的范围,通过它来调整遮罩范围,也是一个比例值,同上,默认是0.5
currentValue: 0, // 当前此chart对应指针指定的数值,如果没有默认是0
pointerLength: 0.75, // 指针长度,是一个比例值,基数是radius的长度,默认是0.75
maxScale: 100, // 当前chart反应的最大值标量,如果没有输入默认显示100%,接受一个浮点型的数
title: 'the speedo chart' // chart的title,默认是speedo chart }
}
你还可以为chart所对应的div做css定义,这样的话就可以有不同的边框和背景了,可以根据你的网站样式作出不同的调整,而meter的颜色定义则还是由flot的定义给出,这里不做修改
下面贴出整个插件的源码:
(function ($) { function init(plot) // this is the "body" of the plugin { var canvas = null; var target = null; var maxRadius = null; var currentRadius = null; var centerLeft = null; var centerTop = null; var total = 0; var redraw = true; var redrawAttempts = 10; var shrink = 0.95; var legendWidth = 0; var processed = false; var raw = false; // interactive variables var highlights = []; // add hook to determine if meter plugin in enabled, and then perform necessary operations plot.hooks.processOptions.push(checkPieEnabled); plot.hooks.bindEvents.push(bindEvents); // check to see if the meter plugin is enabled function checkPieEnabled(plot, options) { if (options.series.meter.show) { //disable grid options.grid.show = false; // set labels.show if (options.series.meter.label.show=='auto') if (options.legend.show) options.series.meter.label.show = false; else options.series.meter.label.show = true; // set radius if (options.series.meter.radius=='auto') if (options.series.meter.label.show) options.series.meter.radius = 3/4; else options.series.meter.radius = 1; if(!options.series.meter.innerRadius) options.series.meter.innerRadius = options.series.meter.radius/2; if (!options.series.meter.title) options.series.meter.title = "Meter Chart"; if(!options.series.meter.currentValue) options.series.meter.currentValue = 0; if(!options.series.meter.pointerLength) options.series.meter.pointerLength = options.series.meter.radius*3/4; // ensure sane tilt if (options.series.meter.tilt>1) options.series.meter.tilt=1; if (options.series.meter.tilt<0) options.series.meter.tilt=0; if (!options.series.meter.maxScale) options.series.meter.maxScale="100%"; // treate as percentage value options.series.meter.startAngle = 1; // add processData hook to do transformations on the data plot.hooks.processDatapoints.push(processDatapoints); plot.hooks.drawOverlay.push(drawOverlay); // add draw hook plot.hooks.draw.push(draw); } } // bind hoverable events function bindEvents(plot, eventHolder) { var options = plot.getOptions(); if (options.series.meter.show && options.grid.hoverable) eventHolder.unbind('mousemove').mousemove(onMouseMove); if (options.series.meter.show && options.grid.clickable) eventHolder.unbind('click').click(onClick); } // debugging function that prints out an object function alertObject(obj) { var msg = ''; function traverse(obj, depth) { if (!depth) depth = 0; for (var i = 0; i < obj.length; ++i) { for (var j=0; j<depth; j++) msg += '\t'; if( typeof obj[i] == "object") { // its an object msg += ''+i+':\n'; traverse(obj[i], depth+1); } else { // its a value msg += ''+i+': '+obj[i]+'\n'; } } } traverse(obj); alert(msg); } function calcTotal(data) { for (var i = 0; i < data.length; ++i) { var item = parseFloat(data[i].data[0][1]); if (item) total += item; } } function processDatapoints(plot, series, data, datapoints) { if (!processed) { processed = true; canvas = plot.getCanvas(); target = $(canvas).parent(); options = plot.getOptions(); plot.setData(combine(plot.getData())); } } function setupPie() { legendWidth = target.children().filter('.legend').children().width(); // calculate maximum radius and center point maxRadius = canvas.width/2; centerTop = (canvas.height)+options.series.meter.offset.top; centerLeft = (canvas.width/2); if (options.series.meter.offset.left=='auto') if (options.legend.position.match('w')) centerLeft += legendWidth/2; else centerLeft -= legendWidth/2; else centerLeft += options.series.meter.offset.left; if (centerLeft<maxRadius) centerLeft = maxRadius; else if (centerLeft>canvas.width-maxRadius) centerLeft = canvas.width-maxRadius; } function fixData(data) { for (var i = 0; i < data.length; ++i) { if (typeof(data[i].data)=='number') data[i].data = [[1,data[i].data]]; else if (typeof(data[i].data)=='undefined' || typeof(data[i].data[0])=='undefined') { if (typeof(data[i].data)!='undefined' && typeof(data[i].data.label)!='undefined') data[i].label = data[i].data.label; // fix weirdness coming from flot data[i].data = [[1,0]]; } } return data; } function combine(data) { data = fixData(data); calcTotal(data); var combined = 0; var numCombined = 0; var color = options.series.meter.combine.color; var newdata = []; for (var i = 0; i < data.length; ++i) { // make sure its a number data[i].data[0][1] = parseFloat(data[i].data[0][1]); if (!data[i].data[0][1]) data[i].data[0][1] = 0; if (data[i].data[0][1]/total<=options.series.meter.combine.threshold) { combined += data[i].data[0][1]; numCombined++; if (!color) color = data[i].color; } else { newdata.push({ data: [[1,data[i].data[0][1]]], color: data[i].color, label: data[i].label, angle: (data[i].data[0][1]*(Math.PI))/total, percent: (data[i].data[0][1]/total*100) }); } } if (numCombined>0) newdata.push({ data: [[1,combined]], color: color, label: options.series.meter.combine.label, angle: (combined*(Math.PI))/total, percent: (combined/total*100) }); return newdata; } function draw(plot, newCtx) { if (!target) return; // if no series were passed ctx = newCtx; setupPie(); var slices = plot.getData(); var attempts = 0; while (redraw && attempts<redrawAttempts) { redraw = false; if (attempts>0) maxRadius *= shrink; attempts += 1; clear(); if (options.series.meter.tilt<=0.8) drawShadow(); drawPie(); } if (attempts >= redrawAttempts) { clear(); target.prepend('<div class="error">Could not draw meter with labels contained inside canvas</div>'); } if ( plot.setSeries && plot.insertLegend ) { plot.setSeries(slices); plot.insertLegend(); } // we're actually done at this point, just defining internal functions at this point function clear() { ctx.clearRect(0,0,canvas.width,canvas.height); target.children().filter('.pieLabel, .pieLabelBackground').remove(); } function drawShadow() { var shadowLeft = 5; var shadowTop = 15; var edge = 10; var alpha = 0.02; // set radius if (options.series.meter.radius>1) var radius = options.series.meter.radius; else var radius = maxRadius * options.series.meter.radius; if (radius>=(canvas.width/2)-shadowLeft || radius*options.series.meter.tilt>=(canvas.height/2)-shadowTop || radius<=edge) return; // shadow would be outside canvas, so don't draw it ctx.save(); ctx.translate(shadowLeft,shadowTop); ctx.globalAlpha = alpha; ctx.fillStyle = '#000'; // center and rotate to starting position ctx.translate(centerLeft,centerTop); ctx.scale(1, options.series.meter.tilt); //radius -= edge; for (var i=1; i<=edge; i++) { ctx.beginPath(); ctx.arc(0,0,radius,0,Math.PI,false); ctx.fill(); radius -= i; } ctx.restore(); } function drawPie() { startAngle = Math.PI*options.series.meter.startAngle; // set radius if (options.series.meter.radius>1) var radius = options.series.meter.radius; else var radius = maxRadius * options.series.meter.radius; // center and rotate to starting position ctx.save(); ctx.translate(centerLeft,centerTop); ctx.scale(1, options.series.meter.tilt); //ctx.rotate(startAngle); // start at top; -- This doesn't work properly in Opera // draw slices ctx.save(); var currentAngle = startAngle; for (var i = 0; i < slices.length; ++i) { slices[i].startAngle = currentAngle; drawSlice(slices[i].angle, slices[i].color, true); } ctx.restore(); // draw slice outlines ctx.save(); ctx.lineWidth = options.series.meter.stroke.width; currentAngle = startAngle; for (var i = 0; i < slices.length; ++i) drawSlice(slices[i].angle, options.series.meter.stroke.color, false); ctx.restore(); // draw donut hole drawDonutHole(ctx); // draw labels if (options.series.meter.label.show) drawLabels(); printTitle(); // restore to original state ctx.restore(); // print scale printScaleText(); // draw pointer drawPointer(); // print current scale printCurentScaleValue(); function drawSlice(angle, color, fill) { if (angle<=0) return; if (fill) ctx.fillStyle = color; else { ctx.strokeStyle = color; ctx.lineJoin = 'round'; } ctx.beginPath(); if (Math.abs(angle - Math.PI) > 0.000000001) ctx.moveTo(0,0); // Center of the meter else if ($.browser.msie) angle -= 0.0001; //ctx.arc(0,0,radius,0,angle,false); // This doesn't work properly in Opera ctx.arc(0,0,radius,currentAngle,currentAngle+angle,false); ctx.closePath(); //ctx.rotate(angle); // This doesn't work properly in Opera currentAngle += angle; if (fill) ctx.fill(); else ctx.stroke(); } function drawLabels() { var currentAngle = startAngle; // set radius if (options.series.meter.label.radius>1) var radius = options.series.meter.label.radius; else var radius = maxRadius * options.series.meter.label.radius; for (var i = 0; i < slices.length; ++i) { if (slices[i].percent >= options.series.meter.label.threshold*100) drawLabel(slices[i], currentAngle, i); currentAngle += slices[i].angle; } function drawLabel(slice, startAngle, index) { if (slice.data[0][1]==0) return; // format label text var lf = options.legend.labelFormatter, text, plf = options.series.meter.label.formatter; if (lf) text = lf(slice.label, slice); else text = slice.label; if (plf) text = plf(text, slice); var halfAngle = ((startAngle+slice.angle) + startAngle)/2; var x = centerLeft + Math.round(Math.cos(halfAngle) * radius); var y = centerTop + Math.round(Math.sin(halfAngle) * radius) * options.series.meter.tilt; var html = '<span class="pieLabel" id="pieLabel'+index+'" style="position:absolute;top:' + y + 'px;left:' + x + 'px;">' + text + "</span>"; target.append(html); var label = target.children('#pieLabel'+index); var labelTop = (y - label.height()/2); var labelLeft = (x - label.width()/2); label.css('top', labelTop); label.css('left', labelLeft); // check to make sure that the label is not outside the canvas if (0-labelTop>0 || 0-labelLeft>0 || canvas.height-(labelTop+label.height())<0 || canvas.width-(labelLeft+label.width())<0) redraw = true; if (options.series.meter.label.background.opacity != 0) { // put in the transparent background separately to avoid blended labels and label boxes var c = options.series.meter.label.background.color; if (c == null) { c = slice.color; } var pos = 'top:'+labelTop+'px;left:'+labelLeft+'px;'; $('<div class="pieLabelBackground" style="position:absolute;width:' + label.width() + 'px;height:' + label.height() + 'px;' + pos +'background-color:' + c + ';"> </div>').insertBefore(label).css('opacity', options.series.meter.label.background.opacity); } } // end individual label function } // end drawLabels function function printTitle() { var html = "<span class='MeterChart_Title' style='font-size:12px; position:absolute; top:10px;'>"+options.series.meter.title+"</span>"; $(html).appendTo(target).css("left", (centerLeft-(options.series.meter.title+"").length*getFontSize(html)/4)+"px"); } // end printTitle function function printScaleText() { var radius = options.series.meter.radius*maxRadius; var innerRadius = options.series.meter.innerRadius*maxRadius; // print min scale var minScaleHTML = "<span style='font-weight: bold; position:absolute; top:"+centerTop+"px; left: "+(radius-innerRadius)/2*0.9+"px;'>0</span>"; target.append(minScaleHTML); var maxScaleHTML = "<span style='font-weight: bold; position:absolute; top:"+centerTop+"px; left: "+(radius+innerRadius+(radius-innerRadius)/2)*0.95+"px;'>"+options.series.meter.maxScale+"</span>"; target.append(maxScaleHTML); } // end printScale function function drawPointer() { var value = options.series.meter.currentValue; var pointerLength = options.series.meter.pointerLength * maxRadius; var maxScale = parseFloat(options.series.meter.maxScale); var angle = Math.PI*value/maxScale; var pointerWidth = 6; var startX = centerLeft - Math.floor(pointerLength*Math.cos(angle))-1; var startY = centerTop - Math.floor(pointerLength*Math.sin(angle))-1; ctx.beginPath(); ctx.moveTo(startX,startY); ctx.lineTo(centerLeft+pointerWidth/2*Math.sin(angle),centerTop-pointerWidth/2*Math.cos(angle)); ctx.lineTo(centerLeft-pointerWidth/2*Math.sin(angle),centerTop+pointerWidth/2*Math.cos(angle)); ctx.fill(); } // end drawPointer function function printCurentScaleValue() { var html = "<div class='MeterChart_Value' style='font-size: 16px; position:absolute; top:"+(centerTop+10)+"px;'>"+options.series.meter.currentValue+"</div>"; $(html).appendTo(target).css("left", (centerLeft-(options.series.meter.currentValue+"").length*getFontSize(html)/4)+"px"); } // end printCurentScaleValue function function getFontSize(html) { var fontSize = $(html).css("fontSize"); if(fontSize.indexOf("px") > -1) { fontSize = parseInt(fontSize); } else if(fontSize.indexOf("em") > -1) { fontSize = Math.floor(parseFloat(fontSize)*16); } else if(fontSize.indexOf("pt") > -1) { fontSize = Math.floor(parseFloat(fontSize)*4/3); } else { fontSize = centerLeft; } return fontSize; } } // end drawPie function } // end draw function // Placed here because it needs to be accessed from multiple locations function drawDonutHole(layer) { // draw donut hole if(options.series.meter.innerRadius > 0) { // subtract the center layer.save(); innerRadius = options.series.meter.innerRadius > 1 ? options.series.meter.innerRadius : maxRadius * options.series.meter.innerRadius; layer.globalCompositeOperation = 'destination-out'; // this does not work with excanvas, but it will fall back to using the stroke color layer.beginPath(); layer.fillStyle = options.series.meter.stroke.color; layer.arc(0,0,innerRadius,0,Math.PI*2,false); layer.fill(); layer.closePath(); layer.restore(); // add inner stroke layer.save(); layer.beginPath(); layer.strokeStyle = options.series.meter.stroke.color; layer.arc(0,0,innerRadius,0,Math.PI*2,false); layer.stroke(); layer.closePath(); layer.restore(); // TODO: add extra shadow inside hole (with a mask) if the meter is tilted. } } //-- Additional Interactive related functions -- function isPointInPoly(poly, pt) { for(var c = false, i = -1, l = poly.length, j = l - 1; ++i < l; j = i) ((poly[i][1] <= pt[1] && pt[1] < poly[j][1]) || (poly[j][1] <= pt[1] && pt[1]< poly[i][1])) && (pt[0] < (poly[j][0] - poly[i][0]) * (pt[1] - poly[i][1]) / (poly[j][1] - poly[i][1]) + poly[i][0]) && (c = !c); return c; } function findNearbySlice(mouseX, mouseY) { var slices = plot.getData(), options = plot.getOptions(), radius = options.series.meter.radius > 1 ? options.series.meter.radius : maxRadius * options.series.meter.radius; for (var i = 0; i < slices.length; ++i) { var s = slices[i]; if(s.meter.show) { ctx.save(); ctx.beginPath(); ctx.moveTo(0,0); // Center of the meter //ctx.scale(1, options.series.meter.tilt); // this actually seems to break everything when here. ctx.arc(0,0,radius,s.startAngle,s.startAngle+s.angle,false); ctx.closePath(); x = mouseX-centerLeft; y = mouseY-centerTop; if(ctx.isPointInPath) { if (ctx.isPointInPath(mouseX-centerLeft, mouseY-centerTop)) { //alert('found slice!'); ctx.restore(); return {datapoint: [s.percent, s.data], dataIndex: 0, series: s, seriesIndex: i}; } } else { // excanvas for IE doesn;t support isPointInPath, this is a workaround. p1X = (radius * Math.cos(s.startAngle)); p1Y = (radius * Math.sin(s.startAngle)); p2X = (radius * Math.cos(s.startAngle+(s.angle/4))); p2Y = (radius * Math.sin(s.startAngle+(s.angle/4))); p3X = (radius * Math.cos(s.startAngle+(s.angle/2))); p3Y = (radius * Math.sin(s.startAngle+(s.angle/2))); p4X = (radius * Math.cos(s.startAngle+(s.angle/1.5))); p4Y = (radius * Math.sin(s.startAngle+(s.angle/1.5))); p5X = (radius * Math.cos(s.startAngle+s.angle)); p5Y = (radius * Math.sin(s.startAngle+s.angle)); arrPoly = [[0,0],[p1X,p1Y],[p2X,p2Y],[p3X,p3Y],[p4X,p4Y],[p5X,p5Y]]; arrPoint = [x,y]; // TODO: perhaps do some mathmatical trickery here with the Y-coordinate to compensate for meter tilt? if(isPointInPoly(arrPoly, arrPoint)) { ctx.restore(); return {datapoint: [s.percent, s.data], dataIndex: 0, series: s, seriesIndex: i}; } } ctx.restore(); } } return null; } function onMouseMove(e) { triggerClickHoverEvent('plothover', e); } function onClick(e) { triggerClickHoverEvent('plotclick', e); } // trigger click or hover event (they send the same parameters so we share their code) function triggerClickHoverEvent(eventname, e) { var offset = plot.offset(), canvasX = parseInt(e.pageX - offset.left), canvasY = parseInt(e.pageY - offset.top), item = findNearbySlice(canvasX, canvasY); if (options.grid.autoHighlight) { // clear auto-highlights for (var i = 0; i < highlights.length; ++i) { var h = highlights[i]; if (h.auto == eventname && !(item && h.series == item.series)) unhighlight(h.series); } } // highlight the slice if (item) highlight(item.series, eventname); // trigger any hover bind events var pos = { pageX: e.pageX, pageY: e.pageY }; target.trigger(eventname, [ pos, item ]); } function highlight(s, auto) { if (typeof s == "number") s = series[s]; var i = indexOfHighlight(s); if (i == -1) { highlights.push({ series: s, auto: auto }); plot.triggerRedrawOverlay(); } else if (!auto) highlights[i].auto = false; } function unhighlight(s) { if (s == null) { highlights = []; plot.triggerRedrawOverlay(); } if (typeof s == "number") s = series[s]; var i = indexOfHighlight(s); if (i != -1) { highlights.splice(i, 1); plot.triggerRedrawOverlay(); } } function indexOfHighlight(s) { for (var i = 0; i < highlights.length; ++i) { var h = highlights[i]; if (h.series == s) return i; } return -1; } function drawOverlay(plot, octx) { //alert(options.series.meter.radius); var options = plot.getOptions(); //alert(options.series.meter.radius); var radius = options.series.meter.radius > 1 ? options.series.meter.radius : maxRadius * options.series.meter.radius; octx.save(); octx.translate(centerLeft, centerTop); octx.scale(1, options.series.meter.tilt); for (i = 0; i < highlights.length; ++i) drawHighlight(highlights[i].series); drawDonutHole(octx); octx.restore(); function drawHighlight(series) { if (series.angle < 0) return; //octx.fillStyle = parseColor(options.series.meter.highlight.color).scale(null, null, null, options.series.meter.highlight.opacity).toString(); octx.fillStyle = "rgba(255, 255, 255, "+options.series.meter.highlight.opacity+")"; // this is temporary until we have access to parseColor octx.beginPath(); if (Math.abs(series.angle - Math.PI) > 0.000000001) octx.moveTo(0,0); // Center of the meter octx.arc(0,0,radius,series.startAngle,series.startAngle+series.angle,false); octx.closePath(); octx.fill(); } } } // end init (plugin body) // define meter specific options and their default values var options = { series: { meter: { show: false, radius: 'auto', // actual radius of the visible meter (based on full calculated radius if <=1, or hard pixel value) innerRadius:0, /* for donut */ startAngle: 3/2, tilt: 1, offset: { top: 0, left: 'auto' }, stroke: { color: '#FFF', width: 1 }, label: { show: 'auto', formatter: function(label, slice){ return '<div style="font-size:x-small;text-align:center;padding:2px;color:'+slice.color+';">'+label+'<br/>'+Math.round(slice.percent)+'%</div>'; }, // formatter function radius: 1, // radius at which to place the labels (based on full calculated radius if <=1, or hard pixel value) background: { color: null, opacity: 0 }, threshold: 0 // percentage at which to hide the label (i.e. the slice is too narrow) }, combine: { threshold: -1, // percentage at which to combine little slices into one larger slice color: null, // color to give the new slice (auto-generated if null) label: 'Other' // label to give the new slice }, highlight: { //color: '#FFF', // will add this functionality once parseColor is available opacity: 0.5 } } } }; $.plot.plugins.push({ init: init, options: options, name: "meter", version: "1.0" }); })(jQuery);
使用的前置条件:需要引入flot的jquery组件,然后使用flot的API
给出一个简单的结果: