Commit b1dab897 authored by Ad Schellevis's avatar Ad Schellevis

(legacy) zap protochart

parent 50e27fc8
/**
* Class: ProtoChart
* Version: v0.5 beta
*
* ProtoChart is a charting lib on top of Prototype.
* This library is heavily motivated by excellent work done by:
* * Flot <http://code.google.com/p/flot/>
* * Flotr <http://solutoire.com/flotr/>
*
* Complete examples can be found at: <http://www.deensoft.com/lab/protochart>
*/
/**
* Events:
* ProtoChart:mousemove - Fired when mouse is moved over the chart
* ProtoChart:plotclick - Fired when graph is clicked
* ProtoChart:dataclick - Fired when graph is clicked AND the click is on a data point
* ProtoChart:selected - Fired when certain region on the graph is selected
* ProtoChart:hit - Fired when mouse is moved near or over certain data point on the graph
*/
if(!Proto) var Proto = {};
Proto.Chart = Class.create({
/**
* Function:
* {Object} elem
* {Object} data
* {Object} options
*/
initialize: function(elem, data, options)
{
options = options || {};
this.graphData = [];
/**
* Property: options
*
* Description: Various options can be set. More details in description.
*
* colors:
* {Array} - pass in a array which contains strings of colors you want to use. Default has 6 color set.
*
* legend:
* {BOOL} - show - if you want to show the legend. Default is false
* {integer} - noColumns - Number of columns for the legend. Default is 1
* {function} - labelFormatter - A function that returns a string. The function is called with a string and is expected to return a string. Default = null
* {string} - labelBoxBorderColor - border color for the little label boxes. Default #CCC
* {HTMLElem} - container - an HTML id or HTML element where the legend should be rendered. If left null means to put the legend on top of the Chart
* {string} - position - position for the legend on the Chart. Default value 'ne'
* {integer} - margin - default valud of 5
* {string} - backgroundColor - default to null (which means auto-detect)
* {float} - backgroundOpacity - leave it 0 to avoid background
*
* xaxis (yaxis) options:
* {string} - mode - default is null but you can pass a string "time" to indicate time series
* {integer} - min
* {integer} - max
* {float} - autoscaleMargin - in % to add if auto-setting min/max
* {mixed} - ticks - either [1, 3] or [[1, "a"], 3] or a function which gets axis info and returns ticks
* {function} - tickFormatter - A function that returns a string as a tick label. Default is null
* {float} - tickDecimals
* {integer} - tickSize
* {integer} - minTickSize
* {array} - monthNames
* {string} - timeformat
*
* Points / Lines / Bars options:
* {bool} - show, default is false
* {integer} - radius: default is 3
* {integer} - lineWidth : default is 2
* {bool} - fill : default is true
* {string} - fillColor: default is #ffffff
*
* Grid options:
* {string} - color
* {string} - backgroundColor - defualt is *null*
* {string} - tickColor - default is *#dddddd*
* {integer} - labelMargin - should be in pixels default is 3
* {integer} - borderWidth - default *1*
* {bool} - clickable - default *null* - pass in TRUE if you wish to monitor click events
* {mixed} - coloredAreas - default *null* - pass in mixed object eg. {x1, x2}
* {string} - coloredAreasColor - default *#f4f4f4*
* {bool} - drawXAxis - default *true*
* {bool} - drawYAxis - default *true*
*
* selection options:
* {string} - mode : either "x", "y" or "xy"
* {string} - color : string
*/
this.options = this.merge(options,{
colors: ["#edc240", "#00A8F0", "#C0D800", "#cb4b4b", "#4da74d", "#9440ed"],
legend: {
show: false,
noColumns: 1,
labelFormatter: null,
labelBoxBorderColor: "#ccc",
container: null,
position: "ne",
margin: 5,
backgroundColor: null,
backgroundOpacity: 0.85
},
xaxis: {
mode: null,
min: null,
max: null,
autoscaleMargin: null,
ticks: null,
tickFormatter: null,
tickDecimals: null,
tickSize: null,
minTickSize: null,
monthNames: null,
timeformat: null
},
yaxis: {
mode: null,
min: null,
max: null,
ticks: null,
tickFormatter: null,
tickDecimals: null,
tickSize: null,
minTickSize: null,
monthNames: null,
timeformat: null,
autoscaleMargin: 0.02
},
points: {
show: false,
radius: 3,
lineWidth: 2,
fill: true,
fillColor: "#ffffff"
},
lines: {
show: false,
lineWidth: 2,
fill: false,
fillColor: null
},
bars: {
show: false,
lineWidth: 2,
barWidth: 1,
fill: true,
fillColor: null,
showShadow: false,
fillOpacity: 0.4,
autoScale: true
},
pies: {
show: false,
radius: 50,
borderWidth: 1,
fill: true,
fillColor: null,
fillOpacity: 0.90,
labelWidth: 30,
fontSize: 11,
autoScale: true
},
grid: {
color: "#545454",
backgroundColor: null,
tickColor: "#dddddd",
labelMargin: 3,
borderWidth: 1,
clickable: null,
coloredAreas: null,
coloredAreasColor: "#f4f4f4",
drawXAxis: true,
drawYAxis: true
},
mouse: {
track: false,
position: 'se',
fixedPosition: true,
clsName: 'mouseValHolder',
trackFormatter: this.defaultTrackFormatter,
margin: 3,
color: '#ff3f19',
trackDecimals: 1,
sensibility: 2,
radius: 5,
lineColor: '#cb4b4b'
},
selection: {
mode: null,
color: "#97CBFF"
},
allowDataClick: true,
makeRandomColor: false,
shadowSize: 4
});
/*
* Local variables.
*/
this.canvas = null;
this.overlay = null;
this.eventHolder = null;
this.context = null;
this.overlayContext = null;
this.domObj = $(elem);
this.xaxis = {};
this.yaxis = {};
this.chartOffset = {left: 0, right: 0, top: 0, bottom: 0};
this.yLabelMaxWidth = 0;
this.yLabelMaxHeight = 0;
this.xLabelBoxWidth = 0;
this.canvasWidth = 0;
this.canvasHeight = 0;
this.chartWidth = 0;
this.chartHeight = 0;
this.hozScale = 0;
this.vertScale = 0;
this.workarounds = {};
this.domObj = $(elem);
this.barDataRange = [];
this.lastMousePos = { pageX: null, pageY: null };
this.selection = { first: { x: -1, y: -1}, second: { x: -1, y: -1} };
this.prevSelection = null;
this.selectionInterval = null;
this.ignoreClick = false;
this.prevHit = null;
if(this.options.makeRandomColor)
this.options.color = this.makeRandomColor(this.options.colors);
this.setData(data);
this.constructCanvas();
this.setupGrid();
this.draw();
},
/**
* Private function internally used.
*/
merge: function(src, dest)
{
var result = dest || {};
for(var i in src){
result[i] = (typeof(src[i]) == 'object' && !(src[i].constructor == Array || src[i].constructor == RegExp)) ? this.merge(src[i], dest[i]) : result[i] = src[i];
}
return result;
},
/**
* Function: setData
* {Object} data
*
* Description:
* Sets datasoruces properly then sets the Bar Width accordingly, then copies the default data options and then processes the graph data
*
* Returns: none
*
*/
setData: function(data)
{
this.graphData = this.parseData(data);
this.setBarWidth();
this.copyGraphDataOptions();
this.processGraphData();
},
/**
* Function: parseData
* {Object} data
*
* Return:
* {Object} result
*
* Description:
* Takes the provided data object and converts it into generic data that we can understand. User can pass in data in 3 different ways:
* - [d1, d2]
* - [{data: d1, label: "data1"}, {data: d2, label: "data2"}]
* - [d1, {data: d1, label: "data1"}]
*
* This function parses these senarios and makes it readable
*/
parseData: function(data)
{
var res = [];
data.each(function(d){
var s;
if(d.data) {
s = {};
for(var v in d) {
s[v] = d[v];
}
}
else {
s = {data: d};
}
res.push(s);
}.bind(this));
return res;
},
/**
* function: makeRandomColor
* {Object} colorSet
*
* Return:
* {Array} result - array containing random colors
*/
makeRandomColor: function(colorSet)
{
var randNum = Math.floor(Math.random() * colorSet.length);
var randArr = [];
var newArr = [];
randArr.push(randNum);
while(randArr.length < colorSet.length)
{
var tempNum = Math.floor(Math.random() * colorSet.length);
while(checkExisted(tempNum, randArr))
tempNum = Math.floor(Math.random() * colorSet.length);
randArr.push(tempNum);
}
randArr.each(function(ra){
newArr.push(colorSet[ra]);
}.bind(this));
return newArr;
},
/**
* function: checkExisted
* {Object} needle
* {Object} haystack
*
* return:
* {bool} existed - true if it finds needle in the haystack
*/
checkExisted: function(needle, haystack)
{
var existed = false;
haystack.each(function(aNeedle){
if(aNeedle == needle) {
existed = true;
throw $break;
}
}.bind(this));
return existed;
},
/**
* function: setBarWidth
*
* Description: sets the bar width for Bar Graph, you should enable *autoScale* property for bar graph
*/
setBarWidth: function()
{
if(this.options.bars.show && this.options.bars.autoScale)
{
this.options.bars.barWidth = 1 / this.graphData.length / 1.2;
}
},
/**
* Function: copyGraphDataOptions
*
* Description: Private function that goes through each graph data (series) and assigned the graph
* properties to it.
*/
copyGraphDataOptions: function()
{
var i, neededColors = this.graphData.length, usedColors = [], assignedColors = [];
this.graphData.each(function(gd){
var sc = gd.color;
if(sc) {
--neededColors;
if(Object.isNumber(sc)) {
assignedColors.push(sc);
}
else {
usedColors.push(this.parseColor(sc));
}
}
}.bind(this));
assignedColors.each(function(ac){
neededColors = Math.max(neededColors, ac + 1);
});
var colors = [];
var variation = 0;
i = 0;
while (colors.length < neededColors) {
var c;
if (this.options.colors.length == i) {
c = new Proto.Color(100, 100, 100);
}
else {
c = this.parseColor(this.options.colors[i]);
}
var sign = variation % 2 == 1 ? -1 : 1;
var factor = 1 + sign * Math.ceil(variation / 2) * 0.2;
c.scale(factor, factor, factor);
colors.push(c);
++i;
if (i >= this.options.colors.length) {
i = 0;
++variation;
}
}
var colorIndex = 0, s;
this.graphData.each(function(gd){
if(gd.color == null)
{
gd.color = colors[colorIndex].toString();
++colorIndex;
}
else if(Object.isNumber(gd.color)) {
gd.color = colors[gd.color].toString();
}
gd.lines = Object.extend(Object.clone(this.options.lines), gd.lines);
gd.points = Object.extend(Object.clone(this.options.points), gd.points);
gd.bars = Object.extend(Object.clone(this.options.bars), gd.bars);
gd.mouse = Object.extend(Object.clone(this.options.mouse), gd.mouse);
if (gd.shadowSize == null) {
gd.shadowSize = this.options.shadowSize;
}
}.bind(this));
},
/**
* Function: processGraphData
*
* Description: processes graph data, setup xaxis and yaxis min and max points.
*/
processGraphData: function() {
this.xaxis.datamin = this.yaxis.datamin = Number.MAX_VALUE;
this.xaxis.datamax = this.yaxis.datamax = Number.MIN_VALUE;
this.graphData.each(function(gd) {
var data = gd.data;
data.each(function(d){
if(d == null) {
return;
}
var x = d[0], y = d[1];
if(!x || !y || isNaN(x = +x) || isNaN(y = +y)) {
d = null;
return;
}
if (x < this.xaxis.datamin)
this.xaxis.datamin = x;
if (x > this.xaxis.datamax)
this.xaxis.datamax = x;
if (y < this.yaxis.datamin)
this.yaxis.datamin = y;
if (y > this.yaxis.datamax)
this.yaxis.datamax = y;
}.bind(this));
}.bind(this));
if (this.xaxis.datamin == Number.MAX_VALUE)
this.xaxis.datamin = 0;
if (this.yaxis.datamin == Number.MAX_VALUE)
this.yaxis.datamin = 0;
if (this.xaxis.datamax == Number.MIN_VALUE)
this.xaxis.datamax = 1;
if (this.yaxis.datamax == Number.MIN_VALUE)
this.yaxis.datamax = 1;
},
/**
* Function: constructCanvas
*
* Description: constructs the main canvas for drawing. It replicates the HTML elem (usually DIV) passed
* in via constructor. If there is no height/width assigned to the HTML elem then we take a default size
* of 400px (width) and 300px (height)
*/
constructCanvas: function() {
this.canvasWidth = this.domObj.getWidth();
this.canvasHeight = this.domObj.getHeight();
this.domObj.update(""); // clear target
this.domObj.setStyle({
"position": "relative"
});
if (this.canvasWidth <= 0) {
this.canvasWdith = 400;
}
if(this.canvasHeight <= 0) {
this.canvasHeight = 300;
}
this.canvas = (Prototype.Browser.IE) ? document.createElement("canvas") : new Element("CANVAS", {'width': this.canvasWidth, 'height': this.canvasHeight});
Element.extend(this.canvas);
this.canvas.style.width = this.canvasWidth + "px";
this.canvas.style.height = this.canvasHeight + "px";
this.domObj.appendChild(this.canvas);
if (Prototype.Browser.IE) // excanvas hack
{
this.canvas = $(window.G_vmlCanvasManager.initElement(this.canvas));
}
this.canvas = $(this.canvas);
this.context = this.canvas.getContext("2d");
this.overlay = (Prototype.Browser.IE) ? document.createElement("canvas") : new Element("CANVAS", {'width': this.canvasWidth, 'height': this.canvasHeight});
Element.extend(this.overlay);
this.overlay.style.width = this.canvasWidth + "px";
this.overlay.style.height = this.canvasHeight + "px";
this.overlay.style.position = "absolute";
this.overlay.style.left = "0px";
this.overlay.style.right = "0px";
this.overlay.setStyle({
'position': 'absolute',
'left': '0px',
'right': '0px'
});
this.domObj.appendChild(this.overlay);
if (Prototype.Browser.IE) {
this.overlay = $(window.G_vmlCanvasManager.initElement(this.overlay));
}
this.overlay = $(this.overlay);
this.overlayContext = this.overlay.getContext("2d");
if(this.options.selection.mode)
{
this.overlay.observe('mousedown', this.onMouseDown.bind(this));
this.overlay.observe('mousemove', this.onMouseMove.bind(this));
}
if(this.options.grid.clickable) {
this.overlay.observe('click', this.onClick.bind(this));
}
if(this.options.mouse.track)
{
this.overlay.observe('mousemove', this.onMouseMove.bind(this));
}
},
/**
* function: setupGrid
*
* Description: a container function that does a few interesting things.
*
* 1. calls <extendXRangeIfNeededByBar> function which makes sure that our axis are expanded if needed
*
* 2. calls <setRange> function providing xaxis options which fixes the ranges according to data points
*
* 3. calls <prepareTickGeneration> function for xaxis which generates ticks according to options provided by user
*
* 4. calls <setTicks> function for xaxis that sets the ticks
*
* similar sequence is called for y-axis.
*
* At the end if this is a pie chart than we insert Labels (around the pie chart) via <insertLabels> and we also call <insertLegend>
*/
setupGrid: function()
{
if(this.options.bars.show)
{
this.xaxis.max += 0.5;
this.xaxis.min -= 0.5;
}
//x-axis
this.extendXRangeIfNeededByBar();
this.setRange(this.xaxis, this.options.xaxis);
this.prepareTickGeneration(this.xaxis, this.options.xaxis);
this.setTicks(this.xaxis, this.options.xaxis);
//y-axis
this.setRange(this.yaxis, this.options.yaxis);
this.prepareTickGeneration(this.yaxis, this.options.yaxis);
this.setTicks(this.yaxis, this.options.yaxis);
this.setSpacing();
if(!this.options.pies.show)
{
this.insertLabels();
}
this.insertLegend();
},
/**
* function: setRange
*
* parameters:
* {Object} axis
* {Object} axisOptions
*/
setRange: function(axis, axisOptions) {
var min = axisOptions.min != null ? axisOptions.min : axis.datamin;
var max = axisOptions.max != null ? axisOptions.max : axis.datamax;
if (max - min == 0.0) {
// degenerate case
var widen;
if (max == 0.0)
widen = 1.0;
else
widen = 0.01;
min -= widen;
max += widen;
}
else {
// consider autoscaling
var margin = axisOptions.autoscaleMargin;
if (margin != null) {
if (axisOptions.min == null) {
min -= (max - min) * margin;
// make sure we don't go below zero if all values
// are positive
if (min < 0 && axis.datamin >= 0)
min = 0;
}
if (axisOptions.max == null) {
max += (max - min) * margin;
if (max > 0 && axis.datamax <= 0)
max = 0;
}
}
}
axis.min = min;
axis.max = max;
},
/**
* function: prepareTickGeneration
*
* Parameters:
* {Object} axis
* {Object} axisOptions
*/
prepareTickGeneration: function(axis, axisOptions) {
// estimate number of ticks
var noTicks;
if (Object.isNumber(axisOptions.ticks) && axisOptions.ticks > 0)
noTicks = axisOptions.ticks;
else if (axis == this.xaxis)
noTicks = this.canvasWidth / 100;
else
noTicks = this.canvasHeight / 60;
var delta = (axis.max - axis.min) / noTicks;
var size, generator, unit, formatter, i, magn, norm;
if (axisOptions.mode == "time") {
function formatDate(d, fmt, monthNames) {
var leftPad = function(n) {
n = "" + n;
return n.length == 1 ? "0" + n : n;
};
var r = [];
var escape = false;
if (monthNames == null)
monthNames = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
for (var i = 0; i < fmt.length; ++i) {
var c = fmt.charAt(i);
if (escape) {
switch (c) {
case 'h': c = "" + d.getHours(); break;
case 'H': c = leftPad(d.getHours()); break;
case 'M': c = leftPad(d.getMinutes()); break;
case 'S': c = leftPad(d.getSeconds()); break;
case 'd': c = "" + d.getDate(); break;
case 'm': c = "" + (d.getMonth() + 1); break;
case 'y': c = "" + d.getFullYear(); break;
case 'b': c = "" + monthNames[d.getMonth()]; break;
}
r.push(c);
escape = false;
}
else {
if (c == "%")
escape = true;
else
r.push(c);
}
}
return r.join("");
}
// map of app. size of time units in milliseconds
var timeUnitSize = {
"second": 1000,
"minute": 60 * 1000,
"hour": 60 * 60 * 1000,
"day": 24 * 60 * 60 * 1000,
"month": 30 * 24 * 60 * 60 * 1000,
"year": 365.2425 * 24 * 60 * 60 * 1000
};
// the allowed tick sizes, after 1 year we use
// an integer algorithm
var spec = [
[1, "second"], [2, "second"], [5, "second"], [10, "second"],
[30, "second"],
[1, "minute"], [2, "minute"], [5, "minute"], [10, "minute"],
[30, "minute"],
[1, "hour"], [2, "hour"], [4, "hour"],
[8, "hour"], [12, "hour"],
[1, "day"], [2, "day"], [3, "day"],
[0.25, "month"], [0.5, "month"], [1, "month"],
[2, "month"], [3, "month"], [6, "month"],
[1, "year"]
];
var minSize = 0;
if (axisOptions.minTickSize != null) {
if (typeof axisOptions.tickSize == "number")
minSize = axisOptions.tickSize;
else
minSize = axisOptions.minTickSize[0] * timeUnitSize[axisOptions.minTickSize[1]];
}
for (i = 0; i < spec.length - 1; ++i) {
if (delta < (spec[i][0] * timeUnitSize[spec[i][1]] + spec[i + 1][0] * timeUnitSize[spec[i + 1][1]]) / 2 && spec[i][0] * timeUnitSize[spec[i][1]] >= minSize) {
break;
}
}
size = spec[i][0];
unit = spec[i][1];
// special-case the possibility of several years
if (unit == "year") {
magn = Math.pow(10, Math.floor(Math.log(delta / timeUnitSize.year) / Math.LN10));
norm = (delta / timeUnitSize.year) / magn;
if (norm < 1.5)
size = 1;
else if (norm < 3)
size = 2;
else if (norm < 7.5)
size = 5;
else
size = 10;
size *= magn;
}
if (axisOptions.tickSize) {
size = axisOptions.tickSize[0];
unit = axisOptions.tickSize[1];
}
var floorInBase = this.floorInBase; //gives us a reference to a global function..
generator = function(axis) {
var ticks = [],
tickSize = axis.tickSize[0], unit = axis.tickSize[1],
d = new Date(axis.min);
var step = tickSize * timeUnitSize[unit];
if (unit == "second")
d.setSeconds(floorInBase(d.getSeconds(), tickSize));
if (unit == "minute")
d.setMinutes(floorInBase(d.getMinutes(), tickSize));
if (unit == "hour")
d.setHours(floorInBase(d.getHours(), tickSize));
if (unit == "month")
d.setMonth(floorInBase(d.getMonth(), tickSize));
if (unit == "year")
d.setFullYear(floorInBase(d.getFullYear(), tickSize));
// reset smaller components
d.setMilliseconds(0);
if (step >= timeUnitSize.minute)
d.setSeconds(0);
if (step >= timeUnitSize.hour)
d.setMinutes(0);
if (step >= timeUnitSize.day)
d.setHours(0);
if (step >= timeUnitSize.day * 4)
d.setDate(1);
if (step >= timeUnitSize.year)
d.setMonth(0);
var carry = 0, v;
do {
v = d.getTime();
ticks.push({ v: v, label: axis.tickFormatter(v, axis) });
if (unit == "month") {
if (tickSize < 1) {
d.setDate(1);
var start = d.getTime();
d.setMonth(d.getMonth() + 1);
var end = d.getTime();
d.setTime(v + carry * timeUnitSize.hour + (end - start) * tickSize);
carry = d.getHours();
d.setHours(0);
}
else
d.setMonth(d.getMonth() + tickSize);
}
else if (unit == "year") {
d.setFullYear(d.getFullYear() + tickSize);
}
else
d.setTime(v + step);
} while (v < axis.max);
return ticks;
};
formatter = function (v, axis) {
var d = new Date(v);
// first check global format
if (axisOptions.timeformat != null)
return formatDate(d, axisOptions.timeformat, axisOptions.monthNames);
var t = axis.tickSize[0] * timeUnitSize[axis.tickSize[1]];
var span = axis.max - axis.min;
if (t < timeUnitSize.minute)
fmt = "%h:%M:%S";
else if (t < timeUnitSize.day) {
if (span < 2 * timeUnitSize.day)
fmt = "%h:%M";
else
fmt = "%b %d %h:%M";
}
else if (t < timeUnitSize.month)
fmt = "%b %d";
else if (t < timeUnitSize.year) {
if (span < timeUnitSize.year)
fmt = "%b";
else
fmt = "%b %y";
}
else
fmt = "%y";
return formatDate(d, fmt, axisOptions.monthNames);
};
}
else {
// pretty rounding of base-10 numbers
var maxDec = axisOptions.tickDecimals;
var dec = -Math.floor(Math.log(delta) / Math.LN10);
if (maxDec != null && dec > maxDec)
dec = maxDec;
magn = Math.pow(10, -dec);
norm = delta / magn; // norm is between 1.0 and 10.0
if (norm < 1.5)
size = 1;
else if (norm < 3) {
size = 2;
// special case for 2.5, requires an extra decimal
if (norm > 2.25 && (maxDec == null || dec + 1 <= maxDec)) {
size = 2.5;
++dec;
}
}
else if (norm < 7.5)
size = 5;
else
size = 10;
size *= magn;
if (axisOptions.minTickSize != null && size < axisOptions.minTickSize)
size = axisOptions.minTickSize;
if (axisOptions.tickSize != null)
size = axisOptions.tickSize;
axis.tickDecimals = Math.max(0, (maxDec != null) ? maxDec : dec);
var floorInBase = this.floorInBase;
generator = function (axis) {
var ticks = [];
var start = floorInBase(axis.min, axis.tickSize);
// then spew out all possible ticks
var i = 0, v;
do {
v = start + i * axis.tickSize;
ticks.push({ v: v, label: axis.tickFormatter(v, axis) });
++i;
} while (v < axis.max);
return ticks;
};
formatter = function (v, axis) {
if(v) {
return v.toFixed(axis.tickDecimals);
}
return 0;
};
}
axis.tickSize = unit ? [size, unit] : size;
axis.tickGenerator = generator;
if (Object.isFunction(axisOptions.tickFormatter))
axis.tickFormatter = function (v, axis) { return "" + axisOptions.tickFormatter(v, axis); };
else
axis.tickFormatter = formatter;
},
/**
* function: extendXRangeIfNeededByBar
*/
extendXRangeIfNeededByBar: function() {
if (this.options.xaxis.max == null) {
// great, we're autoscaling, check if we might need a bump
var newmax = this.xaxis.max;
this.graphData.each(function(gd){
if(gd.bars.show && gd.bars.barWidth + this.xaxis.datamax > newmax)
{
newmax = this.xaxis.datamax + gd.bars.barWidth;
}
}.bind(this));
this.xaxis.nax = newmax;
}
},
/**
* function: setTicks
*
* parameters:
* {Object} axis
* {Object} axisOptions
*/
setTicks: function(axis, axisOptions) {
axis.ticks = [];
if (axisOptions.ticks == null)
axis.ticks = axis.tickGenerator(axis);
else if (typeof axisOptions.ticks == "number") {
if (axisOptions.ticks > 0)
axis.ticks = axis.tickGenerator(axis);
}
else if (axisOptions.ticks) {
var ticks = axisOptions.ticks;
if (Object.isFunction(ticks))
// generate the ticks
ticks = ticks({ min: axis.min, max: axis.max });
// clean up the user-supplied ticks, copy them over
//var i, v;
ticks.each(function(t, i){
var v = null;
var label = null;
if(typeof t == 'object') {
v = t[0];
if(t.length > 1) { label = t[1]; }
}
else {
v = t;
}
if(!label) {
label = axis.tickFormatter(v, axis);
}
axis.ticks[i] = {v: v, label: label}
}.bind(this));
}
if (axisOptions.autoscaleMargin != null && axis.ticks.length > 0) {
if (axisOptions.min == null)
axis.min = Math.min(axis.min, axis.ticks[0].v);
if (axisOptions.max == null && axis.ticks.length > 1)
axis.max = Math.min(axis.max, axis.ticks[axis.ticks.length - 1].v);
}
},
/**
* Function: setSpacing
*
* Parameters: none
*/
setSpacing: function() {
// calculate y label dimensions
var i, labels = [], l;
for (i = 0; i < this.yaxis.ticks.length; ++i) {
l = this.yaxis.ticks[i].label;
if (l)
labels.push('<div class="tickLabel">' + l + '</div>');
}
if (labels.length > 0) {
var dummyDiv = new Element('div', {'style': 'position:absolute;top:-10000px;font-size:smaller'});
dummyDiv.update(labels.join(""));
this.domObj.insert(dummyDiv);
this.yLabelMaxWidth = dummyDiv.getWidth();
this.yLabelMaxHeight = dummyDiv.select('div')[0].getHeight();
dummyDiv.remove();
}
var maxOutset = this.options.grid.borderWidth;
if (this.options.points.show)
maxOutset = Math.max(maxOutset, this.options.points.radius + this.options.points.lineWidth/2);
for (i = 0; i < this.graphData.length; ++i) {
if (this.graphData[i].points.show)
maxOutset = Math.max(maxOutset, this.graphData[i].points.radius + this.graphData[i].points.lineWidth/2);
}
this.chartOffset.left = this.chartOffset.right = this.chartOffset.top = this.chartOffset.bottom = maxOutset;
this.chartOffset.left += this.yLabelMaxWidth + this.options.grid.labelMargin;
this.chartWidth = this.canvasWidth - this.chartOffset.left - this.chartOffset.right;
this.xLabelBoxWidth = this.chartWidth / 6;
labels = [];
for (i = 0; i < this.xaxis.ticks.length; ++i) {
l = this.xaxis.ticks[i].label;
if (l) {
labels.push('<span class="tickLabel" width="' + this.xLabelBoxWidth + '">' + l + '</span>');
}
}
var xLabelMaxHeight = 0;
if (labels.length > 0) {
var dummyDiv = new Element('div', {'style': 'position:absolute;top:-10000px;font-size:smaller'});
dummyDiv.update(labels.join(""));
this.domObj.appendChild(dummyDiv);
xLabelMaxHeight = dummyDiv.getHeight();
dummyDiv.remove();
}
this.chartOffset.bottom += xLabelMaxHeight + this.options.grid.labelMargin;
this.chartHeight = this.canvasHeight - this.chartOffset.bottom - this.chartOffset.top;
this.hozScale = this.chartWidth / (this.xaxis.max - this.xaxis.min);
this.vertScale = this.chartHeight / (this.yaxis.max - this.yaxis.min);
},
/**
* function: draw
*/
draw: function() {
if(this.options.bars.show)
{
this.extendXRangeIfNeededByBar();
this.setSpacing();
this.drawGrid();
this.drawBarGraph(this.graphData, this.barDataRange);
}
else if(this.options.pies.show)
{
this.preparePieData(this.graphData);
this.drawPieGraph(this.graphData);
}
else
{
this.drawGrid();
for (var i = 0; i < this.graphData.length; i++) {
this.drawGraph(this.graphData[i]);
}
}
},
/**
* function: translateHoz
*
* Paramters:
* {Object} x
*
* Description: Given a value this function translate it to relative x coord on canvas
*/
translateHoz: function(x) {
return (x - this.xaxis.min) * this.hozScale;
},
/**
* function: translateVert
*
* parameters:
* {Object} y
*
* Description: Given a value this function translate it to relative y coord on canvas
*/
translateVert: function(y) {
return this.chartHeight - (y - this.yaxis.min) * this.vertScale;
},
/**
* function: drawGrid
*
* parameters: none
*
* description: draws the actual grid on the canvas
*/
drawGrid: function() {
var i;
this.context.save();
this.context.clearRect(0, 0, this.canvasWidth, this.canvasHeight);
this.context.translate(this.chartOffset.left, this.chartOffset.top);
// draw background, if any
if (this.options.grid.backgroundColor != null) {
this.context.fillStyle = this.options.grid.backgroundColor;
this.context.fillRect(0, 0, this.chartWidth, this.chartHeight);
}
// draw colored areas
if (this.options.grid.coloredAreas) {
var areas = this.options.grid.coloredAreas;
if (Object.isFunction(areas)) {
areas = areas({ xmin: this.xaxis.min, xmax: this.xaxis.max, ymin: this.yaxis.min, ymax: this.yaxis.max });
}
areas.each(function(a){
// clip
if (a.x1 == null || a.x1 < this.xaxis.min)
a.x1 = this.xaxis.min;
if (a.x2 == null || a.x2 > this.xaxis.max)
a.x2 = this.xaxis.max;
if (a.y1 == null || a.y1 < this.yaxis.min)
a.y1 = this.yaxis.min;
if (a.y2 == null || a.y2 > this.yaxis.max)
a.y2 = this.yaxis.max;
var tmp;
if (a.x1 > a.x2) {
tmp = a.x1;
a.x1 = a.x2;
a.x2 = tmp;
}
if (a.y1 > a.y2) {
tmp = a.y1;
a.y1 = a.y2;
a.y2 = tmp;
}
if (a.x1 >= this.xaxis.max || a.x2 <= this.xaxis.min || a.x1 == a.x2
|| a.y1 >= this.yaxis.max || a.y2 <= this.yaxis.min || a.y1 == a.y2)
return;
this.context.fillStyle = a.color || this.options.grid.coloredAreasColor;
this.context.fillRect(Math.floor(this.translateHoz(a.x1)), Math.floor(this.translateVert(a.y2)),
Math.floor(this.translateHoz(a.x2) - this.translateHoz(a.x1)), Math.floor(this.translateVert(a.y1) - this.translateVert(a.y2)));
}.bind(this));
}
// draw the inner grid
this.context.lineWidth = 1;
this.context.strokeStyle = this.options.grid.tickColor;
this.context.beginPath();
var v;
if (this.options.grid.drawXAxis) {
this.xaxis.ticks.each(function(aTick){
v = aTick.v;
if(v <= this.xaxis.min || v >= this.xaxis.max) {
return;
}
this.context.moveTo(Math.floor(this.translateHoz(v)) + this.context.lineWidth / 2, 0);
this.context.lineTo(Math.floor(this.translateHoz(v)) + this.context.lineWidth / 2, this.chartHeight);
}.bind(this));
}
if (this.options.grid.drawYAxis) {
this.yaxis.ticks.each(function(aTick){
v = aTick.v;
if(v <= this.yaxis.min || v >= this.yaxis.max) {
return;
}
this.context.moveTo(0, Math.floor(this.translateVert(v)) + this.context.lineWidth / 2);
this.context.lineTo(this.chartWidth, Math.floor(this.translateVert(v)) + this.context.lineWidth / 2);
}.bind(this));
}
this.context.stroke();
if (this.options.grid.borderWidth) {
// draw border
this.context.lineWidth = this.options.grid.borderWidth;
this.context.strokeStyle = this.options.grid.color;
this.context.lineJoin = "round";
this.context.strokeRect(0, 0, this.chartWidth, this.chartHeight);
this.context.restore();
}
},
/**
* function: insertLabels
*
* parameters: none
*
* description: inserts the label with proper spacing. Both on X and Y axis
*/
insertLabels: function() {
this.domObj.select(".tickLabels").invoke('remove');
var i, tick;
var html = '<div class="tickLabels" style="font-size:smaller;color:' + this.options.grid.color + '">';
// do the x-axis
this.xaxis.ticks.each(function(tick){
if (!tick.label || tick.v < this.xaxis.min || tick.v > this.xaxis.max)
return;
html += '<div style="position:absolute;top:' + (this.chartOffset.top + this.chartHeight + this.options.grid.labelMargin) + 'px;left:' + (this.chartOffset.left + this.translateHoz(tick.v) - this.xLabelBoxWidth/2) + 'px;width:' + this.xLabelBoxWidth + 'px;text-align:center" class="tickLabel">' + tick.label + "</div>";
}.bind(this));
// do the y-axis
this.yaxis.ticks.each(function(tick){
if (!tick.label || tick.v < this.yaxis.min || tick.v > this.yaxis.max)
return;
html += '<div id="ylabels" style="position:absolute;top:' + (this.chartOffset.top + this.translateVert(tick.v) - this.yLabelMaxHeight/2) + 'px;left:0;width:' + this.yLabelMaxWidth + 'px;text-align:right" class="tickLabel">' + tick.label + "</div>";
}.bind(this));
html += '</div>';
this.domObj.insert(html);
},
/**
* function: drawGraph
*
* Paramters:
* {Object} graphData
*
* Description: given a graphData (series) this function calls a proper lower level method to draw it.
*/
drawGraph: function(graphData) {
if (graphData.lines.show || (!graphData.bars.show && !graphData.points.show))
this.drawGraphLines(graphData);
if (graphData.bars.show)
this.drawGraphBar(graphData);
if (graphData.points.show)
this.drawGraphPoints(graphData);
},
/**
* function: plotLine
*
* parameters:
* {Object} data
* {Object} offset
*
* description:
* Helper function that plots a line based on the data provided
*/
plotLine: function(data, offset) {
var prev, cur = null, drawx = null, drawy = null;
this.context.beginPath();
for (var i = 0; i < data.length; ++i) {
prev = cur;
cur = data[i];
if (prev == null || cur == null)
continue;
var x1 = prev[0], y1 = prev[1],
x2 = cur[0], y2 = cur[1];
// clip with ymin
if (y1 <= y2 && y1 < this.yaxis.min) {
if (y2 < this.yaxis.min)
continue; // line segment is outside
// compute new intersection point
x1 = (this.yaxis.min - y1) / (y2 - y1) * (x2 - x1) + x1;
y1 = this.yaxis.min;
}
else if (y2 <= y1 && y2 < this.yaxis.min) {
if (y1 < this.yaxis.min)
continue;
x2 = (this.yaxis.min - y1) / (y2 - y1) * (x2 - x1) + x1;
y2 = this.yaxis.min;
}
// clip with ymax
if (y1 >= y2 && y1 > this.yaxis.max) {
if (y2 > this.yaxis.max)
continue;
x1 = (this.yaxis.max - y1) / (y2 - y1) * (x2 - x1) + x1;
y1 = this.yaxis.max;
}
else if (y2 >= y1 && y2 > this.yaxis.max) {
if (y1 > this.yaxis.max)
continue;
x2 = (this.yaxis.max - y1) / (y2 - y1) * (x2 - x1) + x1;
y2 = this.yaxis.max;
}
// clip with xmin
if (x1 <= x2 && x1 < this.xaxis.min) {
if (x2 < this.xaxis.min)
continue;
y1 = (this.xaxis.min - x1) / (x2 - x1) * (y2 - y1) + y1;
x1 = this.xaxis.min;
}
else if (x2 <= x1 && x2 < this.xaxis.min) {
if (x1 < this.xaxis.min)
continue;
y2 = (this.xaxis.min - x1) / (x2 - x1) * (y2 - y1) + y1;
x2 = this.xaxis.min;
}
// clip with xmax
if (x1 >= x2 && x1 > this.xaxis.max) {
if (x2 > this.xaxis.max)
continue;
y1 = (this.xaxis.max - x1) / (x2 - x1) * (y2 - y1) + y1;
x1 = this.xaxis.max;
}
else if (x2 >= x1 && x2 > this.xaxis.max) {
if (x1 > this.xaxis.max)
continue;
y2 = (this.xaxis.max - x1) / (x2 - x1) * (y2 - y1) + y1;
x2 = this.xaxis.max;
}
if (drawx != this.translateHoz(x1) || drawy != this.translateVert(y1) + offset)
this.context.moveTo(this.translateHoz(x1), this.translateVert(y1) + offset);
drawx = this.translateHoz(x2);
drawy = this.translateVert(y2) + offset;
this.context.lineTo(drawx, drawy);
}
this.context.stroke();
},
/**
* function: plotLineArea
*
* parameters:
* {Object} data
*
* description:
* Helper functoin that plots a colored line graph. This function
* takes the data nad then fill in the area on the graph properly
*/
plotLineArea: function(data) {
var prev, cur = null;
var bottom = Math.min(Math.max(0, this.yaxis.min), this.yaxis.max);
var top, lastX = 0;
var areaOpen = false;
for (var i = 0; i < data.length; ++i) {
prev = cur;
cur = data[i];
if (areaOpen && prev != null && cur == null) {
// close area
this.context.lineTo(this.translateHoz(lastX), this.translateVert(bottom));
this.context.fill();
areaOpen = false;
continue;
}
if (prev == null || cur == null)
continue;
var x1 = prev[0], y1 = prev[1],
x2 = cur[0], y2 = cur[1];
// clip x values
// clip with xmin
if (x1 <= x2 && x1 < this.xaxis.min) {
if (x2 < this.xaxis.min)
continue;
y1 = (this.xaxis.min - x1) / (x2 - x1) * (y2 - y1) + y1;
x1 = this.xaxis.min;
}
else if (x2 <= x1 && x2 < this.xaxis.min) {
if (x1 < this.xaxis.min)
continue;
y2 = (this.xaxis.min - x1) / (x2 - x1) * (y2 - y1) + y1;
x2 = this.xaxis.min;
}
// clip with xmax
if (x1 >= x2 && x1 > this.xaxis.max) {
if (x2 > this.xaxis.max)
continue;
y1 = (this.xaxis.max - x1) / (x2 - x1) * (y2 - y1) + y1;
x1 = this.xaxis.max;
}
else if (x2 >= x1 && x2 > this.xaxis.max) {
if (x1 > this.xaxis.max)
continue;
y2 = (this.xaxis.max - x1) / (x2 - x1) * (y2 - y1) + y1;
x2 = this.xaxis.max;
}
if (!areaOpen) {
// open area
this.context.beginPath();
this.context.moveTo(this.translateHoz(x1), this.translateVert(bottom));
areaOpen = true;
}
// now first check the case where both is outside
if (y1 >= this.yaxis.max && y2 >= this.yaxis.max) {
this.context.lineTo(this.translateHoz(x1), this.translateVert(this.yaxis.max));
this.context.lineTo(this.translateHoz(x2), this.translateVert(this.yaxis.max));
continue;
}
else if (y1 <= this.yaxis.min && y2 <= this.yaxis.min) {
this.context.lineTo(this.translateHoz(x1), this.translateVert(this.yaxis.min));
this.context.lineTo(this.translateHoz(x2), this.translateVert(this.yaxis.min));
continue;
}
var x1old = x1, x2old = x2;
// clip with ymin
if (y1 <= y2 && y1 < this.yaxis.min && y2 >= this.yaxis.min) {
x1 = (this.yaxis.min - y1) / (y2 - y1) * (x2 - x1) + x1;
y1 = this.yaxis.min;
}
else if (y2 <= y1 && y2 < this.yaxis.min && y1 >= this.yaxis.min) {
x2 = (this.yaxis.min - y1) / (y2 - y1) * (x2 - x1) + x1;
y2 = this.yaxis.min;
}
// clip with ymax
if (y1 >= y2 && y1 > this.yaxis.max && y2 <= this.yaxis.max) {
x1 = (this.yaxis.max - y1) / (y2 - y1) * (x2 - x1) + x1;
y1 = this.yaxis.max;
}
else if (y2 >= y1 && y2 > this.yaxis.max && y1 <= this.yaxis.max) {
x2 = (this.yaxis.max - y1) / (y2 - y1) * (x2 - x1) + x1;
y2 = this.yaxis.max;
}
// if the x value was changed we got a rectangle
// to fill
if (x1 != x1old) {
if (y1 <= this.yaxis.min)
top = this.yaxis.min;
else
top = this.yaxis.max;
this.context.lineTo(this.translateHoz(x1old), this.translateVert(top));
this.context.lineTo(this.translateHoz(x1), this.translateVert(top));
}
// fill the triangles
this.context.lineTo(this.translateHoz(x1), this.translateVert(y1));
this.context.lineTo(this.translateHoz(x2), this.translateVert(y2));
// fill the other rectangle if it's there
if (x2 != x2old) {
if (y2 <= this.yaxis.min)
top = this.yaxis.min;
else
top = this.yaxis.max;
this.context.lineTo(this.translateHoz(x2old), this.translateVert(top));
this.context.lineTo(this.translateHoz(x2), this.translateVert(top));
}
lastX = Math.max(x2, x2old);
}
if (areaOpen) {
this.context.lineTo(this.translateHoz(lastX), this.translateVert(bottom));
this.context.fill();
}
},
/**
* function: drawGraphLines
*
* parameters:
* {Object} graphData
*
* description:
* Main function that daws the line graph. This function is called
* if <options> lines property is set to show or no other type of
* graph is specified. This function depends on <plotLineArea> and
* <plotLine> functions.
*/
drawGraphLines: function(graphData) {
this.context.save();
this.context.translate(this.chartOffset.left, this.chartOffset.top);
this.context.lineJoin = "round";
var lw = graphData.lines.lineWidth;
var sw = graphData.shadowSize;
// FIXME: consider another form of shadow when filling is turned on
if (sw > 0) {
// draw shadow in two steps
this.context.lineWidth = sw / 2;
this.context.strokeStyle = "rgba(0,0,0,0.1)";
this.plotLine(graphData.data, lw/2 + sw/2 + this.context.lineWidth/2);
this.context.lineWidth = sw / 2;
this.context.strokeStyle = "rgba(0,0,0,0.2)";
this.plotLine(graphData.data, lw/2 + this.context.lineWidth/2);
}
this.context.lineWidth = lw;
this.context.strokeStyle = graphData.color;
if (graphData.lines.fill) {
this.context.fillStyle = graphData.lines.fillColor != null ? graphData.lines.fillColor : this.parseColor(graphData.color).scale(null, null, null, 0.4).toString();
this.plotLineArea(graphData.data, 0);
}
this.plotLine(graphData.data, 0);
this.context.restore();
},
/**
* function: plotPoints
*
* parameters:
* {Object} data
* {Object} radius
* {Object} fill
*
* description:
* Helper function that draws the point graph according to the data provided. Size of each
* point is provided by radius variable and fill specifies if points
* are filled
*/
plotPoints: function(data, radius, fill) {
for (var i = 0; i < data.length; ++i) {
if (data[i] == null)
continue;
var x = data[i][0], y = data[i][1];
if (x < this.xaxis.min || x > this.xaxis.max || y < this.yaxis.min || y > this.yaxis.max)
continue;
this.context.beginPath();
this.context.arc(this.translateHoz(x), this.translateVert(y), radius, 0, 2 * Math.PI, true);
if (fill)
this.context.fill();
this.context.stroke();
}
},
/**
* function: plotPointShadows
*
* parameters:
* {Object} data
* {Object} offset
* {Object} radius
*
* description:
* Helper function that draws the shadows for the points.
*/
plotPointShadows: function(data, offset, radius) {
for (var i = 0; i < data.length; ++i) {
if (data[i] == null)
continue;
var x = data[i][0], y = data[i][1];
if (x < this.xaxis.min || x > this.xaxis.max || y < this.yaxis.min || y > this.yaxis.max)
continue;
this.context.beginPath();
this.context.arc(this.translateHoz(x), this.translateVert(y) + offset, radius, 0, Math.PI, false);
this.context.stroke();
}
},
/**
* function: drawGraphPoints
*
* paramters:
* {Object} graphData
*
* description:
* Draws the point graph onto the canvas. This function depends on helper
* functions <plotPointShadows> and <plotPoints>
*/
drawGraphPoints: function(graphData) {
this.context.save();
this.context.translate(this.chartOffset.left, this.chartOffset.top);
var lw = graphData.lines.lineWidth;
var sw = graphData.shadowSize;
if (sw > 0) {
// draw shadow in two steps
this.context.lineWidth = sw / 2;
this.context.strokeStyle = "rgba(0,0,0,0.1)";
this.plotPointShadows(graphData.data, sw/2 + this.context.lineWidth/2, graphData.points.radius);
this.context.lineWidth = sw / 2;
this.context.strokeStyle = "rgba(0,0,0,0.2)";
this.plotPointShadows(graphData.data, this.context.lineWidth/2, graphData.points.radius);
}
this.context.lineWidth = graphData.points.lineWidth;
this.context.strokeStyle = graphData.color;
this.context.fillStyle = graphData.points.fillColor != null ? graphData.points.fillColor : graphData.color;
this.plotPoints(graphData.data, graphData.points.radius, graphData.points.fill);
this.context.restore();
},
/**
* function: preparePieData
*
* parameters:
* {Object} graphData
*
* Description:
* Helper function that manipulates the given data stream so that it can
* be plotted as a Pie Chart
*/
preparePieData: function(graphData)
{
for(i = 0; i < graphData.length; i++)
{
var data = 0;
for(j = 0; j < graphData[i].data.length; j++){
data += parseInt(graphData[i].data[j][1]);
}
graphData[i].data = data;
}
},
/**
* function: drawPieShadow
*
* {Object} anchorX
* {Object} anchorY
* {Object} radius
*
* description:
* Helper function that draws a shadow for the Pie Chart. This just draws
* a circle with offset that simulates shadow. We do not give each piece
* of the pie an individual shadow.
*/
drawPieShadow: function(anchorX, anchorY, radius)
{
this.context.beginPath();
this.context.moveTo(anchorX, anchorY);
this.context.fillStyle = 'rgba(0,0,0,' + 0.1 + ')';
startAngle = 0;
endAngle = (Math.PI/180)*360;
this.context.arc(anchorX + 2, anchorY +2, radius + (this.options.shadowSize/2), startAngle, endAngle, false);
this.context.fill();
this.context.closePath();
},
/**
* function: drawPieGraph
*
* parameters:
* {Object} graphData
*
* description:
* Draws the actual pie chart. This function depends on helper function
* <drawPieShadow> to draw the actual shadow
*/
drawPieGraph: function(graphData)
{
var sumData = 0;
var radius = 0;
var centerX = this.chartWidth/2;
var centerY = this.chartHeight/2;
var startAngle = 0;
var endAngle = 0;
var fontSize = this.options.pies.fontSize;
var labelWidth = this.options.pies.labelWidth;
//determine Pie Radius
if(!this.options.pies.autoScale)
radius = this.options.pies.radius;
else
radius = (this.chartHeight * 0.85)/2;
var labelRadius = radius * 1.05;
for(i = 0; i < graphData.length; i++)
sumData += graphData[i].data;
// used to adjust labels so that everything adds up to 100%
totalPct = 0;
//lets draw the shadow first.. we don't need an individual shadow to every pie rather we just
//draw a circle underneath to simulate the shadow...
this.drawPieShadow(centerX, centerY, radius, 0, 0);
//lets draw the actual pie chart now.
graphData.each(function(gd, j){
var pct = gd.data / sumData;
startAngle = endAngle;
endAngle += pct * (2 * Math.PI);
var sliceMiddle = (endAngle - startAngle) / 2 + startAngle;
var labelX = centerX + Math.cos(sliceMiddle) * labelRadius;
var labelY = centerY + Math.sin(sliceMiddle) * labelRadius;
var anchorX = centerX;
var anchorY = centerY;
var textAlign = null;
var verticalAlign = null;
var left = 0;
var top = 0;
//draw pie:
//drawing pie
this.context.beginPath();
this.context.moveTo(anchorX, anchorY);
this.context.arc(anchorX, anchorY, radius, startAngle, endAngle, false);
this.context.closePath();
this.context.fillStyle = this.parseColor(gd.color).scale(null, null, null, this.options.pies.fillOpacity).toString();
if(this.options.pies.fill) { this.context.fill(); }
// drawing labels
if (sliceMiddle <= 0.25 * (2 * Math.PI))
{
// text on top and align left
textAlign = "left";
verticalAlign = "top";
left = labelX;
top = labelY + fontSize;
}
else if (sliceMiddle > 0.25 * (2 * Math.PI) && sliceMiddle <= 0.5 * (2 * Math.PI))
{
// text on bottom and align left
textAlign = "left";
verticalAlign = "bottom";
left = labelX - labelWidth;
top = labelY;
}
else if (sliceMiddle > 0.5 * (2 * Math.PI) && sliceMiddle <= 0.75 * (2 * Math.PI))
{
// text on bottom and align right
textAlign = "right";
verticalAlign = "bottom";
left = labelX - labelWidth;
top = labelY - fontSize;
}
else
{
// text on top and align right
textAlign = "right";
verticalAlign = "bottom";
left = labelX;
top = labelY - fontSize;
}
left = left + "px";
top = top + "px";
var textVal = Math.round(pct * 100);
if (j == graphData.length - 1) {
if (textVal + totalPct < 100) {
textVal = textVal + 1;
} else if (textVal + totalPct > 100) {
textVal = textVal - 1;
};
}
var html = "<div style=\"position: absolute;zindex:11; width:" + labelWidth + "px;fontSize:" + fontSize + "px;overflow:hidden;top:"+ top + ";left:"+ left + ";textAlign:" + textAlign + ";verticalAlign:" + verticalAlign +"\">" + textVal + "%</div>";
//$(html).appendTo(target);
this.domObj.insert(html);
totalPct = totalPct + textVal;
}.bind(this));
},
/**
* function: drawBarGraph
*
* parameters:
* {Object} graphData
* {Object} barDataRange
*
* description:
* Goes through each series in graphdata and passes it onto <drawBarGraphs> function
*/
drawBarGraph: function(graphData, barDataRange)
{
graphData.each(function(gd, i){
this.drawGraphBars(gd, i, graphData.size(), barDataRange);
}.bind(this));
},
/**
* function: drawGraphBar
*
* parameters:
* {Object} graphData
*
* description:
* This function is called when an individual series in GraphData is bar graph and plots it
*/
drawGraphBar: function(graphData)
{
this.drawGraphBars(graphData, 0, this.graphData.length, this.barDataRange);
},
/**
* function: plotBars
*
* parameters:
* {Object} graphData
* {Object} data
* {Object} barWidth
* {Object} offset
* {Object} fill
* {Object} counter
* {Object} total
* {Object} barDataRange
*
* description:
* Helper function that draws the bar graph based on data.
*/
plotBars: function(graphData, data, barWidth, offset, fill,counter, total, barDataRange) {
var shift = 0;
if(total % 2 == 0)
{
shift = (1 + ((counter - total /2 ) - 1)) * barWidth;
}
else
{
var interval = 0.5;
if(counter == (total/2 - interval )) {
shift = - barWidth * interval;
}
else {
shift = (interval + (counter - Math.round(total/2))) * barWidth;
}
}
var rangeData = [];
data.each(function(d){
if(!d) return;
var x = d[0], y = d[1];
var drawLeft = true, drawTop = true, drawRight = true;
var left = x + shift, right = x + barWidth + shift, bottom = 0, top = y;
var rangeDataPoint = {};
rangeDataPoint.left = left;
rangeDataPoint.right = right;
rangeDataPoint.value = top;
rangeData.push(rangeDataPoint);
if (right < this.xaxis.min || left > this.xaxis.max || top < this.yaxis.min || bottom > this.yaxis.max)
return;
// clip
if (left < this.xaxis.min) {
left = this.xaxis.min;
drawLeft = false;
}
if (right > this.xaxis.max) {
right = this.xaxis.max;
drawRight = false;
}
if (bottom < this.yaxis.min)
bottom = this.yaxis.min;
if (top > this.yaxis.max) {
top = this.yaxis.max;
drawTop = false;
}
if(graphData.bars.showShadow && graphData.shadowSize > 0)
this.plotShadowOutline(graphData, this.context.strokeStyle, left, bottom, top, right, drawLeft, drawRight, drawTop);
// fill the bar
if (fill) {
this.context.beginPath();
this.context.moveTo(this.translateHoz(left), this.translateVert(bottom) + offset);
this.context.lineTo(this.translateHoz(left), this.translateVert(top) + offset);
this.context.lineTo(this.translateHoz(right), this.translateVert(top) + offset);
this.context.lineTo(this.translateHoz(right), this.translateVert(bottom) + offset);
this.context.fill();
}
// draw outline
if (drawLeft || drawRight || drawTop) {
this.context.beginPath();
this.context.moveTo(this.translateHoz(left), this.translateVert(bottom) + offset);
if (drawLeft)
this.context.lineTo(this.translateHoz(left), this.translateVert(top) + offset);
else
this.context.moveTo(this.translateHoz(left), this.translateVert(top) + offset);
if (drawTop)
this.context.lineTo(this.translateHoz(right), this.translateVert(top) + offset);
else
this.context.moveTo(this.translateHoz(right), this.translateVert(top) + offset);
if (drawRight)
this.context.lineTo(this.translateHoz(right), this.translateVert(bottom) + offset);
else
this.context.moveTo(this.translateHoz(right), this.translateVert(bottom) + offset);
this.context.stroke();
}
}.bind(this));
barDataRange.push(rangeData);
},
/**
* function: plotShadowOutline
*
* parameters:
* {Object} graphData
* {Object} orgStrokeStyle
* {Object} left
* {Object} bottom
* {Object} top
* {Object} right
* {Object} drawLeft
* {Object} drawRight
* {Object} drawTop
*
* description:
* Helper function that draws a outline simulating shadow for bar chart
*/
plotShadowOutline: function(graphData, orgStrokeStyle, left, bottom, top, right, drawLeft, drawRight, drawTop)
{
var orgOpac = 0.3;
for(var n = 1; n <= this.options.shadowSize/2; n++)
{
var opac = orgOpac * n;
this.context.beginPath();
this.context.strokeStyle = "rgba(0,0,0," + opac + ")";
this.context.moveTo(this.translateHoz(left) + n, this.translateVert(bottom));
if(drawLeft)
this.context.lineTo(this.translateHoz(left) + n, this.translateVert(top) - n);
else
this.context.moveTo(this.translateHoz(left) + n, this.translateVert(top) - n);
if(drawTop)
this.context.lineTo(this.translateHoz(right) + n, this.translateVert(top) - n);
else
this.context.moveTo(this.translateHoz(right) + n, this.translateVert(top) - n);
if(drawRight)
this.context.lineTo(this.translateHoz(right) + n, this.translateVert(bottom));
else
this.context.lineTo(this.translateHoz(right) + n, this.translateVert(bottom));
this.context.stroke();
this.context.closePath();
}
this.context.strokeStyle = orgStrokeStyle;
},
/**
* function: drawGraphBars
*
* parameters:
* {Object} graphData
* {Object} counter
* {Object} total
* {Object} barDataRange
*
* description:
* Draws the actual bar graphs. Calls <plotBars> to draw the individual bar
*/
drawGraphBars: function(graphData, counter, total, barDataRange){
this.context.save();
this.context.translate(this.chartOffset.left, this.chartOffset.top);
this.context.lineJoin = "round";
var bw = graphData.bars.barWidth;
var lw = Math.min(graphData.bars.lineWidth, bw);
this.context.lineWidth = lw;
this.context.strokeStyle = graphData.color;
if (graphData.bars.fill) {
this.context.fillStyle = graphData.bars.fillColor != null ? graphData.bars.fillColor : this.parseColor(graphData.color).scale(null, null, null, this.options.bars.fillOpacity).toString();
}
this.plotBars(graphData, graphData.data, bw, 0, graphData.bars.fill, counter, total, barDataRange);
this.context.restore();
},
/**
* function: insertLegend
*
* description:
* inserts legend onto the graph. *legend: {show: true}* must be set in <options>
* for for this to work.
*/
insertLegend: function() {
this.domObj.select(".legend").invoke('remove');
if (!this.options.legend.show)
return;
var fragments = [];
var rowStarted = false;
this.graphData.each(function(gd, index){
if(!gd.label) {
return;
}
if(index % this.options.legend.noColumns == 0) {
if(rowStarted) {
fragments.push('</tr>');
}
fragments.push('<tr>');
rowStarted = true;
}
var label = gd.label;
if(this.options.legend.labelFormatter != null) {
label = this.options.legend.labelFormatter(label);
}
fragments.push(
'<td class="legendColorBox"><div style="border:1px solid ' + this.options.legend.labelBoxBorderColor + ';padding:1px"><div style="width:14px;height:10px;background-color:' + gd.color + ';overflow:hidden"></div></div></td>' +
'<td class="legendLabel">' + label + '</td>');
}.bind(this));
if (rowStarted)
fragments.push('</tr>');
if(fragments.length > 0){
var table = '<table style="font-size:smaller;color:' + this.options.grid.color + '">' + fragments.join("") + '</table>';
if($(this.options.legend.container) != null){
$(this.options.legend.container).insert(table);
}else{
var pos = '';
var p = this.options.legend.position, m = this.options.legend.margin;
if(p.charAt(0) == 'n') pos += 'top:' + (m + this.chartOffset.top) + 'px;';
else if(p.charAt(0) == 's') pos += 'bottom:' + (m + this.chartOffset.bottom) + 'px;';
if(p.charAt(1) == 'e') pos += 'right:' + (m + this.chartOffset.right) + 'px;';
else if(p.charAt(1) == 'w') pos += 'left:' + (m + this.chartOffset.bottom) + 'px;';
var div = this.domObj.insert('<div class="ProtoChart-legend" style="border: 1px solid '+this.options.legend.borderColor+'; position:absolute;z-index:2;' + pos +'">' + table + '</div>').getElementsBySelector('div.ProtoChart-legend').first();
if(this.options.legend.backgroundOpacity != 0.0){
var c = this.options.legend.backgroundColor;
if(c == null){
var tmp = (this.options.grid.backgroundColor != null) ? this.options.grid.backgroundColor : this.extractColor(div);
c = this.parseColor(tmp).adjust(null, null, null, 1).toString();
}
this.domObj.insert('<div class="ProtoChart-legend-bg" style="position:absolute;width:' + div.getWidth() + 'px;height:' + div.getHeight() + 'px;' + pos +'background-color:' + c + ';"> </div>').select('div.ProtoChart-legend-bg').first().setStyle({
'opacity': this.options.legend.backgroundOpacity
});
}
}
}
},
/**
* Function: onMouseMove
*
* parameters:
* event: {Object} ev
*
* Description:
* Called whenever the mouse is moved on the graph. This takes care of the mousetracking.
* This event also fires <ProtoChart:mousemove> event, which gets current position of the
* mouse as a parameters.
*/
onMouseMove: function(ev) {
var e = ev || window.event;
if (e.pageX == null && e.clientX != null) {
var de = document.documentElement, b = $(document.body);
this.lastMousePos.pageX = e.clientX + (de && de.scrollLeft || b.scrollLeft || 0);
this.lastMousePos.pageY = e.clientY + (de && de.scrollTop || b.scrollTop || 0);
}
else {
this.lastMousePos.pageX = e.pageX;
this.lastMousePos.pageY = e.pageY;
}
var offset = this.overlay.cumulativeOffset();
var pos = {
x: this.xaxis.min + (e.pageX - offset.left - this.chartOffset.left) / this.hozScale,
y: this.yaxis.max - (e.pageY - offset.top - this.chartOffset.top) / this.vertScale
};
if(this.options.mouse.track && this.selectionInterval == null) {
this.hit(ev, pos);
}
this.domObj.fire("ProtoChart:mousemove", [ pos ]);
},
/**
* Function: onMouseDown
*
* Parameters:
* Event - {Object} e
*
* Description:
* Called whenever the mouse is clicked.
*/
onMouseDown: function(e) {
if (e.which != 1) // only accept left-click
return;
document.body.focus();
if (document.onselectstart !== undefined && this.workarounds.onselectstart == null) {
this.workarounds.onselectstart = document.onselectstart;
document.onselectstart = function () { return false; };
}
if (document.ondrag !== undefined && this.workarounds.ondrag == null) {
this.workarounds.ondrag = document.ondrag;
document.ondrag = function () { return false; };
}
this.setSelectionPos(this.selection.first, e);
if (this.selectionInterval != null)
clearInterval(this.selectionInterval);
this.lastMousePos.pageX = null;
this.selectionInterval = setInterval(this.updateSelectionOnMouseMove.bind(this), 200);
this.overlay.observe("mouseup", this.onSelectionMouseUp.bind(this));
},
/**
* Function: onClick
* parameters:
* Event - {Object} e
* Description:
* Handles the "click" event on the chart. This function fires <ProtoChart:plotclick> event. If
* <options.allowDataClick> is enabled then it also fires <ProtoChart:dataclick> event which gives
* you access to exact data point where user clicked.
*/
onClick: function(e) {
if (this.ignoreClick) {
this.ignoreClick = false;
return;
}
var offset = this.overlay.cumulativeOffset();
var pos ={
x: this.xaxis.min + (e.pageX - offset.left - this.chartOffset.left) / this.hozScale,
y: this.yaxis.max - (e.pageY - offset.top - this.chartOffset.top) / this.vertScale
};
this.domObj.fire("ProtoChart:plotclick", [ pos ]);
if(this.options.allowDataClick)
{
var dataPoint = {};
if(this.options.points.show)
{
dataPoint = this.getDataClickPoint(pos, this.options);
this.domObj.fire("ProtoChart:dataclick", [dataPoint]);
}
else if(this.options.lines.show && this.options.points.show)
{
dataPoint = this.getDataClickPoint(pos, this.options);
this.domObj.fire("ProtoChart:dataclick", [dataPoint]);
}
else if(this.options.bars.show)
{
if(this.barDataRange.length > 0)
{
dataPoint = this.getDataClickPoint(pos, this.options, this.barDataRange);
this.domObj.fire("ProtoChart:dataclick", [dataPoint]);
}
}
}
},
/**
* Internal function used by onClick method.
*/
getDataClickPoint: function(pos, options, barDataRange)
{
pos.x = parseInt(pos.x);
pos.y = parseInt(pos.y);
var yClick = pos.y.toFixed(0);
var dataVal = {};
dataVal.position = pos;
dataVal.value = '';
if(options.points.show)
{
this.graphData.each(function(gd){
var temp = gd.data;
var xClick = parseInt(pos.x.toFixed(0));
if(xClick < 0) { xClick = 0; }
if(temp[xClick] && yClick >= temp[xClick][1] - (this.options.points.radius * 10) && yClick <= temp[xClick][1] + (this.options.points.radius * 10)) {
dataVal.value = temp[xClick][1];
throw $break;
}
}.bind(this));
}
else if(options.bars.show)
{
xClick = pos.x;
this.barDataRange.each(function(barData){
barData.each(function(data){
var temp = data;
if(xClick > temp.left && xClick < temp.right) {
dataVal.value = temp.value;
throw $break;
}
}.bind(this));
}.bind(this));
}
return dataVal;
},
/**
* Function: triggerSelectedEvent
*
* Description:
* Internal function called when a selection on the graph is made. This function
* fires <ProtoChart:selected> event which has a parameter representing the selection
* {
* x1: {int}, y1: {int},
* x2: {int}, y2: {int}
* }
*/
triggerSelectedEvent: function() {
var x1, x2, y1, y2;
if (this.selection.first.x <= this.selection.second.x) {
x1 = this.selection.first.x;
x2 = this.selection.second.x;
}
else {
x1 = this.selection.second.x;
x2 = this.selection.first.x;
}
if (this.selection.first.y >= this.selection.second.y) {
y1 = this.selection.first.y;
y2 = this.selection.second.y;
}
else {
y1 = this.selection.second.y;
y2 = this.selection.first.y;
}
x1 = this.xaxis.min + x1 / this.hozScale;
x2 = this.xaxis.min + x2 / this.hozScale;
y1 = this.yaxis.max - y1 / this.vertScale;
y2 = this.yaxis.max - y2 / this.vertScale;
this.domObj.fire("ProtoChart:selected", [ { x1: x1, y1: y1, x2: x2, y2: y2 } ]);
},
/**
* Internal function
*/
onSelectionMouseUp: function(e) {
if (document.onselectstart !== undefined)
document.onselectstart = this.workarounds.onselectstart;
if (document.ondrag !== undefined)
document.ondrag = this.workarounds.ondrag;
if (this.selectionInterval != null) {
clearInterval(this.selectionInterval);
this.selectionInterval = null;
}
this.setSelectionPos(this.selection.second, e);
this.clearSelection();
if (!this.selectionIsSane() || e.which != 1)
return false;
this.drawSelection();
this.triggerSelectedEvent();
this.ignoreClick = true;
return false;
},
setSelectionPos: function(pos, e) {
var offset = $(this.overlay).cumulativeOffset();
if (this.options.selection.mode == "y") {
if (pos == this.selection.first)
pos.x = 0;
else
pos.x = this.chartWidth;
}
else {
pos.x = e.pageX - offset.left - this.chartOffset.left;
pos.x = Math.min(Math.max(0, pos.x), this.chartWidth);
}
if (this.options.selection.mode == "x") {
if (pos == this.selection.first)
pos.y = 0;
else
pos.y = this.chartHeight;
}
else {
pos.y = e.pageY - offset.top - this.chartOffset.top;
pos.y = Math.min(Math.max(0, pos.y), this.chartHeight);
}
},
updateSelectionOnMouseMove: function() {
if (this.lastMousePos.pageX == null)
return;
this.setSelectionPos(this.selection.second, this.lastMousePos);
this.clearSelection();
if (this.selectionIsSane())
this.drawSelection();
},
clearSelection: function() {
if (this.prevSelection == null)
return;
var x = Math.min(this.prevSelection.first.x, this.prevSelection.second.x),
y = Math.min(this.prevSelection.first.y, this.prevSelection.second.y),
w = Math.abs(this.prevSelection.second.x - this.prevSelection.first.x),
h = Math.abs(this.prevSelection.second.y - this.prevSelection.first.y);
this.overlayContext.clearRect(x + this.chartOffset.left - this.overlayContext.lineWidth,
y + this.chartOffset.top - this.overlayContext.lineWidth,
w + this.overlayContext.lineWidth*2,
h + this.overlayContext.lineWidth*2);
this.prevSelection = null;
},
/**
* Function: setSelection
*
* Parameters:
* Area - {Object} area represented as a range like: {x1: 3, y1: 3, x2: 4, y2: 8}
*
* Description:
* Sets the current graph selection to the provided range. Calls <drawSelection> and
* <triggerSelectedEvent> functions internally.
*/
setSelection: function(area) {
this.clearSelection();
if (this.options.selection.mode == "x") {
this.selection.first.y = 0;
this.selection.second.y = this.chartHeight;
}
else {
this.selection.first.y = (this.yaxis.max - area.y1) * this.vertScale;
this.selection.second.y = (this.yaxis.max - area.y2) * this.vertScale;
}
if (this.options.selection.mode == "y") {
this.selection.first.x = 0;
this.selection.second.x = this.chartWidth;
}
else {
this.selection.first.x = (area.x1 - this.xaxis.min) * this.hozScale;
this.selection.second.x = (area.x2 - this.xaxis.min) * this.hozScale;
}
this.drawSelection();
this.triggerSelectedEvent();
},
/**
* Function: drawSelection
* Description: Internal function called to draw the selection made on the graph.
*/
drawSelection: function() {
if (this.prevSelection != null &&
this.selection.first.x == this.prevSelection.first.x &&
this.selection.first.y == this.prevSelection.first.y &&
this.selection.second.x == this.prevSelection.second.x &&
this.selection.second.y == this.prevSelection.second.y)
{
return;
}
this.overlayContext.strokeStyle = this.parseColor(this.options.selection.color).scale(null, null, null, 0.8).toString();
this.overlayContext.lineWidth = 1;
this.context.lineJoin = "round";
this.overlayContext.fillStyle = this.parseColor(this.options.selection.color).scale(null, null, null, 0.4).toString();
this.prevSelection = { first: { x: this.selection.first.x,
y: this.selection.first.y },
second: { x: this.selection.second.x,
y: this.selection.second.y } };
var x = Math.min(this.selection.first.x, this.selection.second.x),
y = Math.min(this.selection.first.y, this.selection.second.y),
w = Math.abs(this.selection.second.x - this.selection.first.x),
h = Math.abs(this.selection.second.y - this.selection.first.y);
this.overlayContext.fillRect(x + this.chartOffset.left, y + this.chartOffset.top, w, h);
this.overlayContext.strokeRect(x + this.chartOffset.left, y + this.chartOffset.top, w, h);
},
/**
* Internal function
*/
selectionIsSane: function() {
var minSize = 5;
return Math.abs(this.selection.second.x - this.selection.first.x) >= minSize &&
Math.abs(this.selection.second.y - this.selection.first.y) >= minSize;
},
/**
* Internal function that formats the track. This is the format the text is shown when mouse
* tracking is enabled.
*/
defaultTrackFormatter: function(val)
{
return '['+val.x+', '+val.y+']';
},
/**
* Function: clearHit
*/
clearHit: function(){
if(this.prevHit){
this.overlayContext.clearRect(
this.translateHoz(this.prevHit.x) + this.chartOffset.left - this.options.mouse.radius*2,
this.translateVert(this.prevHit.y) + this.chartOffset.top - this.options.mouse.radius*2,
this.options.mouse.radius*3 + this.options.points.lineWidth*3,
this.options.mouse.radius*3 + this.options.points.lineWidth*3
);
this.prevHit = null;
}
},
/**
* Function: hit
*
* Parameters:
* event - {Object} event object
* mouse - {Object} mouse object that is used to keep track of mouse movement
*
* Description:
* If hit occurs this function will fire a ProtoChart:hit event.
*/
hit: function(event, mouse){
/**
* Nearest data element.
*/
var n = {
dist:Number.MAX_VALUE,
x:null,
y:null,
mouse:null
};
for(var i = 0, data, xsens, ysens; i < this.graphData.length; i++){
if(!this.graphData[i].mouse.track) continue;
data = this.graphData[i].data;
xsens = (this.hozScale*this.graphData[i].mouse.sensibility);
ysens = (this.vertScale*this.graphData[i].mouse.sensibility);
for(var j = 0, xabs, yabs; j < data.length; j++){
xabs = this.hozScale*Math.abs(data[j][0] - mouse.x);
yabs = this.vertScale*Math.abs(data[j][1] - mouse.y);
if(xabs < xsens && yabs < ysens && (xabs+yabs) < n.dist){
n.dist = (xabs+yabs);
n.x = data[j][0];
n.y = data[j][1];
n.mouse = this.graphData[i].mouse;
}
}
}
if(n.mouse && n.mouse.track && !this.prevHit || (this.prevHit && n.x != this.prevHit.x && n.y != this.prevHit.y)){
var el = this.domObj.select('.'+this.options.mouse.clsName).first();
if(!el){
var pos = '', p = this.options.mouse.position, m = this.options.mouse.margin;
if(p.charAt(0) == 'n') pos += 'top:' + (m + this.chartOffset.top) + 'px;';
else if(p.charAt(0) == 's') pos += 'bottom:' + (m + this.chartOffset.bottom) + 'px;';
if(p.charAt(1) == 'e') pos += 'right:' + (m + this.chartOffset.right) + 'px;';
else if(p.charAt(1) == 'w') pos += 'left:' + (m + this.chartOffset.bottom) + 'px;';
this.domObj.insert('<div class="'+this.options.mouse.clsName+'" style="display:none;position:absolute;'+pos+'"></div>');
return;
}
if(n.x !== null && n.y !== null){
el.setStyle({display:'block'});
this.clearHit();
if(n.mouse.lineColor != null){
this.overlayContext.save();
this.overlayContext.translate(this.chartOffset.left, this.chartOffset.top);
this.overlayContext.lineWidth = this.options.points.lineWidth;
this.overlayContext.strokeStyle = n.mouse.lineColor;
this.overlayContext.fillStyle = '#ffffff';
this.overlayContext.beginPath();
this.overlayContext.arc(this.translateHoz(n.x), this.translateVert(n.y), this.options.mouse.radius, 0, 2 * Math.PI, true);
this.overlayContext.fill();
this.overlayContext.stroke();
this.overlayContext.restore();
}
this.prevHit = n;
var decimals = n.mouse.trackDecimals;
if(decimals == null || decimals < 0) decimals = 0;
if(!this.options.mouse.fixedPosition)
{
el.setStyle({
left: (this.translateHoz(n.x) + this.options.mouse.radius + 10) + "px",
top: (this.translateVert(n.y) + this.options.mouse.radius + 10) + "px"
});
}
el.innerHTML = n.mouse.trackFormatter({x: n.x.toFixed(decimals), y: n.y.toFixed(decimals)});
this.domObj.fire( 'ProtoChart:hit', [n] )
}else if(this.options.prevHit){
el.setStyle({display:'none'});
this.clearHit();
}
}
},
/**
* Internal function
*/
floorInBase: function(n, base) {
return base * Math.floor(n / base);
},
/**
* Function: extractColor
*
* Parameters:
* element - HTML element or ID of an HTML element
*
* Returns:
* color in string format
*/
extractColor: function(element)
{
var color;
do
{
color = $(element).getStyle('background-color').toLowerCase();
if(color != '' && color != 'transparent')
{
break;
}
element = element.up(0); //or else just get the parent ....
} while(element.nodeName.toLowerCase() != 'body');
//safari fix
if(color == 'rgba(0, 0, 0, 0)')
return 'transparent';
return color;
},
/**
* Function: parseColor
*
* Parameters:
* str - color string in different formats
*
* Returns:
* a Proto.Color Object - use toString() function to retreive the color in rgba/rgb format
*/
parseColor: function(str)
{
var result;
/**
* rgb(num,num,num)
*/
if((result = /rgb\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*\)/.exec(str)))
return new Proto.Color(parseInt(result[1]), parseInt(result[2]), parseInt(result[3]));
/**
* rgba(num,num,num,num)
*/
if((result = /rgba\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]+(?:\.[0-9]+)?)\s*\)/.exec(str)))
return new Proto.Color(parseInt(result[1]), parseInt(result[2]), parseInt(result[3]), parseFloat(result[4]));
/**
* rgb(num%,num%,num%)
*/
if((result = /rgb\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*\)/.exec(str)))
return new Proto.Color(parseFloat(result[1])*2.55, parseFloat(result[2])*2.55, parseFloat(result[3])*2.55);
/**
* rgba(num%,num%,num%,num)
*/
if((result = /rgba\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\s*\)/.exec(str)))
return new Proto.Color(parseFloat(result[1])*2.55, parseFloat(result[2])*2.55, parseFloat(result[3])*2.55, parseFloat(result[4]));
/**
* #a0b1c2
*/
if((result = /#([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})/.exec(str)))
return new Proto.Color(parseInt(result[1],16), parseInt(result[2],16), parseInt(result[3],16));
/**
* #fff
*/
if((result = /#([a-fA-F0-9])([a-fA-F0-9])([a-fA-F0-9])/.exec(str)))
return new Proto.Color(parseInt(result[1]+result[1],16), parseInt(result[2]+result[2],16), parseInt(result[3]+result[3],16));
/**
* Otherwise, check if user wants transparent .. or we just return a standard color;
*/
var name = str.strip().toLowerCase();
if(name == 'transparent'){
return new Proto.Color(255, 255, 255, 0);
}
return new Proto.Color(100,100,100, 1);
}
});
if(!Proto) var Proto = {};
/**
* Class: Proto.Color
*
* Helper class that manipulates colors using RGBA values.
*
*/
Proto.Color = Class.create({
initialize: function(r, g, b, a) {
this.rgba = ['r', 'g', 'b', 'a'];
var x = 4;
while(-1<--x) {
this[this.rgba[x]] = arguments[x] || ((x==3) ? 1.0 : 0);
}
},
toString: function() {
if(this.a >= 1.0) {
return "rgb(" + [this.r, this.g, this.b].join(",") +")";
}
else {
return "rgba("+[this.r, this.g, this.b, this.a].join(",")+")";
}
},
scale: function(rf, gf, bf, af) {
x = 4;
while(-1<--x) {
if(arguments[x] != null) {
this[this.rgba[x]] *= arguments[x];
}
}
return this.normalize();
},
adjust: function(rd, gd, bd, ad) {
x = 4; //rgba.length
while (-1<--x) {
if (arguments[x] != null)
this[this.rgba[x]] += arguments[x];
}
return this.normalize();
},
clone: function() {
return new Proto.Color(this.r, this.b, this.g, this.a);
},
limit: function(val,minVal,maxVal) {
return Math.max(Math.min(val, maxVal), minVal);
},
normalize: function() {
this.r = this.limit(parseInt(this.r), 0, 255);
this.g = this.limit(parseInt(this.g), 0, 255);
this.b = this.limit(parseInt(this.b), 0, 255);
this.a = this.limit(this.a, 0, 1);
return this;
}
});
if(!window.CanvasRenderingContext2D){(function(){var I=Math,i=I.round,L=I.sin,M=I.cos,m=10,A=m/2,Q={init:function(a){var b=a||document;if(/MSIE/.test(navigator.userAgent)&&!window.opera){var c=this;b.attachEvent("onreadystatechange",function(){c.r(b)})}},r:function(a){if(a.readyState=="complete"){if(!a.namespaces["s"]){a.namespaces.add("g_vml_","urn:schemas-microsoft-com:vml")}var b=a.createStyleSheet();b.cssText="canvas{display:inline-block;overflow:hidden;text-align:left;width:300px;height:150px}g_vml_\\:*{behavior:url(#default#VML)}";
var c=a.getElementsByTagName("canvas");for(var d=0;d<c.length;d++){if(!c[d].getContext){this.initElement(c[d])}}}},q:function(a){var b=a.outerHTML,c=a.ownerDocument.createElement(b);if(b.slice(-2)!="/>"){var d="/"+a.tagName,e;while((e=a.nextSibling)&&e.tagName!=d){e.removeNode()}if(e){e.removeNode()}}a.parentNode.replaceChild(c,a);return c},initElement:function(a){a=this.q(a);a.getContext=function(){if(this.l){return this.l}return this.l=new K(this)};a.attachEvent("onpropertychange",V);a.attachEvent("onresize",
W);var b=a.attributes;if(b.width&&b.width.specified){a.style.width=b.width.nodeValue+"px"}else{a.width=a.clientWidth}if(b.height&&b.height.specified){a.style.height=b.height.nodeValue+"px"}else{a.height=a.clientHeight}return a}};function V(a){var b=a.srcElement;switch(a.propertyName){case "width":b.style.width=b.attributes.width.nodeValue+"px";b.getContext().clearRect();break;case "height":b.style.height=b.attributes.height.nodeValue+"px";b.getContext().clearRect();break}}function W(a){var b=a.srcElement;
if(b.firstChild){b.firstChild.style.width=b.clientWidth+"px";b.firstChild.style.height=b.clientHeight+"px"}}Q.init();var R=[];for(var E=0;E<16;E++){for(var F=0;F<16;F++){R[E*16+F]=E.toString(16)+F.toString(16)}}function J(){return[[1,0,0],[0,1,0],[0,0,1]]}function G(a,b){var c=J();for(var d=0;d<3;d++){for(var e=0;e<3;e++){var g=0;for(var h=0;h<3;h++){g+=a[d][h]*b[h][e]}c[d][e]=g}}return c}function N(a,b){b.fillStyle=a.fillStyle;b.lineCap=a.lineCap;b.lineJoin=a.lineJoin;b.lineWidth=a.lineWidth;b.miterLimit=
a.miterLimit;b.shadowBlur=a.shadowBlur;b.shadowColor=a.shadowColor;b.shadowOffsetX=a.shadowOffsetX;b.shadowOffsetY=a.shadowOffsetY;b.strokeStyle=a.strokeStyle;b.d=a.d;b.e=a.e}function O(a){var b,c=1;a=String(a);if(a.substring(0,3)=="rgb"){var d=a.indexOf("(",3),e=a.indexOf(")",d+1),g=a.substring(d+1,e).split(",");b="#";for(var h=0;h<3;h++){b+=R[Number(g[h])]}if(g.length==4&&a.substr(3,1)=="a"){c=g[3]}}else{b=a}return[b,c]}function S(a){switch(a){case "butt":return"flat";case "round":return"round";
case "square":default:return"square"}}function K(a){this.a=J();this.m=[];this.k=[];this.c=[];this.strokeStyle="#000";this.fillStyle="#000";this.lineWidth=1;this.lineJoin="miter";this.lineCap="butt";this.miterLimit=m*1;this.globalAlpha=1;this.canvas=a;var b=a.ownerDocument.createElement("div");b.style.width=a.clientWidth+"px";b.style.height=a.clientHeight+"px";b.style.overflow="hidden";b.style.position="absolute";a.appendChild(b);this.j=b;this.d=1;this.e=1}var j=K.prototype;j.clearRect=function(){this.j.innerHTML=
"";this.c=[]};j.beginPath=function(){this.c=[]};j.moveTo=function(a,b){this.c.push({type:"moveTo",x:a,y:b});this.f=a;this.g=b};j.lineTo=function(a,b){this.c.push({type:"lineTo",x:a,y:b});this.f=a;this.g=b};j.bezierCurveTo=function(a,b,c,d,e,g){this.c.push({type:"bezierCurveTo",cp1x:a,cp1y:b,cp2x:c,cp2y:d,x:e,y:g});this.f=e;this.g=g};j.quadraticCurveTo=function(a,b,c,d){var e=this.f+0.6666666666666666*(a-this.f),g=this.g+0.6666666666666666*(b-this.g),h=e+(c-this.f)/3,l=g+(d-this.g)/3;this.bezierCurveTo(e,
g,h,l,c,d)};j.arc=function(a,b,c,d,e,g){c*=m;var h=g?"at":"wa",l=a+M(d)*c-A,n=b+L(d)*c-A,o=a+M(e)*c-A,f=b+L(e)*c-A;if(l==o&&!g){l+=0.125}this.c.push({type:h,x:a,y:b,radius:c,xStart:l,yStart:n,xEnd:o,yEnd:f})};j.rect=function(a,b,c,d){this.moveTo(a,b);this.lineTo(a+c,b);this.lineTo(a+c,b+d);this.lineTo(a,b+d);this.closePath()};j.strokeRect=function(a,b,c,d){this.beginPath();this.moveTo(a,b);this.lineTo(a+c,b);this.lineTo(a+c,b+d);this.lineTo(a,b+d);this.closePath();this.stroke()};j.fillRect=function(a,
b,c,d){this.beginPath();this.moveTo(a,b);this.lineTo(a+c,b);this.lineTo(a+c,b+d);this.lineTo(a,b+d);this.closePath();this.fill()};j.createLinearGradient=function(a,b,c,d){var e=new H("gradient");return e};j.createRadialGradient=function(a,b,c,d,e,g){var h=new H("gradientradial");h.n=c;h.o=g;h.i.x=a;h.i.y=b;return h};j.drawImage=function(a,b){var c,d,e,g,h,l,n,o,f=a.runtimeStyle.width,k=a.runtimeStyle.height;a.runtimeStyle.width="auto";a.runtimeStyle.height="auto";var q=a.width,r=a.height;a.runtimeStyle.width=
f;a.runtimeStyle.height=k;if(arguments.length==3){c=arguments[1];d=arguments[2];h=(l=0);n=(e=q);o=(g=r)}else if(arguments.length==5){c=arguments[1];d=arguments[2];e=arguments[3];g=arguments[4];h=(l=0);n=q;o=r}else if(arguments.length==9){h=arguments[1];l=arguments[2];n=arguments[3];o=arguments[4];c=arguments[5];d=arguments[6];e=arguments[7];g=arguments[8]}else{throw"Invalid number of arguments";}var s=this.b(c,d),t=[],v=10,w=10;t.push(" <g_vml_:group",' coordsize="',m*v,",",m*w,'"',' coordorigin="0,0"',
' style="width:',v,";height:",w,";position:absolute;");if(this.a[0][0]!=1||this.a[0][1]){var x=[];x.push("M11='",this.a[0][0],"',","M12='",this.a[1][0],"',","M21='",this.a[0][1],"',","M22='",this.a[1][1],"',","Dx='",i(s.x/m),"',","Dy='",i(s.y/m),"'");var p=s,y=this.b(c+e,d),z=this.b(c,d+g),B=this.b(c+e,d+g);p.x=Math.max(p.x,y.x,z.x,B.x);p.y=Math.max(p.y,y.y,z.y,B.y);t.push("padding:0 ",i(p.x/m),"px ",i(p.y/m),"px 0;filter:progid:DXImageTransform.Microsoft.Matrix(",x.join(""),", sizingmethod='clip');")}else{t.push("top:",
i(s.y/m),"px;left:",i(s.x/m),"px;")}t.push(' ">','<g_vml_:image src="',a.src,'"',' style="width:',m*e,";"," height:",m*g,';"',' cropleft="',h/q,'"',' croptop="',l/r,'"',' cropright="',(q-h-n)/q,'"',' cropbottom="',(r-l-o)/r,'"'," />","</g_vml_:group>");this.j.insertAdjacentHTML("BeforeEnd",t.join(""))};j.stroke=function(a){var b=[],c=O(a?this.fillStyle:this.strokeStyle),d=c[0],e=c[1]*this.globalAlpha,g=10,h=10;b.push("<g_vml_:shape",' fillcolor="',d,'"',' filled="',Boolean(a),'"',' style="position:absolute;width:',
g,";height:",h,';"',' coordorigin="0 0" coordsize="',m*g," ",m*h,'"',' stroked="',!a,'"',' strokeweight="',this.lineWidth,'"',' strokecolor="',d,'"',' path="');var l={x:null,y:null},n={x:null,y:null};for(var o=0;o<this.c.length;o++){var f=this.c[o];if(f.type=="moveTo"){b.push(" m ");var k=this.b(f.x,f.y);b.push(i(k.x),",",i(k.y))}else if(f.type=="lineTo"){b.push(" l ");var k=this.b(f.x,f.y);b.push(i(k.x),",",i(k.y))}else if(f.type=="close"){b.push(" x ")}else if(f.type=="bezierCurveTo"){b.push(" c ");
var k=this.b(f.x,f.y),q=this.b(f.cp1x,f.cp1y),r=this.b(f.cp2x,f.cp2y);b.push(i(q.x),",",i(q.y),",",i(r.x),",",i(r.y),",",i(k.x),",",i(k.y))}else if(f.type=="at"||f.type=="wa"){b.push(" ",f.type," ");var k=this.b(f.x,f.y),s=this.b(f.xStart,f.yStart),t=this.b(f.xEnd,f.yEnd);b.push(i(k.x-this.d*f.radius),",",i(k.y-this.e*f.radius)," ",i(k.x+this.d*f.radius),",",i(k.y+this.e*f.radius)," ",i(s.x),",",i(s.y)," ",i(t.x),",",i(t.y))}if(k){if(l.x==null||k.x<l.x){l.x=k.x}if(n.x==null||k.x>n.x){n.x=k.x}if(l.y==
null||k.y<l.y){l.y=k.y}if(n.y==null||k.y>n.y){n.y=k.y}}}b.push(' ">');if(typeof this.fillStyle=="object"){var v={x:"50%",y:"50%"},w=n.x-l.x,x=n.y-l.y,p=w>x?w:x;v.x=i(this.fillStyle.i.x/w*100+50)+"%";v.y=i(this.fillStyle.i.y/x*100+50)+"%";var y=[];if(this.fillStyle.p=="gradientradial"){var z=this.fillStyle.n/p*100,B=this.fillStyle.o/p*100-z}else{var z=0,B=100}var C={offset:null,color:null},D={offset:null,color:null};this.fillStyle.h.sort(function(T,U){return T.offset-U.offset});for(var o=0;o<this.fillStyle.h.length;o++){var u=
this.fillStyle.h[o];y.push(u.offset*B+z,"% ",u.color,",");if(u.offset>C.offset||C.offset==null){C.offset=u.offset;C.color=u.color}if(u.offset<D.offset||D.offset==null){D.offset=u.offset;D.color=u.color}}y.pop();b.push("<g_vml_:fill",' color="',D.color,'"',' color2="',C.color,'"',' type="',this.fillStyle.p,'"',' focusposition="',v.x,", ",v.y,'"',' colors="',y.join(""),'"',' opacity="',e,'" />')}else if(a){b.push('<g_vml_:fill color="',d,'" opacity="',e,'" />')}else{b.push("<g_vml_:stroke",' opacity="',
e,'"',' joinstyle="',this.lineJoin,'"',' miterlimit="',this.miterLimit,'"',' endcap="',S(this.lineCap),'"',' weight="',this.lineWidth,'px"',' color="',d,'" />')}b.push("</g_vml_:shape>");this.j.insertAdjacentHTML("beforeEnd",b.join(""));this.c=[]};j.fill=function(){this.stroke(true)};j.closePath=function(){this.c.push({type:"close"})};j.b=function(a,b){return{x:m*(a*this.a[0][0]+b*this.a[1][0]+this.a[2][0])-A,y:m*(a*this.a[0][1]+b*this.a[1][1]+this.a[2][1])-A}};j.save=function(){var a={};N(this,a);
this.k.push(a);this.m.push(this.a);this.a=G(J(),this.a)};j.restore=function(){N(this.k.pop(),this);this.a=this.m.pop()};j.translate=function(a,b){var c=[[1,0,0],[0,1,0],[a,b,1]];this.a=G(c,this.a)};j.rotate=function(a){var b=M(a),c=L(a),d=[[b,c,0],[-c,b,0],[0,0,1]];this.a=G(d,this.a)};j.scale=function(a,b){this.d*=a;this.e*=b;var c=[[a,0,0],[0,b,0],[0,0,1]];this.a=G(c,this.a)};j.clip=function(){};j.arcTo=function(){};j.createPattern=function(){return new P};function H(a){this.p=a;this.n=0;this.o=
0;this.h=[];this.i={x:0,y:0}}H.prototype.addColorStop=function(a,b){b=O(b);this.h.push({offset:1-a,color:b})};function P(){}G_vmlCanvasManager=Q;CanvasRenderingContext2D=K;CanvasGradient=H;CanvasPattern=P})()};
// Copyright 2006 Google Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Known Issues:
//
// * Patterns are not implemented.
// * Radial gradient are not implemented. The VML version of these look very
// different from the canvas one.
// * Clipping paths are not implemented.
// * Coordsize. The width and height attribute have higher priority than the
// width and height style values which isn't correct.
// * Painting mode isn't implemented.
// * Canvas width/height should is using content-box by default. IE in
// Quirks mode will draw the canvas using border-box. Either change your
// doctype to HTML5
// (http://www.whatwg.org/specs/web-apps/current-work/#the-doctype)
// or use Box Sizing Behavior from WebFX
// (http://webfx.eae.net/dhtml/boxsizing/boxsizing.html)
// * Optimize. There is always room for speed improvements.
// only add this code if we do not already have a canvas implementation
if (!window.CanvasRenderingContext2D) {
(function () {
// alias some functions to make (compiled) code shorter
var m = Math;
var mr = m.round;
var ms = m.sin;
var mc = m.cos;
// this is used for sub pixel precision
var Z = 10;
var Z2 = Z / 2;
var G_vmlCanvasManager_ = {
init: function (opt_doc) {
var doc = opt_doc || document;
if (/MSIE/.test(navigator.userAgent) && !window.opera) {
var self = this;
doc.attachEvent("onreadystatechange", function () {
self.init_(doc);
});
}
},
init_: function (doc) {
if (doc.readyState == "complete") {
// create xmlns
if (!doc.namespaces["g_vml_"]) {
doc.namespaces.add("g_vml_", "urn:schemas-microsoft-com:vml");
}
// setup default css
var ss = doc.createStyleSheet();
ss.cssText = "canvas{display:inline-block;overflow:hidden;" +
// default size is 300x150 in Gecko and Opera
"text-align:left;width:300px;height:150px}" +
"g_vml_\\:*{behavior:url(#default#VML)}";
// find all canvas elements
var els = doc.getElementsByTagName("canvas");
for (var i = 0; i < els.length; i++) {
if (!els[i].getContext) {
this.initElement(els[i]);
}
}
}
},
fixElement_: function (el) {
// in IE before version 5.5 we would need to add HTML: to the tag name
// but we do not care about IE before version 6
var outerHTML = el.outerHTML;
var newEl = el.ownerDocument.createElement(outerHTML);
// if the tag is still open IE has created the children as siblings and
// it has also created a tag with the name "/FOO"
if (outerHTML.slice(-2) != "/>") {
var tagName = "/" + el.tagName;
var ns;
// remove content
while ((ns = el.nextSibling) && ns.tagName != tagName) {
ns.removeNode();
}
// remove the incorrect closing tag
if (ns) {
ns.removeNode();
}
}
el.parentNode.replaceChild(newEl, el);
return newEl;
},
/**
* Public initializes a canvas element so that it can be used as canvas
* element from now on. This is called automatically before the page is
* loaded but if you are creating elements using createElement you need to
* make sure this is called on the element.
* @param {HTMLElement} el The canvas element to initialize.
* @return {HTMLElement} the element that was created.
*/
initElement: function (el) {
el = this.fixElement_(el);
el.getContext = function () {
if (this.context_) {
return this.context_;
}
return this.context_ = new CanvasRenderingContext2D_(this);
};
// do not use inline function because that will leak memory
el.attachEvent('onpropertychange', onPropertyChange);
el.attachEvent('onresize', onResize);
var attrs = el.attributes;
if (attrs.width && attrs.width.specified) {
// TODO: use runtimeStyle and coordsize
// el.getContext().setWidth_(attrs.width.nodeValue);
el.style.width = attrs.width.nodeValue + "px";
} else {
el.width = el.clientWidth;
}
if (attrs.height && attrs.height.specified) {
// TODO: use runtimeStyle and coordsize
// el.getContext().setHeight_(attrs.height.nodeValue);
el.style.height = attrs.height.nodeValue + "px";
} else {
el.height = el.clientHeight;
}
//el.getContext().setCoordsize_()
return el;
}
};
function onPropertyChange(e) {
var el = e.srcElement;
switch (e.propertyName) {
case 'width':
el.style.width = el.attributes.width.nodeValue + "px";
el.getContext().clearRect();
break;
case 'height':
el.style.height = el.attributes.height.nodeValue + "px";
el.getContext().clearRect();
break;
}
}
function onResize(e) {
var el = e.srcElement;
if (el.firstChild) {
el.firstChild.style.width = el.clientWidth + 'px';
el.firstChild.style.height = el.clientHeight + 'px';
}
}
G_vmlCanvasManager_.init();
// precompute "00" to "FF"
var dec2hex = [];
for (var i = 0; i < 16; i++) {
for (var j = 0; j < 16; j++) {
dec2hex[i * 16 + j] = i.toString(16) + j.toString(16);
}
}
function createMatrixIdentity() {
return [
[1, 0, 0],
[0, 1, 0],
[0, 0, 1]
];
}
function matrixMultiply(m1, m2) {
var result = createMatrixIdentity();
for (var x = 0; x < 3; x++) {
for (var y = 0; y < 3; y++) {
var sum = 0;
for (var z = 0; z < 3; z++) {
sum += m1[x][z] * m2[z][y];
}
result[x][y] = sum;
}
}
return result;
}
function copyState(o1, o2) {
o2.fillStyle = o1.fillStyle;
o2.lineCap = o1.lineCap;
o2.lineJoin = o1.lineJoin;
o2.lineWidth = o1.lineWidth;
o2.miterLimit = o1.miterLimit;
o2.shadowBlur = o1.shadowBlur;
o2.shadowColor = o1.shadowColor;
o2.shadowOffsetX = o1.shadowOffsetX;
o2.shadowOffsetY = o1.shadowOffsetY;
o2.strokeStyle = o1.strokeStyle;
o2.arcScaleX_ = o1.arcScaleX_;
o2.arcScaleY_ = o1.arcScaleY_;
}
function processStyle(styleString) {
var str, alpha = 1;
styleString = String(styleString);
if (styleString.substring(0, 3) == "rgb") {
var start = styleString.indexOf("(", 3);
var end = styleString.indexOf(")", start + 1);
var guts = styleString.substring(start + 1, end).split(",");
str = "#";
for (var i = 0; i < 3; i++) {
str += dec2hex[Number(guts[i])];
}
if ((guts.length == 4) && (styleString.substr(3, 1) == "a")) {
alpha = guts[3];
}
} else {
str = styleString;
}
return [str, alpha];
}
function processLineCap(lineCap) {
switch (lineCap) {
case "butt":
return "flat";
case "round":
return "round";
case "square":
default:
return "square";
}
}
/**
* This class implements CanvasRenderingContext2D interface as described by
* the WHATWG.
* @param {HTMLElement} surfaceElement The element that the 2D context should
* be associated with
*/
function CanvasRenderingContext2D_(surfaceElement) {
this.m_ = createMatrixIdentity();
this.mStack_ = [];
this.aStack_ = [];
this.currentPath_ = [];
// Canvas context properties
this.strokeStyle = "#000";
this.fillStyle = "#000";
this.lineWidth = 1;
this.lineJoin = "miter";
this.lineCap = "butt";
this.miterLimit = Z * 1;
this.globalAlpha = 1;
this.canvas = surfaceElement;
var el = surfaceElement.ownerDocument.createElement('div');
el.style.width = surfaceElement.clientWidth + 'px';
el.style.height = surfaceElement.clientHeight + 'px';
el.style.overflow = 'hidden';
el.style.position = 'absolute';
surfaceElement.appendChild(el);
this.element_ = el;
this.arcScaleX_ = 1;
this.arcScaleY_ = 1;
}
var contextPrototype = CanvasRenderingContext2D_.prototype;
contextPrototype.clearRect = function() {
this.element_.innerHTML = "";
this.currentPath_ = [];
};
contextPrototype.beginPath = function() {
// TODO: Branch current matrix so that save/restore has no effect
// as per safari docs.
this.currentPath_ = [];
};
contextPrototype.moveTo = function(aX, aY) {
this.currentPath_.push({type: "moveTo", x: aX, y: aY});
this.currentX_ = aX;
this.currentY_ = aY;
};
contextPrototype.lineTo = function(aX, aY) {
this.currentPath_.push({type: "lineTo", x: aX, y: aY});
this.currentX_ = aX;
this.currentY_ = aY;
};
contextPrototype.bezierCurveTo = function(aCP1x, aCP1y,
aCP2x, aCP2y,
aX, aY) {
this.currentPath_.push({type: "bezierCurveTo",
cp1x: aCP1x,
cp1y: aCP1y,
cp2x: aCP2x,
cp2y: aCP2y,
x: aX,
y: aY});
this.currentX_ = aX;
this.currentY_ = aY;
};
contextPrototype.quadraticCurveTo = function(aCPx, aCPy, aX, aY) {
// the following is lifted almost directly from
// http://developer.mozilla.org/en/docs/Canvas_tutorial:Drawing_shapes
var cp1x = this.currentX_ + 2.0 / 3.0 * (aCPx - this.currentX_);
var cp1y = this.currentY_ + 2.0 / 3.0 * (aCPy - this.currentY_);
var cp2x = cp1x + (aX - this.currentX_) / 3.0;
var cp2y = cp1y + (aY - this.currentY_) / 3.0;
this.bezierCurveTo(cp1x, cp1y, cp2x, cp2y, aX, aY);
};
contextPrototype.arc = function(aX, aY, aRadius,
aStartAngle, aEndAngle, aClockwise) {
aRadius *= Z;
var arcType = aClockwise ? "at" : "wa";
var xStart = aX + (mc(aStartAngle) * aRadius) - Z2;
var yStart = aY + (ms(aStartAngle) * aRadius) - Z2;
var xEnd = aX + (mc(aEndAngle) * aRadius) - Z2;
var yEnd = aY + (ms(aEndAngle) * aRadius) - Z2;
// IE won't render arches drawn counter clockwise if xStart == xEnd.
if (xStart == xEnd && !aClockwise) {
xStart += 0.125; // Offset xStart by 1/80 of a pixel. Use something
// that can be represented in binary
}
this.currentPath_.push({type: arcType,
x: aX,
y: aY,
radius: aRadius,
xStart: xStart,
yStart: yStart,
xEnd: xEnd,
yEnd: yEnd});
};
contextPrototype.rect = function(aX, aY, aWidth, aHeight) {
this.moveTo(aX, aY);
this.lineTo(aX + aWidth, aY);
this.lineTo(aX + aWidth, aY + aHeight);
this.lineTo(aX, aY + aHeight);
this.closePath();
};
contextPrototype.strokeRect = function(aX, aY, aWidth, aHeight) {
// Will destroy any existing path (same as FF behaviour)
this.beginPath();
this.moveTo(aX, aY);
this.lineTo(aX + aWidth, aY);
this.lineTo(aX + aWidth, aY + aHeight);
this.lineTo(aX, aY + aHeight);
this.closePath();
this.stroke();
};
contextPrototype.fillRect = function(aX, aY, aWidth, aHeight) {
// Will destroy any existing path (same as FF behaviour)
this.beginPath();
this.moveTo(aX, aY);
this.lineTo(aX + aWidth, aY);
this.lineTo(aX + aWidth, aY + aHeight);
this.lineTo(aX, aY + aHeight);
this.closePath();
this.fill();
};
contextPrototype.createLinearGradient = function(aX0, aY0, aX1, aY1) {
var gradient = new CanvasGradient_("gradient");
return gradient;
};
contextPrototype.createRadialGradient = function(aX0, aY0,
aR0, aX1,
aY1, aR1) {
var gradient = new CanvasGradient_("gradientradial");
gradient.radius1_ = aR0;
gradient.radius2_ = aR1;
gradient.focus_.x = aX0;
gradient.focus_.y = aY0;
return gradient;
};
contextPrototype.drawImage = function (image, var_args) {
var dx, dy, dw, dh, sx, sy, sw, sh;
// to find the original width we overide the width and height
var oldRuntimeWidth = image.runtimeStyle.width;
var oldRuntimeHeight = image.runtimeStyle.height;
image.runtimeStyle.width = 'auto';
image.runtimeStyle.height = 'auto';
// get the original size
var w = image.width;
var h = image.height;
// and remove overides
image.runtimeStyle.width = oldRuntimeWidth;
image.runtimeStyle.height = oldRuntimeHeight;
if (arguments.length == 3) {
dx = arguments[1];
dy = arguments[2];
sx = sy = 0;
sw = dw = w;
sh = dh = h;
} else if (arguments.length == 5) {
dx = arguments[1];
dy = arguments[2];
dw = arguments[3];
dh = arguments[4];
sx = sy = 0;
sw = w;
sh = h;
} else if (arguments.length == 9) {
sx = arguments[1];
sy = arguments[2];
sw = arguments[3];
sh = arguments[4];
dx = arguments[5];
dy = arguments[6];
dw = arguments[7];
dh = arguments[8];
} else {
throw "Invalid number of arguments";
}
var d = this.getCoords_(dx, dy);
var w2 = sw / 2;
var h2 = sh / 2;
var vmlStr = [];
var W = 10;
var H = 10;
// For some reason that I've now forgotten, using divs didn't work
vmlStr.push(' <g_vml_:group',
' coordsize="', Z * W, ',', Z * H, '"',
' coordorigin="0,0"' ,
' style="width:', W, ';height:', H, ';position:absolute;');
// If filters are necessary (rotation exists), create them
// filters are bog-slow, so only create them if abbsolutely necessary
// The following check doesn't account for skews (which don't exist
// in the canvas spec (yet) anyway.
if (this.m_[0][0] != 1 || this.m_[0][1]) {
var filter = [];
// Note the 12/21 reversal
filter.push("M11='", this.m_[0][0], "',",
"M12='", this.m_[1][0], "',",
"M21='", this.m_[0][1], "',",
"M22='", this.m_[1][1], "',",
"Dx='", mr(d.x / Z), "',",
"Dy='", mr(d.y / Z), "'");
// Bounding box calculation (need to minimize displayed area so that
// filters don't waste time on unused pixels.
var max = d;
var c2 = this.getCoords_(dx + dw, dy);
var c3 = this.getCoords_(dx, dy + dh);
var c4 = this.getCoords_(dx + dw, dy + dh);
max.x = Math.max(max.x, c2.x, c3.x, c4.x);
max.y = Math.max(max.y, c2.y, c3.y, c4.y);
vmlStr.push("padding:0 ", mr(max.x / Z), "px ", mr(max.y / Z),
"px 0;filter:progid:DXImageTransform.Microsoft.Matrix(",
filter.join(""), ", sizingmethod='clip');");
} else {
vmlStr.push("top:", mr(d.y / Z), "px;left:", mr(d.x / Z), "px;");
}
vmlStr.push(' ">' ,
'<g_vml_:image src="', image.src, '"',
' style="width:', Z * dw, ';',
' height:', Z * dh, ';"',
' cropleft="', sx / w, '"',
' croptop="', sy / h, '"',
' cropright="', (w - sx - sw) / w, '"',
' cropbottom="', (h - sy - sh) / h, '"',
' />',
'</g_vml_:group>');
this.element_.insertAdjacentHTML("BeforeEnd",
vmlStr.join(""));
};
contextPrototype.stroke = function(aFill) {
var lineStr = [];
var lineOpen = false;
var a = processStyle(aFill ? this.fillStyle : this.strokeStyle);
var color = a[0];
var opacity = a[1] * this.globalAlpha;
var W = 10;
var H = 10;
lineStr.push('<g_vml_:shape',
' fillcolor="', color, '"',
' filled="', Boolean(aFill), '"',
' style="position:absolute;width:', W, ';height:', H, ';"',
' coordorigin="0 0" coordsize="', Z * W, ' ', Z * H, '"',
' stroked="', !aFill, '"',
' strokeweight="', this.lineWidth, '"',
' strokecolor="', color, '"',
' path="');
var newSeq = false;
var min = {x: null, y: null};
var max = {x: null, y: null};
for (var i = 0; i < this.currentPath_.length; i++) {
var p = this.currentPath_[i];
if (p.type == "moveTo") {
lineStr.push(" m ");
var c = this.getCoords_(p.x, p.y);
lineStr.push(mr(c.x), ",", mr(c.y));
} else if (p.type == "lineTo") {
lineStr.push(" l ");
var c = this.getCoords_(p.x, p.y);
lineStr.push(mr(c.x), ",", mr(c.y));
} else if (p.type == "close") {
lineStr.push(" x ");
} else if (p.type == "bezierCurveTo") {
lineStr.push(" c ");
var c = this.getCoords_(p.x, p.y);
var c1 = this.getCoords_(p.cp1x, p.cp1y);
var c2 = this.getCoords_(p.cp2x, p.cp2y);
lineStr.push(mr(c1.x), ",", mr(c1.y), ",",
mr(c2.x), ",", mr(c2.y), ",",
mr(c.x), ",", mr(c.y));
} else if (p.type == "at" || p.type == "wa") {
lineStr.push(" ", p.type, " ");
var c = this.getCoords_(p.x, p.y);
var cStart = this.getCoords_(p.xStart, p.yStart);
var cEnd = this.getCoords_(p.xEnd, p.yEnd);
lineStr.push(mr(c.x - this.arcScaleX_ * p.radius), ",",
mr(c.y - this.arcScaleY_ * p.radius), " ",
mr(c.x + this.arcScaleX_ * p.radius), ",",
mr(c.y + this.arcScaleY_ * p.radius), " ",
mr(cStart.x), ",", mr(cStart.y), " ",
mr(cEnd.x), ",", mr(cEnd.y));
}
// TODO: Following is broken for curves due to
// move to proper paths.
// Figure out dimensions so we can do gradient fills
// properly
if(c) {
if (min.x == null || c.x < min.x) {
min.x = c.x;
}
if (max.x == null || c.x > max.x) {
max.x = c.x;
}
if (min.y == null || c.y < min.y) {
min.y = c.y;
}
if (max.y == null || c.y > max.y) {
max.y = c.y;
}
}
}
lineStr.push(' ">');
if (typeof this.fillStyle == "object") {
var focus = {x: "50%", y: "50%"};
var width = (max.x - min.x);
var height = (max.y - min.y);
var dimension = (width > height) ? width : height;
focus.x = mr((this.fillStyle.focus_.x / width) * 100 + 50) + "%";
focus.y = mr((this.fillStyle.focus_.y / height) * 100 + 50) + "%";
var colors = [];
// inside radius (%)
if (this.fillStyle.type_ == "gradientradial") {
var inside = (this.fillStyle.radius1_ / dimension * 100);
// percentage that outside radius exceeds inside radius
var expansion = (this.fillStyle.radius2_ / dimension * 100) - inside;
} else {
var inside = 0;
var expansion = 100;
}
var insidecolor = {offset: null, color: null};
var outsidecolor = {offset: null, color: null};
// We need to sort 'colors' by percentage, from 0 > 100 otherwise ie
// won't interpret it correctly
this.fillStyle.colors_.sort(function (cs1, cs2) {
return cs1.offset - cs2.offset;
});
for (var i = 0; i < this.fillStyle.colors_.length; i++) {
var fs = this.fillStyle.colors_[i];
colors.push( (fs.offset * expansion) + inside, "% ", fs.color, ",");
if (fs.offset > insidecolor.offset || insidecolor.offset == null) {
insidecolor.offset = fs.offset;
insidecolor.color = fs.color;
}
if (fs.offset < outsidecolor.offset || outsidecolor.offset == null) {
outsidecolor.offset = fs.offset;
outsidecolor.color = fs.color;
}
}
colors.pop();
lineStr.push('<g_vml_:fill',
' color="', outsidecolor.color, '"',
' color2="', insidecolor.color, '"',
' type="', this.fillStyle.type_, '"',
' focusposition="', focus.x, ', ', focus.y, '"',
' colors="', colors.join(""), '"',
' opacity="', opacity, '" />');
} else if (aFill) {
lineStr.push('<g_vml_:fill color="', color, '" opacity="', opacity, '" />');
} else {
lineStr.push(
'<g_vml_:stroke',
' opacity="', opacity,'"',
' joinstyle="', this.lineJoin, '"',
' miterlimit="', this.miterLimit, '"',
' endcap="', processLineCap(this.lineCap) ,'"',
' weight="', this.lineWidth, 'px"',
' color="', color,'" />'
);
}
lineStr.push("</g_vml_:shape>");
this.element_.insertAdjacentHTML("beforeEnd", lineStr.join(""));
//this.currentPath_ = [];
};
contextPrototype.fill = function() {
this.stroke(true);
};
contextPrototype.closePath = function() {
this.currentPath_.push({type: "close"});
};
/**
* @private
*/
contextPrototype.getCoords_ = function(aX, aY) {
return {
x: Z * (aX * this.m_[0][0] + aY * this.m_[1][0] + this.m_[2][0]) - Z2,
y: Z * (aX * this.m_[0][1] + aY * this.m_[1][1] + this.m_[2][1]) - Z2
};
};
contextPrototype.save = function() {
var o = {};
copyState(this, o);
this.aStack_.push(o);
this.mStack_.push(this.m_);
this.m_ = matrixMultiply(createMatrixIdentity(), this.m_);
};
contextPrototype.restore = function() {
copyState(this.aStack_.pop(), this);
this.m_ = this.mStack_.pop();
};
contextPrototype.translate = function(aX, aY) {
var m1 = [
[1, 0, 0],
[0, 1, 0],
[aX, aY, 1]
];
this.m_ = matrixMultiply(m1, this.m_);
};
contextPrototype.rotate = function(aRot) {
var c = mc(aRot);
var s = ms(aRot);
var m1 = [
[c, s, 0],
[-s, c, 0],
[0, 0, 1]
];
this.m_ = matrixMultiply(m1, this.m_);
};
contextPrototype.scale = function(aX, aY) {
this.arcScaleX_ *= aX;
this.arcScaleY_ *= aY;
var m1 = [
[aX, 0, 0],
[0, aY, 0],
[0, 0, 1]
];
this.m_ = matrixMultiply(m1, this.m_);
};
/******** STUBS ********/
contextPrototype.clip = function() {
// TODO: Implement
};
contextPrototype.arcTo = function() {
// TODO: Implement
};
contextPrototype.createPattern = function() {
return new CanvasPattern_;
};
// Gradient / Pattern Stubs
function CanvasGradient_(aType) {
this.type_ = aType;
this.radius1_ = 0;
this.radius2_ = 0;
this.colors_ = [];
this.focus_ = {x: 0, y: 0};
}
CanvasGradient_.prototype.addColorStop = function(aOffset, aColor) {
aColor = processStyle(aColor);
this.colors_.push({offset: 1-aOffset, color: aColor});
};
function CanvasPattern_() {}
// set up externs
G_vmlCanvasManager = G_vmlCanvasManager_;
CanvasRenderingContext2D = CanvasRenderingContext2D_;
CanvasGradient = CanvasGradient_;
CanvasPattern = CanvasPattern_;
})();
} // if
This source diff could not be displayed because it is too large. You can view the blob instead.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment