You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
450 lines
15 KiB
JavaScript
450 lines
15 KiB
JavaScript
/*
|
|
* --------------------------------------------------------------------
|
|
* jQuery inputToButton plugin
|
|
* Author: Scott Jehl, scott@filamentgroup.com
|
|
* Copyright (c) 2009 Filament Group
|
|
* licensed under MIT (filamentgroup.com/examples/mit-license.txt)
|
|
* --------------------------------------------------------------------
|
|
*/
|
|
(function($) {
|
|
$.fn.visualize = function(options, container){
|
|
return $(this).each(function(){
|
|
//configuration
|
|
var o = $.extend({
|
|
type: 'bar', //also available: area, pie, line
|
|
width: $(this).width(), //height of canvas - defaults to table height
|
|
height: $(this).height(), //height of canvas - defaults to table height
|
|
appendTitle: true, //table caption text is added to chart
|
|
title: null, //grabs from table caption if null
|
|
appendKey: true, //color key is added to chart
|
|
colors: ['#be1e2d','#666699','#92d5ea','#ee8310','#8d10ee','#5a3b16','#26a4ed','#f45a90','#e9e744'],
|
|
textColors: [], //corresponds with colors array. null/undefined items will fall back to CSS
|
|
parseDirection: 'x', //which direction to parse the table data
|
|
pieMargin: 20, //pie charts only - spacing around pie
|
|
pieLabelPos: 'inside',
|
|
lineWeight: 4, //for line and area - stroke weight
|
|
barGroupMargin: 10,
|
|
barMargin: 1, //space around bars in bar chart (added to both sides of bar)
|
|
yLabelInterval: 40 //distance between y labels
|
|
},options);
|
|
|
|
//reset width, height to numbers
|
|
o.width = parseFloat(o.width);
|
|
o.height = parseFloat(o.height);
|
|
|
|
|
|
var self = $(this);
|
|
|
|
//function to scrape data from html table
|
|
function scrapeTable(){
|
|
var colors = o.colors;
|
|
var textColors = o.textColors;
|
|
var tableData = {
|
|
dataGroups: function(){
|
|
var dataGroups = [];
|
|
if(o.parseDirection == 'x'){
|
|
self.find('tr:gt(0)').each(function(i){
|
|
dataGroups[i] = {};
|
|
dataGroups[i].points = [];
|
|
dataGroups[i].color = colors[i];
|
|
if(textColors[i]){ dataGroups[i].textColor = textColors[i]; }
|
|
$(this).find('td').each(function(){
|
|
dataGroups[i].points.push( parseFloat($(this).text()) );
|
|
});
|
|
});
|
|
}
|
|
else {
|
|
var cols = self.find('tr:eq(1) td').size();
|
|
for(var i=0; i<cols; i++){
|
|
dataGroups[i] = {};
|
|
dataGroups[i].points = [];
|
|
dataGroups[i].color = colors[i];
|
|
if(textColors[i]){ dataGroups[i].textColor = textColors[i]; }
|
|
self.find('tr:gt(0)').each(function(){
|
|
dataGroups[i].points.push( $(this).find('td').eq(i).text()*1 );
|
|
});
|
|
};
|
|
}
|
|
return dataGroups;
|
|
},
|
|
allData: function(){
|
|
var allData = [];
|
|
$(this.dataGroups()).each(function(){
|
|
allData.push(this.points);
|
|
});
|
|
return allData;
|
|
},
|
|
dataSum: function(){
|
|
var dataSum = 0;
|
|
var allData = this.allData().join(',').split(',');
|
|
$(allData).each(function(){
|
|
dataSum += parseFloat(this);
|
|
});
|
|
return dataSum
|
|
},
|
|
topValue: function(){
|
|
var topValue = 0;
|
|
var allData = this.allData().join(',').split(',');
|
|
$(allData).each(function(){
|
|
if(parseFloat(this,10)>topValue) topValue = parseFloat(this);
|
|
});
|
|
return topValue;
|
|
},
|
|
bottomValue: function(){
|
|
var bottomValue = 0;
|
|
var allData = this.allData().join(',').split(',');
|
|
$(allData).each(function(){
|
|
if(this<bottomValue) bottomValue = parseFloat(this);
|
|
});
|
|
return bottomValue;
|
|
},
|
|
memberTotals: function(){
|
|
var memberTotals = [];
|
|
var dataGroups = this.dataGroups();
|
|
$(dataGroups).each(function(l){
|
|
var count = 0;
|
|
$(dataGroups[l].points).each(function(m){
|
|
count +=dataGroups[l].points[m];
|
|
});
|
|
memberTotals.push(count);
|
|
});
|
|
return memberTotals;
|
|
},
|
|
yTotals: function(){
|
|
var yTotals = [];
|
|
var dataGroups = this.dataGroups();
|
|
var loopLength = this.xLabels().length;
|
|
for(var i = 0; i<loopLength; i++){
|
|
yTotals[i] =[];
|
|
var thisTotal = 0;
|
|
$(dataGroups).each(function(l){
|
|
yTotals[i].push(this.points[i]);
|
|
});
|
|
yTotals[i].join(',').split(',');
|
|
$(yTotals[i]).each(function(){
|
|
thisTotal += parseFloat(this);
|
|
});
|
|
yTotals[i] = thisTotal;
|
|
|
|
}
|
|
return yTotals;
|
|
},
|
|
topYtotal: function(){
|
|
var topYtotal = 0;
|
|
var yTotals = this.yTotals().join(',').split(',');
|
|
$(yTotals).each(function(){
|
|
if(parseFloat(this,10)>topYtotal) topYtotal = parseFloat(this);
|
|
});
|
|
return topYtotal;
|
|
},
|
|
totalYRange: function(){
|
|
return this.topValue() - this.bottomValue();
|
|
},
|
|
xLabels: function(){
|
|
var xLabels = [];
|
|
if(o.parseDirection == 'x'){
|
|
self.find('tr:eq(0) th').each(function(){
|
|
xLabels.push($(this).html());
|
|
});
|
|
}
|
|
else {
|
|
self.find('tr:gt(0) th').each(function(){
|
|
xLabels.push($(this).html());
|
|
});
|
|
}
|
|
return xLabels;
|
|
},
|
|
yLabels: function(){
|
|
var yLabels = [];
|
|
yLabels.push(bottomValue);
|
|
var numLabels = Math.round(o.height / o.yLabelInterval);
|
|
var loopInterval = Math.ceil(totalYRange / numLabels) || 1;
|
|
while( yLabels[yLabels.length-1] < topValue - loopInterval){
|
|
yLabels.push(yLabels[yLabels.length-1] + loopInterval);
|
|
}
|
|
yLabels.push(topValue);
|
|
return yLabels;
|
|
}
|
|
};
|
|
|
|
return tableData;
|
|
};
|
|
|
|
|
|
//function to create a chart
|
|
var createChart = {
|
|
pie: function(){
|
|
|
|
canvasContain.addClass('visualize-pie');
|
|
|
|
if(o.pieLabelPos == 'outside'){ canvasContain.addClass('visualize-pie-outside'); }
|
|
|
|
var centerx = Math.round(canvas.width()/2);
|
|
var centery = Math.round(canvas.height()/2);
|
|
var radius = centery - o.pieMargin;
|
|
var counter = 0.0;
|
|
var toRad = function(integer){ return (Math.PI/180)*integer; };
|
|
var labels = $('<ul class="visualize-labels"></ul>')
|
|
.insertAfter(canvas);
|
|
|
|
//draw the pie pieces
|
|
$.each(memberTotals, function(i){
|
|
var fraction = (this <= 0 || isNaN(this))? 0 : this / dataSum;
|
|
ctx.beginPath();
|
|
ctx.moveTo(centerx, centery);
|
|
ctx.arc(centerx, centery, radius,
|
|
counter * Math.PI * 2 - Math.PI * 0.5,
|
|
(counter + fraction) * Math.PI * 2 - Math.PI * 0.5,
|
|
false);
|
|
ctx.lineTo(centerx, centery);
|
|
ctx.closePath();
|
|
ctx.fillStyle = dataGroups[i].color;
|
|
ctx.fill();
|
|
// draw labels
|
|
var sliceMiddle = (counter + fraction/2);
|
|
var distance = o.pieLabelPos == 'inside' ? radius/1.5 : radius + radius / 5;
|
|
var labelx = Math.round(centerx + Math.sin(sliceMiddle * Math.PI * 2) * (distance));
|
|
var labely = Math.round(centery - Math.cos(sliceMiddle * Math.PI * 2) * (distance));
|
|
var leftRight = (labelx > centerx) ? 'right' : 'left';
|
|
var topBottom = (labely > centery) ? 'bottom' : 'top';
|
|
var percentage = Math.round(fraction*100);
|
|
if(percentage){
|
|
var labeltext = $('<span class="visualize-label">' + percentage + '%</span>')
|
|
.css(leftRight, 0)
|
|
.css(topBottom, 0);
|
|
if(labeltext)
|
|
var label = $('<li class="visualize-label-pos"></li>')
|
|
.appendTo(labels)
|
|
.css({left: labelx, top: labely})
|
|
.append(labeltext);
|
|
labeltext
|
|
.css('font-size', radius / 8)
|
|
.css('margin-'+leftRight, -labeltext.width()/2)
|
|
.css('margin-'+topBottom, -labeltext.outerHeight()/2);
|
|
|
|
|
|
if(dataGroups[i].textColor){ labeltext.css('color', dataGroups[i].textColor); }
|
|
}
|
|
counter+=fraction;
|
|
});
|
|
},
|
|
|
|
line: function(area){
|
|
|
|
if(area){ canvasContain.addClass('visualize-area'); }
|
|
else{ canvasContain.addClass('visualize-line'); }
|
|
|
|
//write X labels
|
|
var xInterval = canvas.width() / (xLabels.length -1);
|
|
var xlabelsUL = $('<ul class="visualize-labels-x"></ul>')
|
|
.width(canvas.width())
|
|
.height(canvas.height())
|
|
.insertBefore(canvas);
|
|
$.each(xLabels, function(i){
|
|
var thisLi = $('<li><span>'+this+'</span></li>')
|
|
.prepend('<span class="line" />')
|
|
.css('left', xInterval * i)
|
|
.appendTo(xlabelsUL);
|
|
var label = thisLi.find('span:not(.line)');
|
|
var leftOffset = label.width()/-2;
|
|
if(i == 0){ leftOffset = 0; }
|
|
else if(i== xLabels.length-1){ leftOffset = -label.width(); }
|
|
label
|
|
.css('margin-left', leftOffset)
|
|
.addClass('label');
|
|
});
|
|
|
|
//write Y labels
|
|
var yScale = canvas.height() / totalYRange;
|
|
var liBottom = canvas.height() / (yLabels.length-1);
|
|
var ylabelsUL = $('<ul class="visualize-labels-y"></ul>')
|
|
.width(canvas.width())
|
|
.height(canvas.height())
|
|
.insertBefore(canvas);
|
|
|
|
$.each(yLabels, function(i){
|
|
var thisLi = $('<li><span>'+this+'</span></li>')
|
|
.prepend('<span class="line" />')
|
|
.css('bottom',liBottom*i)
|
|
.prependTo(ylabelsUL);
|
|
var label = thisLi.find('span:not(.line)');
|
|
var topOffset = label.height()/-2;
|
|
if(i == 0){ topOffset = -label.height(); }
|
|
else if(i== yLabels.length-1){ topOffset = 0; }
|
|
label
|
|
.css('margin-top', topOffset)
|
|
.addClass('label');
|
|
});
|
|
|
|
//start from the bottom left
|
|
ctx.translate(0,zeroLoc);
|
|
//iterate and draw
|
|
$.each(dataGroups,function(h){
|
|
ctx.beginPath();
|
|
ctx.lineWidth = o.lineWeight;
|
|
ctx.lineJoin = 'round';
|
|
var points = this.points;
|
|
var integer = 0;
|
|
ctx.moveTo(0,-(points[0]*yScale));
|
|
$.each(points, function(){
|
|
ctx.lineTo(integer,-(this*yScale));
|
|
integer+=xInterval;
|
|
});
|
|
ctx.strokeStyle = this.color;
|
|
ctx.stroke();
|
|
if(area){
|
|
ctx.lineTo(integer,0);
|
|
ctx.lineTo(0,0);
|
|
ctx.closePath();
|
|
ctx.fillStyle = this.color;
|
|
ctx.globalAlpha = .3;
|
|
ctx.fill();
|
|
ctx.globalAlpha = 1.0;
|
|
}
|
|
else {ctx.closePath();}
|
|
});
|
|
},
|
|
|
|
area: function(){
|
|
createChart.line(true);
|
|
},
|
|
|
|
bar: function(){
|
|
|
|
canvasContain.addClass('visualize-bar');
|
|
|
|
//write X labels
|
|
var xInterval = canvas.width() / (xLabels.length);
|
|
var xlabelsUL = $('<ul class="visualize-labels-x"></ul>')
|
|
.width(canvas.width())
|
|
.height(canvas.height())
|
|
.insertBefore(canvas);
|
|
$.each(xLabels, function(i){
|
|
var thisLi = $('<li><span class="label">'+this+'</span></li>')
|
|
.prepend('<span class="line" />')
|
|
.css('left', xInterval * i)
|
|
.width(xInterval)
|
|
.appendTo(xlabelsUL);
|
|
var label = thisLi.find('span.label');
|
|
label.addClass('label');
|
|
});
|
|
|
|
//write Y labels
|
|
var yScale = canvas.height() / totalYRange;
|
|
var liBottom = canvas.height() / (yLabels.length-1);
|
|
var ylabelsUL = $('<ul class="visualize-labels-y"></ul>')
|
|
.width(canvas.width())
|
|
.height(canvas.height())
|
|
.insertBefore(canvas);
|
|
$.each(yLabels, function(i){
|
|
var thisLi = $('<li><span>'+this+'</span></li>')
|
|
.prepend('<span class="line" />')
|
|
.css('bottom',liBottom*i)
|
|
.prependTo(ylabelsUL);
|
|
var label = thisLi.find('span:not(.line)');
|
|
var topOffset = label.height()/-2;
|
|
if(i == 0){ topOffset = -label.height(); }
|
|
else if(i== yLabels.length-1){ topOffset = 0; }
|
|
label
|
|
.css('margin-top', topOffset)
|
|
.addClass('label');
|
|
});
|
|
|
|
//start from the bottom left
|
|
ctx.translate(0,zeroLoc);
|
|
//iterate and draw
|
|
for(var h=0; h<dataGroups.length; h++){
|
|
ctx.beginPath();
|
|
var linewidth = (xInterval-o.barGroupMargin*2) / dataGroups.length; //removed +1
|
|
var strokeWidth = linewidth - (o.barMargin*2);
|
|
ctx.lineWidth = strokeWidth;
|
|
var points = dataGroups[h].points;
|
|
var integer = 0;
|
|
for(var i=0; i<points.length; i++){
|
|
var xVal = (integer-o.barGroupMargin)+(h*linewidth)+linewidth/2;
|
|
xVal += o.barGroupMargin*2;
|
|
|
|
ctx.moveTo(xVal, 0);
|
|
ctx.lineTo(xVal, Math.round(-points[i]*yScale));
|
|
integer+=xInterval;
|
|
}
|
|
ctx.strokeStyle = dataGroups[h].color;
|
|
ctx.stroke();
|
|
ctx.closePath();
|
|
}
|
|
}
|
|
};
|
|
|
|
//create new canvas, set w&h attrs (not inline styles)
|
|
var canvasNode = document.createElement("canvas");
|
|
canvasNode.setAttribute('height',o.height);
|
|
canvasNode.setAttribute('width',o.width);
|
|
var canvas = $(canvasNode);
|
|
|
|
//get title for chart
|
|
var title = o.title || self.find('caption').text();
|
|
|
|
//create canvas wrapper div, set inline w&h, append
|
|
var canvasContain = (container || $('<div class="visualize" role="img" aria-label="Chart representing data from the table: '+ title +'" />'))
|
|
.height(o.height)
|
|
.width(o.width)
|
|
.append(canvas);
|
|
|
|
//scrape table (this should be cleaned up into an obj)
|
|
var tableData = scrapeTable();
|
|
var dataGroups = tableData.dataGroups();
|
|
var allData = tableData.allData();
|
|
var dataSum = tableData.dataSum();
|
|
var topValue = tableData.topValue();
|
|
var bottomValue = tableData.bottomValue();
|
|
var memberTotals = tableData.memberTotals();
|
|
var totalYRange = tableData.totalYRange();
|
|
var zeroLoc = o.height * (topValue/totalYRange);
|
|
var xLabels = tableData.xLabels();
|
|
var yLabels = tableData.yLabels();
|
|
|
|
//title/key container
|
|
if(o.appendTitle || o.appendKey){
|
|
var infoContain = $('<div class="visualize-info"></div>')
|
|
.appendTo(canvasContain);
|
|
}
|
|
|
|
//append title
|
|
if(o.appendTitle){
|
|
$('<div class="visualize-title">'+ title +'</div>').appendTo(infoContain);
|
|
}
|
|
|
|
|
|
//append key
|
|
if(o.appendKey){
|
|
var newKey = $('<ul class="visualize-key"></ul>');
|
|
var selector = (o.parseDirection == 'x') ? 'tr:gt(0) th' : 'tr:eq(0) th' ;
|
|
self.find(selector).each(function(i){
|
|
$('<li><span class="visualize-key-color" style="background: '+dataGroups[i].color+'"></span><span class="visualize-key-label">'+ $(this).text() +'</span></li>')
|
|
.appendTo(newKey);
|
|
});
|
|
newKey.appendTo(infoContain);
|
|
};
|
|
|
|
//append new canvas to page
|
|
|
|
if(!container){canvasContain.insertAfter(this); }
|
|
if( typeof(G_vmlCanvasManager) != 'undefined' ){ G_vmlCanvasManager.init(); G_vmlCanvasManager.initElement(canvas[0]); }
|
|
|
|
//set up the drawing board
|
|
var ctx = canvas[0].getContext('2d');
|
|
|
|
//create chart
|
|
createChart[o.type]();
|
|
|
|
//clean up some doubled lines that sit on top of canvas borders (done via JS due to IE)
|
|
$('.visualize-line li:first-child span.line, .visualize-line li:last-child span.line, .visualize-area li:first-child span.line, .visualize-area li:last-child span.line, .visualize-bar li:first-child span.line,.visualize-bar .visualize-labels-y li:last-child span.line').css('border','none');
|
|
if(!container){
|
|
//add event for updating
|
|
canvasContain.bind('visualizeRefresh', function(){
|
|
self.visualize(o, $(this).empty());
|
|
});
|
|
}
|
|
}).next(); //returns canvas(es)
|
|
};
|
|
})(jQuery); |